Files
customerTicket/tests/e2e/test_public_ticket_workflows_e2e.py
T
2026-09-16 14:16:05 +03:30

415 lines
19 KiB
Python

import io
import re
import pytest
from django.core.management import call_command
from playwright.sync_api import expect
from apps.accounts.models import User
from apps.customers.models import CustomerProfile
from apps.staff.models import OrganizationalUnit, StaffProfile
from apps.tickets.models import Ticket, TicketCategory
from tests.e2e.pages.auth import AuthenticationPage
from tests.e2e.pages.tickets import PublicTicketCreatePage
pytestmark = [pytest.mark.e2e, pytest.mark.django_db(transaction=True)]
PASSWORD = "E2e-public-workflow-2026"
def image_file(name, image_format, color):
pillow = pytest.importorskip("PIL.Image")
stream = io.BytesIO()
pillow.new("RGB", (8, 8), color).save(stream, format=image_format)
return {
"name": name,
"mimeType": "image/jpeg" if image_format == "JPEG" else "image/png",
"buffer": stream.getvalue(),
}
def pdf_file(name, marker="one"):
return {
"name": name,
"mimeType": "application/pdf",
"buffer": (
"%PDF-1.4\n1 0 obj<</Type/Catalog/Pages 2 0 R>>endobj\n"
"2 0 obj<</Type/Pages/Count 0/Kids[]>>endobj\n"
f"% {marker}\n%%EOF\n"
).encode(),
}
@pytest.fixture
def public_workflow(settings, tmp_path):
settings.MEDIA_ROOT = tmp_path
call_command("bootstrap")
customer = User.objects.create_user(
username="e2e-public-workflow",
mobile="09120008301",
password=PASSWORD,
account_type=User.AccountType.CUSTOMER,
)
CustomerProfile.objects.create(user=customer, first_name="مشتری", last_name="مرورگر")
units = {
slug: OrganizationalUnit.objects.get(slug=slug)
for slug in ("sales", "warehouse-loading", "finance")
}
categories = {
"sales_general": TicketCategory.objects.get(slug="sales-general"),
"sales_secondary": TicketCategory.objects.create(
unit=units["sales"],
title="شرایط فروش E2E",
slug="sales-secondary-e2e",
is_active=True,
show_in_customer_form=True,
),
"warehouse": TicketCategory.objects.get(slug="warehouse-loading-general"),
"finance": TicketCategory.objects.get(slug="finance-general"),
}
return customer, units, categories
def open_form(page, live_server, customer):
AuthenticationPage(page, live_server.url).login_customer(customer, PASSWORD)
form = PublicTicketCreatePage(page, live_server.url)
form.open()
return form
def assert_ticket_record(subject, *, unit, category, expert_name, attachment_names=()):
ticket = Ticket.objects.select_related("selected_expert__staff_profile").get(subject=subject)
assert ticket.owning_unit == unit
assert ticket.assigned_unit == unit
assert ticket.category == category
actual_expert = ticket.selected_expert.staff_profile.display_name if ticket.selected_expert_id else "صف عمومی"
assert actual_expert == expert_name
attachments = ticket.ticket_messages.get().attachments.order_by("original_name")
assert set(attachments.values_list("original_name", flat=True)) == set(attachment_names)
def assert_final_ticket_ui(page, *, category, expert_name, attachment_names=()):
expect(page.locator("#ticket-info")).to_contain_text(category.title)
expect(page.locator("#ticket-info")).to_contain_text(expert_name)
for name in attachment_names:
expect(page.locator("[data-conversation]")).to_contain_text(name)
def test_e2e_07_sales_general_queue_creates_verifiable_ticket(live_server, browser_page, public_workflow):
customer, units, categories = public_workflow
subject = "ثبت واقعی در صف عمومی فروش E2E"
with browser_page() as page:
form = open_form(page, live_server, customer)
form.select_unit(units["sales"].pk)
form.select_category(categories["sales_general"].pk)
form.select_general_queue()
form.submit(subject=subject, body="شرح کامل تیکت بدون پیوست")
assert_final_ticket_ui(page, category=categories["sales_general"], expert_name="صف عمومی")
assert_ticket_record(
subject, unit=units["sales"], category=categories["sales_general"], expert_name="صف عمومی"
)
def test_e2e_08_repeated_staff_changes_store_only_last_choice(live_server, browser_page, public_workflow):
customer, units, categories = public_workflow
subject = "آخرین کارشناس انتخابی E2E"
with browser_page() as page:
form = open_form(page, live_server, customer)
form.select_unit(units["sales"].pk)
form.select_category(categories["sales_general"].pk)
for name in ("کیمیا غفاری", "زهرا جمالی", "مریم فتوت", "زهرا جمالی"):
form.select_expert(name)
form.submit(subject=subject, body="تغییر چندباره کارشناس")
assert_final_ticket_ui(page, category=categories["sales_general"], expert_name="زهرا جمالی")
assert_ticket_record(
subject, unit=units["sales"], category=categories["sales_general"], expert_name="زهرا جمالی"
)
def test_e2e_subject_stays_empty_for_manual_and_search_routing_until_user_types(
live_server, browser_page, public_workflow
):
customer, units, categories = public_workflow
subject = "موضوع کاملاً دستی E2E"
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
assert abs(unit_box["y"] - category_box["y"]) < 2
assert unit_box["x"] + unit_box["width"] <= category_box["x"] or category_box["x"] + category_box["width"] <= unit_box["x"]
expect(subject_input).to_have_value("")
expect(subject_input).to_have_attribute("required", "")
form.select_unit(units["finance"].pk)
form.select_category(categories["finance"].pk)
expect(subject_input).to_have_value("")
form.select_search_result("عمومی فروش", "عمومی فروش")
expect(form.unit).to_have_value(str(units["sales"].pk))
expect(subject_input).to_have_value("")
subject_input.fill(subject)
form.select_unit(units["finance"].pk)
expect(subject_input).to_have_value(subject)
form.select_unit(units["sales"].pk)
form.select_category(categories["sales_general"].pk)
form.select_general_queue()
form.submit(subject=subject, body="موضوع فقط توسط کاربر وارد شده است")
assert_final_ticket_ui(page, category=categories["sales_general"], expert_name="صف عمومی")
assert_ticket_record(
subject, unit=units["sales"], category=categories["sales_general"], expert_name="صف عمومی"
)
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)
wizard = page.locator('[data-ticket-wizard]')
expect(wizard).to_be_focused()
expect(wizard).to_be_in_viewport()
assert wizard.bounding_box()["y"] < page.viewport_size["height"] / 3
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.matches('[aria-invalid=\"true\"]:not(:disabled)')")
expect(page.locator(".is-guided-field")).to_have_count(1)
form.select_search_result("عمومی فروش", "عمومی فروش")
expect(page.locator('[data-ticket-subject]')).to_have_value("")
page.locator('[data-wizard-step="1"] [data-wizard-next]').click()
expect(page.locator('[data-ticket-subject]')).to_be_focused()
expect(page.locator('.ticket-subject-field')).to_have_class(re.compile(r"is-guided-field"))
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()
expect(wizard).to_be_focused()
expect(wizard).to_be_in_viewport()
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(wizard).to_be_focused()
expect(wizard).to_be_in_viewport()
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 = {
"زهرا جمالی": 10,
"مریم فتوت": 20,
"کیمیا غفاری": 30,
"مرتضی سخائی فرد": 40,
}
for name, order in configured.items():
StaffProfile.objects.filter(primary_unit=units["sales"], display_name=name).update(sort_order=order)
with browser_page() as page:
form = open_form(page, live_server, customer)
form.select_unit(units["sales"].pk)
names = form.expert_picker.locator(
"label.expert-card:not([data-general-queue]) strong"
).all_text_contents()
assert names == ["زهرا جمالی", "مریم فتوت", "کیمیا غفاری", "مرتضی سخائی فرد"]
@pytest.mark.parametrize(
"suffix,images,documents,expected",
[
("jpg", [image_file("photo.jpg", "JPEG", "red")], [], ["photo.jpg"]),
("pdf", [], [pdf_file("document.pdf")], ["document.pdf"]),
(
"mixed",
[image_file("first.jpg", "JPEG", "blue"), image_file("second.png", "PNG", "green")],
[pdf_file("combined.pdf")],
["first.jpg", "second.png", "combined.pdf"],
),
(
"multi-file",
[],
[pdf_file("first.pdf", "first"), pdf_file("second.pdf", "second")],
["first.pdf", "second.pdf"],
),
],
)
def test_e2e_09_10_11_attachment_variants_are_saved_and_rendered(
live_server, browser_page, public_workflow, suffix, images, documents, expected
):
customer, units, categories = public_workflow
subject = f"پیوست واقعی {suffix} E2E"
with browser_page() as page:
form = open_form(page, live_server, customer)
form.select_unit(units["sales"].pk)
form.select_category(categories["sales_general"].pk)
form.select_general_queue()
if images:
form.upload_gallery(images)
if documents:
form.upload_documents(documents, total_count=len(images) + len(documents))
form.submit(subject=subject, body="بررسی ذخیره و نمایش تمام پیوست‌ها")
assert_final_ticket_ui(
page, category=categories["sales_general"], expert_name="صف عمومی", attachment_names=expected
)
assert_ticket_record(
subject,
unit=units["sales"],
category=categories["sales_general"],
expert_name="صف عمومی",
attachment_names=expected,
)
def test_e2e_11_removed_attachment_is_not_submitted(live_server, browser_page, public_workflow):
customer, units, categories = public_workflow
subject = "حذف پیوست پیش از ثبت E2E"
keep = image_file("keep.png", "PNG", "yellow")
remove = pdf_file("remove.pdf")
with browser_page() as page:
form = open_form(page, live_server, customer)
form.select_unit(units["sales"].pk)
form.select_category(categories["sales_general"].pk)
form.upload_gallery([keep])
form.upload_documents([remove], total_count=2)
form.remove_attachment("remove.pdf")
form.submit(subject=subject, body="فقط تصویر باید ذخیره شود")
assert_final_ticket_ui(
page, category=categories["sales_general"], expert_name="صف عمومی", attachment_names=["keep.png"]
)
assert_ticket_record(
subject,
unit=units["sales"],
category=categories["sales_general"],
expert_name="صف عمومی",
attachment_names=["keep.png"],
)
def test_e2e_12_sequential_unit_staff_category_and_attachment_state(live_server, browser_page, public_workflow):
customer, units, categories = public_workflow
subject = "مدیریت وضعیت متوالی فرم E2E"
image_one = image_file("state-one.jpg", "JPEG", "purple")
image_two = image_file("state-two.png", "PNG", "orange")
removed_pdf = pdf_file("state-remove.pdf")
with browser_page() as page:
form = open_form(page, live_server, customer)
form.select_unit(units["sales"].pk)
form.select_expert("کیمیا غفاری")
form.select_category(categories["sales_secondary"].pk)
form.upload_gallery([image_one])
form.upload_documents([removed_pdf], total_count=2)
form.select_unit(units["finance"].pk)
expect(form.expert_picker).not_to_contain_text("کیمیا غفاری")
expect(form.queue).to_contain_text("صف عمومی مالی / حسابداری")
form.select_category(categories["finance"].pk)
form.select_unit(units["sales"].pk)
form.select_category(categories["sales_general"].pk)
form.select_expert("کیمیا غفاری")
form.select_expert("زهرا جمالی")
form.select_expert("کیمیا غفاری")
form.remove_attachment("state-remove.pdf")
form.upload_gallery([image_two], total_count=2)
expect(form.file_preview.locator(".file-chip")).to_have_count(2)
form.submit(subject=subject, body="فرم پس از تغییرات متوالی سالم ماند")
assert_final_ticket_ui(
page,
category=categories["sales_general"],
expert_name="کیمیا غفاری",
attachment_names=["state-one.jpg", "state-two.png"],
)
assert_ticket_record(
subject,
unit=units["sales"],
category=categories["sales_general"],
expert_name="کیمیا غفاری",
attachment_names=["state-one.jpg", "state-two.png"],
)