407 lines
19 KiB
Python
407 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)
|
|
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()
|
|
assert page.evaluate("document.activeElement === document.querySelector('[data-ticket-subject]')")
|
|
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()
|
|
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 = {
|
|
"زهرا جمالی": 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"],
|
|
)
|