mirror of
https://github.com/Sea-Haven-Industries/meal-order-manager.git
synced 2026-09-30 06:33:12 +00:00
fix(form): wrap Google user bar at phone widths (#84)
* fix(form): wrap Google user bar at phone widths Allow the signed-in Google identity and admin controls to wrap below 480px while preserving the desktop row, with generated-form Playwright coverage for phone widths. * fix(form): preserve 480px user bar boundary Keep the mobile layout below 480px and lock both sides of the breakpoint with browser coverage. * style(tests): apply ruff formatting * test(form): guarantee Playwright browser cleanup * test(form): strengthen phone-width coverage * fix: add @classmethod and use cls in tests/test_generate_form.py Co-authored-by: seahaven-openswe[bot] <296972425+seahaven-openswe[bot]@users.noreply.github.com> * fix(tests): restore class fixture discovery --------- Co-authored-by: seahaven-openswe[bot] <296972425+seahaven-openswe[bot]@users.noreply.github.com>
This commit is contained in:
parent
216618a862
commit
30fa7fe352
3 changed files with 192 additions and 4 deletions
|
|
@ -75,7 +75,7 @@ header p { font-size: 0.85rem; opacity: 0.8; }
|
|||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.user-info-row { display: flex; align-items: center; gap: 12px; }
|
||||
.user-bar { display: flex; align-items: center; gap: 12px; }
|
||||
|
||||
.user-avatar {
|
||||
width: 40px;
|
||||
|
|
@ -87,9 +87,13 @@ header p { font-size: 0.85rem; opacity: 0.8; }
|
|||
|
||||
.user-avatar:not(.is-hidden) { display: block; }
|
||||
|
||||
.user-meta { flex: 1; }
|
||||
.user-meta { flex: 1; min-width: 0; }
|
||||
.user-name { font-weight: 600; font-size: 0.95rem; }
|
||||
.user-email { font-size: 0.85rem; color: var(--color-muted); }
|
||||
.user-email {
|
||||
font-size: 0.85rem;
|
||||
color: var(--color-muted);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
.count-display { font-size: 0.8rem; color: var(--color-muted); margin-left: 8px; }
|
||||
.g-signin-wrap { display: flex; justify-content: center; }
|
||||
.admin-muted { font-size: 0.8rem; color: #9ca3af; }
|
||||
|
|
@ -338,6 +342,12 @@ header p { font-size: 0.85rem; opacity: 0.8; }
|
|||
font-size: 1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 479px) {
|
||||
.user-bar { flex-wrap: wrap; }
|
||||
.user-meta { flex-basis: calc(100% - 52px); }
|
||||
.user-bar #admin-btn { margin-left: 52px; }
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.meal-card {
|
||||
display: grid;
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@
|
|||
{% if config.authMode == 'google' %}
|
||||
<div class="employee-info">
|
||||
<div id="user-info">
|
||||
<div class="user-info-row">
|
||||
<div class="user-bar">
|
||||
<img id="user-avatar" class="user-avatar is-hidden" alt="">
|
||||
<div class="user-meta">
|
||||
<div id="user-name" class="user-name"></div>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
|
|
@ -66,6 +67,21 @@ def _render(*, google: bool = False, api_key: str = "test-key") -> str:
|
|||
)
|
||||
|
||||
|
||||
def _google_credential() -> str:
|
||||
payload = json.dumps(
|
||||
{
|
||||
"name": "Test Admin",
|
||||
"email": "test-admin@example.com",
|
||||
"picture": (
|
||||
"data:image/svg+xml,"
|
||||
"<svg xmlns='http://www.w3.org/2000/svg' width='40' height='40'/>"
|
||||
),
|
||||
}
|
||||
).encode()
|
||||
encoded = base64.urlsafe_b64encode(payload).decode().rstrip("=")
|
||||
return f"e30.{encoded}.signature"
|
||||
|
||||
|
||||
class TestGenerateFormStructural:
|
||||
def test_local_and_google_render(self):
|
||||
local = _render(google=False)
|
||||
|
|
@ -292,3 +308,165 @@ class TestGenerateFormPlaywright:
|
|||
"() => document.getElementById('count-display').textContent === '2 meals'"
|
||||
)
|
||||
assert page.locator("#total-display").inner_text() == "$10.80"
|
||||
|
||||
@pytest.mark.parametrize("width", [320, 375, 479])
|
||||
def test_local_roster_has_no_horizontal_overflow(self, browser_page, width):
|
||||
page = browser_page
|
||||
page.set_viewport_size({"width": width, "height": 800})
|
||||
dimensions = page.evaluate(
|
||||
"""() => ({
|
||||
scrollWidth: document.documentElement.scrollWidth,
|
||||
clientWidth: document.documentElement.clientWidth,
|
||||
})"""
|
||||
)
|
||||
assert dimensions["scrollWidth"] == dimensions["clientWidth"]
|
||||
|
||||
|
||||
class TestGenerateFormGooglePlaywright:
|
||||
@pytest.fixture(scope="class")
|
||||
@classmethod
|
||||
def signed_in_admin_page(cls, tmp_path_factory):
|
||||
try:
|
||||
from playwright.sync_api import sync_playwright
|
||||
except ImportError as exc: # pragma: no cover
|
||||
raise RuntimeError(
|
||||
"playwright package is required; install requirements.txt"
|
||||
) from exc
|
||||
|
||||
out = tmp_path_factory.mktemp("google-form") / "order-form.html"
|
||||
out.write_text(_render(google=True, api_key=""))
|
||||
|
||||
with sync_playwright() as p:
|
||||
try:
|
||||
browser = p.chromium.launch(headless=True)
|
||||
except Exception as exc:
|
||||
raise RuntimeError(
|
||||
"Chromium is required for form Playwright tests. "
|
||||
"Run: playwright install --with-deps chromium"
|
||||
) from exc
|
||||
try:
|
||||
page = browser.new_page()
|
||||
page.add_init_script(
|
||||
"""window.google = {
|
||||
accounts: {
|
||||
id: {
|
||||
initialize() {},
|
||||
renderButton() {},
|
||||
disableAutoSelect() {},
|
||||
},
|
||||
},
|
||||
};"""
|
||||
)
|
||||
page.route(
|
||||
"https://accounts.google.com/gsi/client",
|
||||
lambda route: route.abort(),
|
||||
)
|
||||
|
||||
def handle_api(route):
|
||||
if "/api/form-status/" in route.request.url:
|
||||
route.fulfill(
|
||||
status=200,
|
||||
content_type="application/json",
|
||||
body='{"status":"open"}',
|
||||
)
|
||||
elif "/api/admin/orders" in route.request.url:
|
||||
route.fulfill(
|
||||
status=200,
|
||||
content_type="application/json",
|
||||
body='{"weeks":["2026-W30"]}',
|
||||
)
|
||||
else:
|
||||
route.fulfill(
|
||||
status=404,
|
||||
content_type="application/json",
|
||||
body="{}",
|
||||
)
|
||||
|
||||
page.route("https://api.example.com/**", handle_api)
|
||||
page.goto(out.as_uri(), wait_until="domcontentloaded")
|
||||
page.evaluate(
|
||||
"(credential) => handleCredentialResponse({ credential })",
|
||||
_google_credential(),
|
||||
)
|
||||
page.locator("#admin-btn").wait_for(state="visible")
|
||||
yield page
|
||||
finally:
|
||||
browser.close()
|
||||
|
||||
@pytest.mark.parametrize("width", [320, 375])
|
||||
def test_signed_in_admin_user_bar_fits_phone_width(
|
||||
self, signed_in_admin_page, width
|
||||
):
|
||||
page = signed_in_admin_page
|
||||
page.set_viewport_size({"width": width, "height": 800})
|
||||
|
||||
for selector in (
|
||||
"#user-avatar",
|
||||
"#user-name",
|
||||
"#user-email",
|
||||
"#admin-btn",
|
||||
"#sign-out-btn",
|
||||
):
|
||||
locator = page.locator(selector)
|
||||
assert locator.is_visible()
|
||||
bounds = locator.evaluate(
|
||||
"""element => {
|
||||
const rect = element.getBoundingClientRect();
|
||||
return {
|
||||
left: rect.left,
|
||||
top: rect.top,
|
||||
right: rect.right,
|
||||
bottom: rect.bottom,
|
||||
viewportWidth: window.innerWidth,
|
||||
viewportHeight: window.innerHeight,
|
||||
};
|
||||
}"""
|
||||
)
|
||||
assert bounds["left"] >= 0
|
||||
assert bounds["top"] >= 0
|
||||
assert bounds["right"] <= bounds["viewportWidth"]
|
||||
assert bounds["bottom"] <= bounds["viewportHeight"]
|
||||
|
||||
for selector in ("#user-name", "#user-email"):
|
||||
assert page.locator(selector).evaluate(
|
||||
"""element =>
|
||||
element.scrollWidth <= element.clientWidth &&
|
||||
element.scrollHeight <= element.clientHeight"""
|
||||
)
|
||||
assert page.locator("#admin-btn").is_enabled()
|
||||
assert page.locator("#sign-out-btn").is_enabled()
|
||||
|
||||
dimensions = page.evaluate(
|
||||
"""() => ({
|
||||
scrollWidth: document.documentElement.scrollWidth,
|
||||
clientWidth: document.documentElement.clientWidth,
|
||||
})"""
|
||||
)
|
||||
assert dimensions["scrollWidth"] == dimensions["clientWidth"]
|
||||
|
||||
def test_user_bar_stays_single_row_at_480(self, signed_in_admin_page):
|
||||
page = signed_in_admin_page
|
||||
page.set_viewport_size({"width": 480, "height": 800})
|
||||
assert (
|
||||
page.locator(".user-bar").evaluate(
|
||||
"(element) => getComputedStyle(element).flexWrap"
|
||||
)
|
||||
== "nowrap"
|
||||
)
|
||||
dimensions = page.evaluate(
|
||||
"""() => ({
|
||||
scrollWidth: document.documentElement.scrollWidth,
|
||||
clientWidth: document.documentElement.clientWidth,
|
||||
})"""
|
||||
)
|
||||
assert dimensions["scrollWidth"] == dimensions["clientWidth"]
|
||||
|
||||
def test_user_bar_wraps_below_shared_mobile_breakpoint(self, signed_in_admin_page):
|
||||
page = signed_in_admin_page
|
||||
page.set_viewport_size({"width": 479, "height": 800})
|
||||
assert (
|
||||
page.locator(".user-bar").evaluate(
|
||||
"(element) => getComputedStyle(element).flexWrap"
|
||||
)
|
||||
== "wrap"
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue