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:
Adam Moussa 2026-07-31 10:15:42 -04:00 • committed by GitHub
parent 216618a862
commit 30fa7fe352
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 192 additions and 4 deletions

View file

@ -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;

View file

@ -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>

View file

@ -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"
)