style(13-03): apply Prettier formatting across repo

Mechanical reformat — no logic changes. 398 files changed, 19125
insertions(+), 16457 deletions(-). Prettier 3.8.4 with .prettierrc
(singleQuote:true, semi:true, tabWidth:2, trailingComma:all,
printWidth:100). Isolated per D-13-08 for reviewability.
This commit is contained in:
Lucas Berger
2026-06-11 20:35:18 -04:00
parent 4bc0445173
commit 982438dc10
398 changed files with 19050 additions and 16382 deletions
@@ -24,13 +24,13 @@ framing: quality-bar-contract
N/A — test harness, no new UI. The existing design system is declared in
`apps/pwa/src/styles/tokens.css` and consumed by the assertions below.
| Property | Value |
|----------|-------|
| Tool | none (no shadcn; inline CSS custom properties) |
| Preset | not applicable |
| Component library | none (lucide-react icons; Schedule-X calendar widget) |
| Icon library | lucide-react (via npm dep, no CDN) |
| Font | `system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif` |
| Property | Value |
| ----------------- | ---------------------------------------------------------------------- |
| Tool | none (no shadcn; inline CSS custom properties) |
| Preset | not applicable |
| Component library | none (lucide-react icons; Schedule-X calendar widget) |
| Icon library | lucide-react (via npm dep, no CDN) |
| Font | `system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif` |
---
@@ -65,19 +65,19 @@ Copywriting that the harness **must** be able to locate by text in assertions.
These are the exact strings emitted by the existing components; the harness
uses them as stable locator anchors.
| Element | Exact String | Source Component |
|---------|-------------|------------------|
| Calendar empty-state heading | `Nothing here` | `EmptyState.tsx` |
| Calendar empty-state body | `No events in this period. Try a different date or switch views.` | `EmptyState.tsx` |
| Lists empty-state heading | `No lists yet` | `ListsEmptyState.tsx` |
| Lists empty-state body | `Tap + to create your first shared list` (contains) | `ListsEmptyState.tsx` |
| Calendar error heading | `Couldn't load events` | `CalendarShell.tsx` |
| Calendar error CTA | `Retry` (button text) | `CalendarShell.tsx` |
| New Event FAB | `aria-label="New Event"` | `CalendarShell.tsx` |
| Bottom nav — Calendar tab | `aria-label="Calendar"` | `BottomTabBar.tsx` |
| Bottom nav — Lists tab | `aria-label="Lists"` | `BottomTabBar.tsx` |
| Top nav (phone) | `FamilySync` (visible text) | `AppNav.tsx``PhoneNav` |
| Settings button | `aria-label` contains `open settings` | `AppNav.tsx``PhoneNav` |
| Element | Exact String | Source Component |
| ---------------------------- | ----------------------------------------------------------------- | ------------------------- |
| Calendar empty-state heading | `Nothing here` | `EmptyState.tsx` |
| Calendar empty-state body | `No events in this period. Try a different date or switch views.` | `EmptyState.tsx` |
| Lists empty-state heading | `No lists yet` | `ListsEmptyState.tsx` |
| Lists empty-state body | `Tap + to create your first shared list` (contains) | `ListsEmptyState.tsx` |
| Calendar error heading | `Couldn't load events` | `CalendarShell.tsx` |
| Calendar error CTA | `Retry` (button text) | `CalendarShell.tsx` |
| New Event FAB | `aria-label="New Event"` | `CalendarShell.tsx` |
| Bottom nav — Calendar tab | `aria-label="Calendar"` | `BottomTabBar.tsx` |
| Bottom nav — Lists tab | `aria-label="Lists"` | `BottomTabBar.tsx` |
| Top nav (phone) | `FamilySync` (visible text) | `AppNav.tsx``PhoneNav` |
| Settings button | `aria-label` contains `open settings` | `AppNav.tsx``PhoneNav` |
> Stable copywriting anchor rule: **always locate interactive elements by
> `aria-label` or `role` + accessible name first.** Text-content locators
@@ -102,10 +102,10 @@ quality-bar rules that the harness enforces.
### Device / Viewport Matrix
| Profile ID | Playwright Descriptor | Engine | Viewport | UA Type |
|---|---|---|---|---|
| `iphone` | `'iPhone 14'` | WebKit | 390×844 logical px | Mobile Safari |
| `pixel` | `'Pixel 7'` | Chromium | 412×915 logical px | Chrome Android |
| Profile ID | Playwright Descriptor | Engine | Viewport | UA Type |
| ---------- | --------------------- | -------- | ------------------ | -------------- |
| `iphone` | `'iPhone 14'` | WebKit | 390×844 logical px | Mobile Safari |
| `pixel` | `'Pixel 7'` | Chromium | 412×915 logical px | Chrome Android |
**Source:** D-03 (iPhone + Pixel matrix), D-04 (WebKit for iPhone, Chromium
for Pixel). These are the exact Playwright device descriptor strings to pass
@@ -127,6 +127,7 @@ only). Accept the larger CI image cost — this was a deliberate call (D-04,
have a computed bounding box of **≥ 44 × 44 logical pixels**.
**Basis:**
- Apple Human Interface Guidelines: minimum touch target 44×44 pt.
- WCAG 2.5.5 (Level AAA): minimum 44×44 CSS px.
- The existing codebase declares this as a hard constraint: `BottomTabBar`
@@ -135,14 +136,16 @@ have a computed bounding box of **≥ 44 × 44 logical pixels**.
button uses `minHeight: '44px'`; nav links use `minHeight: '44px'`.
**Measurement approach:**
```typescript
// Use boundingBox() on the element handle, not CSS-declared values.
const box = await element.boundingBox()
expect(box!.width).toBeGreaterThanOrEqual(44)
expect(box!.height).toBeGreaterThanOrEqual(44)
const box = await element.boundingBox();
expect(box!.width).toBeGreaterThanOrEqual(44);
expect(box!.height).toBeGreaterThanOrEqual(44);
```
**What counts as an interactive target:**
- `<button>` elements (including FAB, Retry, settings avatar button)
- `<a>` and `NavLink` elements (BottomTabBar tabs, sidebar nav links)
- Any element with `role="button"`, `role="link"`, or `tabindex="0"` that
@@ -150,13 +153,13 @@ expect(box!.height).toBeGreaterThanOrEqual(44)
**Explicit elements to assert on both profiles:**
| Element | Expected min size | Locator strategy |
|---|---|---|
| BottomTabBar Calendar tab | 44×44 | `getByRole('link', { name: 'Calendar' })` |
| BottomTabBar Lists tab | 44×44 | `getByRole('link', { name: 'Lists' })` |
| PhoneNav settings button | 44×44 | `getByRole('button', { name: /open settings/i })` |
| New Event FAB | 56×56 | `getByRole('button', { name: 'New Event' })` |
| Retry button (error state) | 44×44 | `getByRole('button', { name: 'Retry' })` |
| Element | Expected min size | Locator strategy |
| -------------------------- | ----------------- | ------------------------------------------------- |
| BottomTabBar Calendar tab | 44×44 | `getByRole('link', { name: 'Calendar' })` |
| BottomTabBar Lists tab | 44×44 | `getByRole('link', { name: 'Lists' })` |
| PhoneNav settings button | 44×44 | `getByRole('button', { name: /open settings/i })` |
| New Event FAB | 56×56 | `getByRole('button', { name: 'New Event' })` |
| Retry button (error state) | 44×44 | `getByRole('button', { name: 'Retry' })` |
**BottomTabBar phone-only gate:** `BottomTabBar` renders `null` on desktop
(`matchMedia('(max-width: 767px)')`). Assert it is present on both mobile
@@ -172,22 +175,23 @@ must equal `document.documentElement.clientWidth`. No horizontal scrollbar;
no content overflow.
**Measurement approach:**
```typescript
const overflow = await page.evaluate(() => ({
scrollWidth: document.documentElement.scrollWidth,
clientWidth: document.documentElement.clientWidth,
}))
expect(overflow.scrollWidth).toBeLessThanOrEqual(overflow.clientWidth)
}));
expect(overflow.scrollWidth).toBeLessThanOrEqual(overflow.clientWidth);
```
**Routes to assert on both profiles:**
| Route | State to assert |
|---|---|
| `/calendar` | populated (seeded events) |
| `/calendar` | error state (simulated — mock API to 500) |
| `/lists` | populated (seeded list + items) |
| `/lists` | empty state (no lists — dev-bypass user 1 native state) |
| Route | State to assert |
| ----------- | ------------------------------------------------------- |
| `/calendar` | populated (seeded events) |
| `/calendar` | error state (simulated — mock API to 500) |
| `/lists` | populated (seeded list + items) |
| `/lists` | empty state (no lists — dev-bypass user 1 native state) |
**Allowed exceptions:** none. The Schedule-X calendar widget historically
caused overflow on narrow viewports (see memory entry `schedule-x-allday-event-styling`).
@@ -202,13 +206,13 @@ Assertion: each element below must be visible (`isVisible() === true`) **and**
within the viewport (`boundingBox().y >= 0`, `boundingBox().y + height <=
viewport.height`) on initial load, before any scroll.
| Element | Route | Profile |
|---|---|---|
| BottomTabBar | `/calendar`, `/lists` | iPhone + Pixel |
| PhoneNav header | `/calendar`, `/lists` | iPhone + Pixel |
| Schedule-X calendar grid | `/calendar` (populated) | iPhone + Pixel |
| New Event FAB | `/calendar` | iPhone + Pixel |
| Lists index cards (≥1 card) | `/lists` (seeded) | iPhone + Pixel |
| Element | Route | Profile |
| --------------------------- | ----------------------- | -------------- |
| BottomTabBar | `/calendar`, `/lists` | iPhone + Pixel |
| PhoneNav header | `/calendar`, `/lists` | iPhone + Pixel |
| Schedule-X calendar grid | `/calendar` (populated) | iPhone + Pixel |
| New Event FAB | `/calendar` | iPhone + Pixel |
| Lists index cards (≥1 card) | `/lists` (seeded) | iPhone + Pixel |
**BottomTabBar position assertion (safe-area-inset):** the bar uses
`env(safe-area-inset-bottom, 0px)`. In the emulated context there is no
@@ -225,19 +229,20 @@ queries without needing a CSS selector fallback.
**Required accessible names (exact or pattern):**
| Element | Role | Expected accessible name |
|---|---|---|
| BottomTabBar Calendar tab | `link` | `"Calendar"` |
| BottomTabBar Lists tab | `link` | `"Lists"` |
| PhoneNav settings button | `button` | matches `/open settings/i` |
| New Event FAB | `button` | `"New Event"` |
| Retry button | `button` | `"Retry"` |
| Main navigation landmark | `navigation` | `"Main navigation"` |
| Element | Role | Expected accessible name |
| ------------------------- | ------------ | -------------------------- |
| BottomTabBar Calendar tab | `link` | `"Calendar"` |
| BottomTabBar Lists tab | `link` | `"Lists"` |
| PhoneNav settings button | `button` | matches `/open settings/i` |
| New Event FAB | `button` | `"New Event"` |
| Retry button | `button` | `"Retry"` |
| Main navigation landmark | `navigation` | `"Main navigation"` |
Locator pattern:
```typescript
page.getByRole('link', { name: 'Calendar' })
page.getByRole('button', { name: /open settings/i })
page.getByRole('link', { name: 'Calendar' });
page.getByRole('button', { name: /open settings/i });
```
If an element cannot be found by role + name, the test fails. This doubles as
@@ -260,12 +265,12 @@ event-create 422s for user 1).
**Assertions:**
| State | Route | Assert |
|---|---|---|
| Populated calendar | `/calendar` (after seeding) | Schedule-X grid is visible; `<EmptyState>` is NOT in DOM |
| Populated lists | `/lists` (after seeding) | ≥1 list card is visible; `ListsEmptyState` is NOT in DOM |
| Empty lists (pre-seed teardown or clean run) | `/lists` | `getByText('No lists yet')` is visible; `getByText(/Tap \+ to create/)` is visible |
| Calendar error | `/calendar` (API mocked to 500) | `getByRole('heading', { name: "Couldn't load events" })` is visible; `getByRole('button', { name: 'Retry' })` is visible |
| State | Route | Assert |
| -------------------------------------------- | ------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| Populated calendar | `/calendar` (after seeding) | Schedule-X grid is visible; `<EmptyState>` is NOT in DOM |
| Populated lists | `/lists` (after seeding) | ≥1 list card is visible; `ListsEmptyState` is NOT in DOM |
| Empty lists (pre-seed teardown or clean run) | `/lists` | `getByText('No lists yet')` is visible; `getByText(/Tap \+ to create/)` is visible |
| Calendar error | `/calendar` (API mocked to 500) | `getByRole('heading', { name: "Couldn't load events" })` is visible; `getByRole('button', { name: 'Retry' })` is visible |
**Empty-state assertion depth:** each empty state must additionally pass Rule
1 (touch targets on any interactive elements within it) and Rule 2 (no
@@ -278,6 +283,7 @@ horizontal overflow).
**Decision: OMIT `toHaveScreenshot()` assertions entirely for this phase.**
Rationale (from D-08-area steer in 07-CONTEXT.md):
- The Schedule-X calendar widget renders dynamic content (current date
highlighted, event chips placed by the widget's internal layout engine)
that will differ between host and CI renderers on different dates and OS
@@ -306,14 +312,14 @@ These are not UI-quality assertions but are preconditions that must hold for
all other assertions to be valid. They are enforced in global-setup and
browser context options.
| Precondition | Enforcement | Source |
|---|---|---|
| `DEV_AUTH_BYPASS=true` in API process | Env var set before dev-server launch | D-01 / Pitfall 14 |
| `serviceWorkers: 'block'` on every context | `playwright.config.ts` contextOptions | D-02 / Pitfall 15 |
| No `storageState` file | `playwright.config.ts` — omit `storageState` | D-01 / Pitfall 14 |
| PWA reachable before specs run | global-setup polls `GET /health` until 200 | D-08 / SC #3 |
| DB fixtures reset before run | global-setup truncate + insert | D-06 |
| No SW-sourced responses | Playwright trace shows no `(ServiceWorker)` source | D-02 / Pitfall 15 |
| Precondition | Enforcement | Source |
| ------------------------------------------ | -------------------------------------------------- | ----------------- |
| `DEV_AUTH_BYPASS=true` in API process | Env var set before dev-server launch | D-01 / Pitfall 14 |
| `serviceWorkers: 'block'` on every context | `playwright.config.ts` contextOptions | D-02 / Pitfall 15 |
| No `storageState` file | `playwright.config.ts` — omit `storageState` | D-01 / Pitfall 14 |
| PWA reachable before specs run | global-setup polls `GET /health` until 200 | D-08 / SC #3 |
| DB fixtures reset before run | global-setup truncate + insert | D-06 |
| No SW-sourced responses | Playwright trace shows no `(ServiceWorker)` source | D-02 / Pitfall 15 |
**SW-source verification (in trace):** after a run, if a test fails with
unexpected data, inspect the `.zip` trace artifact. Any response with source
@@ -325,7 +331,7 @@ option should prevent this. Log a test failure if detected programmatically:
page.on('response', (resp) => {
// Playwright does not expose SW-source in the Response object directly;
// rely on serviceWorkers: 'block' and trace inspection for post-hoc audit.
})
});
```
---
@@ -334,20 +340,21 @@ page.on('response', (resp) => {
Assertions and harness configuration must produce identical pass/fail results
when run:
1. Locally against the operator's already-running dev stack (Vite PWA +
API + compose MariaDB/Redis).
2. In Gitea CI against a runner-brought-up dev stack (Phase 8).
**Contract rules:**
| Rule | Enforcement |
|---|---|
| `baseURL` is env-driven (`PLAYWRIGHT_BASE_URL`, fallback `http://localhost:5173`) | `playwright.config.ts` `use.baseURL` |
| No hardcoded `localhost:5173` in spec files | Lint / code review gate |
| Readiness gate in global-setup polls `baseURL + '/health'` until 200 or timeout 60s | `playwright.config.ts` `globalSetup` |
| DB seed uses `DB_HOST` env (fallback `127.0.0.1`), port 3306, same `.env` creds | global-setup `mysql2` connection |
| No spec imports a dev-only module path that does not exist in CI | Jest/Playwright import resolution |
| Browser binaries installed at `apps/pwa` level via `@playwright/test` dep | `apps/pwa/package.json` `devDependencies` |
| Rule | Enforcement |
| ----------------------------------------------------------------------------------- | ----------------------------------------- |
| `baseURL` is env-driven (`PLAYWRIGHT_BASE_URL`, fallback `http://localhost:5173`) | `playwright.config.ts` `use.baseURL` |
| No hardcoded `localhost:5173` in spec files | Lint / code review gate |
| Readiness gate in global-setup polls `baseURL + '/health'` until 200 or timeout 60s | `playwright.config.ts` `globalSetup` |
| DB seed uses `DB_HOST` env (fallback `127.0.0.1`), port 3306, same `.env` creds | global-setup `mysql2` connection |
| No spec imports a dev-only module path that does not exist in CI | Jest/Playwright import resolution |
| Browser binaries installed at `apps/pwa` level via `@playwright/test` dep | `apps/pwa/package.json` `devDependencies` |
---
@@ -369,20 +376,20 @@ when run:
## Source Decisions
| Decision | Source |
|---|---|
| D-01 DEV_AUTH_BYPASS, no storage-state | 07-CONTEXT.md |
| D-02 serviceWorkers: 'block' | 07-CONTEXT.md |
| D-03 iPhone + Pixel two-profile matrix | 07-CONTEXT.md |
| D-04 WebKit for iPhone, Chromium for Pixel | 07-CONTEXT.md |
| D-05 hybrid seed strategy | 07-CONTEXT.md |
| D-06 deterministic reset-per-run seed | 07-CONTEXT.md |
| D-07 global-setup for seeding | 07-CONTEXT.md |
| D-08 readiness gate + configurable baseURL | 07-CONTEXT.md |
| D-09 stack lifecycle is caller's responsibility | 07-CONTEXT.md |
| D-10 optional webServer for Vite | 07-CONTEXT.md |
| 44px threshold | Apple HIG; WCAG 2.5.5; existing codebase pattern |
| Screenshot omission | D-08-area steer; Schedule-X drift risk; 07-CONTEXT.md |
| Pitfall 14 (storage-state stale) | PITFALLS.md §Pitfall 14 |
| Pitfall 15 (SW intercept) | PITFALLS.md §Pitfall 15 |
| Existing tokens/copy strings | `tokens.css`, `EmptyState.tsx`, `ListsEmptyState.tsx`, `CalendarShell.tsx`, `AppNav.tsx`, `BottomTabBar.tsx` |
| Decision | Source |
| ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------ |
| D-01 DEV_AUTH_BYPASS, no storage-state | 07-CONTEXT.md |
| D-02 serviceWorkers: 'block' | 07-CONTEXT.md |
| D-03 iPhone + Pixel two-profile matrix | 07-CONTEXT.md |
| D-04 WebKit for iPhone, Chromium for Pixel | 07-CONTEXT.md |
| D-05 hybrid seed strategy | 07-CONTEXT.md |
| D-06 deterministic reset-per-run seed | 07-CONTEXT.md |
| D-07 global-setup for seeding | 07-CONTEXT.md |
| D-08 readiness gate + configurable baseURL | 07-CONTEXT.md |
| D-09 stack lifecycle is caller's responsibility | 07-CONTEXT.md |
| D-10 optional webServer for Vite | 07-CONTEXT.md |
| 44px threshold | Apple HIG; WCAG 2.5.5; existing codebase pattern |
| Screenshot omission | D-08-area steer; Schedule-X drift risk; 07-CONTEXT.md |
| Pitfall 14 (storage-state stale) | PITFALLS.md §Pitfall 14 |
| Pitfall 15 (SW intercept) | PITFALLS.md §Pitfall 15 |
| Existing tokens/copy strings | `tokens.css`, `EmptyState.tsx`, `ListsEmptyState.tsx`, `CalendarShell.tsx`, `AppNav.tsx`, `BottomTabBar.tsx` |