some improve for sms delivery
This commit is contained in:
1 parent
1d3cff0b63
commit
ffc1ec0dcf
10 files changed
+134
-11
No files matched your search
@@ -18,6 +18,21 @@ class TicketConversationPage(BasePage):
|
||||
assert "reply_result=reply" in self.page.url
|
||||
expect(self.page.locator("[data-conversation]")).to_contain_text(body)
|
||||
|
||||
def open_composer(self):
|
||||
launch = self.page.locator("[data-composer-toggle]")
|
||||
expect(launch).to_be_visible()
|
||||
launch.click()
|
||||
expect(self.page.locator("[data-composer-shell]:not(.is-collapsed)")).to_be_visible()
|
||||
|
||||
def expect_latest_message_above_composer(self):
|
||||
latest = self.page.locator("[data-message]").last
|
||||
expect(latest).to_be_visible()
|
||||
latest.scroll_into_view_if_needed()
|
||||
message_box = latest.bounding_box()
|
||||
composer_box = self.page.locator("[data-composer-shell]").bounding_box()
|
||||
assert message_box and composer_box
|
||||
assert message_box["y"] + message_box["height"] <= composer_box["y"] + 1
|
||||
|
||||
@property
|
||||
def chat_guidance(self):
|
||||
return self.page.locator(".chat-coach-floating")
|
||||
|
||||
@@ -3,6 +3,7 @@ from django.urls import reverse
|
||||
|
||||
from apps.accounts.models import User
|
||||
from apps.customers.models import CustomerProfile
|
||||
from apps.messaging.models import Message
|
||||
from apps.staff.models import StaffProfile
|
||||
from apps.tickets.models import TicketCategory
|
||||
from apps.tickets.services import create_ticket
|
||||
@@ -76,3 +77,32 @@ def test_staff_sees_chat_guidance_after_two_rapid_replies(live_server, browser_p
|
||||
conversation.send_reply("پاسخ سوم کارشناس")
|
||||
conversation.expect_guidance_visible()
|
||||
conversation.dismiss_guidance()
|
||||
|
||||
|
||||
def test_customer_mobile_composer_never_covers_latest_message(live_server, browser_page):
|
||||
category = TicketCategory.objects.create(title="مکاتبات موبایل", slug="e2e-customer-mobile-composer")
|
||||
customer = User.objects.create_user(
|
||||
username="e2e-mobile-customer",
|
||||
mobile="09120009021",
|
||||
password=PASSWORD,
|
||||
account_type=User.AccountType.CUSTOMER,
|
||||
)
|
||||
CustomerProfile.objects.create(user=customer, first_name="مشتری", last_name="موبایل")
|
||||
staff = User.objects.create_user(
|
||||
username="e2e-mobile-staff",
|
||||
mobile="09120009022",
|
||||
account_type=User.AccountType.STAFF,
|
||||
)
|
||||
StaffProfile.objects.create(user=staff, display_name="کارشناس موبایل")
|
||||
ticket = create_ticket(customer=customer, category=category, subject="کنترل composer مشتری", body="پیام آغازین")
|
||||
for index in range(24):
|
||||
Message.objects.create(ticket=ticket, author=staff, body=f"پاسخ طولانی کارشناس شماره {index + 1}")
|
||||
|
||||
with browser_page() as page:
|
||||
page.set_viewport_size({"width": 390, "height": 844})
|
||||
AuthenticationPage(page, live_server.url).login_customer(customer, PASSWORD)
|
||||
conversation = TicketConversationPage(page, live_server.url)
|
||||
conversation.goto(reverse("tickets:detail", args=[ticket.public_id]))
|
||||
conversation.expect_latest_message_above_composer()
|
||||
conversation.open_composer()
|
||||
conversation.expect_latest_message_above_composer()
|
||||
@@ -9,11 +9,13 @@ from playwright.sync_api import expect
|
||||
from apps.accounts.models import TicketMagicLink, User
|
||||
from apps.customers.models import ContactGroup, CustomerProfile
|
||||
from apps.core.models import PageTitle, SiteBranding
|
||||
from apps.messaging.models import Message
|
||||
from apps.notifications.models import NotificationJob, SMSTemplate
|
||||
from apps.staff.models import OrganizationalUnit, StaffProfile
|
||||
from apps.tickets.models import TicketCategory
|
||||
from apps.tickets.services import create_ticket, dispatch_group_tickets
|
||||
from tests.e2e.pages.auth import AuthenticationPage
|
||||
from tests.e2e.pages.tickets import TicketConversationPage
|
||||
|
||||
|
||||
pytestmark = [pytest.mark.e2e, pytest.mark.django_db(transaction=True)]
|
||||
@@ -153,6 +155,8 @@ def test_dispatch_report_presents_delivery_and_engagement_professionally(live_se
|
||||
delivery_description="0098SMS [کد 5]: پیامک به گوشی گیرنده رسیده است.",
|
||||
delivery_check_attempts=2,delivery_checked_at=timezone.now(),delivery_next_check_at=None,
|
||||
)
|
||||
for index in range(24):
|
||||
Message.objects.create(ticket=ticket,author=staff,body=f"مکاتبه طولانی شماره {index + 1}")
|
||||
links=[TicketMagicLink.issue_with_record(ticket,created_by=staff)[1] for _ in range(4)]
|
||||
links[0].view_count=2; links[0].used_at=timezone.now(); links[0].save(update_fields=["view_count","used_at"])
|
||||
|
||||
@@ -190,6 +194,8 @@ def test_dispatch_report_presents_delivery_and_engagement_professionally(live_se
|
||||
assert box["x"] >= 0 and box["x"] + box["width"] <= 390
|
||||
assert box["y"] >= 0 and box["y"] + box["height"] <= 844
|
||||
assert page.evaluate("document.documentElement.scrollWidth <= document.documentElement.clientWidth + 1")
|
||||
conversation=TicketConversationPage(page,live_server.url)
|
||||
conversation.expect_latest_message_above_composer()
|
||||
launch.click()
|
||||
close_composer = page.get_by_role("button", name="بستن", exact=True)
|
||||
expect(close_composer).to_be_visible()
|
||||
@@ -197,5 +203,6 @@ def test_dispatch_report_presents_delivery_and_engagement_professionally(live_se
|
||||
assert close_box is not None
|
||||
assert close_box["x"] >= 0 and close_box["x"] + close_box["width"] <= 390
|
||||
assert close_box["y"] >= 0 and close_box["y"] + close_box["height"] <= 844
|
||||
conversation.expect_latest_message_above_composer()
|
||||
dismiss_context.click()
|
||||
expect(page.locator(".ticket-dispatch-context")).to_have_count(0)
|
||||
+31
-1
@@ -8,6 +8,7 @@ from django.utils import timezone
|
||||
from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from apps.attachments.models import Attachment, AttachmentPolicy
|
||||
from apps.accounts.models import User, normalize_mobile
|
||||
from apps.messaging.models import MessageReadReceipt
|
||||
from apps.notifications.models import NotificationJob
|
||||
from apps.staff.models import OrganizationalUnit, StaffProfile
|
||||
from apps.tickets.models import Ticket, TicketCategory
|
||||
@@ -499,7 +500,11 @@ def test_due_unread_staff_reply_is_sent_once(category,customer,monkeypatch):
|
||||
from apps.integrations.sms.base import SMSResult
|
||||
from apps.notifications.management.commands import process_notifications
|
||||
a=make_staff("a"); ticket=create_ticket(customer=customer,category=category,subject="پاسخ خوانده نشده",selected_expert=a,body="متن")
|
||||
reply_ticket(ticket_id=ticket.pk,user=a,body="پاسخ کارشناس")
|
||||
message=reply_ticket(ticket_id=ticket.pk,user=a,body="پاسخ کارشناس")
|
||||
observer=make_staff("observer")
|
||||
other_customer=User.objects.create_user(username="other-sms-customer",mobile="09123334444",account_type="CUSTOMER")
|
||||
MessageReadReceipt.objects.create(message=message,user=observer)
|
||||
MessageReadReceipt.objects.create(message=message,user=other_customer)
|
||||
job=NotificationJob.objects.get(ticket=ticket,event_type="STAFF_REPLY")
|
||||
NotificationJob.objects.filter(pk=job.pk).update(next_attempt_at=timezone.now()-timedelta(seconds=1))
|
||||
ticket.last_activity_at=timezone.now()-timedelta(hours=1); ticket.save(update_fields=["last_activity_at"])
|
||||
@@ -514,6 +519,31 @@ def test_due_unread_staff_reply_is_sent_once(category,customer,monkeypatch):
|
||||
assert len(sent)==1
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_due_staff_reply_is_cancelled_only_when_ticket_customer_has_seen_it(category,customer,monkeypatch):
|
||||
from django.core.management import call_command
|
||||
from apps.notifications.management.commands import process_notifications
|
||||
|
||||
staff=make_staff("a")
|
||||
ticket=create_ticket(customer=customer,category=category,subject="پاسخ دیده شده",selected_expert=staff,body="متن")
|
||||
message=reply_ticket(ticket_id=ticket.pk,user=staff,body="پاسخ کارشناس")
|
||||
MessageReadReceipt.objects.create(message=message,user=customer)
|
||||
job=NotificationJob.objects.get(ticket=ticket,event_type="STAFF_REPLY")
|
||||
NotificationJob.objects.filter(pk=job.pk).update(next_attempt_at=timezone.now()-timedelta(seconds=1))
|
||||
ticket.last_activity_at=timezone.now()-timedelta(hours=1)
|
||||
ticket.save(update_fields=["last_activity_at"])
|
||||
|
||||
class Provider:
|
||||
def send_message(self,mobile,message):
|
||||
raise AssertionError("پیام دیدهشده توسط خود مشتری نباید به درگاه ارسال شود")
|
||||
|
||||
monkeypatch.setattr(process_notifications,"get_sms_provider",lambda:Provider())
|
||||
call_command("process_notifications",once=True,job_id=job.pk)
|
||||
job.refresh_from_db()
|
||||
assert job.status==NotificationJob.Status.CANCELLED
|
||||
assert job.last_error=="customer_read"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_every_failed_sms_gets_exactly_three_retries_three_minutes_apart(category,customer,monkeypatch,settings):
|
||||
from django.core.management import call_command
|
||||
|
||||
+41
-1
@@ -252,6 +252,42 @@ def test_staff_reply_feedback_and_new_reply_action_remain_available(client):
|
||||
assert 'composer staff-composer is-collapsed' in content
|
||||
assert 'accept="image/*"' in content and "data-gallery-input" in content and "data-document-input" in content
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_staff_ticket_sms_status_is_compact_expandable_and_time_precise(client):
|
||||
from apps.core.templatetags.jalali import jalali_datetime
|
||||
from apps.notifications.models import NotificationJob
|
||||
|
||||
category=TicketCategory.objects.create(title="رهگیری پیامک",slug="sms-timeline")
|
||||
customer=User.objects.create_user(username="sms-timeline-customer",mobile="09120000031",account_type="CUSTOMER")
|
||||
expert=User.objects.create_user(username="sms-timeline-expert",mobile="09120000032",account_type="STAFF")
|
||||
StaffProfile.objects.create(user=expert,display_name="کارشناس رهگیری")
|
||||
expert.user_permissions.add(Permission.objects.get(content_type__app_label="tickets",codename="view_ticket"))
|
||||
ticket=create_ticket(customer=customer,category=category,subject="نمایش دقیق وضعیت پیامک",selected_expert=expert,body="شرح")
|
||||
job=NotificationJob.objects.get(ticket=ticket,event_type="CUSTOMER_TICKET_CREATED")
|
||||
sent_at=timezone.now()-timedelta(minutes=4)
|
||||
delivered_at=timezone.now()-timedelta(minutes=1)
|
||||
NotificationJob.objects.filter(pk=job.pk).update(
|
||||
status=NotificationJob.Status.SENT,
|
||||
sent_at=sent_at,
|
||||
provider_message_id="123456789",
|
||||
delivery_status=NotificationJob.DeliveryStatus.DELIVERED,
|
||||
delivery_status_code="5",
|
||||
delivery_description="پیامک به گوشی گیرنده رسیده است.",
|
||||
delivery_checked_at=delivered_at,
|
||||
delivery_next_check_at=None,
|
||||
)
|
||||
|
||||
client.force_login(expert)
|
||||
content=client.get(reverse("staff_tickets:detail",args=[ticket.public_id])).content.decode()
|
||||
assert 'class="sms-delivery-item"' in content
|
||||
assert 'aria-label="نمایش جزئیات زمانبندی پیامک"' in content
|
||||
for label in ("ثبت درخواست ارسال","پذیرش توسط درگاه پیامک","تأیید رسیدن به گوشی"):
|
||||
assert label in content
|
||||
assert jalali_datetime(sent_at) in content
|
||||
assert jalali_datetime(delivered_at) in content
|
||||
assert "123456789" in content
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_staff_sidebar_hides_admin_link_and_shows_developer_credit(client):
|
||||
expert=User.objects.create_superuser(username="sidebar-admin",mobile="09384999655",password="test-pass",account_type="STAFF")
|
||||
@@ -392,7 +428,7 @@ def test_ticket_polling_is_versioned_incremental_and_permission_safe(client):
|
||||
|
||||
def test_ticket_polling_javascript_keeps_background_sync_and_recovers_reliably():
|
||||
script=Path("static/js/conversation.js").read_text(encoding="utf-8")
|
||||
for feature in ("visibilitychange","document.visibilityState==='visible'","AbortController","pollRunning","nextPollAt","pollHeartbeat","setInterval(startPolling,1000)","cache:'no-store'","const restartPolling=()=>{stopPolling();startPolling()}","window.addEventListener('pageshow',restartPolling)","window.addEventListener('online',restartPolling)","data-scroll-to-submit","scrollIntoView","submit-attention"):
|
||||
for feature in ("visibilitychange","document.visibilityState==='visible'","AbortController","pollRunning","nextPollAt","pollHeartbeat","setInterval(startPolling,1000)","cache:'no-store'","const restartPolling=()=>{stopPolling();startPolling()}","window.addEventListener('pageshow',restartPolling)","window.addEventListener('online',restartPolling)","data-scroll-to-submit","scrollIntoView","submit-attention","keepLatestVisible","requestAnimationFrame"):
|
||||
assert feature in script
|
||||
assert "pollUrl&&navigator.onLine" not in script
|
||||
assert "window.addEventListener('offline',stopPolling)" not in script
|
||||
@@ -403,6 +439,10 @@ def test_ticket_polling_javascript_keeps_background_sync_and_recovers_reliably()
|
||||
assert "scroller.scrollTo({top,behavior:'smooth'})" in script
|
||||
assert "data-message-id" in Path("templates/tickets/_messages.html").read_text(encoding="utf-8")
|
||||
assert "acknowledgeVisibleMessages" in script and "document.visibilityState!=='visible'" in script
|
||||
source_css=Path("static/src/tailwind.css").read_text(encoding="utf-8")
|
||||
assert ".staff-ticket-workspace .composer.is-collapsed{position:fixed" not in source_css
|
||||
assert ".staff-ticket-workspace .composer:not(.is-collapsed){position:fixed" not in source_css
|
||||
assert "max-height:min(45dvh,28rem)" in source_css
|
||||
for menu_feature in ("data-message-menu","data-message-menu-close","closeMessageMenu","event.key==='Escape'","!menu.contains(event.target)"):
|
||||
assert menu_feature in script or menu_feature in Path("templates/tickets/_messages.html").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
Reference in new issue
Block a user