Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a7a6c4ae5 | ||
|
|
8ea825a8a1 | ||
|
|
236909be64 | ||
|
|
68d9efbda3 |
@@ -53,9 +53,9 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
corepack enable
|
corepack enable
|
||||||
corepack pnpm install --frozen-lockfile
|
corepack pnpm install --frozen-lockfile
|
||||||
- name: Install Chromium
|
- name: Install Playwright browsers
|
||||||
if: ${{ matrix.browser }}
|
if: ${{ matrix.browser }}
|
||||||
run: corepack pnpm exec playwright install --with-deps chromium
|
run: corepack pnpm exec playwright install --with-deps chromium firefox webkit
|
||||||
- name: Run blocking gate
|
- name: Run blocking gate
|
||||||
run: corepack pnpm ci:gate -- ${{ matrix.gate }}
|
run: corepack pnpm ci:gate -- ${{ matrix.gate }}
|
||||||
- name: Upload gate evidence
|
- name: Upload gate evidence
|
||||||
@@ -91,9 +91,9 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
corepack enable
|
corepack enable
|
||||||
corepack pnpm install --frozen-lockfile
|
corepack pnpm install --frozen-lockfile
|
||||||
- name: Install Chromium
|
- name: Install Playwright browsers
|
||||||
if: ${{ matrix.browser }}
|
if: ${{ matrix.browser }}
|
||||||
run: corepack pnpm exec playwright install --with-deps chromium
|
run: corepack pnpm exec playwright install --with-deps chromium firefox webkit
|
||||||
- name: Run blocking gate
|
- name: Run blocking gate
|
||||||
run: corepack pnpm ci:gate -- ${{ matrix.gate }}
|
run: corepack pnpm ci:gate -- ${{ matrix.gate }}
|
||||||
- name: Upload gate evidence
|
- name: Upload gate evidence
|
||||||
|
|||||||
@@ -16,6 +16,27 @@ corepack pnpm dev
|
|||||||
Runtime-public settings live in `public/config.json` and are validated before
|
Runtime-public settings live in `public/config.json` and are validated before
|
||||||
the product tree mounts. Client secrets are forbidden.
|
the product tree mounts. Client secrets are forbidden.
|
||||||
|
|
||||||
|
## Included starter experience
|
||||||
|
|
||||||
|
The default build mounts a domain-neutral application shell with a header,
|
||||||
|
responsive sidebar, route focus management, session integration status, and a
|
||||||
|
persistent `system` / `light` / `dark` theme selector.
|
||||||
|
|
||||||
|
| Route | Purpose |
|
||||||
|
| --- | --- |
|
||||||
|
| `/` | implementation readiness and starter links |
|
||||||
|
| `/examples/ui` | buttons, fields, cards, alerts, badges, modal, and tokens |
|
||||||
|
| `/examples/states` | loading, refresh, empty, error, auth, forbidden, and not-found states |
|
||||||
|
| `/examples/auth` | reactive external-auth integration seam |
|
||||||
|
| `/sample/resources` | protected, domain-neutral integration route |
|
||||||
|
|
||||||
|
`AUTH_MODE=demo` is credential-free and accepted only in local/development
|
||||||
|
environments. Deployments use `AUTH_MODE=external` and provide the opaque auth
|
||||||
|
owner described in
|
||||||
|
[`docs/architecture/starter-experience.md`](docs/architecture/starter-experience.md).
|
||||||
|
The client route policy is user experience only; server authorization remains
|
||||||
|
authoritative.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
Dependencies point inward:
|
Dependencies point inward:
|
||||||
@@ -27,9 +48,11 @@ bootstrap composes concrete adapters
|
|||||||
contracts own cross-cutting registries
|
contracts own cross-cutting registries
|
||||||
```
|
```
|
||||||
|
|
||||||
See `docs/architecture/overview.md` and `docs/architecture/layers.md`. The
|
See `docs/architecture/overview.md`, `docs/architecture/layers.md`, and
|
||||||
removable sample slice is under `src/sample/contract-fixture`; product code is
|
`docs/architecture/starter-experience.md`. The removable sample slice is under
|
||||||
not allowed to import it.
|
`src/sample/contract-fixture`; product code is not allowed to import it. The
|
||||||
|
visible starter routes do not depend on that fixture and continue to build
|
||||||
|
after it is removed.
|
||||||
|
|
||||||
## Verification
|
## Verification
|
||||||
|
|
||||||
@@ -52,9 +75,17 @@ corepack pnpm drill:runbooks
|
|||||||
corepack pnpm check:ci
|
corepack pnpm check:ci
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Install the pinned Playwright browser engines before the first cross-browser
|
||||||
|
run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
corepack pnpm exec playwright install --with-deps chromium firefox webkit
|
||||||
|
```
|
||||||
|
|
||||||
Two gates intentionally need external evidence:
|
Two gates intentionally need external evidence:
|
||||||
|
|
||||||
- `review:a11y-manual` needs a signed human keyboard/focus/screen-reader review.
|
- `review:a11y-manual` needs a signed human keyboard/focus/screen-reader review
|
||||||
|
for all six registered routes.
|
||||||
- `collect:web-vitals-evidence` stays `FAIL_UNVERIFIED` until a reviewed minimum
|
- `collect:web-vitals-evidence` stays `FAIL_UNVERIFIED` until a reviewed minimum
|
||||||
eligible-sample threshold and 28 days of production data exist.
|
eligible-sample threshold and 28 days of production data exist.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# EXAMPLES_AUTH accessibility review
|
||||||
|
|
||||||
|
Status: pending-manual-review
|
||||||
|
Route ID: EXAMPLES_AUTH
|
||||||
|
Release ID:
|
||||||
|
Reviewer:
|
||||||
|
Reviewed at:
|
||||||
|
Signature:
|
||||||
|
Attestation: pending
|
||||||
|
M1 Keyboard: pending
|
||||||
|
M2 Visible focus: pending
|
||||||
|
M3 Route focus: pending
|
||||||
|
M4 Modal focus: not-applicable (no modal on this route)
|
||||||
|
M5 Error association: not-applicable (no form error on this route)
|
||||||
|
M6 Color signal: pending
|
||||||
|
M7 Reduced motion: pending
|
||||||
|
Screen reader: pending
|
||||||
|
Notes: Review session state announcements and unavailable external-integration behavior.
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# EXAMPLES_STATES accessibility review
|
||||||
|
|
||||||
|
Status: pending-manual-review
|
||||||
|
Route ID: EXAMPLES_STATES
|
||||||
|
Release ID:
|
||||||
|
Reviewer:
|
||||||
|
Reviewed at:
|
||||||
|
Signature:
|
||||||
|
Attestation: pending
|
||||||
|
M1 Keyboard: pending
|
||||||
|
M2 Visible focus: pending
|
||||||
|
M3 Route focus: pending
|
||||||
|
M4 Modal focus: not-applicable (no modal on this route)
|
||||||
|
M5 Error association: not-applicable (no form error on this route)
|
||||||
|
M6 Color signal: pending
|
||||||
|
M7 Reduced motion: pending
|
||||||
|
Screen reader: pending
|
||||||
|
Notes: Review loading, refresh, empty, error, authentication, forbidden, and not-found announcements.
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# EXAMPLES_UI accessibility review
|
||||||
|
|
||||||
|
Status: pending-manual-review
|
||||||
|
Route ID: EXAMPLES_UI
|
||||||
|
Release ID:
|
||||||
|
Reviewer:
|
||||||
|
Reviewed at:
|
||||||
|
Signature:
|
||||||
|
Attestation: pending
|
||||||
|
M1 Keyboard: pending
|
||||||
|
M2 Visible focus: pending
|
||||||
|
M3 Route focus: pending
|
||||||
|
M4 Modal focus: pending
|
||||||
|
M5 Error association: pending
|
||||||
|
M6 Color signal: pending
|
||||||
|
M7 Reduced motion: pending
|
||||||
|
Screen reader: pending
|
||||||
|
Notes: Review the text-field error association and modal focus containment/restoration.
|
||||||
@@ -125,6 +125,9 @@
|
|||||||
"evidence": [
|
"evidence": [
|
||||||
"artifacts/tests/a11y.json",
|
"artifacts/tests/a11y.json",
|
||||||
"artifacts/tests/a11y-manual/APP_HOME.md",
|
"artifacts/tests/a11y-manual/APP_HOME.md",
|
||||||
|
"artifacts/tests/a11y-manual/EXAMPLES_UI.md",
|
||||||
|
"artifacts/tests/a11y-manual/EXAMPLES_STATES.md",
|
||||||
|
"artifacts/tests/a11y-manual/EXAMPLES_AUTH.md",
|
||||||
"artifacts/tests/a11y-manual/SAMPLE_RESOURCE_LIST.md",
|
"artifacts/tests/a11y-manual/SAMPLE_RESOURCE_LIST.md",
|
||||||
"artifacts/tests/a11y-manual/NOT_FOUND.md",
|
"artifacts/tests/a11y-manual/NOT_FOUND.md",
|
||||||
"artifacts/tests/a11y-manual/report.json"
|
"artifacts/tests/a11y-manual/report.json"
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
# Manual accessibility review checklist
|
# Manual accessibility review checklist
|
||||||
|
|
||||||
Automated axe checks do not establish WCAG conformance. A human reviewer must
|
Automated axe checks do not establish WCAG conformance. A human reviewer must
|
||||||
review all three route records in `artifacts/tests/a11y-manual/` against one
|
review all six route records in `artifacts/tests/a11y-manual/` against one
|
||||||
release candidate and sign them. Copy the template fields exactly; the gate
|
release candidate and sign them. The required scope is derived from the route
|
||||||
rejects blank identity/timestamp/signature fields, pending verdicts, mismatched
|
registry: `APP_HOME`, `EXAMPLES_UI`, `EXAMPLES_STATES`, `EXAMPLES_AUTH`,
|
||||||
release IDs, or missing routes.
|
`SAMPLE_RESOURCE_LIST`, and `NOT_FOUND`. Copy the template fields exactly; the
|
||||||
|
gate rejects blank identity/timestamp/signature fields, pending verdicts,
|
||||||
|
mismatched release IDs, or missing routes.
|
||||||
|
|
||||||
Allowed item verdicts:
|
Allowed item verdicts:
|
||||||
|
|
||||||
@@ -43,5 +45,7 @@ The reviewer must verify:
|
|||||||
- M7: non-essential motion is suppressed with reduced-motion preference
|
- M7: non-essential motion is suppressed with reduced-motion preference
|
||||||
- Screen reader: headings, live regions, errors, and actions are announced once
|
- Screen reader: headings, live regions, errors, and actions are announced once
|
||||||
|
|
||||||
Passing automated evidence means only that tested pages had no critical or
|
`EXAMPLES_UI` requires real M4 modal and M5 field-error review; those items must
|
||||||
serious axe findings under the recorded browser run.
|
not be marked not-applicable on that route. Passing automated evidence means
|
||||||
|
only that tested pages had no critical or serious axe findings under the
|
||||||
|
recorded Chromium, Firefox, and WebKit runs.
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ in `review-ledger.json`, not automatically to edits in this file.
|
|||||||
flowchart LR
|
flowchart LR
|
||||||
Bootstrap[bootstrap / composition root] --> Presentation[presentation]
|
Bootstrap[bootstrap / composition root] --> Presentation[presentation]
|
||||||
Bootstrap --> Adapters[adapters]
|
Bootstrap --> Adapters[adapters]
|
||||||
|
Presentation --> Shell[app shell and route surfaces]
|
||||||
|
Shell --> Providers[session and theme providers]
|
||||||
Presentation --> Application[application]
|
Presentation --> Application[application]
|
||||||
Adapters --> Application
|
Adapters --> Application
|
||||||
Application --> Domain[domain]
|
Application --> Domain[domain]
|
||||||
@@ -21,3 +23,9 @@ implement application ports, and only the composition root selects concrete
|
|||||||
adapters. Contract registries are the single named source for routes, API
|
adapters. Contract registries are the single named source for routes, API
|
||||||
operations, environment values, storage keys, errors, queries, telemetry, and
|
operations, environment values, storage keys, errors, queries, telemetry, and
|
||||||
release tokens.
|
release tokens.
|
||||||
|
|
||||||
|
The executable route tree is mounted only after runtime configuration and
|
||||||
|
release-manifest coherence pass. It receives the composed query client,
|
||||||
|
credential-opaque session port, storage port, telemetry port, and immutable
|
||||||
|
build ID. Visible starter pages depend on those ports and contracts, never on a
|
||||||
|
concrete adapter or the removable sample fixture.
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
# Starter experience contract
|
||||||
|
|
||||||
|
The repository provides a runnable, domain-neutral application rather than
|
||||||
|
only infrastructure contracts. The starter experience is intentionally
|
||||||
|
replaceable at the page level while the shell, providers, boundaries,
|
||||||
|
primitives, and state surfaces remain reusable.
|
||||||
|
|
||||||
|
## Runtime composition
|
||||||
|
|
||||||
|
```text
|
||||||
|
validated config + coherent release manifest
|
||||||
|
-> concrete adapters
|
||||||
|
-> QueryClientProvider
|
||||||
|
-> ThemeProvider
|
||||||
|
-> SessionProvider
|
||||||
|
-> BrowserRouter
|
||||||
|
-> AppShell
|
||||||
|
-> lazy route boundary
|
||||||
|
```
|
||||||
|
|
||||||
|
Boot stops before product mount when configuration or release coherence fails.
|
||||||
|
Each lazy page renders inside Suspense and a telemetry-aware route boundary.
|
||||||
|
Route changes move focus to the new page heading; the skip link and landmarks
|
||||||
|
remain stable in the shell.
|
||||||
|
|
||||||
|
## Registered routes
|
||||||
|
|
||||||
|
| ID | Path | Access | Surface |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `APP_HOME` | `/` | public | readiness dashboard |
|
||||||
|
| `EXAMPLES_UI` | `/examples/ui` | public | interactive primitives and tokens |
|
||||||
|
| `EXAMPLES_STATES` | `/examples/states` | public | async and access state matrix |
|
||||||
|
| `EXAMPLES_AUTH` | `/examples/auth` | public | session integration controls |
|
||||||
|
| `SAMPLE_RESOURCE_LIST` | `/sample/resources` | integration-defined | protected integration seam |
|
||||||
|
| `NOT_FOUND` | `*` | public | safe navigation recovery |
|
||||||
|
|
||||||
|
Navigation labels and order come from `ROUTE_REGISTRY`; the sidebar does not
|
||||||
|
maintain a second route list. The client access decision never claims to be
|
||||||
|
authorization.
|
||||||
|
|
||||||
|
## Authentication seam
|
||||||
|
|
||||||
|
The session port exposes state subscription, sign-in start, sign-out,
|
||||||
|
credential attachment, recovery, and unauthenticated notification. Credentials
|
||||||
|
remain opaque to the application and presentation layers.
|
||||||
|
|
||||||
|
- `demo`: local/development-only state transition with no credentials
|
||||||
|
- `external`: delegates to `globalThis.__CA_FRONTEND_AUTH_OWNER__`
|
||||||
|
- missing/invalid external owner: fails closed as `integration-failed`
|
||||||
|
|
||||||
|
An external owner implements `readState`, `subscribe`, `beginSignIn`,
|
||||||
|
`signOut`, `attachCredential`, `recoverSession`, and
|
||||||
|
`notifyUnauthenticated`. It owns token acquisition and storage.
|
||||||
|
|
||||||
|
## Extending the starter
|
||||||
|
|
||||||
|
1. Register the route path, access hint, title, chunk, loading surface, error
|
||||||
|
surface, and optional navigation metadata in `src/contracts/routes.js`.
|
||||||
|
2. Add a lazy page in `src/presentation/` and render it through `RouteSurface`.
|
||||||
|
3. Use application ports or use cases; do not import concrete adapters.
|
||||||
|
4. Use the semantic tokens, UI primitives, and state surfaces before adding a
|
||||||
|
project-specific variant.
|
||||||
|
5. Add component behavior, all-engine E2E, automated axe, and signed manual
|
||||||
|
route evidence.
|
||||||
|
6. Run `test:sample-removal` to prove the visible starter still builds without
|
||||||
|
`src/sample/contract-fixture`.
|
||||||
|
|
||||||
|
Theme preference is the public `COLOR_SCHEME` storage contract. Authentication
|
||||||
|
tokens and other secrets remain forbidden storage keys.
|
||||||
@@ -30,6 +30,11 @@ registry: merge evidence through the PR decision, coherent release evidence
|
|||||||
through the next release promotion, drill evidence through the next production
|
through the next release promotion, drill evidence through the next production
|
||||||
promotion, and field evidence through aggregation.
|
promotion, and field evidence through aggregation.
|
||||||
|
|
||||||
|
Browser-backed merge gates install and execute the pinned Chromium, Firefox,
|
||||||
|
and WebKit engines. This makes route behavior, reflow, native dialog semantics,
|
||||||
|
theme persistence, and automated accessibility a cross-engine contract rather
|
||||||
|
than a Chromium-only smoke check.
|
||||||
|
|
||||||
Repository variables required by higher tiers:
|
Repository variables required by higher tiers:
|
||||||
|
|
||||||
- `HOSTING_BASE_URL` for live header verification
|
- `HOSTING_BASE_URL` for live header verification
|
||||||
|
|||||||
@@ -1,8 +1,32 @@
|
|||||||
# Design-token styling contract
|
# Design-token styling contract
|
||||||
|
|
||||||
`src/presentation/styles/theme.css` is the styling SSOT. Components consume
|
`src/presentation/styles/theme.css` is the styling SSOT. Components consume
|
||||||
semantic color, spacing, typography, and radius tokens through static Tailwind
|
semantic color, spacing, typography, status, and radius tokens through static
|
||||||
classes.
|
classes. `/examples/ui` is the executable token and primitive gallery.
|
||||||
|
|
||||||
|
## Theme contract
|
||||||
|
|
||||||
|
The supported public preference is the closed set `system`, `light`, and
|
||||||
|
`dark`. `ThemeProvider` reads and writes `COLOR_SCHEME` through the application
|
||||||
|
storage port; it does not access a raw storage key. `system` subscribes to
|
||||||
|
`prefers-color-scheme` changes. Bootstrap applies the persisted preference
|
||||||
|
before React paints.
|
||||||
|
|
||||||
|
Components use semantic tokens such as `--color-panel`, `--color-content`,
|
||||||
|
`--color-border`, `--color-action`, and status surface/content/border triples.
|
||||||
|
They must not hard-code a light-only panel or text color. Both light and dark
|
||||||
|
surfaces are included in automated axe checks.
|
||||||
|
|
||||||
|
## Included primitives
|
||||||
|
|
||||||
|
- `Button`: primary, secondary, danger, and ghost intent
|
||||||
|
- `TextField`: label, help text, required state, and associated validation error
|
||||||
|
- `Card`: labelled surface with optional footer
|
||||||
|
- `Alert` and `Badge`: non-color-only status feedback
|
||||||
|
- `Dialog`: native modal semantics, Escape/backdrop close, and trigger focus
|
||||||
|
restoration
|
||||||
|
- async and access state surfaces: loading, refresh, empty, terminal error,
|
||||||
|
auth required, forbidden, and not found
|
||||||
|
|
||||||
Arbitrary-value policy:
|
Arbitrary-value policy:
|
||||||
|
|
||||||
@@ -13,5 +37,5 @@ Arbitrary-value policy:
|
|||||||
- user-controlled or runtime-composed class strings are forbidden
|
- user-controlled or runtime-composed class strings are forbidden
|
||||||
- class variants must be selected from a closed static map
|
- class variants must be selected from a closed static map
|
||||||
|
|
||||||
The removable sample may demonstrate tokens, but product modules must not
|
The removable sample may demonstrate tokens, but production starter modules do
|
||||||
import from `src/sample/contract-fixture`.
|
not import from `src/sample/contract-fixture`.
|
||||||
|
|||||||
@@ -12,6 +12,13 @@ Each gate is blocking in its declared scope. Failures are not downgraded with
|
|||||||
| end-to-end | `pnpm test:e2e` | `artifacts/tests/e2e/` |
|
| end-to-end | `pnpm test:e2e` | `artifacts/tests/e2e/` |
|
||||||
| accessibility | `pnpm test:a11y` | `artifacts/tests/a11y.json` |
|
| accessibility | `pnpm test:a11y` | `artifacts/tests/a11y.json` |
|
||||||
|
|
||||||
|
End-to-end and automated accessibility scenarios run on the pinned Chromium,
|
||||||
|
Firefox, and WebKit engines. The responsive contract explicitly exercises
|
||||||
|
320px reflow, a mobile navigation drawer, and a wide two-column gallery.
|
||||||
|
Theme scenarios verify persistence, operating-system changes, and dark-surface
|
||||||
|
axe results. Native modal focus and validation association are exercised in
|
||||||
|
both component and browser tests.
|
||||||
|
|
||||||
A control is verified only when a positive fixture passes and its deliberately
|
A control is verified only when a positive fixture passes and its deliberately
|
||||||
failing negative fixture is rejected. Generated evidence is retained by CI;
|
failing negative fixture is rejected. Generated evidence is retained by CI;
|
||||||
the repository tracks only the evidence directory structure.
|
the repository tracks only the evidence directory structure.
|
||||||
|
|||||||
@@ -19,5 +19,7 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
projects: [
|
projects: [
|
||||||
{ name: "chromium", use: { ...devices["Desktop Chrome"] } },
|
{ name: "chromium", use: { ...devices["Desktop Chrome"] } },
|
||||||
|
{ name: "firefox", use: { ...devices["Desktop Firefox"] } },
|
||||||
|
{ name: "webkit", use: { ...devices["Desktop Safari"] } },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
export const MANUAL_A11Y_ROUTE_IDS = Object.freeze([
|
export const MANUAL_A11Y_ROUTE_IDS = Object.freeze([
|
||||||
"APP_HOME",
|
"APP_HOME",
|
||||||
|
"EXAMPLES_UI",
|
||||||
|
"EXAMPLES_STATES",
|
||||||
|
"EXAMPLES_AUTH",
|
||||||
"SAMPLE_RESOURCE_LIST",
|
"SAMPLE_RESOURCE_LIST",
|
||||||
"NOT_FOUND",
|
"NOT_FOUND",
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import process from "node:process";
|
|||||||
import { chromium } from "@playwright/test";
|
import { chromium } from "@playwright/test";
|
||||||
|
|
||||||
import { evaluateLabBudget } from "../src/application/policies/performance-budgets.js";
|
import { evaluateLabBudget } from "../src/application/policies/performance-budgets.js";
|
||||||
|
import { ROUTE_REGISTRY } from "../src/contracts/routes.js";
|
||||||
|
|
||||||
const server = spawn(
|
const server = spawn(
|
||||||
"corepack",
|
"corepack",
|
||||||
@@ -66,8 +67,13 @@ try {
|
|||||||
}).observe({ type: "layout-shift", buffered: true });
|
}).observe({ type: "layout-shift", buffered: true });
|
||||||
});
|
});
|
||||||
await page.goto(baseUrl, { waitUntil: "networkidle" });
|
await page.goto(baseUrl, { waitUntil: "networkidle" });
|
||||||
|
const targetLabel =
|
||||||
|
ROUTE_REGISTRY.SAMPLE_RESOURCE_LIST.navigationLabel;
|
||||||
|
if (!targetLabel) {
|
||||||
|
throw new Error("Performance route must be present in navigation.");
|
||||||
|
}
|
||||||
const interactionStarted = performance.now();
|
const interactionStarted = performance.now();
|
||||||
await page.getByRole("link", { name: "샘플 리소스" }).click();
|
await page.getByRole("link", { name: targetLabel }).click();
|
||||||
await page.getByRole("heading", { name: "세션이 필요합니다." }).waitFor();
|
await page.getByRole("heading", { name: "세션이 필요합니다." }).waitFor();
|
||||||
const namedInteractionMs = performance.now() - interactionStarted;
|
const namedInteractionMs = performance.now() - interactionStarted;
|
||||||
const paint = await page.evaluate(
|
const paint = await page.evaluate(
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { mkdir, writeFile } from "node:fs/promises";
|
import { mkdir, writeFile } from "node:fs/promises";
|
||||||
|
|
||||||
|
import { MANUAL_A11Y_ROUTE_IDS } from "./lib/manual-a11y-evidence.mjs";
|
||||||
|
|
||||||
await mkdir("artifacts/tests", { recursive: true });
|
await mkdir("artifacts/tests", { recursive: true });
|
||||||
await writeFile(
|
await writeFile(
|
||||||
"artifacts/tests/a11y.json",
|
"artifacts/tests/a11y.json",
|
||||||
@@ -7,7 +9,7 @@ await writeFile(
|
|||||||
{
|
{
|
||||||
schemaVersion: 1,
|
schemaVersion: 1,
|
||||||
generatedAt: new Date().toISOString(),
|
generatedAt: new Date().toISOString(),
|
||||||
scope: ["APP_HOME", "SAMPLE_RESOURCE_LIST", "NOT_FOUND"],
|
scope: MANUAL_A11Y_ROUTE_IDS,
|
||||||
threshold: { critical: 0, serious: 0 },
|
threshold: { critical: 0, serious: 0 },
|
||||||
automatedStatus: "passed",
|
automatedStatus: "passed",
|
||||||
manualReview: "see artifacts/tests/a11y-manual/report.json",
|
manualReview: "see artifacts/tests/a11y-manual/report.json",
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { expect, test } from "@playwright/test";
|
||||||
|
|
||||||
|
test("reflows the UI gallery at the 320px minimum without horizontal overflow", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
await page.setViewportSize({ width: 320, height: 720 });
|
||||||
|
await page.goto("/examples/ui");
|
||||||
|
await expect(
|
||||||
|
page.getByRole("heading", { level: 1, name: "UI 구성요소" }),
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
page.getByRole("button", { name: "메뉴", exact: true }),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
const viewport = await page.evaluate(() => ({
|
||||||
|
clientWidth: document.documentElement.clientWidth,
|
||||||
|
scrollWidth: document.documentElement.scrollWidth,
|
||||||
|
}));
|
||||||
|
expect(viewport.scrollWidth).toBeLessThanOrEqual(viewport.clientWidth);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("keeps desktop navigation and two-column examples at wide viewports", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
await page.setViewportSize({ width: 1440, height: 900 });
|
||||||
|
await page.goto("/examples/ui");
|
||||||
|
|
||||||
|
await expect(page.getByRole("navigation", { name: "주요 탐색" })).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
page.getByRole("button", { name: "메뉴", exact: true }),
|
||||||
|
).toBeHidden();
|
||||||
|
await expect(page.locator(".component-grid--two").first()).toHaveCSS(
|
||||||
|
"grid-template-columns",
|
||||||
|
/.+px .+px/,
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -1,6 +1,10 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
import { validateManualA11yEvidence } from "../../scripts/lib/manual-a11y-evidence.mjs";
|
import {
|
||||||
|
MANUAL_A11Y_ROUTE_IDS,
|
||||||
|
validateManualA11yEvidence,
|
||||||
|
} from "../../scripts/lib/manual-a11y-evidence.mjs";
|
||||||
|
import { ROUTE_REGISTRY } from "../../src/contracts/routes.js";
|
||||||
|
|
||||||
const reviewed = `Status: reviewed
|
const reviewed = `Status: reviewed
|
||||||
Route ID: APP_HOME
|
Route ID: APP_HOME
|
||||||
@@ -20,6 +24,12 @@ Screen reader: pass
|
|||||||
Notes: no defects`;
|
Notes: no defects`;
|
||||||
|
|
||||||
describe("manual accessibility evidence", () => {
|
describe("manual accessibility evidence", () => {
|
||||||
|
it("requires evidence for every registered user-visible route", () => {
|
||||||
|
expect([...MANUAL_A11Y_ROUTE_IDS].sort()).toEqual(
|
||||||
|
Object.keys(ROUTE_REGISTRY).sort(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it("accepts a complete signed human review record", () => {
|
it("accepts a complete signed human review record", () => {
|
||||||
expect(validateManualA11yEvidence(reviewed)).toMatchObject({
|
expect(validateManualA11yEvidence(reviewed)).toMatchObject({
|
||||||
failures: [],
|
failures: [],
|
||||||
|
|||||||
Reference in New Issue
Block a user