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.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)] PASSWORD = "E2e-safe-password-2026" def test_staff_operations_pages_are_authorized_and_visually_styled(live_server, browser_page): branding = SiteBranding.load() branding.staff_brand_title = "مرکز عملیات E2E" branding.staff_brand_subtitle = "ارتباطات یکپارچه E2E" branding.login_intro_eyebrow = "معرفی ورود E2E" branding.developer_credit_text = "توسعه E2E | 09120000000" branding.staff_sidebar_credit_text = "امضای مستقل sidebar E2E" branding.save() PageTitle.objects.update_or_create(route_name="staff_tickets:dispatch_create", defaults={"title": "ردیابی E2E"}) SMSTemplate.objects.update_or_create(unit=None, event_type=SMSTemplate.Event.STAFF_REPLY, defaults={"title": "الگوی E2E", "body": "{ticket_number}: {magic_link}"}) unit = OrganizationalUnit.objects.create(name="عملیات E2E", slug="operations-e2e") permissions = { "customers": {"view_customerprofile", "add_customerprofile", "change_customerprofile", "view_contactgroup", "add_contactgroup", "change_contactgroup"}, "tickets": {"view_ticket", "add_ticket", "dispatch_group_tickets"}, "notifications": {"view_smstemplate", "add_smstemplate", "change_smstemplate"}, "accounts": {"view_ticketmagiclink"}, } query = None from django.db.models import Q for app_label, codenames in permissions.items(): part = Q(content_type__app_label=app_label, codename__in=codenames) query = part if query is None else query | part unit.permissions.add(*Permission.objects.filter(query)) staff = User.objects.create_user(username="e2e-operations", password=PASSWORD, account_type=User.AccountType.STAFF) StaffProfile.objects.create(user=staff, display_name="کارشناس عملیات", primary_unit=unit) customer = User.objects.create_user(username="e2e-carrier", mobile="09120009121", account_type=User.AccountType.CUSTOMER) profile = CustomerProfile.objects.create(user=customer, first_name="باربری", stakeholder_type=CustomerProfile.StakeholderType.CARRIER) group = ContactGroup.objects.create(unit=unit, name="باربری‌ها") secondary_group = ContactGroup.objects.create(unit=unit, name="مخاطبان منتخب") profile.groups.add(group) category = TicketCategory.objects.create(title="استعلام E2E", slug="operations-e2e") ticket = create_ticket(customer=customer, created_by=staff, unit=unit, category=category, subject="استعلام آزمایشی", body="قیمت را اعلام کنید") TicketMagicLink.issue_with_record(ticket, created_by=staff) with browser_page() as page: page.goto(f"{live_server.url}{reverse('accounts:login')}", wait_until="domcontentloaded") expect(page.locator(".auth-brand strong")).to_have_count(0) expect(page.locator(".auth-intro .eyebrow")).to_have_text("معرفی ورود E2E") expect(page.locator(".auth-footer")).to_contain_text("توسعه E2E") AuthenticationPage(page, live_server.url).login_staff(staff, PASSWORD) for route, heading in ( ("staff_tickets:contacts", "مخاطبان و مشتریان"), ("staff_tickets:groups", "گروه‌های مخاطبان"), ("staff_tickets:contact_create", "ثبت مخاطب بیرونی"), ("staff_tickets:sms_templates", "الگوهای پیامک واحدها"), ("staff_tickets:dispatch_create", "ارسال گروهی ایزوله"), ("staff_tickets:magic_audit", "ممیزی لینک‌های جادویی"), ): page.goto(f"{live_server.url}{reverse(route)}", wait_until="domcontentloaded") expect(page.get_by_role("heading", name=heading, level=1)).to_be_visible() expect(page.locator('link[href*="operations.css"]')).to_have_count(1) assert page.evaluate("getComputedStyle(document.querySelector('.operations-page')).display") == "grid" assert page.evaluate("document.documentElement.scrollWidth <= document.documentElement.clientWidth + 1") page.goto(f"{live_server.url}{reverse('staff_tickets:dispatch_create')}", wait_until="domcontentloaded") expect(page).to_have_title("ردیابی E2E") expect(page.locator(".staff-brand-copy b")).to_have_text("مرکز عملیات E2E") 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() page.goto(f"{live_server.url}{reverse('staff_tickets:contact_create')}", wait_until="domcontentloaded") province = page.locator('select[name="province"]') city = page.locator('select[name="city"]') expect(province.locator("option")).not_to_have_count(1) province.select_option(label="اصفهان") expect(city.locator("option", has_text="کاشان")).to_have_count(1) page.goto(f"{live_server.url}{reverse('staff_tickets:contact_edit', args=[customer.public_id])}", wait_until="domcontentloaded") first_group = page.locator(f'input[name="groups"][value="{group.pk}"]') second_group = page.locator(f'input[name="groups"][value="{secondary_group.pk}"]') expect(first_group).to_be_checked() expect(second_group).not_to_be_checked() second_group.check() first_group.uncheck() expect(page.locator("[data-group-selection-count]")).to_contain_text("۱") page.get_by_role("button", name="ذخیره مخاطب").click() page.wait_for_url(f"**{reverse('staff_tickets:contacts')}") page.goto(f"{live_server.url}{reverse('staff_tickets:contact_edit', args=[customer.public_id])}", wait_until="domcontentloaded") expect(page.locator(f'input[name="groups"][value="{group.pk}"]')).not_to_be_checked() 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") 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}, ]) expect(page.locator("[data-file-preview] .file-chip")).to_have_count(2) expect(page.locator("[data-file-preview] .file-chip img")).to_have_count(2) 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, ) 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"]) 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") page.goto(f"{live_server.url}{reverse('staff_tickets:detail',args=[ticket.public_id])}", wait_until="domcontentloaded") expect(page.locator(".ticket-dispatch-context")).to_contain_text("استعلام حرفه‌ای") dismiss_context = page.get_by_role("button", name="بستن پیام ارسال گروهی") expect(dismiss_context).to_be_visible() launch = page.locator(".composer-launch") expect(launch).to_be_visible() box = launch.bounding_box() assert box is not None 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() close_box = close_composer.bounding_box() 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)