fix sms and ticket user frindly

This commit is contained in:
javad committed 2026-09-14 15:48:13 +03:30
1 parent 1ee83374fd
commit b8b3aba855
30 files changed
+2220 -119

No files matched your search

+27 -7
View File
@@ -37,7 +37,8 @@ class TicketConversationPage(BasePage):
class PublicTicketCreatePage(BasePage):
def open(self):
self.goto("/tickets/new/")
expect(self.unit).to_be_visible()
expect(self.page.locator('[data-wizard-step="1"]')).to_be_visible()
expect(self.page.locator('[data-ticket-subject]')).to_be_visible()
@property
def unit(self):
@@ -66,7 +67,10 @@ class PublicTicketCreatePage(BasePage):
and f"unit={unit_id}" in response.url
and response.status == 200
) as response_info:
self.unit.select_option(unit_id)
self.unit.evaluate(
"(node, value) => { node.value = value; node.dispatchEvent(new Event('change', {bubbles: true})); }",
unit_id,
)
payload = response_info.value.json()
expect(self.page.locator("[data-general-queue]")).to_have_attribute("data-unit-id", unit_id)
expected_ids = {str(item["id"]) for item in payload["experts"]}
@@ -84,17 +88,24 @@ class PublicTicketCreatePage(BasePage):
card = self.expert_picker.locator("label.expert-card").filter(has_text=display_name)
expect(card).to_have_count(1)
radio = card.locator('input[name="selected_expert"]')
radio.check()
radio.evaluate(
"node => { node.checked = true; node.dispatchEvent(new Event('change', {bubbles: true})); }"
)
expect(radio).to_be_checked()
return radio.get_attribute("value")
def select_general_queue(self):
radio = self.queue.locator('input[name="selected_expert"]')
radio.check()
radio.evaluate(
"node => { node.checked = true; node.dispatchEvent(new Event('change', {bubbles: true})); }"
)
expect(radio).to_be_checked()
def select_category(self, category_id):
self.category.select_option(str(category_id))
self.category.evaluate(
"(node, value) => { node.value = value; node.dispatchEvent(new Event('change', {bubbles: true})); }",
str(category_id),
)
expect(self.category).to_have_value(str(category_id))
def upload_gallery(self, files, *, total_count=None):
@@ -108,13 +119,22 @@ class PublicTicketCreatePage(BasePage):
def remove_attachment(self, filename):
chip = self.file_preview.locator(".file-chip").filter(has_text=filename)
expect(chip).to_have_count(1)
chip.get_by_role("button", name="حذف فایل").click()
chip.locator("button").evaluate("node => node.click()")
expect(self.file_preview.locator(".file-chip").filter(has_text=filename)).to_have_count(0)
def submit(self, *, subject, body):
self.page.locator('input[name="subject"]').fill(subject)
self.page.locator('[data-wizard-step="1"] [data-wizard-next]').click()
expect(self.page.locator('[data-wizard-step="2"]')).to_be_visible()
self.page.locator('[data-wizard-step="2"] [data-wizard-next]').click()
expect(self.page.locator('[data-wizard-step="3"]')).to_be_visible()
self.page.locator('[data-wizard-step="3"] [data-wizard-next]').click()
expect(self.page.locator('[data-wizard-step="4"]')).to_be_visible()
self.page.locator('textarea[name="body"]').fill(body)
self.page.locator('[data-wizard-step="4"] [data-wizard-next]').click()
expect(self.page.locator('[data-wizard-step="5"]')).to_be_visible()
expect(self.page.locator('[data-review-subject]')).to_have_text(subject)
with self.page.expect_navigation(wait_until="domcontentloaded"):
self.page.get_by_role("button", name="ثبت و ارسال درخواست").click()
self.page.get_by_role("button", name="ثبت و ارسال نهایی").click()
expect(self.page).to_have_url(re.compile(r"/tickets/(?!new/)[^/]+/$"))
expect(self.page.locator("h1")).to_have_text(subject)
@@ -1,4 +1,5 @@
import io
import re
import pytest
from django.core.management import call_command
@@ -133,6 +134,8 @@ def test_e2e_subject_stays_empty_for_manual_and_search_routing_until_user_types(
with browser_page() as page:
form = open_form(page, live_server, customer)
subject_input = page.locator("[data-ticket-subject]")
page.locator(".ticket-manual-routing summary").click()
expect(form.unit).to_be_visible()
unit_box = form.unit.bounding_box()
category_box = form.category.bounding_box()
assert unit_box and category_box
@@ -159,6 +162,115 @@ def test_e2e_subject_stays_empty_for_manual_and_search_routing_until_user_types(
)
def test_e2e_customer_wizard_validation_preview_edit_and_floating_submit(
live_server, browser_page, public_workflow
):
customer, units, categories = public_workflow
subject = "بازبینی مرحله‌ای و ویرایش E2E"
with browser_page() as page:
page.set_viewport_size({"width": 390, "height": 640})
form = open_form(page, live_server, customer)
expect(page.locator('[data-wizard-step="1"]')).to_be_visible()
expect(page.locator('[data-wizard-step="2"]')).to_be_hidden()
expect(page.locator('[data-ticket-search-results]')).to_be_hidden()
page.locator('[data-wizard-step="1"] [data-wizard-next]').click()
error = page.locator('[data-wizard-error="1"]')
expect(error).to_be_visible()
expect(error).to_be_in_viewport()
expect(page.locator('[data-ticket-subject]')).to_have_attribute("aria-invalid", "true")
expect(form.category).to_be_visible()
assert page.evaluate("document.activeElement === document.querySelector('[data-wizard-error=\"1\"]')")
form.select_search_result("عمومی فروش", "عمومی فروش")
expect(page.locator('[data-ticket-subject]')).to_have_value("")
page.locator('[data-ticket-subject]').fill(subject)
expect(page.locator('[data-ticket-subject]')).to_have_value(subject)
page.locator('[data-wizard-step="1"] [data-wizard-next]').click()
page.locator('[data-wizard-step="2"] [data-wizard-next]').click()
page.locator('[data-wizard-step="3"] [data-wizard-next]').click()
page.locator('textarea[name="body"]').fill("شرح خوانا برای پیش‌نمایش مرحله پنجم")
page.locator('[data-wizard-step="4"] [data-wizard-next]').click()
expect(page.locator('[data-wizard-step="5"]')).to_be_visible()
expect(page.locator('[data-review-subject]')).to_have_text(subject)
expect(page.locator('[data-review-body]')).to_contain_text("شرح خوانا")
final_submit = page.get_by_role("button", name="ثبت و ارسال نهایی")
expect(final_submit).to_be_visible()
submit_box = final_submit.bounding_box()
assert submit_box and submit_box["y"] + submit_box["height"] <= page.viewport_size["height"]
assert page.locator('[data-review-subject]').evaluate("node => parseFloat(getComputedStyle(node).fontSize)") >= 16
assert page.locator('[data-review-body]').evaluate("node => parseFloat(getComputedStyle(node).fontSize)") >= 14
page.locator('[data-wizard-edit="1"]').click()
expect(page.locator('[data-ticket-subject]')).to_have_value(subject)
expect(page.locator('[data-ticket-unit]')).to_have_value(str(units["sales"].pk))
expect(page.locator('[data-ticket-category]')).to_have_value(str(categories["sales_general"].pk))
def test_e2e_customer_wizard_auto_stops_voice_and_submits_audio_only_ticket(
live_server, browser_page, public_workflow
):
customer, units, categories = public_workflow
subject = "ثبت تیکت فقط با پیام صوتی E2E"
with browser_page() as page:
page.add_init_script(
"""
window.__microphoneTrackStops = 0;
Object.defineProperty(navigator, 'mediaDevices', {
configurable: true,
value: {getUserMedia: async () => ({getTracks: () => [{stop: () => {window.__microphoneTrackStops += 1;}}]})},
});
class FakeMediaRecorder {
constructor(stream, options = {}) {this.stream = stream; this.mimeType = options.mimeType || 'audio/wav'; this.state = 'inactive';}
start() {this.state = 'recording';}
pause() {this.state = 'paused';}
resume() {this.state = 'recording';}
stop() {
this.state = 'inactive';
const wav = new Uint8Array([
82,73,70,70,40,0,0,0,87,65,86,69,102,109,116,32,
16,0,0,0,1,0,1,0,68,172,0,0,136,88,1,0,
2,0,16,0,100,97,116,97,4,0,0,0,0,0,0,0,
]);
this.ondataavailable?.({data: new Blob([wav], {type: this.mimeType})});
setTimeout(() => this.onstop?.(), 0);
}
static isTypeSupported(type) {return type === 'audio/wav';}
}
Object.defineProperty(window, 'MediaRecorder', {configurable: true, value: FakeMediaRecorder});
"""
)
form = open_form(page, live_server, customer)
form.select_unit(units["sales"].pk)
form.select_category(categories["sales_general"].pk)
form.select_general_queue()
page.locator('[data-ticket-subject]').fill(subject)
page.locator('[data-wizard-step="1"] [data-wizard-next]').click()
page.locator('[data-wizard-step="2"] [data-wizard-next]').click()
page.locator('[data-wizard-step="3"] [data-wizard-next]').click()
expect(page.locator('[data-wizard-step="4"]')).to_be_visible()
page.get_by_role("button", name="شروع ضبط صدا").click()
expect(page.locator('[data-voice-live]')).to_be_visible()
page.get_by_role("button", name="مشاهده پیش‌نمایش").click()
expect(page.locator('[data-wizard-step="5"]')).to_be_visible()
expect(page.locator('[data-review-audio]')).to_be_visible()
assert page.evaluate("window.__microphoneTrackStops") == 1
with page.expect_navigation(wait_until="domcontentloaded"):
page.get_by_role("button", name="ثبت و ارسال نهایی").click()
expect(page).to_have_url(re.compile(r"/tickets/(?!new/)[^/]+/$"))
ticket = Ticket.objects.get(subject=subject)
assert ticket.owning_unit == units["sales"]
assert ticket.category == categories["sales_general"]
attachments = list(ticket.ticket_messages.get().attachments.all())
assert len(attachments) == 1
assert attachments[0].mime_type.startswith("audio/")
assert attachments[0].original_name.startswith("voice-")
def test_e2e_experts_follow_configured_numeric_order(live_server, browser_page, public_workflow):
customer, units, _ = public_workflow
configured = {
+66 -3
View File
@@ -3,15 +3,16 @@ import base64
import pytest
from django.contrib.auth.models import Permission
from django.urls import reverse
from django.utils import timezone
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.notifications.models import SMSTemplate
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
from apps.tickets.services import create_ticket, dispatch_group_tickets
from tests.e2e.pages.auth import AuthenticationPage
@@ -79,6 +80,20 @@ def test_staff_operations_pages_are_authorized_and_visually_styled(live_server,
expect(page.locator(".staff-developer-credit")).to_contain_text("امضای مستقل sidebar E2E")
expect(page.get_by_role("searchbox", name="جستجو")).to_be_visible()
expect(page.locator('link[href*="dispatches.css"]')).to_have_count(1)
expect(page.get_by_text("افزودن عکس از گالری")).to_be_visible()
expect(page.locator('label[for="dispatch-file-input"]')).to_be_visible()
tiny_png = base64.b64decode("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=")
page.locator("#dispatch-gallery-input").set_input_files([
{"name":"dispatch-first.png","mimeType":"image/png","buffer":tiny_png},
{"name":"dispatch-second.png","mimeType":"image/png","buffer":tiny_png},
])
expect(page.locator(".dispatch-attachment-picker [data-file-preview] .file-chip")).to_have_count(2)
expect(page.locator(".dispatch-attachment-picker [data-file-preview] img")).to_have_count(2)
page.locator("#dispatch-file-input").set_input_files({"name":"dispatch-document.pdf","mimeType":"application/pdf","buffer":b"%PDF-1.4\n%%EOF"})
expect(page.locator(".dispatch-attachment-picker [data-file-preview] .file-chip")).to_have_count(3)
page.locator(".dispatch-attachment-picker [data-file-preview] .file-chip button").first.click()
expect(page.locator(".dispatch-attachment-picker [data-file-preview] .file-chip")).to_have_count(2)
assert page.locator('.dispatch-attachment-picker input[name="files"]').evaluate("input => input.files.length") == 2
page.goto(f"{live_server.url}{reverse('staff_tickets:sms_templates')}", wait_until="domcontentloaded")
expect(page.get_by_text("الگوی عمومی سیستم").first).to_be_visible()
@@ -105,7 +120,6 @@ def test_staff_operations_pages_are_authorized_and_visually_styled(live_server,
expect(page.locator(f'input[name="groups"][value="{secondary_group.pk}"]')).to_be_checked()
page.goto(f"{live_server.url}{reverse('staff_tickets:create')}", wait_until="domcontentloaded")
tiny_png = base64.b64decode("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=")
page.locator("#staff-ticket-gallery-input").set_input_files([
{"name": "first.png", "mimeType": "image/png", "buffer": tiny_png},
{"name": "second.png", "mimeType": "image/png", "buffer": tiny_png},
@@ -115,3 +129,52 @@ def test_staff_operations_pages_are_authorized_and_visually_styled(live_server,
page.locator("[data-file-preview] .file-chip button").first.click()
expect(page.locator("[data-file-preview] .file-chip")).to_have_count(1)
assert page.locator('input[name="files"]').evaluate("input => input.files.length") == 1
def test_dispatch_report_presents_delivery_and_engagement_professionally(live_server, browser_page):
from apps.core.templatetags.jalali import jalali_datetime
unit=OrganizationalUnit.objects.create(name="فروش گزارش E2E",slug="dispatch-report-e2e")
unit.permissions.add(*Permission.objects.filter(content_type__app_label="tickets",codename__in=("view_ticket","dispatch_group_tickets")))
staff=User.objects.create_user(username="dispatch-report-agent",password=PASSWORD,account_type=User.AccountType.STAFF)
StaffProfile.objects.create(user=staff,display_name="کارشناس گزارش",primary_unit=unit)
customer=User.objects.create_user(username="dispatch-report-customer",mobile="09120009131",account_type=User.AccountType.CUSTOMER)
profile=CustomerProfile.objects.create(user=customer,first_name="مخاطب",last_name="آزمایشی")
group=ContactGroup.objects.create(unit=unit,name="گروه گزارش حرفه‌ای")
profile.groups.add(group)
category=TicketCategory.objects.create(unit=unit,title="استعلام گزارش",slug="dispatch-report-category-e2e")
dispatch,tickets=dispatch_group_tickets(user=staff,unit=unit,contact_group=group,category=category,subject="استعلام حرفه‌ای",body="این متن اولیه کارشناس باید بالای گزارش دیده شود.")
ticket=tickets[0]
job=NotificationJob.objects.get(ticket=ticket,event_type=SMSTemplate.Event.STAFF_TICKET_CREATED)
NotificationJob.objects.filter(pk=job.pk).update(
status=NotificationJob.Status.SENT,attempts=1,sent_at=timezone.now(),
provider_message_id="1111182358",provider_status_code="1111182358",
delivery_status=NotificationJob.DeliveryStatus.DELIVERED,delivery_status_code="5",
delivery_description="0098SMS [کد 5]: پیامک به گوشی گیرنده رسیده است.",
delivery_check_attempts=2,delivery_checked_at=timezone.now(),delivery_next_check_at=None,
)
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"])
with browser_page() as page:
AuthenticationPage(page,live_server.url).login_staff(staff,PASSWORD)
page.goto(f"{live_server.url}{reverse('staff_tickets:dispatch_detail',args=[dispatch.public_id])}",wait_until="domcontentloaded")
expect(page.get_by_role("heading",name="استعلام حرفه‌ای",level=1)).to_be_visible()
expect(page.get_by_text("این متن اولیه کارشناس باید بالای گزارش دیده شود.")).to_be_visible()
expect(page.locator(".sms-state-card")).to_contain_text("تحویل به گوشی تأیید شد")
expect(page.locator(".engagement-state")).to_contain_text("2 بازدید")
expect(page.locator('[role="progressbar"]')).to_have_attribute("aria-valuenow","100")
expect(page.locator(".delivery-progress-copy")).to_contain_text("100٪")
assert float(page.locator(".sms-state-card strong").evaluate("node => parseFloat(getComputedStyle(node).fontSize)")) >= 12
assert float(page.locator(".delivery-progress-track").evaluate("node => parseFloat(getComputedStyle(node).height)")) <= 8
created_text=page.locator(".dispatch-meta-list time").inner_text()
assert created_text == jalali_datetime(dispatch.created_at)
assert "0 + 0" not in page.locator("body").inner_text()
page.get_by_text("جزئیات فنی و رهگیری").click()
expect(page.get_by_text("1111182358")).to_be_visible()
expect(page.get_by_text("موفق در اولین تلاش")).to_be_visible()
assert page.evaluate("document.documentElement.scrollWidth <= document.documentElement.clientWidth + 1")
page.set_viewport_size({"width":390,"height":844})
expect(page.locator(".dispatch-detail-table tbody tr").first).to_be_visible()
assert page.evaluate("document.documentElement.scrollWidth <= document.documentElement.clientWidth + 1")