import base64 import pytest from django.contrib.auth.models import Permission from django.urls import reverse 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.staff.models import OrganizationalUnit, StaffProfile from apps.tickets.models import TicketCategory from apps.tickets.services import create_ticket from tests.e2e.pages.auth import AuthenticationPage 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) 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") 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}, ]) 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