Files
customerTicket/tests/e2e/test_staff_operations_ui.py
T

261 lines
18 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import base64
from unittest.mock import Mock, patch
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, VoipConfiguration
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.dispatches import DispatchWorkspacePage
from tests.e2e.pages.tickets import TicketConversationPage
pytestmark = [pytest.mark.e2e, pytest.mark.django_db(transaction=True)]
PASSWORD = "E2e-safe-password-2026"
@pytest.fixture
def immediate_announcement_case():
unit = OrganizationalUnit.objects.create(name="واحد تماس فوری", slug="immediate-call-e2e")
category = TicketCategory.objects.create(unit=unit, title="تماس فوری", slug="immediate-call-e2e")
staff = User.objects.create_superuser(username="immediate-call-e2e", password=PASSWORD, account_type=User.AccountType.STAFF)
StaffProfile.objects.create(user=staff, display_name="کارشناس تماس فوری", primary_unit=unit)
customer = User.objects.create_user(username="immediate-call-customer-e2e", mobile="09120009122", account_type=User.AccountType.CUSTOMER)
ticket = create_ticket(customer=customer, created_by=staff, category=category, subject="پیگیری تلفنی فوری", body="پیام نخست")
VoipConfiguration.objects.create(announcement_url="http://127.0.0.1:9000/v1/announcements", announcement_id="custom/test")
return staff, ticket
def test_staff_can_request_immediate_announcement_from_ticket_sidebar(live_server, browser_page, immediate_announcement_case):
staff, ticket = immediate_announcement_case
with browser_page() as page:
AuthenticationPage(page, live_server.url).login_staff(staff, PASSWORD)
conversation = TicketConversationPage(page, live_server.url)
conversation.goto(reverse("staff_tickets:detail", args=[ticket.public_id]))
with patch("apps.integrations.announcements.requests.post", return_value=Mock(content=b"")) as post:
conversation.request_immediate_announcement()
post.assert_called_once()
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,
voip_extension="300",
voip_mobile="09384999655",
)
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:contacts')}", wait_until="domcontentloaded")
call_trigger = page.locator("[data-voip-call-trigger]").first
call_trigger.click()
call_menu = page.locator("body > .voip-call-menu:not([hidden])")
expect(call_menu).to_be_visible()
expect(call_menu.get_by_role("menuitem")).to_have_count(2)
assert call_menu.evaluate("menu => menu.parentElement === document.body")
assert int(call_menu.evaluate("menu => getComputedStyle(menu).zIndex")) > 100000
menu_box = call_menu.bounding_box()
assert menu_box is not None
assert menu_box["x"] >= 0 and menu_box["y"] >= 0
assert menu_box["x"] + menu_box["width"] <= page.viewport_size["width"]
assert menu_box["y"] + menu_box["height"] <= page.viewport_size["height"]
page.keyboard.press("Escape")
expect(call_menu).to_be_hidden()
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")
workspace = DispatchWorkspacePage(page, live_server.url)
workspace.open_history()
expect(page.get_by_role("searchbox", name="جستجو")).to_be_visible()
workspace.return_to_composer()
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_hidden()
expect(page.locator('form[data-ticket-composer] textarea')).to_be_focused()
page.set_viewport_size({"width":390,"height":430})
expect(page.locator("[data-composer-shell]")).to_have_attribute("data-keyboard-open","true")
page.set_viewport_size({"width":390,"height":844})
expect(page.locator("[data-composer-shell].is-collapsed")).to_be_visible()
conversation.expect_latest_message_above_composer()
dismiss_context.click()
expect(page.locator(".ticket-dispatch-context")).to_have_count(0)