fix payment UI UX
This commit is contained in:
1 parent
bb3ddd1610
commit
90931a5a64
6 files changed
+104
-10
No files matched your search
@@ -4,12 +4,13 @@
|
|||||||
let toast;
|
let toast;
|
||||||
function status(text){if(!toast){toast=document.createElement('div');toast.className='pay-copy-status';toast.setAttribute('role','status');document.body.append(toast);}toast.textContent=text;toast.hidden=false;setTimeout(()=>{toast.hidden=true;},3000);}
|
function status(text){if(!toast){toast=document.createElement('div');toast.className='pay-copy-status';toast.setAttribute('role','status');document.body.append(toast);}toast.textContent=text;toast.hidden=false;setTimeout(()=>{toast.hidden=true;},3000);}
|
||||||
async function copy(text){
|
async function copy(text){
|
||||||
try{await navigator.clipboard.writeText(text);status('کپی شد');}
|
try{await navigator.clipboard.writeText(text);status('کپی شد');return true;}
|
||||||
catch{
|
catch{
|
||||||
const dialog=document.createElement('dialog');dialog.className='pay-manual-copy';dialog.setAttribute('aria-label','کپی اطلاعات');
|
const dialog=document.createElement('dialog');dialog.className='pay-manual-copy';dialog.setAttribute('aria-label','کپی اطلاعات');
|
||||||
const title=document.createElement('h2');title.textContent='متن را انتخاب و کپی کنید';const area=document.createElement('textarea');area.readOnly=true;area.value=text;
|
const title=document.createElement('h2');title.textContent='متن را انتخاب و کپی کنید';const area=document.createElement('textarea');area.readOnly=true;area.value=text;
|
||||||
const select=document.createElement('button');select.className='pay-button';select.textContent='انتخاب متن';select.onclick=()=>{area.focus();area.select();};
|
const select=document.createElement('button');select.className='pay-button';select.textContent='انتخاب متن';select.onclick=()=>{area.focus();area.select();};
|
||||||
const close=document.createElement('button');close.className='pay-button secondary';close.textContent='بستن';close.onclick=()=>dialog.close();dialog.append(title,area,select,close);document.body.append(dialog);dialog.addEventListener('close',()=>dialog.remove());dialog.showModal();area.focus();area.select();
|
const close=document.createElement('button');close.className='pay-button secondary';close.textContent='بستن';close.onclick=()=>dialog.close();dialog.append(title,area,select,close);document.body.append(dialog);dialog.addEventListener('close',()=>dialog.remove());dialog.showModal();area.focus();area.select();
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
document.querySelectorAll('[data-copy]').forEach(button=>button.addEventListener('click',()=>copy(button.dataset.copy)));
|
document.querySelectorAll('[data-copy]').forEach(button=>button.addEventListener('click',()=>copy(button.dataset.copy)));
|
||||||
@@ -17,18 +18,26 @@ document.querySelectorAll('[data-notice-sms]').forEach(form=>form.addEventListen
|
|||||||
if(form.dataset.sending==='true'){event.preventDefault();return;}
|
if(form.dataset.sending==='true'){event.preventDefault();return;}
|
||||||
form.dataset.sending='true';const button=form.querySelector('button');button.disabled=true;button.textContent='در حال ثبت درخواست ارسال…';
|
form.dataset.sending='true';const button=form.querySelector('button');button.disabled=true;button.textContent='در حال ثبت درخواست ارسال…';
|
||||||
}));
|
}));
|
||||||
document.querySelectorAll('[data-share-link]').forEach(form=>form.addEventListener('submit',async event=>{
|
document.querySelectorAll('[data-share-link]').forEach(form=>{
|
||||||
|
const button=form.querySelector('button'),label=button.querySelector('span'),originalLabel=label.textContent;
|
||||||
|
const feedback=form.querySelector('[role="status"]');
|
||||||
|
form.addEventListener('submit',async event=>{
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const button=form.querySelector('button'),feedback=form.querySelector('[role="status"]');
|
// This AJAX form owns its button state; keep the generic navigation submit handler out.
|
||||||
button.disabled=true;button.setAttribute('aria-busy','true');feedback.textContent='در حال آمادهسازی لینک…';
|
event.stopPropagation();
|
||||||
|
if(button.disabled)return;
|
||||||
|
button.disabled=true;button.setAttribute('aria-busy','true');label.textContent='در حال آمادهسازی لینک…';feedback.textContent='در حال آمادهسازی لینک…';
|
||||||
try{
|
try{
|
||||||
const response=await fetch(form.action,{method:'POST',body:new FormData(form),credentials:'same-origin',cache:'no-store',headers:{'Accept':'application/json'}});
|
const response=await fetch(form.action,{method:'POST',body:new FormData(form),credentials:'same-origin',cache:'no-store',headers:{'Accept':'application/json'}});
|
||||||
if(!response.ok || !response.headers.get('Content-Type')?.includes('application/json'))throw new Error('share unavailable');
|
if(!response.ok || !response.headers.get('Content-Type')?.includes('application/json'))throw new Error('share unavailable');
|
||||||
const data=await response.json();
|
const data=await response.json();
|
||||||
await copy(data.url);feedback.textContent='لینک آماده است؛ میتوانید آن را برای دیگران ارسال کنید.';
|
const copied=await copy(data.url);
|
||||||
}catch{feedback.textContent='دریافت لینک ممکن نشد. دوباره تلاش کنید یا لینک پرداخت دریافتی را باز کنید.';}
|
label.textContent=copied?'لینک کپی شد؛ کپی دوباره':originalLabel;
|
||||||
|
feedback.textContent=copied?'لینک کوتاه کپی شد؛ میتوانید آن را برای دیگران ارسال کنید.':'لینک آماده است؛ کپی خودکار ممکن نشد. متن را از پنجرهٔ بازشده انتخاب و کپی کنید.';
|
||||||
|
}catch{label.textContent=originalLabel;feedback.textContent='دریافت لینک ممکن نشد. دوباره تلاش کنید یا لینک پرداخت دریافتی را باز کنید.';}
|
||||||
finally{button.disabled=false;button.removeAttribute('aria-busy');}
|
finally{button.disabled=false;button.removeAttribute('aria-busy');}
|
||||||
}));
|
});
|
||||||
|
});
|
||||||
const identifiers=document.querySelector('#identifier-accounts'),account=document.querySelector('#id_account'),deposit=document.querySelector('#id_deposit_id');
|
const identifiers=document.querySelector('#identifier-accounts'),account=document.querySelector('#id_account'),deposit=document.querySelector('#id_deposit_id');
|
||||||
if(identifiers && account && deposit){const ids=JSON.parse(identifiers.textContent).map(String);function update(){deposit.required=ids.includes(account.value);document.querySelector('#identifier-help').textContent=deposit.required?'این حساب شناسهدار است؛ شناسه واریز مشتری الزامی است.':'شناسه واریز در صورت اعلام بانک وارد شود.';}account.addEventListener('change',update);update();}
|
if(identifiers && account && deposit){const ids=JSON.parse(identifiers.textContent).map(String);function update(){deposit.required=ids.includes(account.value);document.querySelector('#identifier-help').textContent=deposit.required?'این حساب شناسهدار است؛ شناسه واریز مشتری الزامی است.':'شناسه واریز در صورت اعلام بانک وارد شود.';}account.addEventListener('change',update);update();}
|
||||||
const badge=document.querySelector('[data-payment-count]');
|
const badge=document.querySelector('[data-payment-count]');
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
const input=document.querySelector('#id_paid_amount, #id_amount');if(!input)return;
|
const input=document.querySelector('#id_paid_amount, #id_amount');if(!input)return;
|
||||||
input.inputMode='numeric';
|
input.inputMode='numeric';
|
||||||
const words=document.querySelector('#amount-words');
|
const words=document.querySelector('#amount-words');
|
||||||
|
if(words)input.setAttribute('aria-describedby',[input.getAttribute('aria-describedby'),words.id].filter(Boolean).join(' '));
|
||||||
const small=['صفر','یک','دو','سه','چهار','پنج','شش','هفت','هشت','نه','ده','یازده','دوازده','سیزده','چهارده','پانزده','شانزده','هفده','هجده','نوزده'];
|
const small=['صفر','یک','دو','سه','چهار','پنج','شش','هفت','هشت','نه','ده','یازده','دوازده','سیزده','چهارده','پانزده','شانزده','هفده','هجده','نوزده'];
|
||||||
const tens=['','','بیست','سی','چهل','پنجاه','شصت','هفتاد','هشتاد','نود'];
|
const tens=['','','بیست','سی','چهل','پنجاه','شصت','هفتاد','هشتاد','نود'];
|
||||||
const hundreds=['','صد','دویست','سیصد','چهارصد','پانصد','ششصد','هفتصد','هشتصد','نهصد'];
|
const hundreds=['','صد','دویست','سیصد','چهارصد','پانصد','ششصد','هفتصد','هشتصد','نهصد'];
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<div class="pay-field {% if field.errors %}has-error{% endif %} {% if wide %}pay-field-wide{% endif %}">
|
<div class="pay-field {% if field.errors %}has-error{% endif %} {% if wide %}pay-field-wide{% endif %}">
|
||||||
<label for="{{ field.id_for_label }}">{{ label|default:field.label }}{% if field.field.required %}<span class="pay-required">الزامی</span>{% else %}<span class="pay-optional">اختیاری</span>{% endif %}</label>
|
<label for="{{ field.id_for_label }}">{{ label|default:field.label }}{% if field.field.required %}<span class="pay-required">الزامی</span>{% else %}<span class="pay-optional">اختیاری</span>{% endif %}</label>
|
||||||
{{ field }}
|
{{ field }}
|
||||||
|
{% if field.name == 'amount' %}<p id="amount-words" class="pay-field-hint" role="status" aria-live="polite" aria-atomic="true"></p>{% endif %}
|
||||||
{% if hint %}<p class="pay-field-hint">{{ hint }}</p>{% endif %}
|
{% if hint %}<p class="pay-field-hint">{{ hint }}</p>{% endif %}
|
||||||
{% if field.help_text %}<p class="pay-field-hint">{{ field.help_text }}</p>{% endif %}
|
{% if field.help_text %}<p class="pay-field-hint">{{ field.help_text }}</p>{% endif %}
|
||||||
{% if field.errors %}<div class="field-error" role="alert">{{ field.errors }}</div>{% endif %}
|
{% if field.errors %}<div class="field-error" role="alert">{{ field.errors }}</div>{% endif %}
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
from playwright.sync_api import expect
|
||||||
|
|
||||||
|
from .payments import PaymentsPage
|
||||||
|
|
||||||
|
|
||||||
|
class PaymentFeedbackPage(PaymentsPage):
|
||||||
|
def check_amount(self, raw, expected):
|
||||||
|
self.page.locator('#id_amount').fill(raw)
|
||||||
|
expect(self.page.locator('#amount-words')).to_have_text(expected)
|
||||||
|
|
||||||
|
def request_share(self):
|
||||||
|
self.page.locator('[data-share-link] button').click()
|
||||||
|
|
||||||
|
def expect_pending_share(self):
|
||||||
|
button = self.page.locator('[data-share-link] button')
|
||||||
|
expect(button).to_be_disabled()
|
||||||
|
expect(button).to_have_attribute('aria-busy', 'true')
|
||||||
|
expect(button).to_have_text('در حال آمادهسازی لینک…')
|
||||||
|
|
||||||
|
def expect_failed_share(self):
|
||||||
|
button = self.page.locator('[data-share-link] button')
|
||||||
|
expect(button).to_be_enabled()
|
||||||
|
expect(button).not_to_have_attribute('aria-busy', 'true')
|
||||||
|
expect(button).to_have_text('کپی لینک کوتاه این صفحه برای ارسال به دیگران')
|
||||||
|
expect(self.page.locator('.pay-share-feedback')).to_contain_text('دریافت لینک ممکن نشد')
|
||||||
@@ -12,9 +12,10 @@ class PaymentLinkPage(PaymentsPage):
|
|||||||
self.page.context.grant_permissions(['clipboard-read', 'clipboard-write'])
|
self.page.context.grant_permissions(['clipboard-read', 'clipboard-write'])
|
||||||
|
|
||||||
def copied_text(self, button_name):
|
def copied_text(self, button_name):
|
||||||
self.page.get_by_role('button', name=button_name, exact=True).click()
|
button = self.page.locator('[data-share-link] button') if button_name == 'کپی لینک کوتاه این صفحه برای ارسال به دیگران' else self.page.get_by_role('button', name=button_name, exact=True)
|
||||||
|
button.click()
|
||||||
if button_name == 'کپی لینک کوتاه این صفحه برای ارسال به دیگران':
|
if button_name == 'کپی لینک کوتاه این صفحه برای ارسال به دیگران':
|
||||||
expect(self.page.locator('.pay-share-feedback')).to_contain_text('لینک آماده است')
|
expect(self.page.locator('.pay-share-feedback')).to_contain_text('لینک آماده است' if self.manual_copy else 'لینک کوتاه کپی شد')
|
||||||
if self.manual_copy:
|
if self.manual_copy:
|
||||||
dialog = self.page.get_by_role('dialog')
|
dialog = self.page.get_by_role('dialog')
|
||||||
expect(dialog).to_be_visible()
|
expect(dialog).to_be_visible()
|
||||||
@@ -26,7 +27,9 @@ class PaymentLinkPage(PaymentsPage):
|
|||||||
|
|
||||||
def copy_short_link(self):
|
def copy_short_link(self):
|
||||||
value = self.copied_text('کپی لینک کوتاه این صفحه برای ارسال به دیگران')
|
value = self.copied_text('کپی لینک کوتاه این صفحه برای ارسال به دیگران')
|
||||||
expect(self.page.locator('.pay-share-feedback')).to_contain_text('لینک آماده است')
|
expect(self.page.locator('[data-share-link] button')).to_be_enabled()
|
||||||
|
expect(self.page.locator('[data-share-link] button')).not_to_have_attribute('aria-busy', 'true')
|
||||||
|
expect(self.page.locator('[data-share-link] button')).to_have_text('کپی لینک کوتاه این صفحه برای ارسال به دیگران' if self.manual_copy else 'لینک کپی شد؛ کپی دوباره')
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def open_link(self, path):
|
def open_link(self, path):
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import pytest
|
||||||
|
from django.urls import reverse
|
||||||
|
from playwright.sync_api import expect
|
||||||
|
|
||||||
|
from apps.payments import services
|
||||||
|
from tests.e2e.pages.auth import AuthenticationPage
|
||||||
|
from tests.e2e.pages.payment_feedback import PaymentFeedbackPage
|
||||||
|
from tests.e2e.pages.payment_link import PaymentLinkPage
|
||||||
|
from tests.e2e.test_payments_e2e import payment_case, PASSWORD
|
||||||
|
|
||||||
|
pytestmark = [pytest.mark.e2e, pytest.mark.django_db(transaction=True)]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('width', [320, 1440])
|
||||||
|
def test_notice_toman_words_are_exact_for_small_large_and_localized_rial_inputs(live_server, browser_page, payment_case, width):
|
||||||
|
customer, account, reviewers = payment_case
|
||||||
|
samples = [('0', 'صفر'), ('1', 'یک دهم'), ('10', 'یک'), ('19', 'یک و نه دهم'), ('20', 'دو'),
|
||||||
|
('101', 'ده و یک دهم'), ('1001', 'صد و یک دهم'), ('1000000', 'صد هزار'),
|
||||||
|
('۱۲٬۳۴۵', 'هزار و دویست و سی و چهار و پنج دهم'), ('١٢٣٤٥', 'هزار و دویست و سی و چهار و پنج دهم'),
|
||||||
|
('1234567890', 'صد و بیست و سه میلیون و چهارصد و پنجاه و شش هزار و هفتصد و هشتاد و نه'),
|
||||||
|
('9007199254740991', 'نهصد تریلیون و هفتصد و نوزده میلیارد و نهصد و بیست و پنج میلیون و چهارصد و هفتاد و چهار هزار و نود و نه و یک دهم')]
|
||||||
|
with browser_page() as page:
|
||||||
|
page.set_viewport_size({'width': width, 'height': 900})
|
||||||
|
AuthenticationPage(page, live_server.url).login_staff(reviewers[0], PASSWORD)
|
||||||
|
view = PaymentFeedbackPage(page, live_server.url)
|
||||||
|
view.goto(reverse('staff_payments:notice_new'))
|
||||||
|
for raw, words in samples:
|
||||||
|
view.check_amount(raw, 'معادل: ' + words + ' تومان')
|
||||||
|
for invalid in ['', '12.5', '-100', '12abc', '9007199254740992']:
|
||||||
|
view.check_amount(invalid, '')
|
||||||
|
assert page.evaluate('document.documentElement.scrollWidth <= document.documentElement.clientWidth + 1')
|
||||||
|
|
||||||
|
|
||||||
|
def test_share_pending_failure_retry_and_repeated_success_restore_button(live_server, browser_page, payment_case):
|
||||||
|
customer, account, reviewers = payment_case
|
||||||
|
notice = services.issue_notice(reviewers[0], {'customer': customer, 'account': account,
|
||||||
|
'subject': 'آزمون وضعیت کپی لینک', 'amount': 10000})
|
||||||
|
endpoint = live_server.url + reverse('payments:notice_share_link', args=[notice.public_id])
|
||||||
|
pending = []
|
||||||
|
with browser_page() as page:
|
||||||
|
view = PaymentLinkPage(page, live_server.url)
|
||||||
|
view.prepare_copy()
|
||||||
|
AuthenticationPage(page, live_server.url).login_customer(customer, PASSWORD)
|
||||||
|
view.goto(reverse('payments:notice_detail', args=[notice.public_id]))
|
||||||
|
page.route(endpoint, lambda route: pending.append(route))
|
||||||
|
feedback = PaymentFeedbackPage(page, live_server.url)
|
||||||
|
feedback.request_share()
|
||||||
|
feedback.expect_pending_share()
|
||||||
|
expect(page.locator('.pay-share-feedback')).to_have_text('در حال آمادهسازی لینک…')
|
||||||
|
assert len(pending) == 1
|
||||||
|
pending[0].fulfill(status=503, content_type='application/json', body='{}')
|
||||||
|
feedback.expect_failed_share()
|
||||||
|
page.unroute(endpoint)
|
||||||
|
first = view.copy_short_link()
|
||||||
|
assert view.copy_short_link() == first
|
||||||
Reference in new issue
Block a user