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:
@@ -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` |
|
||||
|
||||
Reference in New Issue
Block a user