Files
HealthLog/tests/fixtures/v137/e2e-journeys.ts
T
Marc-André Bombeck d44af06467 docs: publish the UI standards in docs/contributing
The binding UI rulebook (card anatomy, spacing scale, text colour,
tokens, primitive mapping, enforcement) lived outside the repository,
so a contributor could follow a code comment to a file they could not
open. It now lives at docs/contributing/ui-standards.md, every comment
that cited it points there, and the docs index and ui-guidelines.md
say which document wins when they disagree.
2026-10-21 19:37:22 +02:00

177 lines
6.9 KiB
TypeScript

/**
* What a browser has to prove about v1.37 sharing, enumerated.
*
* Two lists, and both exist for the same reason: a browser suite reports what
* it ran, never what it did not. A state nobody wrote a test for and a journey
* somebody quietly dropped both look like a green run. Naming them in one
* place that a fast test reads back turns "we cover the error state" from a
* claim in a summary into something that fails.
*
* Nothing here drives a browser. The enumeration is the contract; the spec is
* the implementation, and `src/__tests__/sharing-accessibility-states.test.ts`
* is what holds the two together.
*/
/* -------------------------------------------------------------------------- */
/* The five states */
/* -------------------------------------------------------------------------- */
/**
* Every state a shared-record surface can be in, and all five have to be
* usable — not merely reachable.
*
* Accessibility work concentrates on the state where everything worked,
* because that is the state somebody building the screen is looking at. The
* other four are where a person who cannot see the screen is most stranded: a
* spinner nobody announces, an error card that reads as an empty page, a
* refusal whose only way out is a button no keyboard reaches.
*/
export const ACCESSIBILITY_STATES = [
"success",
"empty",
"loading",
"error",
"refusal",
] as const;
export type AccessibilityState = (typeof ACCESSIBILITY_STATES)[number];
export interface AccessibilityStateCase {
readonly state: AccessibilityState;
/**
* The `data-slot` that proves the state is actually on screen.
*
* A scan of a page that never entered the state passes trivially, so every
* case names the element that says it arrived, and the fast guard checks the
* component tree really renders that slot.
*/
readonly anchor: string;
/** The module that renders {@link anchor}, so a state cannot be invented. */
readonly rendersIn: string;
/**
* The control keyboard focus must reach, or `null` where the state offers
* none at all. A loading state has nothing to press; every other state here
* has a way forward, and a way forward a keyboard cannot reach is not one.
*/
readonly keyboardTarget: string | null;
/**
* Copy the person has to read and act on, which must render as content
* rather than as muted meta (ui-standards.md §3). `null` where the state's only
* text is a screen-reader label, which has no visual contrast to check.
*/
readonly contrastTarget: string | null;
/** Why this state is on the list, in one line. */
readonly claim: string;
}
export const SHARING_ACCESSIBILITY_STATES = [
{
state: "success",
anchor: '[data-slot="profile-summary"]',
rendersIn: "src/components/records/profile-summary.tsx",
keyboardTarget: '[data-slot="shared-record-banner-exit"]',
contrastTarget: '[data-slot="shared-record-banner-context"]',
claim:
"inside somebody else's record, the line saying whose it is and the way out are both usable",
},
{
state: "empty",
anchor: '[data-slot="grants-given-card"] [data-slot="empty-state"]',
rendersIn: "src/components/ui/empty-state.tsx",
keyboardTarget: '[data-slot="grant-invite-identifier"]',
contrastTarget:
'[data-slot="grants-given-card"] [data-slot="empty-state"] p',
claim:
"nothing shared yet reads as an answer rather than as a page that failed to load",
},
{
state: "loading",
anchor: '[data-slot="record-scope-hydration-gate"]',
rendersIn: "src/components/layout/record-scope-hydration-gate.tsx",
keyboardTarget: null,
contrastTarget: null,
claim:
"the hold before the active record is known is announced, not a silent blank",
},
{
state: "error",
anchor: '[data-slot="query-error-card"]',
rendersIn: "src/components/ui/query-error-card.tsx",
keyboardTarget: '[data-slot="query-error-retry"]',
contrastTarget: '[data-slot="query-error-card"] p',
claim: "a failed read says so and offers the retry to a keyboard",
},
{
state: "refusal",
anchor: '[data-slot="shared-record-unavailable"]',
rendersIn: "src/components/layout/shared-record-unavailable.tsx",
keyboardTarget: '[data-slot="shared-record-unavailable-leave"]',
contrastTarget: '[data-slot="shared-record-unavailable"] p',
claim:
"a surface sharing does not cover explains itself and keeps a way back to one's own record",
},
] as const satisfies readonly AccessibilityStateCase[];
/* -------------------------------------------------------------------------- */
/* The eight journeys */
/* -------------------------------------------------------------------------- */
export interface JourneyClaim {
/** Stable id, quoted in the spec's test title so the two can be matched. */
readonly id: string;
/** What the journey proves, in the release's own words. */
readonly claim: string;
}
/**
* The eight end-to-end proofs the release is required to run, transcribed from
* the phase context rather than restated: a paraphrase is where a requirement
* quietly narrows.
*
* The list is here rather than in a spec file because more than one spec runs
* parts of it, and because the integration plan reconciles the whole set at
* the end. An id that no test title mentions is a journey nobody ran.
*/
export const V137_E2E_JOURNEYS = [
{
id: "J1-adult-levels-lifecycle",
claim:
"Adult full READ, scoped READ, WRITE and MANAGE through invite, accept, switch, expiry, revoke and direct URL.",
},
{
id: "J2-manage-edits-without-settings",
claim:
"Adult MANAGE edits and deletes with specific activity, and zero Guardian-only settings or integration reach.",
},
{
id: "J3-managed-profile-lifecycle",
claim:
"Truthful managed-profile creation, two Guardians, concurrent last-Guardian protection, record settings and deletion.",
},
{
id: "J4-guardian-notification-matrix",
claim:
"Two Guardians by two profiles through APNs, Web Push, Telegram and ntfy with recipient locale and preferences, non-interactive payloads and subject/recipient attempts.",
},
{
id: "J5-revoked-idempotent-replay",
claim:
"Revoked MANAGE idempotency replay returns 403, no cached DTO and no second mutation.",
},
{
id: "J6-no-delegated-provider-egress",
claim:
"An invited MANAGE mutation with a configured provider produces no provider job or call; the owner positive control enqueues.",
},
{
id: "J7-durable-import-actor",
claim:
"Asynchronous import completion retains record, actor and job identity.",
},
{
id: "J8-old-client-compatibility",
claim:
"Old-client compatibility preserves legacy decode, scoped fail-closed behaviour and MANAGE decode until the canonical client is ready.",
},
] as const satisfies readonly JourneyClaim[];