fix ui view

This commit is contained in:
javad committed 2026-09-17 09:41:15 +03:30
1 parent 863ca89485
commit 83f4d7c728
9 files changed
+182 -17

No files matched your search

+103
View File
@@ -0,0 +1,103 @@
import pytest
from django.urls import reverse
from playwright.sync_api import expect
from apps.accounts.models import TicketMagicLink, User
from apps.core.models import SiteBranding
from apps.customers.models import CustomerProfile
from apps.messaging.models import Message
from apps.staff.models import StaffProfile
from apps.tickets.models import TicketCategory
from apps.tickets.services import create_ticket
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 assert_workspace_fits_visual_viewport(page):
metrics = page.evaluate(
"""() => {
const box = document.querySelector('.ticket-workspace').getBoundingClientRect();
const viewportHeight = window.visualViewport?.height || window.innerHeight;
return {
top: box.top,
bottom: box.bottom,
viewportHeight,
pageWidth: document.documentElement.scrollWidth,
viewportWidth: document.documentElement.clientWidth,
};
}"""
)
assert metrics["top"] >= -1
assert metrics["bottom"] <= metrics["viewportHeight"] + 1
assert metrics["pageWidth"] <= metrics["viewportWidth"] + 1
def test_magic_link_mobile_viewport_keeps_composer_messages_and_credit_visible(live_server, browser_page):
branding = SiteBranding.load()
branding.developer_credit_text = "امضای مقدس توسعه‌دهنده E2E"
branding.save()
category = TicketCategory.objects.create(title="موبایل لینک امن", slug="magic-mobile-viewport")
customer = User.objects.create_user(username="magic-mobile", mobile="09120009101", account_type=User.AccountType.CUSTOMER)
CustomerProfile.objects.create(user=customer, first_name="مشتری", last_name="لینک امن")
staff = User.objects.create_user(username="magic-mobile-staff", mobile="09120009102", account_type=User.AccountType.STAFF)
StaffProfile.objects.create(user=staff, display_name="کارشناس لینک امن")
ticket = create_ticket(customer=customer, category=category, subject="بررسی موبایل لینک امن", body="پیام آغازین")
for index in range(12):
Message.objects.create(ticket=ticket, author=staff, body=f"پاسخ کارشناس شماره {index + 1}")
token = TicketMagicLink.issue(ticket)
with browser_page() as page:
page.set_viewport_size({"width": 390, "height": 844})
page.goto(f"{live_server.url}{reverse('accounts:magic', args=[token])}", wait_until="domcontentloaded")
conversation = TicketConversationPage(page, live_server.url)
credit = page.locator(".ticket-detail-credit")
expect(credit).to_be_visible()
expect(credit).to_contain_text("امضای مقدس توسعه‌دهنده E2E")
assert_workspace_fits_visual_viewport(page)
conversation.expect_latest_message_above_composer()
conversation.open_composer()
page.set_viewport_size({"width": 390, "height": 620})
expect(page.locator('form[data-ticket-composer] textarea')).to_be_focused()
expect(credit).to_be_visible()
assert_workspace_fits_visual_viewport(page)
conversation.expect_latest_message_above_composer()
page.locator("[data-composer-close]").click()
expect(page.locator("[data-composer-shell].is-collapsed")).to_be_visible()
expect(credit).to_be_visible()
assert_workspace_fits_visual_viewport(page)
def test_staff_ticket_mobile_viewport_survives_open_close_and_rotation(live_server, browser_page):
category = TicketCategory.objects.create(title="موبایل کارشناس", slug="staff-mobile-viewport")
customer = User.objects.create_user(username="staff-mobile-customer", mobile="09120009103", account_type=User.AccountType.CUSTOMER)
CustomerProfile.objects.create(user=customer, first_name="مشتری", last_name="پنل کارشناس")
staff = User.objects.create_user(username="staff-mobile-user", mobile="09120009104", password=PASSWORD, account_type=User.AccountType.STAFF)
StaffProfile.objects.create(user=staff, display_name="کارشناس موبایل")
ticket = create_ticket(customer=customer, category=category, subject="بررسی موبایل پنل کارشناس", selected_expert=staff, body="شرح درخواست مشتری")
for index in range(12):
Message.objects.create(ticket=ticket, author=customer, body=f"پیام مشتری شماره {index + 1}")
with browser_page() as page:
page.set_viewport_size({"width": 390, "height": 844})
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]))
expect(page.get_by_role("button", name="ثبت اقدام جدید در تیکت")).to_be_visible()
assert_workspace_fits_visual_viewport(page)
conversation.expect_latest_message_above_composer()
conversation.open_composer()
page.set_viewport_size({"width": 844, "height": 390})
assert_workspace_fits_visual_viewport(page)
conversation.expect_latest_message_above_composer()
expect(page.locator("[data-composer-close]")).to_be_visible()
page.locator("[data-composer-close]").click()
expect(page.locator("[data-composer-shell].is-collapsed")).to_be_visible()
assert_workspace_fits_visual_viewport(page)
+11 -3
View File
@@ -136,9 +136,17 @@ def test_e2e_subject_stays_empty_for_manual_and_search_routing_until_user_types(
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
boxes = page.locator("[data-routing-selectors]").evaluate(
"""root => {
const unit = root.querySelector('[data-ticket-unit]').getBoundingClientRect();
const category = root.querySelector('[data-ticket-category]').getBoundingClientRect();
return {
unit: {x: unit.x, y: unit.y, width: unit.width},
category: {x: category.x, y: category.y, width: category.width},
};
}"""
)
unit_box, category_box = boxes["unit"], boxes["category"]
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("")
+19 -1
View File
@@ -442,10 +442,27 @@ def test_ticket_polling_javascript_keeps_background_sync_and_recovers_reliably()
source_css=Path("static/src/tailwind.css").read_text(encoding="utf-8")
assert ".staff-ticket-workspace .composer.is-collapsed{position:fixed" not in source_css
assert ".staff-ticket-workspace .composer:not(.is-collapsed){position:fixed" not in source_css
assert "max-height:min(45dvh,28rem)" in source_css
assert "max-height:min(45svh,max(7rem,calc(var(--visual-viewport-height,100svh) - 18rem)),28rem)" in source_css
for menu_feature in ("data-message-menu","data-message-menu-close","closeMessageMenu","event.key==='Escape'","!menu.contains(event.target)"):
assert menu_feature in script or menu_feature in Path("templates/tickets/_messages.html").read_text(encoding="utf-8")
def test_ticket_workspace_uses_mobile_viewport_and_keyboard_safe_layout_contract():
base=Path("templates/base.html").read_text(encoding="utf-8")
source_css=Path("static/src/tailwind.css").read_text(encoding="utf-8")
ui_script=Path("static/js/ui.js").read_text(encoding="utf-8")
conversation_script=Path("static/js/conversation.js").read_text(encoding="utf-8")
magic_template=Path("templates/tickets/customer_detail.html").read_text(encoding="utf-8")
assert "interactive-widget=resizes-content" in base
for feature in ("100svh","--visual-viewport-height","overscroll-behavior:none","display-mode:standalone"):
assert feature in source_css
assert "min-height:32rem" not in source_css
assert "max-height:min(45svh,max(7rem,calc(var(--visual-viewport-height,100svh) - 18rem)),28rem)" in source_css
for feature in ("window.visualViewport","--visual-viewport-height","appviewportchange","orientationchange"):
assert feature in ui_script
assert "layoutObserver?.observe(composer)" in conversation_script
assert "focus({preventScroll:true})" in conversation_script
assert '<footer class="ticket-detail-credit">{{ developer_credit }}</footer>' in magic_template
def test_frontend_runtime_libraries_are_local_and_versioned():
template=Path("templates/base.html").read_text(encoding="utf-8")
assert "unpkg.com" not in template
@@ -527,6 +544,7 @@ def test_pwa_prompt_is_mobile_only_and_uses_square_app_icon(client):
script=Path("static/js/pwa.js").read_text(encoding="utf-8")
manifest=client.get(reverse("web_manifest")).json()
assert "mobileDevice" in script and "banner&&mobileDevice" in script
assert "hasWorkerController=Boolean(navigator.serviceWorker.controller)" in script
assert all(icon["src"].endswith("pwa-icon.png") for icon in manifest["icons"])