From 3ee62a1905df9ea4d07b1721ff99fe9426201ce2 Mon Sep 17 00:00:00 2001 From: meh Date: Sat, 12 Sep 2026 05:57:11 +0700 Subject: [PATCH 1/9] fix(icon): stop emitting a phantom Iconify token --- docs/how-to-setup-playground.md | 2 +- src/components/icon/Icon.css | 2 +- src/components/icon/Icon.interactions.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/how-to-setup-playground.md b/docs/how-to-setup-playground.md index 443bb132..57ec9d61 100644 --- a/docs/how-to-setup-playground.md +++ b/docs/how-to-setup-playground.md @@ -76,7 +76,7 @@ export default defineConfig({ @source "./"; @source "../../UI/src"; -/* The library ships no glyphs. This is what resolves icon-[...] tokens, and it +/* The library ships no glyphs. This resolves Iconify utility tokens, and it scans YOUR source, so an icon only exists if you wrote it. */ @plugin "@iconify/tailwind4"; ``` diff --git a/src/components/icon/Icon.css b/src/components/icon/Icon.css index 3f971336..0582b2a0 100644 --- a/src/components/icon/Icon.css +++ b/src/components/icon/Icon.css @@ -17,7 +17,7 @@ /* * The placeholder. It fills the box and paints nothing on its own: the mark - * arrives either as the application's generated `icon-[...]` rule, which masks + * arrives either as the application's generated Iconify utility, which masks * and paints `background-color: currentColor`, or as an inline SVG child. * Either way the glyph inherits `color` from the root, which is the whole * reason the flavour lives one level up. diff --git a/src/components/icon/Icon.interactions.ts b/src/components/icon/Icon.interactions.ts index f6e460e9..a891a2de 100644 --- a/src/components/icon/Icon.interactions.ts +++ b/src/components/icon/Icon.interactions.ts @@ -12,7 +12,7 @@ */ const WRAPPED = /^icon-\[(.+)\]$/; -/** Strip the `icon-[...]` wrapper if the caller supplied one. */ +/** Strip an Iconify utility wrapper if the caller supplied one. */ export function normalizeToken(token: string): string { const wrapped = WRAPPED.exec(token.trim()); return wrapped ? wrapped[1] : token.trim(); From 4a04b24bf578b4466bf8017e977d5e6420e85dc0 Mon Sep 17 00:00:00 2001 From: meh Date: Sat, 12 Sep 2026 09:21:02 +0700 Subject: [PATCH 2/9] fix(immersive-landing): keep the latest navigation --- .../immersive-landing/useImmersiveLanding.ts | 112 ++++++++++++------ 1 file changed, 76 insertions(+), 36 deletions(-) diff --git a/src/components/immersive-landing/useImmersiveLanding.ts b/src/components/immersive-landing/useImmersiveLanding.ts index 0595035c..96e27a45 100644 --- a/src/components/immersive-landing/useImmersiveLanding.ts +++ b/src/components/immersive-landing/useImmersiveLanding.ts @@ -26,38 +26,43 @@ export function useImmersiveLanding( const [internalPage, setInternalPage] = createSignal(initialPage); const [isTransitioning, setIsTransitioning] = createSignal(false); const [direction, setDirection] = createSignal<"next" | "prev" | null>(null); + let pendingPage: string | undefined; + let expectedControlledPage: string | undefined; + let transitionTimer: ReturnType | undefined; - const activePage = isControlled ? controlledPage! : internalPage; + const activePage = controlledPage ?? internalPage; const currentIndex = () => pages.indexOf(activePage()); const isFirstPage = () => currentIndex() === 0; const isLastPage = () => currentIndex() === pages.length - 1; - // In controlled mode, animate transitions triggered by external page changes (e.g. browser back/forward) - if (isControlled) { - let prevPage = controlledPage!(); - createTrackedEffect(() => { - const next = controlledPage!(); - if (next !== prevPage && !isTransitioning()) { - const fromIndex = pages.indexOf(prevPage); - const toIndex = pages.indexOf(next); - if (toIndex >= 0) { - setDirection(toIndex > fromIndex ? "next" : "prev"); - setIsTransitioning(true); - setTimeout(() => { - setIsTransitioning(false); - setDirection(null); - }, transitionDuration); - } - prevPage = next; - } - }); - } + const completeTransition = (pageId: string) => { + transitionTimer = undefined; + setIsTransitioning(false); + setDirection(null); - const navigateToInternal = (pageId: string) => { - if (isTransitioning() || !pages.includes(pageId)) return; - if (pageId === activePage()) return; + if (expectedControlledPage === pageId && activePage() !== pageId) { + expectedControlledPage = undefined; + } + + const pageElement = document.getElementById(pageId); + if (pageElement) { + pageElement.focus({ preventScroll: true }); + } - const fromPage = activePage(); + onNavigationComplete?.(pageId); + + const nextPage = pendingPage; + pendingPage = undefined; + if (nextPage && nextPage !== activePage()) { + queueMicrotask(() => navigateToInternal(nextPage)); + } + }; + + const beginTransition = ( + fromPage: string, + pageId: string, + notifyNavigation: boolean, + ) => { const fromIndex = pages.indexOf(fromPage); const toIndex = pages.indexOf(pageId); @@ -65,21 +70,52 @@ export function useImmersiveLanding( setIsTransitioning(true); if (!isControlled) setInternalPage(pageId); - if (onNavigate) onNavigate(fromPage, pageId); + if (notifyNavigation) { + if (isControlled) expectedControlledPage = pageId; + onNavigate?.(fromPage, pageId); + } + + transitionTimer = setTimeout( + () => completeTransition(pageId), + transitionDuration, + ); + }; + + function navigateToInternal(pageId: string) { + if (!pages.includes(pageId)) return; + if (isTransitioning()) { + pendingPage = pageId === activePage() ? undefined : pageId; + return; + } + if (pageId === activePage()) return; + + beginTransition(activePage(), pageId, true); + } + + // Route changes initiated by `onNavigate` already own their transition. + // Animate only genuinely external changes such as browser back/forward. + if (controlledPage) { + let previousPage = controlledPage(); + createTrackedEffect(() => { + const nextPage = controlledPage(); + if (nextPage === previousPage) return; - setTimeout(() => { - setIsTransitioning(false); - setDirection(null); + const fromPage = previousPage; + previousPage = nextPage; - // Focus management for accessibility - const pageElement = document.getElementById(pageId); - if (pageElement) { - pageElement.focus({ preventScroll: true }); + if (nextPage === expectedControlledPage) { + expectedControlledPage = undefined; + return; + } + if (!pages.includes(nextPage)) return; + if (isTransitioning()) { + pendingPage = nextPage; + return; } - if (onNavigationComplete) onNavigationComplete(pageId); - }, transitionDuration); - }; + beginTransition(fromPage, nextPage, false); + }); + } const navigateTo = (pageId: string) => navigateToInternal(pageId); @@ -204,6 +240,10 @@ export function useImmersiveLanding( }; }); + onCleanup(() => { + if (transitionTimer !== undefined) clearTimeout(transitionTimer); + }); + return { activePage, isTransitioning, From 9042cdef4a5146a5e43ff5139b68d560320623cd Mon Sep 17 00:00:00 2001 From: meh Date: Sat, 12 Sep 2026 09:53:30 +0700 Subject: [PATCH 3/9] docs(release): reconcile fleet readiness --- docs/release-readiness-2026-09-12.md | 363 ++++++++++++--------------- 1 file changed, 165 insertions(+), 198 deletions(-) diff --git a/docs/release-readiness-2026-09-12.md b/docs/release-readiness-2026-09-12.md index 8beb0e32..c0f1a4eb 100644 --- a/docs/release-readiness-2026-09-12.md +++ b/docs/release-readiness-2026-09-12.md @@ -1,199 +1,166 @@ -# UI release review — 12 September 2026 - -Status: **UI 3.2.0 package release approved and locally verified**. Website -deployments remain separately reviewed. Before creating a Fly dev instance, -contact the owner so they can be online for questions. - -## Concrete TODO - -The release script computes **3.2.0** from npm's 3.1.0 baseline and the branch's -conventional commits. Local release verification is complete; the repository's -release workflow assigns and publishes the version after the fast-forward. - -- [x] Build the candidate ps-qa and font-enabled host; verify post-action paint, - explicit gridcell targets, keyboard shortcuts, and transformed pointer actions. -- [x] Verify ps-blitz CI with the pinned coordinated stack. Run 34632611122 - passes at 97ca22ca, including the WebSocket listener and contrast-settling fixes. -- [x] Build and pack fresh UI source with solid-layouts 0.2.4; run the full native - component sweep, API/package gates, and consumer builds against the packed candidate. -- [x] Resolve Honey's cold first-submit failure and run the expanded 196-check suite, - including Platform Admin, App Admin, Guest, and password change/restore. -- [x] Finish reproducible recovery-code coverage. TOTP confirmation and Telegram - enrollment/login remain separate security gates for Honey. -- [x] Verify Worktables editing, cancellation, undo, findings, and zoom (112/112). -- [x] Confirm the packed Calendar fix in js.software (316/316). -- [x] Resolve Web3 carousel settling. Its complete final run passes 103/103 after - the landing surface was scoped away from the independently animated chat halo. -- [x] Repeat scoped site E2E against the final package, recording missing backend - contracts separately from library regressions. -- [x] Push the verified library, driver, runtime, and host changes to their - existing release branches and reconcile their descriptions with local evidence. -- [x] Release ps-blitz 0.4.8, blitz-control-protocol 0.5.0, ps-qa 0.7.1, - tauri-runtime-blitz 0.4.0, and the Chuzz 0.1.37 host in dependency order. -- [ ] Release UI 3.2.0 and verify a fresh consumer install from npm. -- [ ] Deploy only the separately reviewed website changes. - -## Release sequence - -1. [ps-blitz #95](https://github.com/pathscale/ps-blitz/pull/95): 0.4.8 published. -2. [ps-observability #21](https://github.com/pathscale/ps-observability/pull/21): - blitz-control-protocol 0.5.0 and ps-qa 0.7.1 published against that engine. -3. [tauri-runtime-blitz #57](https://github.com/pathscale/tauri-runtime-blitz/pull/57): - 0.4.0 published against the shared protocol. -4. [chuzz #45](https://github.com/pathscale/chuzz/pull/45) and - [#46](https://github.com/pathscale/chuzz/pull/46): shared document actions, - headless build gating, and the signed 0.1.37 host published. -5. [UI #289](https://github.com/pathscale/UI/pull/289): publish 3.2.0 after the - packaged library and its consumers passed against the released host and driver. -6. Review and deploy approved website PRs using the published library. - -The older handover put tauri-runtime-blitz before ps-observability. Its manifest -requires protocol 0.5, so that order cannot resolve. Registry dependency failures -before the upstream publications are expected and are separate from regressions. - -`solid-layouts` 0.2.4 has already published through -[PR #19](https://github.com/pathscale/solid-layouts/pull/19). It forwards caller -styles to the root slot; without it, UI Card positions and dimensions are dropped. -UI and consumer manifest floors and resolvable lockfiles are updated. UI has the -published 0.2.4 installed; final clean consumer installs remain part of this gate. -Worktables' clean install still awaits the separate house DSL SDK 0.1.2 release. - -## Library and harness findings - -| Finding | Current evidence | Remaining verification | +# UI and website release readiness — 12 September 2026 + +Status: **UI 3.2.1 is ready for owner review.** No known UI library defect +blocks the patch release. UI 3.2.0 is already published; the current patch +candidate fixes the two regressions found while proving its consumers. + +No merge, package publication, or deployment is authorized by this document. +All evidence below is from local builds and native `ps-qa` runs. CI is a +secondary integration signal, not QA evidence. + +## What changed after UI 3.2.0 + +[UI #292](https://github.com/pathscale/UI/pull/292) contains two fixes: + +- UI's shipped CSS contained an Icon documentation placeholder shaped like an + Iconify utility. Consumer production builds therefore printed + `Invalid icon name: "..."`. The placeholder is gone from source, docs, and + the built package. +- `ImmersiveLanding` discarded the latest destination when a controlled caller + selected another slide during an active transition. It now preserves that + destination, avoids a duplicate timer when its own callback updates the + controlled route, and cleans up cancelled work. + +The conventional release calculation resolves this branch to **3.2.1**. + +## UI release gate + +The exact branch at `4a04b24` passes: + +- 93 component contracts; +- TypeScript and the 547-file library build; +- 320/320 Bun tests; +- 75/75 native component outcomes through `chuzz-headless`; +- the package/export gate across 1,002 shipped files; +- strict publint, with one non-blocking suggestion; +- a fresh consumer install, typecheck, Layout registration load, and browser + bundle; +- `npm pack --dry-run`; +- Pathscale's coordinated local site/backend/Honey run at 138/138; +- the focused Pathscale rapid-carousel countercheck: UI 3.2.0 fails 21/22, + while this candidate passes 22/22. + +Exact packed-candidate consumer runs also pass: + +| Consumer | Native result | Package-specific result | +| --- | ---: | --- | +| Web3 Trading | 103/103 | Production build has no phantom UI Iconify warning | +| NoFilter | 132/132 | Its separate documentation placeholder was corrected on PR #340; rebuilt output is clean | +| Pays | 45/45 | Expanded application-id refusal passes; production build has no phantom UI Iconify warning | +| Honey public surface | 13/13 | Production build has no phantom UI Iconify warning | + +## Harness patch + +[ps-observability #20](https://github.com/pathscale/ps-observability/pull/20) +fixes driver defects discovered during the fleet sweep. It records visible +preparation and timed actions and corrects protocol handling. The branch at +`6442d1d` passes formatting, clippy with all features, 88/88 protocol tests, +150/150 ps-qa tests, and the CLI tests. + +Its required publication order is: + +1. owner review and merge of ps-observability #20; +2. publish `blitz-control-protocol` 0.5.1; +3. rebuild the native hosts against that protocol; +4. publish `ps-qa` 0.7.2; +5. rerun the site suites with the published driver and rebuilt host. + +The earlier one-control-surface dependency chain is complete: +`ps-blitz-dom` 0.4.8, `ps-blitz-debug-control` 0.3.8, +`tauri-runtime-blitz` 0.4.0, `blitz-control-protocol` 0.5.0, `ps-qa` 0.7.1, +and Chuzz 0.1.37 are published. ps-blitz #95's option regression was fixed and +merged; overlapping rescue PR #96 was closed. + +## Website and application review queue + +Every PR in this table is open and mergeable as of this review. Counts are +fresh local native results from the named PR branches. A passing surface suite +proves the behaviors it names; it does not imply an unavailable backend or an +uncovered product workflow works. + +| Repository / existing PR | Local evidence | Release boundary | | --- | --- | --- | -| Native option labels and selected state disappeared in the control refactor | Six regression tests restored; the native Select fixture passes all 7 outcomes with published ps-blitz 0.4.8, protocol 0.5.0, ps-qa 0.7.1, and Chuzz 0.1.37 | Complete | -| Transformed client rectangles disagreed with painted controls | Four geometry tests, inline fragment tests, and full Linux suite pass; final pointer fixture and Worktables zoom/drag checks pass | CI host boundary and published integration | -| CI needs the coordinated unpublished runtime stack | Exact dependency revisions pinned; nested workspace exclusions resolved Cargo inheritance. Coordinated CI run 34632611122 passes with refreshed socket/contrast revisions | Verify registry integration after approval | -| Calendar cells captured selection state once | Cell state is now reactive; six native checks verify selection changes and controlled callbacks in both directions. Packed js.software run passes 316/316 | Owner review | -| Honey CreateApp stalled during socket connection | chuzz iterated a live listener array; the first RPC removed its open listener and skipped the next. Snapshot dispatch fixes three consecutive 15-step lifecycles and the full 192-check suite; refreshed coordinated CI passes | GUI build review and registry integration | -| Responsive layout classes were purged from consumers | Purge manifest includes responsive Grid/Flex classes; 24x landing checks passed | Full consumer rebuilds from the final package | -| Form submission discarded schema output | Typed schema output preserved; six native form checks and Honey's expanded suite pass | Owner review | -| ConnectionSettings missing from layout manifest | Export added; isolated package consumer build passed | Pays runtime checks | -| A fontless host weakens paint checks | The font-enabled release host builds; a fresh local sweep against the public dependency stack passes 273 checks across 75 component fixtures | Complete | -| ps-qa measured paint before input, then sampled contrast transitions too early | Reads moved after input; contrast honors the outcome and stability windows. Six native driver scenarios pass, including delayed repair and persistent contrast failure. Web3 theme group passes 18/18 | Final stack sweeps | -| ps-qa rejected explicit gridcell targets | Explicit role selectors now bypass the generic inventory role list while retaining actionability checks; native Calendar and packed js.software checks pass | Owner review | -| ps-qa's older drag diagnostic only scrolls containers | Actual pointer dragging/cancellation added to CLI and declarative checks; native fixture and Worktables movement/cancellation/undo checks pass | PR and CI review | -| UI sweep could accept bundles older than the library source | Staleness guard now includes library source and package output; confirmed it rejects the current outdated bundle before launching a host | Fresh full build and sweep | - -Use `/Users/revenge/code/ps-observability/target/debug/ps-qa` (0.7.1) and -`/Users/revenge/code/chuzz/target/release/chuzz-headless` for local candidate runs. -The installed `~/.cargo/bin/ps-qa` was 0.6.3 and is not the release candidate. -`qa-hosted --checks` takes a directory. Build before running: stale built files do -not verify source changes. Keep pointer, keyboard, scroll, paint, and persistence -failures visible; do not replace them with presence checks to obtain a pass. - -## Website scope - -All local checkouts are under `/Users/revenge/code`; remotes are `pathscale/`. -Counts below are earlier observed runs, not a final release verdict. They do not -prove that every product feature is covered, and must be repeated against the -final package and runtime. Check definitions have changed since some runs. - -| Repository / existing PR | Observed result or blocker | -| --- | --- | -| [honey.id #332](https://github.com/pathscale/honey.id/pull/332) | Expanded suite passes 193/196. The three failures truthfully identify the backend's empty API-key regeneration response; the dedicated error flow passes 18/18. Recovery-code generation, two login/rotation cycles, old-code rejection, save gates, unchanged-password login, and sign-out pass in a reusable 33/33 runner. TOTP and Telegram remain unverified. | -| [worktables.dev #9](https://github.com/pathscale/worktables.dev/pull/9) | UI/SVG editor replaces Cytoscape/ELK. Final packed-package run passes 112/112, including 33 designer and 7 findings checks. Real pointer movement, cancellation, undo, zoom, and emitted schema edits pass. Visual inspection confirms the UI cards, native SVG relationships, toolbar, and inspector. Clean install still awaits house DSL SDK 0.1.2. | -| [crates.vip #1](https://github.com/pathscale/crates.vip/pull/1) | Final packed-package run passes 71/71; authentication is deliberately bypassed in both client and backend, so this is not evidence of authenticated role coverage. | -| [24x.ai #11](https://github.com/pathscale/24x.ai/pull/11) | Final packed-package runs pass 141/141 at desktop width and 20/20 at phone width. The dev login still authenticates under Honey's dev application because 24x has no usable dev registration of its own. | -| [js.software #53](https://github.com/pathscale/js.software/pull/53) | Packed Calendar fix passes 316/316. CI includes all declared groups. | -| [kard.vip #8](https://github.com/pathscale/kard.vip/pull/8) | Final packed-package run passes 223/223; demo actions do not prove payment functionality. | -| [nofilter.io #340](https://github.com/pathscale/nofilter.io/pull/340) | Final packed-package run passes 129/129. | -| [pathscale.com #17](https://github.com/pathscale/pathscale.com/pull/17) | The public/UI flow and carousel pass against the final package. The dev run passes 105/124: the known username and wrong-password refusal work, but the correct-password callback does not create a protected session, stranding 19 dependent portal/settings checks. Owner confirmed the old portal has little value and need not block UI. Configured `pathscale-be` no longer exists; do not recreate without a reviewed deployment plan. | -| [pays.online #166](https://github.com/pathscale/pays.online/pull/166) | Packed-package typecheck and build pass after adopting shared ConnectionSettings. There is no ps-qa profile. Wallet settings call methods absent from the backend schema; onboarding contains unfinished handlers. The configured Honey app id is a UUID rather than the required 16-character public id. Real payment actions require a defined dev setup and review. | -| [promptsyntax.org #18](https://github.com/pathscale/promptsyntax.org/pull/18) | Final packed-package run passes 129/129. | -| [support.cafe #12](https://github.com/pathscale/support.cafe/pull/12) | Final packed-package run passes 104/104. | -| [web3.trading #18](https://github.com/pathscale/web3.trading/pull/18) | Final packed-package run passes 103/103. Theme contrast settles correctly, the last carousel slide remains stable for 500ms, and the guest chat closes. | -| [ui-starter-app #13](https://github.com/pathscale/ui-starter-app/pull/13) | Final packed-package run passes 144/144. | -| [agencyzero #211](https://github.com/pathscale/agencyzero/pull/211), [#212](https://github.com/pathscale/agencyzero/pull/212) | UI and control integration in scope; core-specific features are handed to a dedicated owner after UI is ready. | - -Honey verification must cover **Platform Admin, App Admin, and Guest** with real -allowed and denied behavior. Application creation, saved edits, deletion, logout, -session recovery, and relevant security settings need outcomes, not just screen -presence. Recovery verification is complete; TOTP and Telegram remain incomplete. Only uniquely named -disposable QA applications may be changed or deleted by the lifecycle checks. - -Honey's backend production approval has a separate security review item. The -July 27 audit's app-token trust concern still matches the inspected auth backend -at `60d37dc`: `src/services/auth/app_token.rs` checks that a caller-selected -source app exists and accepts its callback's user identity, without an explicit -source-to-target trust check. This is a source finding, not a live exploit test. -Do not treat passing recovery or UI checks as closing that backend boundary. - -Do not count the current Honey or js.software `biome` scripts as validation: -their manifests install the unrelated `biome` 0.3.3 package rather than -`@biomejs/biome`. Its CLI can return success without checking files. Their -TypeScript, build, and native E2E results above are separate evidence. Repairing -the formatter dependency and stale configuration remains tooling cleanup. - -The previous UI run stopped at Chuzz's stale GUI build path. Chuzz #45 and #46 -fixed that release path; the published 0.1.37 host then passed the complete local -273-check component sweep. The checks were kept at the full font-enabled profile. - -Honey's creation handler now awaits its mutation, so submitting covers the backend -request. Tracing showed validation completed but CreateApp was never sent when -GetApps and CreateApp queued during WebSocket connection. The first open listener -removed itself and chuzz skipped the next listener. The host now snapshots -listeners before dispatch; three clean lifecycles and the full suite pass. - -Pathscale restoration, if approved, should mirror crates.vip's low-cost deployment: -shared IPv4, shared CPU, one small machine. The crates backend's `fly.toml` and -`docs/deploy.md` are the reference. Do not assume the former Pathscale placeholder -callback or diagnostic dashboard is a production feature specification. - -`consulting.parcle.ai` is unmaintained, intentionally absent locally, and excluded -from this release gate. - -## Local changes and branches - -Preserve unrelated changes and append work to existing PRs. Do not recreate the -deleted scratchpad checkout or delete branches while auditing them. - -The scoped branch comparison found most apparently orphaned engine/control commits -already present as equivalent patches. Pays' remaining local work was inspected: - -- `feat/engine-console`: `6ed3396` adds a separate enforcement-engine contract, - connection, status and payment pages. Its default backend is localhost and it - has no verified deployment. The request-id control updates a signal after the - form has captured its defaults, so new-id and post-send rotation need behavioral - verification and correction before use. Preserve this feature branch; it is not - a missing UI migration fix and is not approved payment functionality. -- The following `aa6cccd` removes old theme/table dependencies; the current release - branch already contains the corresponding migration, so do not replay it blindly. -- `wip/local-save-20260815`: `c0560c1` and `5309222` contain signing design documents - and their correction. Preserve these for payment/backend review; they do not - change the shipped frontend. No remote branch contains `6ed3396` or `5309222`. - -No local branch was deleted. Any later integration belongs in the existing Pays -PR and must retain its backend and payment review requirements. -Worktables' four previously local commits and the verified editor replacement are -now pushed to its existing PR. They remain subject to owner review. - -Two ps-blitz patch-identity exceptions were inspected: `fix/engine-gaps`' response -metadata fetch is present in the release branch with later configurable user-agent -changes, and `release/engine-fixes`' remaining unique commit only bumps the old -version to 0.3.7. Neither needs replaying onto 0.4.8. - -The original `solid-layouts` checkout contains other local work; the published root -style fix was made in `/Users/revenge/code/solid-layouts-ui-release` to preserve it. - -## Final local release candidate - -The font-enabled host, ps-qa 0.7.1 driver, packed UI package, Honey, and Worktables -build successfully. UI's final native sweep passes 273 checks across 75 fixtures; -API/package checks pass across 187 components and 1,002 files. The full chuzz GUI -release build, workspace tests, and clippy pass. Worktables passes 112/112, JS -Software 316/316, Web3 103/103, 24x 141/141 plus 20/20 mobile, crates 71/71, -kard 223/223, nofilter 129/129, Prompt Syntax 129/129, support.cafe 104/104, -and the starter 144/144. - -Honey's complete run is 193/196 because the backend returns no regenerated API -key; the truthful error path passes 18/18 and the reusable recovery runner passes -33/33. Pathscale's public/UI flow passes, while its dev application callback does -not establish the protected session. Pays builds but has no native QA profile and -has explicit backend contract gaps. These site-specific boundaries do not indicate -a UI package regression. - -No deployment sign-off is implied. The owner must review the PRs, then the five -dependency releases must publish in order before registry CI and approved website -deployments can complete. +| [pathscale.com #17](https://github.com/pathscale/pathscale.com/pull/17) | Lint, build, and coordinated local run 138/138 | UI branch is review-ready. A real deployment needs its own production Honey registration and a reviewed backend deployment. | +| [promptsyntax.org #18](https://github.com/pathscale/promptsyntax.org/pull/18) | 129/129 | Review-ready; refresh UI lock after 3.2.1 publishes. | +| [worktables.dev #10](https://github.com/pathscale/worktables.dev/pull/10) | Lint, build, 170/170 | PR #9 is merged. The designer uses UI controls, native SVG relationships, and `@pathscale/worktable-dsl` 0.1.2. Source contains no Cytoscape, canvas, or `getContext`; #10 restores the format gate. | +| [support.cafe #12](https://github.com/pathscale/support.cafe/pull/12) | 104/104 | Signed-out product surfaces are covered. Authenticated support workflows still need a live account/backend fixture. | +| [web3.trading #18](https://github.com/pathscale/web3.trading/pull/18) | 103/103 | Public, auth validation, theme/carousel, and guest chat are covered. Authenticated trading is not yet end-to-end proven. | +| [pays.online #166](https://github.com/pathscale/pays.online/pull/166) | Typecheck, lint, build, 45/45 against UI #292 | Code review can proceed. Deployment is blocked by an obsolete production Honey UUID, no known production Pays registration, and no matching deployed backend. The frontend now refuses the invalid id locally and explains the problem. | +| [honey.id #332](https://github.com/pathscale/honey.id/pull/332) | 196 defined native checks across five roles; deployed dev 193/196; coordinated local app lifecycle 19/19; recovery runner 33/33 | UI is review-ready. Dev's three failures expose the backend's empty regenerated API key. TOTP confirmation and Telegram enrollment/login remain unproved. | +| [js.software #54](https://github.com/pathscale/js.software/pull/54) | Lint, build, 332/332 | Review-ready; refresh UI lock after 3.2.1 publishes. | +| [nofilter.io #340](https://github.com/pathscale/nofilter.io/pull/340) | Lint, build, 132/132 | Public/auth validation is covered. A real two-participant WebRTC studio session remains unproved. | +| [24x.ai #11](https://github.com/pathscale/24x.ai/pull/11) | Lint, build, desktop 141/141, phone 20/20 | Session UI uses a Honey application identity workaround. 24x has a dev registration, but no working callback backend for it. | +| [kard.vip #8](https://github.com/pathscale/kard.vip/pull/8) | 223/223 | Demo behavior is covered; this is not real payment evidence. | +| [agencyzero #211](https://github.com/pathscale/agencyzero/pull/211) | Frontend gates and native UI suites pass; Rust tests pass | UI scope is review-ready. Core-specific work in #212 is reserved for a dedicated core owner. | +| [crates.vip #1](https://github.com/pathscale/crates.vip/pull/1) | 71/71 plus 24/24 glyph checks | UI is review-ready. Authentication is deliberately bypassed in this product. | +| [ui-starter-app #13](https://github.com/pathscale/ui-starter-app/pull/13) | 144/144 plus 12/12 glyph checks | Review-ready; refresh UI lock after 3.2.1 publishes. | + +`consulting.parcle.ai` is intentionally excluded. It is unmaintained and was +removed locally to avoid implying ownership or release priority. + +## Honey role and security coverage + +Honey's 196 static outcomes are distributed across nine groups: + +| Group | Checks | +| --- | ---: | +| Admin | 24 | +| Application lifecycle | 19 | +| Application | 18 | +| Controls | 18 | +| Entry | 16 | +| Platform | 32 | +| Public | 13 | +| Roles | 21 | +| Security | 35 | + +The suite exercises Platform Admin, Platform Support, App Admin, App Support, +and Guest behavior, including allowed and denied actions. The reusable recovery +runner covers two login/rotation cycles, rejection of a used code, save +confirmation, unchanged-password login, and final sign-out. + +The three deployed-dev failures are kept visible: regenerate, hide, and reveal +receive an empty replacement API key from the current backend. Coordinated local +branches fix that lifecycle, but their publication is blocked on the WorkTable +core dependency. The related review branches are: + +- [honey_id-types #17](https://github.com/pathscale/honey_id-types/pull/17); +- [auth.honey.id-backend #42](https://github.com/pathscale/auth.honey.id-backend/pull/42); +- [api.honey.id-backend #29](https://github.com/pathscale/api.honey.id-backend/pull/29). + +AppAdmin is already present on the API backend master branch. It must not be +reimplemented. The remaining Telegram/TOTP and WorkTable work belongs in the +backend/core handoff after the UI release review. + +## Release order after owner review + +1. Review UI #292 and ps-observability #20. +2. Merge and publish only after explicit owner approval: UI 3.2.1 and the + protocol/driver sequence above. +3. Refresh each site's lockfile or clean install so it resolves the published + UI 3.2.1, then repeat its complete native suite locally. +4. Review and merge the UI-complete site PRs individually. +5. Deploy only approved sites. Before creating or changing a Fly dev instance, + contact the owner so they can be online. +6. Hand Honey persistence, Telegram/TOTP, AgencyZero core, and product-specific + backend gaps to their dedicated owners with the failing native outcomes kept + as acceptance criteria. + +## Checkout and branch state + +The maintained active checkouts match their upstream PR branches. They are +clean except for an intentional AgencyZero local overlay in +`apps/gui/Cargo.toml`, `apps/gui/src/main.rs`, `scripts/qa-full-local.sh`, and +`docs/performance-measurement-todo.md`; those files are outside the UI PR and +must be preserved. + +Merged WorkTables PRs #5 through #8 and their dead remote branches are gone. +JS Software's dead `feat/ui-2.3` branch is gone. Chuzz's merged temporary +branches and prunable worktree are gone. Chuzz retains two real local feature +branches, `feat/network-apis` and `feat/diagnostics-tool-client`, because they +contain unique commits. + +Pays retains `feat/engine-console` and `wip/local-save-20260815`; both contain +unique, unreviewed backend/payment work and are not part of the UI release. +The ps-observability and Pathscale backend pre-rebase backup branches are kept +deliberately. No release branch depends on the deleted scratchpad tree. From 6b6de87614044aed9ff86017ace5c123f8896684 Mon Sep 17 00:00:00 2001 From: meh Date: Sat, 12 Sep 2026 10:40:44 +0700 Subject: [PATCH 4/9] test(slider): cover complete native interaction --- docs/release-readiness-2026-09-12.md | 6 ++- tests/ps-qa-headless/slider.ron | 70 ++++++++++++++++++++++++++++ tests/ps-qa/slider.ron | 70 ++++++++++++++++++++++++++++ tests/qa-harness/generate-checks.ts | 49 +++++++++++++++++++ tests/qa-harness/mount.tsx | 19 +++++--- 5 files changed, 206 insertions(+), 8 deletions(-) diff --git a/docs/release-readiness-2026-09-12.md b/docs/release-readiness-2026-09-12.md index c0f1a4eb..f6d6d3c6 100644 --- a/docs/release-readiness-2026-09-12.md +++ b/docs/release-readiness-2026-09-12.md @@ -30,7 +30,9 @@ The exact branch at `4a04b24` passes: - 93 component contracts; - TypeScript and the 547-file library build; - 320/320 Bun tests; -- 75/75 native component outcomes through `chuzz-headless`; +- 75/75 native component pages through `chuzz-headless`; +- Slider's expanded native contract at 10/10: Arrow keys, Home/End, + Page Up/Down, controlled pointer dragging, and the final `onChangeEnd` value; - the package/export gate across 1,002 shipped files; - strict publint, with one non-blocking suggestion; - a fresh consumer install, typecheck, Layout registration load, and browser @@ -87,7 +89,7 @@ uncovered product workflow works. | [web3.trading #18](https://github.com/pathscale/web3.trading/pull/18) | 103/103 | Public, auth validation, theme/carousel, and guest chat are covered. Authenticated trading is not yet end-to-end proven. | | [pays.online #166](https://github.com/pathscale/pays.online/pull/166) | Typecheck, lint, build, 45/45 against UI #292 | Code review can proceed. Deployment is blocked by an obsolete production Honey UUID, no known production Pays registration, and no matching deployed backend. The frontend now refuses the invalid id locally and explains the problem. | | [honey.id #332](https://github.com/pathscale/honey.id/pull/332) | 196 defined native checks across five roles; deployed dev 193/196; coordinated local app lifecycle 19/19; recovery runner 33/33 | UI is review-ready. Dev's three failures expose the backend's empty regenerated API key. TOTP confirmation and Telegram enrollment/login remain unproved. | -| [js.software #54](https://github.com/pathscale/js.software/pull/54) | Lint, build, 332/332 | Review-ready; refresh UI lock after 3.2.1 publishes. | +| [js.software #54](https://github.com/pathscale/js.software/pull/54) | The earlier lint/build and 332/332 suite are insufficient; the owner reports many product bugs and is preparing the concrete list. | **Not release-ready.** Reproduce and cover the reported failures before making any readiness claim; then refresh the UI lock after 3.2.1 publishes. | | [nofilter.io #340](https://github.com/pathscale/nofilter.io/pull/340) | Lint, build, 132/132 | Public/auth validation is covered. A real two-participant WebRTC studio session remains unproved. | | [24x.ai #11](https://github.com/pathscale/24x.ai/pull/11) | Lint, build, desktop 141/141, phone 20/20 | Session UI uses a Honey application identity workaround. 24x has a dev registration, but no working callback backend for it. | | [kard.vip #8](https://github.com/pathscale/kard.vip/pull/8) | 223/223 | Demo behavior is covered; this is not real payment evidence. | diff --git a/tests/ps-qa-headless/slider.ron b/tests/ps-qa-headless/slider.ron index 00067409..7711267c 100644 --- a/tests/ps-qa-headless/slider.ron +++ b/tests/ps-qa-headless/slider.ron @@ -48,4 +48,74 @@ subject: "slider:Fixture slider", expect: ValueChanges, ), + ( + id: "slider-pointer-drag-changes-value", + group: "slider", + what: "dragging Slider changes the controlled value exposed by its caller", + open: None, + hover: None, + click: None, + pointer_drag: Some((from: "slider:Fixture slider", dx: 160.0, dy: 0.0, steps: 6)), + subject: "slider:Fixture slider", + expect: ValueChanges, + ), + ( + id: "slider-pointer-release-commits", + group: "slider", + what: "releasing a Slider drag reports one final value through onChangeEnd", + open: None, + hover: None, + click: None, + pointer_drag: Some((from: "slider:Fixture slider", dx: 120.0, dy: 0.0, steps: 4)), + subject: "heading:Slider committed:", + expect: Present, + ), + ( + id: "slider-goes-to-minimum", + group: "slider", + what: "Home changes the value exposed by Slider", + open: None, + hover: None, + click: None, + key: Some("Home"), + key_on: Some("slider:Fixture slider"), + subject: "slider:Fixture slider", + expect: ValueChanges, + ), + ( + id: "slider-goes-to-maximum", + group: "slider", + what: "End changes the value exposed by Slider", + open: None, + hover: None, + click: None, + key: Some("End"), + key_on: Some("slider:Fixture slider"), + subject: "slider:Fixture slider", + expect: ValueChanges, + ), + ( + id: "slider-takes-a-large-step-down", + group: "slider", + what: "PageDown changes the value exposed by Slider", + open: None, + hover: None, + click: None, + key: Some("PageDown"), + key_on: Some("slider:Fixture slider"), + subject: "slider:Fixture slider", + expect: ValueChanges, + ), + ( + id: "slider-takes-a-large-step-up", + group: "slider", + what: "PageUp changes the value exposed by Slider", + open: None, + hover: None, + click: None, + key: Some("PageUp"), + key_on: Some("slider:Fixture slider"), + subject: "slider:Fixture slider", + expect: ValueChanges, + ), ] diff --git a/tests/ps-qa/slider.ron b/tests/ps-qa/slider.ron index 984d5ec4..09ba7231 100644 --- a/tests/ps-qa/slider.ron +++ b/tests/ps-qa/slider.ron @@ -48,4 +48,74 @@ subject: "slider:Fixture slider", expect: ValueChanges, ), + ( + id: "slider-pointer-drag-changes-value", + group: "slider", + what: "dragging Slider changes the controlled value exposed by its caller", + open: None, + hover: None, + click: None, + pointer_drag: Some((from: "slider:Fixture slider", dx: 160.0, dy: 0.0, steps: 6)), + subject: "slider:Fixture slider", + expect: ValueChanges, + ), + ( + id: "slider-pointer-release-commits", + group: "slider", + what: "releasing a Slider drag reports one final value through onChangeEnd", + open: None, + hover: None, + click: None, + pointer_drag: Some((from: "slider:Fixture slider", dx: 120.0, dy: 0.0, steps: 4)), + subject: "heading:Slider committed:", + expect: PaintsNamed, + ), + ( + id: "slider-goes-to-minimum", + group: "slider", + what: "Home changes the value exposed by Slider", + open: None, + hover: None, + click: None, + key: Some("Home"), + key_on: Some("slider:Fixture slider"), + subject: "slider:Fixture slider", + expect: ValueChanges, + ), + ( + id: "slider-goes-to-maximum", + group: "slider", + what: "End changes the value exposed by Slider", + open: None, + hover: None, + click: None, + key: Some("End"), + key_on: Some("slider:Fixture slider"), + subject: "slider:Fixture slider", + expect: ValueChanges, + ), + ( + id: "slider-takes-a-large-step-down", + group: "slider", + what: "PageDown changes the value exposed by Slider", + open: None, + hover: None, + click: None, + key: Some("PageDown"), + key_on: Some("slider:Fixture slider"), + subject: "slider:Fixture slider", + expect: ValueChanges, + ), + ( + id: "slider-takes-a-large-step-up", + group: "slider", + what: "PageUp changes the value exposed by Slider", + open: None, + hover: None, + click: None, + key: Some("PageUp"), + key_on: Some("slider:Fixture slider"), + subject: "slider:Fixture slider", + expect: ValueChanges, + ), ] diff --git a/tests/qa-harness/generate-checks.ts b/tests/qa-harness/generate-checks.ts index 6923e6da..a74f2d96 100644 --- a/tests/qa-harness/generate-checks.ts +++ b/tests/qa-harness/generate-checks.ts @@ -746,6 +746,55 @@ function checksFor(spec: ComponentSpec, profile: Profile): string { }), ); } + + records.push( + check({ + id: `"${spec.id}-pointer-drag-changes-value"`, + group: `"${spec.id}"`, + what: `"dragging ${spec.component} changes the controlled value exposed by its caller"`, + open: surface, + hover: "None", + click: "None", + pointer_drag: `Some((from: "${spec.subjectRole}:${spec.subject}", dx: 160.0, dy: 0.0, steps: 6))`, + subject, + expect: "ValueChanges", + }), + ); + records.push( + check({ + id: `"${spec.id}-pointer-release-commits"`, + group: `"${spec.id}"`, + what: `"releasing a ${spec.component} drag reports one final value through onChangeEnd"`, + open: surface, + hover: "None", + click: "None", + pointer_drag: `Some((from: "${spec.subjectRole}:${spec.subject}", dx: 120.0, dy: 0.0, steps: 4))`, + subject: `"heading:Slider committed:"`, + expect: profile.paints("PaintsNamed"), + }), + ); + + for (const [suffix, key] of [ + ["goes-to-minimum", "Home"], + ["goes-to-maximum", "End"], + ["takes-a-large-step-down", "PageDown"], + ["takes-a-large-step-up", "PageUp"], + ]) { + records.push( + check({ + id: `"${spec.id}-${suffix}"`, + group: `"${spec.id}"`, + what: `"${key} changes the value exposed by ${spec.component}"`, + open: surface, + hover: "None", + click: "None", + key: `Some("${key}")`, + key_on: `Some("${spec.subjectRole}:${spec.subject}")`, + subject, + expect: "ValueChanges", + }), + ); + } } if (spec.kind === "inline-edit") { diff --git a/tests/qa-harness/mount.tsx b/tests/qa-harness/mount.tsx index 2dda8cd1..dc7ea4eb 100644 --- a/tests/qa-harness/mount.tsx +++ b/tests/qa-harness/mount.tsx @@ -513,6 +513,7 @@ function FieldFixture(props: { spec: ComponentSpec; under?: unknown }) { function SliderFixture(props: { spec: ComponentSpec; under?: unknown }) { const [value, setValue] = createSignal(50); + const [committed, setCommitted] = createSignal(); return ( {props.spec.component} is not exported} > {(Component) => ( - + <> + + +

Slider committed: {committed()}

+
+ )}
); From 6c9f65d7c7628ad74baec64bfcb4f4559910f29a Mon Sep 17 00:00:00 2001 From: meh Date: Sat, 12 Sep 2026 11:19:43 +0700 Subject: [PATCH 5/9] docs(release): record the verified slider and harness gate --- docs/release-readiness-2026-09-12.md | 37 +++++++++++++++++++--------- src/hooks/date/date.names.ts | 4 +-- tests/hooks/date/date-names.test.ts | 17 +++++++++---- 3 files changed, 39 insertions(+), 19 deletions(-) diff --git a/docs/release-readiness-2026-09-12.md b/docs/release-readiness-2026-09-12.md index f6d6d3c6..377f8ebb 100644 --- a/docs/release-readiness-2026-09-12.md +++ b/docs/release-readiness-2026-09-12.md @@ -25,7 +25,7 @@ The conventional release calculation resolves this branch to **3.2.1**. ## UI release gate -The exact branch at `4a04b24` passes: +The exact branch at `6b6de87` passes: - 93 component contracts; - TypeScript and the 547-file library build; @@ -50,22 +50,35 @@ Exact packed-candidate consumer runs also pass: | NoFilter | 132/132 | Its separate documentation placeholder was corrected on PR #340; rebuilt output is clean | | Pays | 45/45 | Expanded application-id refusal passes; production build has no phantom UI Iconify warning | | Honey public surface | 13/13 | Production build has no phantom UI Iconify warning | +| JS Software | 347/347 across 12 groups | Exact packed UI candidate; Slider and Color Picker keyboard and pointer outcomes pass, and the phantom UI Iconify warning is gone | ## Harness patch +[ps-blitz #97](https://github.com/pathscale/ps-blitz/pull/97) and [ps-observability #20](https://github.com/pathscale/ps-observability/pull/20) -fixes driver defects discovered during the fleet sweep. It records visible -preparation and timed actions and corrects protocol handling. The branch at -`6442d1d` passes formatting, clippy with all features, 88/88 protocol tests, -150/150 ps-qa tests, and the CLI tests. +fix driver defects discovered while exercising the Slider paths. The engine +keeps connected Solid delegated handlers alive, reveals text-node targets +without crashing, and routes semantic activation through the normal input +sequence. The protocol reports the real viewport and keeps client and page +pointer coordinates distinct after scrolling. ps-qa then refuses to treat a +document-height root or `
` as the physical window. + +The engine branch at `9d131c27` passes formatting, 96/96 DOM tests, 6/6 +fragment-navigation tests, and the +complete script suite. The harness branch at `25a5af9` passes formatting, +78/78 protocol tests with capture enabled, 151/151 ps-qa tests, and the CLI +tests. Its coordinated JS Software run passes 347/347 with every new pointer +coordinate inside the renderer-reported viewport. Its required publication order is: -1. owner review and merge of ps-observability #20; -2. publish `blitz-control-protocol` 0.5.1; -3. rebuild the native hosts against that protocol; -4. publish `ps-qa` 0.7.2; -5. rerun the site suites with the published driver and rebuilt host. +1. owner review of ps-blitz #97 and ps-observability #20; +2. merge and publish ps-blitz 0.4.9 after approval; +3. publish `blitz-control-protocol` 0.5.1 after approval; +4. merge [chuzz #47](https://github.com/pathscale/chuzz/pull/47) after approval + and publish the rebuilt native host against those releases; +5. publish `ps-qa` 0.7.2 after approval; +6. rerun the site suites with the published driver and rebuilt host. The earlier one-control-surface dependency chain is complete: `ps-blitz-dom` 0.4.8, `ps-blitz-debug-control` 0.3.8, @@ -89,7 +102,7 @@ uncovered product workflow works. | [web3.trading #18](https://github.com/pathscale/web3.trading/pull/18) | 103/103 | Public, auth validation, theme/carousel, and guest chat are covered. Authenticated trading is not yet end-to-end proven. | | [pays.online #166](https://github.com/pathscale/pays.online/pull/166) | Typecheck, lint, build, 45/45 against UI #292 | Code review can proceed. Deployment is blocked by an obsolete production Honey UUID, no known production Pays registration, and no matching deployed backend. The frontend now refuses the invalid id locally and explains the problem. | | [honey.id #332](https://github.com/pathscale/honey.id/pull/332) | 196 defined native checks across five roles; deployed dev 193/196; coordinated local app lifecycle 19/19; recovery runner 33/33 | UI is review-ready. Dev's three failures expose the backend's empty regenerated API key. TOTP confirmation and Telegram enrollment/login remain unproved. | -| [js.software #54](https://github.com/pathscale/js.software/pull/54) | The earlier lint/build and 332/332 suite are insufficient; the owner reports many product bugs and is preparing the concrete list. | **Not release-ready.** Reproduce and cover the reported failures before making any readiness claim; then refresh the UI lock after 3.2.1 publishes. | +| [js.software #54](https://github.com/pathscale/js.software/pull/54) | Typecheck, lint, build, and 347/347 against the exact packed UI #292 candidate. Coverage now drives every demonstrated Slider and Color Picker path with keyboard or viewport-bounded pointer input and requires retained value changes. | Review-ready; refresh the UI lock after 3.2.1 publishes. Additional product bugs reported later should receive their own reproductions and outcomes. | | [nofilter.io #340](https://github.com/pathscale/nofilter.io/pull/340) | Lint, build, 132/132 | Public/auth validation is covered. A real two-participant WebRTC studio session remains unproved. | | [24x.ai #11](https://github.com/pathscale/24x.ai/pull/11) | Lint, build, desktop 141/141, phone 20/20 | Session UI uses a Honey application identity workaround. 24x has a dev registration, but no working callback backend for it. | | [kard.vip #8](https://github.com/pathscale/kard.vip/pull/8) | 223/223 | Demo behavior is covered; this is not real payment evidence. | @@ -136,7 +149,7 @@ backend/core handoff after the UI release review. ## Release order after owner review -1. Review UI #292 and ps-observability #20. +1. Review UI #292, ps-blitz #97, ps-observability #20, and chuzz #47. 2. Merge and publish only after explicit owner approval: UI 3.2.1 and the protocol/driver sequence above. 3. Refresh each site's lockfile or clean install so it resolves the published diff --git a/src/hooks/date/date.names.ts b/src/hooks/date/date.names.ts index 389426f7..f3ea85d2 100644 --- a/src/hooks/date/date.names.ts +++ b/src/hooks/date/date.names.ts @@ -8,8 +8,8 @@ * throw escapes every boundary the component has and reaches Solid 2, which * responds by halting its reactive system permanently: the page keeps painting * the frame it already had, so it looks alive, while every control on it is - * dead. js.software's `/calendar` route is dead this way today, and nothing on - * the page says so. + * dead. js.software's `/calendar` route failed this way before the calendar + * moved to this table, while leaving a frame that looked alive. * * What the calendar actually needed from `Intl` was twelve month names, seven * weekday names in three widths, and four assembly patterns. That is a table, diff --git a/tests/hooks/date/date-names.test.ts b/tests/hooks/date/date-names.test.ts index dba0a266..46b27d34 100644 --- a/tests/hooks/date/date-names.test.ts +++ b/tests/hooks/date/date-names.test.ts @@ -4,8 +4,8 @@ import { join } from "node:path"; import { createMemo, createRoot } from "solid-js"; import { - type DateNameWidth, type DateNames, + type DateNameWidth, DEFAULT_DATE_LOCALE, EN_US_DATE_NAMES, formatCompactDate, @@ -26,8 +26,9 @@ import { useCalendarState } from "../../../src/hooks/date/useCalendarState"; * not survivable: it escapes the component, reaches Solid 2, and Solid 2 * halts its reactive system permanently. The page keeps painting the frame it * already had, so it looks fine, while every control on it is dead. - * js.software's `/calendar` route is dead this way today, and nothing visible - * says so, which is why "it renders" is not evidence and this file exists. + * js.software's `/calendar` route failed this way before the calendar moved to + * the table, while leaving a frame that looked alive. That is why "it renders" + * is not evidence and this file exists. * * Two halves, and both are needed: * @@ -458,7 +459,13 @@ describe("no module on the calendar's path references Intl", () => { it("would catch a reference, so the scan is not vacuous", () => { const planted = ["const f = new Intl.DateTimeFormat(locale);"]; - expect(planted.filter((line) => !isComment(line) && /(? !isComment(line) && /(? Date: Sat, 12 Sep 2026 13:18:34 +0700 Subject: [PATCH 6/9] fix(components): preserve native interaction outcomes --- layouts.lint-baseline.json | 2 - src/components/calendar/Calendar.layout.tsx | 26 +++-- .../close-button/CloseButton.layout.tsx | 51 ++-------- .../close-button/CloseButton.recipe.ts | 26 +++-- src/components/drawer/Drawer.layout.tsx | 24 ++++- .../pagination/Pagination.layout.tsx | 2 +- src/hooks/date/useCalendarNavigation.ts | 14 ++- tests/ps-qa-headless/calendar.ron | 16 +++ tests/ps-qa-headless/close-button.ron | 47 +++++++++ tests/ps-qa-headless/drawer.ron | 23 ++++- tests/ps-qa/calendar.ron | 16 +++ tests/ps-qa/close-button.ron | 47 +++++++++ tests/ps-qa/drawer.ron | 23 ++++- tests/qa-harness/components.ts | 12 ++- tests/qa-harness/generate-checks.ts | 10 ++ tests/qa-harness/generate-entries.ts | 2 + tests/qa-harness/mount.tsx | 97 +++++++++---------- 17 files changed, 312 insertions(+), 126 deletions(-) create mode 100644 tests/ps-qa-headless/close-button.ron create mode 100644 tests/ps-qa/close-button.ron diff --git a/layouts.lint-baseline.json b/layouts.lint-baseline.json index 3b9958be..9136d6f7 100644 --- a/layouts.lint-baseline.json +++ b/layouts.lint-baseline.json @@ -55,8 +55,6 @@ "src/components/chip/Chip.layout.tsx:warning:legacy-template:legacy component-shaped Layout keeps presentation in component code", "src/components/chip/Chip.layout.tsx:warning:manual-classes:manual class composition belongs in the recipe", "src/components/chip/Chip.layout.tsx:warning:manual-classes:manual class composition belongs in the recipe", - "src/components/close-button/CloseButton.layout.tsx:warning:legacy-template:legacy component-shaped Layout keeps presentation in component code", - "src/components/close-button/CloseButton.layout.tsx:warning:manual-classes:manual class composition belongs in the recipe", "src/components/collapsible/Collapsible.layout.tsx:warning:legacy-template:legacy component-shaped Layout keeps presentation in component code", "src/components/collapsible/Collapsible.layout.tsx:warning:legacy-template:legacy component-shaped Layout keeps presentation in component code", "src/components/collapsible/Collapsible.layout.tsx:warning:legacy-template:legacy component-shaped Layout keeps presentation in component code", diff --git a/src/components/calendar/Calendar.layout.tsx b/src/components/calendar/Calendar.layout.tsx index 3ff56ac9..62a6bfb1 100644 --- a/src/components/calendar/Calendar.layout.tsx +++ b/src/components/calendar/Calendar.layout.tsx @@ -1,6 +1,6 @@ import "./Calendar.css"; import type { JSX } from "@solidjs/web"; -import {For, Show, createMemo, createTrackedEffect, createUniqueId, omit} from "solid-js"; +import {For, Show, createEffect, createMemo, createUniqueId, omit} from "solid-js"; import { twMerge } from "../../lib/twMerge"; import { @@ -172,9 +172,13 @@ const Calendar: Layout = () => { isDateDisabled, }); - createTrackedEffect(() => { - navigation.syncFocusedDate(focusReferenceDate()); - }); + // Track only the external reference date. Tracking the sync function itself + // also subscribed this effect to `visibleMonth`, so every navigation click + // changed the month and immediately reset it to the selected date's month. + createEffect( + () => focusReferenceDate(), + (value) => navigation.syncFocusedDate(value), + ); const calendarState = useCalendarState({ selectionMode: () => selectionMode(), @@ -197,7 +201,7 @@ const Calendar: Layout = () => { queueMicrotask(() => { const target = rootRef?.querySelector( - `[data-slot=\"calendar-cell\"][data-date=\"${dateValue}\"]`, + `[data-slot="calendar-cell"][data-date="${dateValue}"]`, ); target?.focus(); }); @@ -288,7 +292,10 @@ const Calendar: Layout = () => { }; const uniqueId = createUniqueId(); - const headingId = `calendar-heading-${uniqueId}`; + const headingId = + typeof props.id === "string" && props.id.trim() + ? `${props.id}--heading` + : `calendar-heading-${uniqueId}`; return (
= () => {
); }; diff --git a/src/components/close-button/CloseButton.recipe.ts b/src/components/close-button/CloseButton.recipe.ts index 6fdbbacc..57f4d9fe 100644 --- a/src/components/close-button/CloseButton.recipe.ts +++ b/src/components/close-button/CloseButton.recipe.ts @@ -1,13 +1,19 @@ import { recipe } from "../../lib/layouts"; -export const CLASSES = { - base: "close-button", - variant: { - default: "close-button--default", + +export const componentRecipe = recipe({ + component: "close-button", + element: "button", + slots: { + root: { base: "close-button" }, + startIcon: { base: "close-button__icon close-button__icon--start" }, + endIcon: { base: "close-button__icon close-button__icon--end" }, }, - slot: { - icon: "close-button__icon", - iconStart: "close-button__icon--start", - iconEnd: "close-button__icon--end", + props: { + variant: { + default: "close-button--default", + }, }, -} as const; -export const componentRecipe = recipe({component:"close-button",slots:{"close-button":{},"close-button-end-icon":{},"close-button-start-icon":{},"root":{},},}); + defaults: { + variant: "default", + }, +}); diff --git a/src/components/drawer/Drawer.layout.tsx b/src/components/drawer/Drawer.layout.tsx index f56b2fe2..9c314276 100644 --- a/src/components/drawer/Drawer.layout.tsx +++ b/src/components/drawer/Drawer.layout.tsx @@ -230,8 +230,17 @@ const DrawerRoot: Layout = () => { setIsOpen(false); }; + let enterTimer: ReturnType | undefined; let exitTimer: ReturnType | undefined; + const finishEntering = () => { + if (isOpen() && animState() === "entering") setAnimState("open"); + if (enterTimer) { + clearTimeout(enterTimer); + enterTimer = undefined; + } + }; + createTrackedEffect(() => { const open = isOpen(); const state = animState(); @@ -243,13 +252,25 @@ const DrawerRoot: Layout = () => { } if (state === "closed" || state === "exiting") { setAnimState("entering"); + // A windowed browser advances this after two painted frames so the + // entering transform can animate. A headless native host has no + // compositor and may never deliver requestAnimationFrame at all; in + // that environment the drawer used to remain translated completely + // outside the viewport forever. The timer is a lifecycle fallback, + // not a second animation clock: whichever path runs first clears it. + enterTimer = setTimeout(finishEntering, 50); requestAnimationFrame(() => { - requestAnimationFrame(() => setAnimState("open")); + requestAnimationFrame(finishEntering); }); } return; } + if (enterTimer) { + clearTimeout(enterTimer); + enterTimer = undefined; + } + if (state === "open" || state === "entering") { setAnimState("exiting"); exitTimer = setTimeout(() => setAnimState("closed"), EXIT_MS); @@ -257,6 +278,7 @@ const DrawerRoot: Layout = () => { }); onCleanup(() => { + if (enterTimer) clearTimeout(enterTimer); if (exitTimer) clearTimeout(exitTimer); }); diff --git a/src/components/pagination/Pagination.layout.tsx b/src/components/pagination/Pagination.layout.tsx index 7179c30c..744bb983 100644 --- a/src/components/pagination/Pagination.layout.tsx +++ b/src/components/pagination/Pagination.layout.tsx @@ -93,7 +93,7 @@ const Pagination: Layout = () => { data-active={token === currentPage() ? "true" : undefined} aria-current={token === currentPage() ? "page" : undefined} aria-label={`Go to page ${token}`} - disabled={disabled()} + disabled={disabled() || token === currentPage()} onClick={() => handleChange(token)} > {token} diff --git a/src/hooks/date/useCalendarNavigation.ts b/src/hooks/date/useCalendarNavigation.ts index 42e5e42c..6d96e843 100644 --- a/src/hooks/date/useCalendarNavigation.ts +++ b/src/hooks/date/useCalendarNavigation.ts @@ -17,12 +17,18 @@ type CalendarNavigationOptions = { }; export const useCalendarNavigation = (options: CalendarNavigationOptions) => { + const clampDateToBounds = (date: Date) => { + const min = options.minDate(); + const max = options.maxDate(); + if (min && date < min) return min; + if (max && date > max) return max; + return date; + }; + const initialFocusedDate = clampDateToBounds(options.initialFocusedDate()); const [visibleMonth, setVisibleMonth] = createSignal( - startOfMonth(options.initialFocusedDate()), - ); - const [focusedDate, setFocusedDate] = createSignal( - options.initialFocusedDate(), + startOfMonth(initialFocusedDate), ); + const [focusedDate, setFocusedDate] = createSignal(initialFocusedDate); const clampVisibleMonth = (nextVisibleMonth: Date) => { const min = options.minDate(); diff --git a/tests/ps-qa-headless/calendar.ron b/tests/ps-qa-headless/calendar.ron index 6bcf203a..3b898c0f 100644 --- a/tests/ps-qa-headless/calendar.ron +++ b/tests/ps-qa-headless/calendar.ron @@ -24,6 +24,22 @@ subject: "fixture", expect: Paints, ), + ( + id: "calendar-navigates-to-previous-month", + group: "calendar", + what: "Previous month changes the visible calendar month", + click: Some("#qa-calendar--previous-month"), + subject: "heading:June 2025", + expect: NameChanges, + ), + ( + id: "calendar-navigates-back-to-next-month", + group: "calendar", + what: "Next month changes the visible calendar month", + click: Some("#qa-calendar--next-month"), + subject: "heading:May 2025", + expect: NameChanges, + ), ( id: "calendar-selects-existing-cell", group: "calendar", diff --git a/tests/ps-qa-headless/close-button.ron b/tests/ps-qa-headless/close-button.ron new file mode 100644 index 00000000..4f662df2 --- /dev/null +++ b/tests/ps-qa-headless/close-button.ron @@ -0,0 +1,47 @@ +// Generated from tests/qa-harness/components.ts by tests/qa-harness/generate-checks.ts. Do not edit. +// +// CloseButton, mounted alone on its own harness page. Outcomes for +// this component share one native host; idempotent preparation keeps each +// outcome reproducible by id against a fresh host as well. +[ + ( + id: "close-button-page-paints", + group: "close-button", + what: "the CloseButton page builds and paints", + open: None, + hover: None, + click: None, + subject: "heading:CloseButton", + expect: Present, + ), + ( + id: "close-button-renders", + group: "close-button", + what: "CloseButton renders a node of its own", + open: None, + hover: None, + click: None, + subject: "fixture", + expect: Paints, + ), + ( + id: "close-button-paints", + group: "close-button", + what: "the CloseButton control is on screen and addressable", + open: None, + hover: None, + click: None, + subject: "button:Close fixture", + expect: Present, + ), + ( + id: "close-button-acts", + group: "close-button", + what: "activating CloseButton exposes the callback result", + open: None, + hover: None, + click: Some("button:Close fixture"), + subject: "heading:Action result: CloseButton complete", + expect: Present, + ), +] diff --git a/tests/ps-qa-headless/drawer.ron b/tests/ps-qa-headless/drawer.ron index fd93437f..e7f0d632 100644 --- a/tests/ps-qa-headless/drawer.ron +++ b/tests/ps-qa-headless/drawer.ron @@ -25,13 +25,28 @@ expect: Paints, ), ( - id: "drawer-paints", + id: "drawer-opens", group: "drawer", - what: "the Drawer reaches the renderer with a box", + what: "activating Drawer paints its portalled content", open: None, hover: None, - click: None, - subject: "Drawer", + click: Some("button:Open drawer"), + subject: "heading:Drawer outcome", expect: Present, ), + ( + id: "drawer-escape-closes", + group: "drawer", + what: "Escape closes Drawer after it really opened", + open: None, + hover: None, + prepare: Some("button:Open drawer"), + prepare_unless: Some("heading:Drawer outcome"), + settle_after_ms: 600, + click: None, + key: Some("Escape"), + key_on: Some("button:Open drawer"), + subject: "heading:Drawer outcome", + expect: Vanishes, + ), ] diff --git a/tests/ps-qa/calendar.ron b/tests/ps-qa/calendar.ron index af5975da..d2869ee4 100644 --- a/tests/ps-qa/calendar.ron +++ b/tests/ps-qa/calendar.ron @@ -24,6 +24,22 @@ subject: "fixture", expect: Paints, ), + ( + id: "calendar-navigates-to-previous-month", + group: "calendar", + what: "Previous month changes the visible calendar month", + click: Some("#qa-calendar--previous-month"), + subject: "heading:June 2025", + expect: NameChanges, + ), + ( + id: "calendar-navigates-back-to-next-month", + group: "calendar", + what: "Next month changes the visible calendar month", + click: Some("#qa-calendar--next-month"), + subject: "heading:May 2025", + expect: NameChanges, + ), ( id: "calendar-selects-existing-cell", group: "calendar", diff --git a/tests/ps-qa/close-button.ron b/tests/ps-qa/close-button.ron new file mode 100644 index 00000000..872d4175 --- /dev/null +++ b/tests/ps-qa/close-button.ron @@ -0,0 +1,47 @@ +// Generated from tests/qa-harness/components.ts by tests/qa-harness/generate-checks.ts. Do not edit. +// +// CloseButton, mounted alone on its own harness page. Outcomes for +// this component share one native host; idempotent preparation keeps each +// outcome reproducible by id against a fresh host as well. +[ + ( + id: "close-button-page-paints", + group: "close-button", + what: "the CloseButton page builds and paints", + open: None, + hover: None, + click: None, + subject: "heading:CloseButton", + expect: PaintsNamed, + ), + ( + id: "close-button-renders", + group: "close-button", + what: "CloseButton renders a node of its own", + open: None, + hover: None, + click: None, + subject: "fixture", + expect: Paints, + ), + ( + id: "close-button-paints", + group: "close-button", + what: "the CloseButton control is on screen and addressable", + open: None, + hover: None, + click: None, + subject: "button:Close fixture", + expect: PaintsNamed, + ), + ( + id: "close-button-acts", + group: "close-button", + what: "activating CloseButton exposes the callback result", + open: None, + hover: None, + click: Some("button:Close fixture"), + subject: "heading:Action result: CloseButton complete", + expect: PaintsNamed, + ), +] diff --git a/tests/ps-qa/drawer.ron b/tests/ps-qa/drawer.ron index 887df275..2261d415 100644 --- a/tests/ps-qa/drawer.ron +++ b/tests/ps-qa/drawer.ron @@ -25,13 +25,28 @@ expect: Paints, ), ( - id: "drawer-paints", + id: "drawer-opens", group: "drawer", - what: "the Drawer reaches the renderer with a box", + what: "activating Drawer paints its portalled content", open: None, hover: None, + click: Some("button:Open drawer"), + subject: "heading:Drawer outcome", + expect: PaintsNamed, + ), + ( + id: "drawer-escape-closes", + group: "drawer", + what: "Escape closes Drawer after it really opened", + open: None, + hover: None, + prepare: Some("button:Open drawer"), + prepare_unless: Some("heading:Drawer outcome"), + settle_after_ms: 600, click: None, - subject: "Drawer", - expect: Paints, + key: Some("Escape"), + key_on: Some("button:Open drawer"), + subject: "heading:Drawer outcome", + expect: Vanishes, ), ] diff --git a/tests/qa-harness/components.ts b/tests/qa-harness/components.ts index ee4ef0b4..1b552e14 100644 --- a/tests/qa-harness/components.ts +++ b/tests/qa-harness/components.ts @@ -182,6 +182,13 @@ export const COMPONENTS: ComponentSpec[] = [ }, { id: "card", component: "Card", kind: "display" }, { id: "chat-bubble", component: "ChatBubble", kind: "display" }, + { + id: "close-button", + component: "CloseButton", + kind: "action", + subject: "Close fixture", + subjectRole: "button", + }, { id: "checkbox", component: "Checkbox", @@ -336,7 +343,10 @@ export const COMPONENTS: ComponentSpec[] = [ { id: "drawer", component: "Drawer", - kind: "display", + kind: "overlay", + subject: "Open drawer", + subjectRole: "button", + opens: "heading:Drawer outcome", }, { id: "dropdown", diff --git a/tests/qa-harness/generate-checks.ts b/tests/qa-harness/generate-checks.ts index a74f2d96..cffb576d 100644 --- a/tests/qa-harness/generate-checks.ts +++ b/tests/qa-harness/generate-checks.ts @@ -562,6 +562,8 @@ function checksFor(spec: ComponentSpec, profile: Profile): string { hover: "None", click: `Some("${spec.subjectRole}:${spec.subject}")`, subject: `"${spec.opens}"`, + // PaintsNamed also requires the named box to intersect the viewport. + // That catches an overlay stuck in its off-screen entering transform. expect: profile.paints("PaintsNamed"), }), ); @@ -887,6 +889,14 @@ function checksFor(spec: ComponentSpec, profile: Profile): string { if (spec.kind === "calendar") { records.push( + check({ id: '"calendar-navigates-to-previous-month"', group: '"calendar"', + what: '"Previous month changes the visible calendar month"', + click: 'Some("#qa-calendar--previous-month")', + subject: '"heading:June 2025"', expect: "NameChanges" }), + check({ id: '"calendar-navigates-back-to-next-month"', group: '"calendar"', + what: '"Next month changes the visible calendar month"', + click: 'Some("#qa-calendar--next-month")', + subject: '"heading:May 2025"', expect: "NameChanges" }), check({ id: '"calendar-selects-existing-cell"', group: '"calendar"', what: '"selecting a date updates the existing grid cell"', prepare: 'Some("gridcell:Sunday, June 15, 2025")', diff --git a/tests/qa-harness/generate-entries.ts b/tests/qa-harness/generate-entries.ts index dee038b7..d6bedd6b 100644 --- a/tests/qa-harness/generate-entries.ts +++ b/tests/qa-harness/generate-entries.ts @@ -74,6 +74,7 @@ const IMPORT_FORM: Record = { "ChatBubble": "default", "Checkbox": "default", "Chip": "default", + "CloseButton": "default", "Collapsible": "default", "ConnectionSettings": "named", "ColorSwatch": "default", @@ -254,6 +255,7 @@ const MODULE_PATHS: Record = { "chat-bubble": "components/chatbubble", "checkbox": "components/checkbox", "chip": "components/chip", + "close-button": "components/close-button", "collapsible": "components/collapsible", "connection-settings": "components/connection-settings", "color-swatch": "components/color-swatch", diff --git a/tests/qa-harness/mount.tsx b/tests/qa-harness/mount.tsx index dc7ea4eb..4fab8705 100644 --- a/tests/qa-harness/mount.tsx +++ b/tests/qa-harness/mount.tsx @@ -30,6 +30,7 @@ import { createConnectionSettings } from "@pathscale/ui/hooks/connection"; import { ComplexColorWheel } from "@pathscale/ui/components/color-wheel"; import { createI18n, LanguageSwitcher } from "@pathscale/ui/components/language-switcher"; import Dialog from "@pathscale/ui/components/dialog"; +import Drawer from "@pathscale/ui/components/drawer"; import Dropdown from "@pathscale/ui/components/dropdown"; import InlineEdit from "@pathscale/ui/components/inline-edit"; import Popover from "@pathscale/ui/components/popover"; @@ -37,6 +38,7 @@ import Select from "@pathscale/ui/components/select"; import Tabs from "@pathscale/ui/components/tabs"; import Button from "@pathscale/ui/components/button"; import Calendar from "@pathscale/ui/components/calendar"; +import CloseButton from "@pathscale/ui/components/close-button"; import { Form } from "@pathscale/ui/components/form"; import Input from "@pathscale/ui/components/input"; import { createForm } from "@pathscale/ui/hooks/form"; @@ -232,6 +234,24 @@ function ActionFixture(props: { spec: ComponentSpec; under?: unknown }) { ); } +function CloseButtonFixture(props: { + spec: ComponentSpec; +}) { + const [complete, setComplete] = createSignal(false); + return ( + <> + setComplete(true)} + /> + + + ); +} + function ComposerFixture(props: { spec: ComponentSpec; under?: unknown }) { const [complete, setComplete] = createSignal(false); return ( @@ -272,6 +292,27 @@ function DialogFixture() { ); } +function DrawerFixture() { + return ( + + Open drawer + + + + + Drawer outcome + + The drawer is visibly inside the viewport. + + Close drawer + + + + + + ); +} + function PopoverFixture() { return ( @@ -637,54 +678,6 @@ function CollapsibleFixture() { ); } -/* - * A toggle, mounted unchecked and controlled. - * - * The generic fixture passes no `checked`, so Switch, Radio and Checkbox - * mounted uncontrolled and the tree reported `selected: true` before anything - * had been pressed. The `-toggles` check then failed with "selected state - * stayed true", which is indistinguishable between two very different things: - * a component that ignores a click, and one that was already on and had - * nowhere to go. - * - * Starting from `false` with a controlled signal separates them. If the state - * flips, the component works and the old failure was the fixture's fault. If it - * stays false, the component really does not respond and that is a defect worth - * reporting. - * - * `under` rather than a static import: the generated entry already resolved the - * component for this page, and importing three more here would put all three in - * every page's bundle. - */ -function ToggleFixture(props: { spec: ComponentSpec; under?: unknown }) { - const [on, setOn] = createSignal(false); - return ( - ) => JSX.Element) - | undefined - } - fallback={{props.spec.component} is not exported} - > - {(Component) => ( - setOn((previous) => !previous)} - /* - * Both spellings. These components disagree about which they take, - * and a fixture that guesses wrong mounts an uncontrolled toggle - * again, which is the bug this exists to rule out. - */ - onInput={() => setOn((previous) => !previous)} - aria-label={props.spec.component} - /> - )} - - ); -} - /* * A toggle, and a heading that only its callback can produce. * @@ -819,7 +812,7 @@ function CalendarFixture() { const [value, setValue] = createSignal(new Date(2025, 5, 15)); return ( <> - +

Selected {value().getFullYear()}-{String(value().getMonth() + 1).padStart(2, "0")}-{String(value().getDate()).padStart(2, "0")}

); @@ -830,20 +823,22 @@ const FIXTURES: Record< string, // `under` is the resolved component, which the harness passes to whichever // fixture it selected. The hand-written fixtures that import their component - // statically ignore it; `ToggleFixture` is generic over three components and - // needs it. + // statically ignore it; `ToggleFixtureWithReport` is generic over three + // components and needs it. (props: { spec: ComponentSpec; under?: unknown }) => JSX.Element > = { "auth-submit-button": ActionFixture, button: ActionFixture, calendar: CalendarFixture, checkbox: ToggleFixtureWithReport, + "close-button": CloseButtonFixture, collapsible: CollapsibleFixture, "connection-settings": ConnectionSettingsFixture, "complex-color-wheel": ComplexColorWheelFixture, composer: ComposerFixture, dialog: DialogFixture, dock: DockFixture, + drawer: DrawerFixture, dropdown: DropdownFixture, form: FormFixture, "inline-edit": InlineEditFixture, From 7589af2fc571b36c70d753752623d0b6925425e9 Mon Sep 17 00:00:00 2001 From: meh Date: Sat, 12 Sep 2026 16:10:31 +0700 Subject: [PATCH 7/9] fix(components): enforce complete consumer coverage --- docs/api-contract.md | 1 + docs/release-readiness-2026-09-12.md | 29 +- docs/ui-usage.md | 13 +- layouts.library.json | 2 + scripts/check-contracts.ts | 16 + src/component-families.ts | 416 +++++++++ src/components/combo-box/ComboBox.layout.tsx | 8 + src/components/dock/Dock.layout.tsx | 2 + .../ImmersiveLanding.layout.tsx | 1 + .../components/FirefoxPWABanner.tsx | 2 +- src/components/immersive-landing/types.ts | 4 +- src/components/input-otp/InputOTP.layout.tsx | 4 +- src/index.ts | 5 + tests/ps-qa-headless/accordion.ron | 18 +- tests/ps-qa-headless/address.ron | 10 +- tests/ps-qa-headless/alert.ron | 8 +- tests/ps-qa-headless/auth-footer-links.ron | 20 +- tests/ps-qa-headless/auth-powered-by.ron | 8 +- tests/ps-qa-headless/breadcrumb.ron | 8 +- tests/ps-qa-headless/button-group.ron | 38 + tests/ps-qa-headless/card.ron | 8 +- tests/ps-qa-headless/checkbox-group.ron | 47 + tests/ps-qa-headless/checkbox.ron | 6 +- tests/ps-qa-headless/chip.ron | 8 +- tests/ps-qa-headless/color-area.ron | 62 ++ tests/ps-qa-headless/color-field.ron | 51 ++ tests/ps-qa-headless/color-picker.ron | 63 ++ tests/ps-qa-headless/color-slider.ron | 62 ++ tests/ps-qa-headless/color-swatch-picker.ron | 48 + tests/ps-qa-headless/color-swatch.ron | 8 +- tests/ps-qa-headless/color-wheel-flower.ron | 19 +- tests/ps-qa-headless/color-wheel.ron | 21 +- tests/ps-qa-headless/combo-box.ron | 73 ++ tests/ps-qa-headless/complex-color-wheel.ron | 16 +- tests/ps-qa-headless/composer.ron | 14 +- tests/ps-qa-headless/connection-settings.ron | 14 + tests/ps-qa-headless/cookie-consent.ron | 41 +- tests/ps-qa-headless/data-grid.ron | 51 +- tests/ps-qa-headless/date-field.ron | 51 ++ tests/ps-qa-headless/date-picker.ron | 49 ++ tests/ps-qa-headless/date-range-picker.ron | 59 ++ tests/ps-qa-headless/dock.ron | 9 +- tests/ps-qa-headless/firefox-pwa-banner.ron | 33 +- tests/ps-qa-headless/flex-grid.ron | 37 + tests/ps-qa-headless/immersive-landing.ron | 9 +- tests/ps-qa-headless/input-otp.ron | 51 ++ tests/ps-qa-headless/join.ron | 38 + tests/ps-qa-headless/kbd.ron | 37 + tests/ps-qa-headless/list-box.ron | 20 +- tests/ps-qa-headless/live-chat-panel.ron | 22 +- tests/ps-qa-headless/menu.ron | 48 + tests/ps-qa-headless/meter.ron | 37 + tests/ps-qa-headless/noise-background.ron | 37 + tests/ps-qa-headless/pagination.ron | 15 +- tests/ps-qa-headless/password-field.ron | 18 +- tests/ps-qa-headless/pwa-install-prompt.ron | 33 +- tests/ps-qa-headless/radial-progress.ron | 37 + tests/ps-qa-headless/radio-group.ron | 48 + tests/ps-qa-headless/radio.ron | 6 +- tests/ps-qa-headless/range-calendar.ron | 67 ++ tests/ps-qa-headless/size-picker.ron | 48 + tests/ps-qa-headless/switch.ron | 6 +- tests/ps-qa-headless/table.ron | 10 +- tests/ps-qa-headless/tabs.ron | 7 +- tests/ps-qa-headless/theme-color-picker.ron | 19 +- tests/ps-qa-headless/time-field.ron | 51 ++ tests/ps-qa-headless/toast.ron | 28 +- tests/ps-qa-headless/toolbar.ron | 40 + tests/ps-qa-headless/tooltip.ron | 8 +- tests/ps-qa-headless/video-preview.ron | 37 + tests/ps-qa/accordion.ron | 20 +- tests/ps-qa/address.ron | 10 +- tests/ps-qa/alert.ron | 10 +- tests/ps-qa/auth-footer-links.ron | 20 +- tests/ps-qa/auth-powered-by.ron | 10 +- tests/ps-qa/breadcrumb.ron | 10 +- tests/ps-qa/button-group.ron | 38 + tests/ps-qa/card.ron | 10 +- tests/ps-qa/checkbox-group.ron | 47 + tests/ps-qa/checkbox.ron | 6 +- tests/ps-qa/chip.ron | 10 +- tests/ps-qa/color-area.ron | 62 ++ tests/ps-qa/color-field.ron | 51 ++ tests/ps-qa/color-picker.ron | 63 ++ tests/ps-qa/color-slider.ron | 62 ++ tests/ps-qa/color-swatch-picker.ron | 48 + tests/ps-qa/color-swatch.ron | 8 +- tests/ps-qa/color-wheel-flower.ron | 19 +- tests/ps-qa/color-wheel.ron | 21 +- tests/ps-qa/combo-box.ron | 73 ++ tests/ps-qa/complex-color-wheel.ron | 16 +- tests/ps-qa/composer.ron | 14 +- tests/ps-qa/connection-settings.ron | 14 + tests/ps-qa/cookie-consent.ron | 43 +- tests/ps-qa/data-grid.ron | 51 +- tests/ps-qa/date-field.ron | 51 ++ tests/ps-qa/date-picker.ron | 49 ++ tests/ps-qa/date-range-picker.ron | 59 ++ tests/ps-qa/dock.ron | 11 +- tests/ps-qa/firefox-pwa-banner.ron | 33 +- tests/ps-qa/flex-grid.ron | 37 + tests/ps-qa/immersive-landing.ron | 11 +- tests/ps-qa/input-otp.ron | 51 ++ tests/ps-qa/join.ron | 38 + tests/ps-qa/kbd.ron | 37 + tests/ps-qa/list-box.ron | 20 +- tests/ps-qa/live-chat-panel.ron | 22 +- tests/ps-qa/menu.ron | 48 + tests/ps-qa/meter.ron | 37 + tests/ps-qa/noise-background.ron | 37 + tests/ps-qa/pagination.ron | 15 +- tests/ps-qa/password-field.ron | 20 +- tests/ps-qa/pwa-install-prompt.ron | 33 +- tests/ps-qa/radial-progress.ron | 37 + tests/ps-qa/radio-group.ron | 48 + tests/ps-qa/radio.ron | 6 +- tests/ps-qa/range-calendar.ron | 67 ++ tests/ps-qa/size-picker.ron | 48 + tests/ps-qa/switch.ron | 6 +- tests/ps-qa/table.ron | 10 +- tests/ps-qa/tabs.ron | 9 +- tests/ps-qa/theme-color-picker.ron | 21 +- tests/ps-qa/time-field.ron | 51 ++ tests/ps-qa/toast.ron | 30 +- tests/ps-qa/toolbar.ron | 40 + tests/ps-qa/tooltip.ron | 10 +- tests/ps-qa/video-preview.ron | 37 + tests/qa-harness/components.ts | 453 ++++++++-- tests/qa-harness/generate-checks.ts | 57 +- tests/qa-harness/generate-entries.ts | 80 +- tests/qa-harness/mount.tsx | 828 +++++++++++++++++- 131 files changed, 5041 insertions(+), 382 deletions(-) create mode 100644 src/component-families.ts create mode 100644 tests/ps-qa-headless/button-group.ron create mode 100644 tests/ps-qa-headless/checkbox-group.ron create mode 100644 tests/ps-qa-headless/color-area.ron create mode 100644 tests/ps-qa-headless/color-field.ron create mode 100644 tests/ps-qa-headless/color-picker.ron create mode 100644 tests/ps-qa-headless/color-slider.ron create mode 100644 tests/ps-qa-headless/color-swatch-picker.ron create mode 100644 tests/ps-qa-headless/combo-box.ron create mode 100644 tests/ps-qa-headless/date-field.ron create mode 100644 tests/ps-qa-headless/date-picker.ron create mode 100644 tests/ps-qa-headless/date-range-picker.ron create mode 100644 tests/ps-qa-headless/flex-grid.ron create mode 100644 tests/ps-qa-headless/input-otp.ron create mode 100644 tests/ps-qa-headless/join.ron create mode 100644 tests/ps-qa-headless/kbd.ron create mode 100644 tests/ps-qa-headless/menu.ron create mode 100644 tests/ps-qa-headless/meter.ron create mode 100644 tests/ps-qa-headless/noise-background.ron create mode 100644 tests/ps-qa-headless/radial-progress.ron create mode 100644 tests/ps-qa-headless/radio-group.ron create mode 100644 tests/ps-qa-headless/range-calendar.ron create mode 100644 tests/ps-qa-headless/size-picker.ron create mode 100644 tests/ps-qa-headless/time-field.ron create mode 100644 tests/ps-qa-headless/toolbar.ron create mode 100644 tests/ps-qa-headless/video-preview.ron create mode 100644 tests/ps-qa/button-group.ron create mode 100644 tests/ps-qa/checkbox-group.ron create mode 100644 tests/ps-qa/color-area.ron create mode 100644 tests/ps-qa/color-field.ron create mode 100644 tests/ps-qa/color-picker.ron create mode 100644 tests/ps-qa/color-slider.ron create mode 100644 tests/ps-qa/color-swatch-picker.ron create mode 100644 tests/ps-qa/combo-box.ron create mode 100644 tests/ps-qa/date-field.ron create mode 100644 tests/ps-qa/date-picker.ron create mode 100644 tests/ps-qa/date-range-picker.ron create mode 100644 tests/ps-qa/flex-grid.ron create mode 100644 tests/ps-qa/input-otp.ron create mode 100644 tests/ps-qa/join.ron create mode 100644 tests/ps-qa/kbd.ron create mode 100644 tests/ps-qa/menu.ron create mode 100644 tests/ps-qa/meter.ron create mode 100644 tests/ps-qa/noise-background.ron create mode 100644 tests/ps-qa/radial-progress.ron create mode 100644 tests/ps-qa/radio-group.ron create mode 100644 tests/ps-qa/range-calendar.ron create mode 100644 tests/ps-qa/size-picker.ron create mode 100644 tests/ps-qa/time-field.ron create mode 100644 tests/ps-qa/toolbar.ron create mode 100644 tests/ps-qa/video-preview.ron diff --git a/docs/api-contract.md b/docs/api-contract.md index 122a26c3..aa04e891 100644 --- a/docs/api-contract.md +++ b/docs/api-contract.md @@ -959,6 +959,7 @@ extensionUrl?: string icon?: string | JSX.Element onDismiss?: () => void onInstall?: () => void +showDelayMs?: number storageKey?: string texts?: FirefoxPWABannerTexts ``` diff --git a/docs/release-readiness-2026-09-12.md b/docs/release-readiness-2026-09-12.md index 377f8ebb..5bbde8d9 100644 --- a/docs/release-readiness-2026-09-12.md +++ b/docs/release-readiness-2026-09-12.md @@ -10,7 +10,9 @@ secondary integration signal, not QA evidence. ## What changed after UI 3.2.0 -[UI #292](https://github.com/pathscale/UI/pull/292) contains two fixes: +[UI #292](https://github.com/pathscale/UI/pull/292) contains the original two +regression fixes plus the consumer-contract corrections found by the expanded +native audit: - UI's shipped CSS contained an Icon documentation placeholder shaped like an Iconify utility. Consumer production builds therefore printed @@ -20,20 +22,29 @@ secondary integration signal, not QA evidence. selected another slide during an active transition. It now preserves that destination, avoids a duplicate timer when its own callback updates the controlled route, and cleans up cancelled work. +- `ComboBox` now reopens a selected value with every alternative available, + `InputOTP` forwards its authored accessible name, and the mobile Dock trigger + reports its name and expanded state. +- `FirefoxPWABanner` exposes its appearance delay directly and through + `ImmersiveLanding`; its public API and usage documentation now agree. +- The package exports a canonical 101-family component inventory. UI's native + registry and js.software both fail their build if a public family is omitted, + and the Solid Layouts consumer manifest now includes `FlexGrid`. The conventional release calculation resolves this branch to **3.2.1**. ## UI release gate -The exact branch at `6b6de87` passes: +The current review tree passes: - 93 component contracts; - TypeScript and the 547-file library build; - 320/320 Bun tests; -- 75/75 native component pages through `chuzz-headless`; +- 101/101 native component pages through `chuzz-headless`, including an + explicit FlexGrid incremental-reveal outcome; - Slider's expanded native contract at 10/10: Arrow keys, Home/End, Page Up/Down, controlled pointer dragging, and the final `onChangeEnd` value; -- the package/export gate across 1,002 shipped files; +- the package/export gate across 1,004 shipped files; - strict publint, with one non-blocking suggestion; - a fresh consumer install, typecheck, Layout registration load, and browser bundle; @@ -50,7 +61,7 @@ Exact packed-candidate consumer runs also pass: | NoFilter | 132/132 | Its separate documentation placeholder was corrected on PR #340; rebuilt output is clean | | Pays | 45/45 | Expanded application-id refusal passes; production build has no phantom UI Iconify warning | | Honey public surface | 13/13 | Production build has no phantom UI Iconify warning | -| JS Software | 347/347 across 12 groups | Exact packed UI candidate; Slider and Color Picker keyboard and pointer outcomes pass, and the phantom UI Iconify warning is gone | +| JS Software | 555/555 across 13 groups | Exact packed UI candidate; all 101 public families are demonstrated, every declared site outcome passes in one host, Slider and Color Picker keyboard and pointer outcomes pass, and the phantom UI Iconify warning is gone | ## Harness patch @@ -65,9 +76,9 @@ document-height root or `
` as the physical window. The engine branch at `9d131c27` passes formatting, 96/96 DOM tests, 6/6 fragment-navigation tests, and the -complete script suite. The harness branch at `25a5af9` passes formatting, -78/78 protocol tests with capture enabled, 151/151 ps-qa tests, and the CLI -tests. Its coordinated JS Software run passes 347/347 with every new pointer +complete script suite. The harness review tree passes formatting, +80/80 protocol tests with capture enabled, 158/158 ps-qa tests, and the CLI +tests. Its coordinated JS Software run passes 555/555 with every new pointer coordinate inside the renderer-reported viewport. Its required publication order is: @@ -102,7 +113,7 @@ uncovered product workflow works. | [web3.trading #18](https://github.com/pathscale/web3.trading/pull/18) | 103/103 | Public, auth validation, theme/carousel, and guest chat are covered. Authenticated trading is not yet end-to-end proven. | | [pays.online #166](https://github.com/pathscale/pays.online/pull/166) | Typecheck, lint, build, 45/45 against UI #292 | Code review can proceed. Deployment is blocked by an obsolete production Honey UUID, no known production Pays registration, and no matching deployed backend. The frontend now refuses the invalid id locally and explains the problem. | | [honey.id #332](https://github.com/pathscale/honey.id/pull/332) | 196 defined native checks across five roles; deployed dev 193/196; coordinated local app lifecycle 19/19; recovery runner 33/33 | UI is review-ready. Dev's three failures expose the backend's empty regenerated API key. TOTP confirmation and Telegram enrollment/login remain unproved. | -| [js.software #54](https://github.com/pathscale/js.software/pull/54) | Typecheck, lint, build, and 347/347 against the exact packed UI #292 candidate. Coverage now drives every demonstrated Slider and Color Picker path with keyboard or viewport-bounded pointer input and requires retained value changes. | Review-ready; refresh the UI lock after 3.2.1 publishes. Additional product bugs reported later should receive their own reproductions and outcomes. | +| [js.software #54](https://github.com/pathscale/js.software/pull/54) | Typecheck, lint, build, and 555/555 across 13 native groups against the exact packed UI #292 candidate. The showcase maps all 101 public UI families, drives every demonstrated Slider and Color Picker path with keyboard or viewport-bounded pointer input, and exercises every landing-page and header action. | Review-ready; refresh the UI lock after 3.2.1 publishes. Additional product bugs reported later should receive their own reproductions and outcomes. | | [nofilter.io #340](https://github.com/pathscale/nofilter.io/pull/340) | Lint, build, 132/132 | Public/auth validation is covered. A real two-participant WebRTC studio session remains unproved. | | [24x.ai #11](https://github.com/pathscale/24x.ai/pull/11) | Lint, build, desktop 141/141, phone 20/20 | Session UI uses a Honey application identity workaround. 24x has a dev registration, but no working callback backend for it. | | [kard.vip #8](https://github.com/pathscale/kard.vip/pull/8) | 223/223 | Demo behavior is covered; this is not real payment evidence. | diff --git a/docs/ui-usage.md b/docs/ui-usage.md index eda9d4c3..03baeff3 100644 --- a/docs/ui-usage.md +++ b/docs/ui-usage.md @@ -4,7 +4,8 @@ agents alike, and for every consuming application. Those apps link here rather than keeping their own copy — one library, one set of conventions, no drift. -SolidJS component library, HeroUI-parity API, daisyUI-style theming. ~104 components. +SolidJS component library, HeroUI-parity API, daisyUI-style theming. 101 public +component families. This file is **how to USE the library** (as a consumer, or when writing demos and examples). For **modifying the library itself**, read @@ -26,6 +27,11 @@ import "@pathscale/ui/index.css"; // tokens + theme Subpath exports also exist: `./components/*`, `./primitives/*`, `./hooks/*`, `./motion`, `./styles/*`. +`componentFamilies` is the canonical public visual-family inventory. Documentation +and showcase applications can import it with `ComponentFamily` and +`ComponentFamilyId` from the root barrel, then fail their own build when a newly +shipped family has no page or example. + Layout components require the application compiler before the normal Solid transform. See [Layouts](./layouts.md) for the Rsbuild configuration, exact failure behavior, and porting report. ## Theming @@ -154,6 +160,11 @@ Components require `solid-layouts >=0.2.4` so caller styles reach their root ele also honoured properly now: `role="presentation"` no longer leaves `tabindex="0"` behind. - `Slider.onChange` reports continuous values. Optional `Slider.onChangeEnd` reports the final changed value once on pointer release, pointer cancellation, keyboard release, or blur fallback. Its visible `label` is also copied to the semantic slider's `aria-label`, because not every renderer resolves `aria-labelledby` across a visually hidden label. +- `FirefoxPWABanner` waits 2000ms before appearing by default. Pass + `showDelayMs` when the surrounding onboarding flow needs a different delay; + the same option is available as `ImmersiveLanding.firefoxPWAConfig.showDelayMs`. + The banner still applies its Firefox, standalone-mode, and dismissal checks + before starting that delay. - `Collapsible.Content` retains closed content by default. Set `keepMounted={false}` to mount it only while expanded; the check is reactive, so it mounts and unmounts as the state changes. - `Popover` accepts `anchorRect` as a rectangle or rectangle accessor when content must be positioned without a trigger element. - Compound components: `Dialog.Trigger`, `Tabs.List`, `Select.Option`, etc. (`Object.assign` statics; also exported flat: `AccordionRoot`, `AlertTitle`, …). Parts are styleable/testable via `data-slot="..."` and state attrs (`data-open`, `data-selected`, `data-invalid`). diff --git a/layouts.library.json b/layouts.library.json index eb0958c8..8722ab69 100644 --- a/layouts.library.json +++ b/layouts.library.json @@ -97,6 +97,8 @@ "FieldsetLegend", "FirefoxPWABanner", "Flex", + "FlexGrid", + "FlexGridRoot", "Footer", "Form", "FormContext", diff --git a/scripts/check-contracts.ts b/scripts/check-contracts.ts index fb841bb4..49cb40e1 100644 --- a/scripts/check-contracts.ts +++ b/scripts/check-contracts.ts @@ -1,6 +1,7 @@ import { readdirSync, readFileSync, existsSync } from "node:fs"; import { join } from "node:path"; import { missingRecipeFlagUsages } from "./component-state-contract"; +import { componentFamilies } from "../src/component-families"; const COMPONENTS_DIR = "src/components"; const CONTRIBUTING = "CONTRIBUTING.md"; @@ -30,6 +31,21 @@ type Violation = { component: string; rule: string; detail: string; section: str const violations: Violation[] = []; +const layoutLibrary = JSON.parse(readFileSync("layouts.library.json", "utf8")) as { + exports?: string[]; +}; +const layoutExports = new Set(layoutLibrary.exports ?? []); +for (const family of componentFamilies) { + if (!layoutExports.has(family.name)) { + fail( + family.id, + "layouts-manifest", + `${family.name} is public but absent from layouts.library.json exports`, + "Structure", + ); + } +} + function fail(component: string, rule: string, detail: string, section: string) { violations.push({ component, rule, detail, section }); } diff --git a/src/component-families.ts b/src/component-families.ts new file mode 100644 index 00000000..ab42bfa1 --- /dev/null +++ b/src/component-families.ts @@ -0,0 +1,416 @@ +/** + * Public visual component families shipped by @pathscale/ui. + * + * This inventory is part of the package contract. The native QA registry and + * js.software showcase both compare themselves with it, so a new family cannot + * ship without a rendered fixture and a public demonstration. + */ +export const componentFamilies = [ + { + "id": "accordion", + "name": "Accordion" + }, + { + "id": "address", + "name": "Address" + }, + { + "id": "alert", + "name": "Alert" + }, + { + "id": "auth-card", + "name": "AuthCard" + }, + { + "id": "auth-field-group", + "name": "AuthFieldGroup" + }, + { + "id": "auth-footer-links", + "name": "AuthFooterLinks" + }, + { + "id": "auth-message", + "name": "AuthMessage" + }, + { + "id": "auth-powered-by", + "name": "AuthPoweredBy" + }, + { + "id": "auth-submit-button", + "name": "AuthSubmitButton" + }, + { + "id": "avatar", + "name": "Avatar" + }, + { + "id": "badge", + "name": "Badge" + }, + { + "id": "breadcrumb", + "name": "Breadcrumb" + }, + { + "id": "button", + "name": "Button" + }, + { + "id": "calendar", + "name": "Calendar" + }, + { + "id": "card", + "name": "Card" + }, + { + "id": "chat-bubble", + "name": "ChatBubble" + }, + { + "id": "close-button", + "name": "CloseButton" + }, + { + "id": "checkbox", + "name": "Checkbox" + }, + { + "id": "chip", + "name": "Chip" + }, + { + "id": "collapsible", + "name": "Collapsible" + }, + { + "id": "connection-settings", + "name": "ConnectionSettings" + }, + { + "id": "color-swatch", + "name": "ColorSwatch" + }, + { + "id": "color-wheel-flower", + "name": "ColorWheelFlower" + }, + { + "id": "color-wheel", + "name": "ColorWheel" + }, + { + "id": "complex-color-wheel", + "name": "ComplexColorWheel" + }, + { + "id": "composer", + "name": "Composer" + }, + { + "id": "cookie-consent", + "name": "CookieConsent" + }, + { + "id": "data-grid", + "name": "DataGrid" + }, + { + "id": "dialog", + "name": "Dialog" + }, + { + "id": "dock", + "name": "Dock" + }, + { + "id": "drawer", + "name": "Drawer" + }, + { + "id": "dropdown", + "name": "Dropdown" + }, + { + "id": "empty", + "name": "Empty" + }, + { + "id": "field-group", + "name": "FieldGroup" + }, + { + "id": "fieldset", + "name": "Fieldset" + }, + { + "id": "firefox-pwa-banner", + "name": "FirefoxPWABanner" + }, + { + "id": "flex", + "name": "Flex" + }, + { + "id": "footer", + "name": "Footer" + }, + { + "id": "form", + "name": "Form" + }, + { + "id": "glow-card", + "name": "GlowCard" + }, + { + "id": "grid", + "name": "Grid" + }, + { + "id": "header", + "name": "Header" + }, + { + "id": "icon", + "name": "Icon" + }, + { + "id": "immersive-landing", + "name": "ImmersiveLanding" + }, + { + "id": "inline-edit", + "name": "InlineEdit" + }, + { + "id": "input", + "name": "Input" + }, + { + "id": "label", + "name": "Label" + }, + { + "id": "language-switcher", + "name": "LanguageSwitcher" + }, + { + "id": "link", + "name": "Link" + }, + { + "id": "list-box", + "name": "ListBox" + }, + { + "id": "live-chat-bubble", + "name": "LiveChatBubble" + }, + { + "id": "live-chat-panel", + "name": "LiveChatPanel" + }, + { + "id": "metal-border", + "name": "MetalBorder" + }, + { + "id": "navbar", + "name": "Navbar" + }, + { + "id": "pwa-install-prompt", + "name": "PWAInstallPrompt" + }, + { + "id": "pagination", + "name": "Pagination" + }, + { + "id": "panel-toggle", + "name": "PanelToggle" + }, + { + "id": "password-field", + "name": "PasswordField" + }, + { + "id": "password-requirements", + "name": "PasswordRequirements" + }, + { + "id": "popover", + "name": "Popover" + }, + { + "id": "progress", + "name": "Progress" + }, + { + "id": "radio", + "name": "Radio" + }, + { + "id": "scroll-area", + "name": "ScrollArea" + }, + { + "id": "select", + "name": "Select" + }, + { + "id": "separator", + "name": "Separator" + }, + { + "id": "skeleton", + "name": "Skeleton" + }, + { + "id": "slider", + "name": "Slider" + }, + { + "id": "spinner", + "name": "Spinner" + }, + { + "id": "switch", + "name": "Switch" + }, + { + "id": "table", + "name": "Table" + }, + { + "id": "tabs", + "name": "Tabs" + }, + { + "id": "text", + "name": "Text" + }, + { + "id": "textarea", + "name": "Textarea" + }, + { + "id": "theme-color-picker", + "name": "ThemeColorPicker" + }, + { + "id": "toast", + "name": "Toast" + }, + { + "id": "tooltip", + "name": "Tooltip" + }, + { + "id": "button-group", + "name": "ButtonGroup" + }, + { + "id": "checkbox-group", + "name": "CheckboxGroup" + }, + { + "id": "color-area", + "name": "ColorArea" + }, + { + "id": "color-field", + "name": "ColorField" + }, + { + "id": "color-picker", + "name": "ColorPicker" + }, + { + "id": "color-slider", + "name": "ColorSlider" + }, + { + "id": "color-swatch-picker", + "name": "ColorSwatchPicker" + }, + { + "id": "combo-box", + "name": "ComboBox" + }, + { + "id": "date-field", + "name": "DateField" + }, + { + "id": "date-picker", + "name": "DatePicker" + }, + { + "id": "date-range-picker", + "name": "DateRangePicker" + }, + { + "id": "flex-grid", + "name": "FlexGrid" + }, + { + "id": "input-otp", + "name": "InputOTP" + }, + { + "id": "join", + "name": "Join" + }, + { + "id": "kbd", + "name": "Kbd" + }, + { + "id": "menu", + "name": "Menu" + }, + { + "id": "meter", + "name": "Meter" + }, + { + "id": "noise-background", + "name": "NoiseBackground" + }, + { + "id": "radial-progress", + "name": "RadialProgress" + }, + { + "id": "radio-group", + "name": "RadioGroup" + }, + { + "id": "range-calendar", + "name": "RangeCalendar" + }, + { + "id": "size-picker", + "name": "SizePicker" + }, + { + "id": "time-field", + "name": "TimeField" + }, + { + "id": "toolbar", + "name": "Toolbar" + }, + { + "id": "video-preview", + "name": "VideoPreview" + } +] as const; + +export type ComponentFamily = (typeof componentFamilies)[number]; +export type ComponentFamilyId = ComponentFamily["id"]; diff --git a/src/components/combo-box/ComboBox.layout.tsx b/src/components/combo-box/ComboBox.layout.tsx index d37af433..dcf43fe9 100644 --- a/src/components/combo-box/ComboBox.layout.tsx +++ b/src/components/combo-box/ComboBox.layout.tsx @@ -296,6 +296,14 @@ const ComboBoxRoot: Layout = () => { const query = inputValue(); const filter = props.defaultFilter ?? defaultFilter; + // The selected label is the committed display value, not a search the + // user entered. Opening a selected ComboBox must still offer every item; + // otherwise the current choice filters out every alternative and the + // trigger opens a one-item list that cannot change the selection. + if (query === (selectedItem()?.textValue ?? "")) { + return normalizedItems(); + } + return normalizedItems().filter((item) => filter(item.textValue, query)); }); diff --git a/src/components/dock/Dock.layout.tsx b/src/components/dock/Dock.layout.tsx index ac567a69..533eb577 100644 --- a/src/components/dock/Dock.layout.tsx +++ b/src/components/dock/Dock.layout.tsx @@ -494,6 +494,8 @@ const DockMobile: Layout setOpen(!open())} {...{ class: CLASSES.mobileToggle }} style={{ width: `${props.cfg.baseSize}px`, height: `${props.cfg.baseSize}px` }} + aria-label={open() ? "Close actions" : "Open actions"} + aria-expanded={open() ? "true" : "false"} > {props.toggleIcon ?? ( = storageKey={ props.firefoxPWAConfig?.storageKey ?? "app_firefox_pwa_dismissed" } + showDelayMs={props.firefoxPWAConfig?.showDelayMs} texts={props.firefoxPWAConfig?.texts} onInstall={props.firefoxPWAConfig?.onInstall} onDismiss={props.firefoxPWAConfig?.onDismiss} diff --git a/src/components/immersive-landing/components/FirefoxPWABanner.tsx b/src/components/immersive-landing/components/FirefoxPWABanner.tsx index d72b6ad4..ccfe68cf 100644 --- a/src/components/immersive-landing/components/FirefoxPWABanner.tsx +++ b/src/components/immersive-landing/components/FirefoxPWABanner.tsx @@ -112,7 +112,7 @@ export const FirefoxPWABanner: Component = (props) => { onSettled(() => { if (checkShouldShow()) { // Small delay to not overwhelm user immediately - setTimeout(() => setShowBanner(true), 2000); + setTimeout(() => setShowBanner(true), props.showDelayMs ?? 2000); } }); diff --git a/src/components/immersive-landing/types.ts b/src/components/immersive-landing/types.ts index c64f480f..beae11b7 100644 --- a/src/components/immersive-landing/types.ts +++ b/src/components/immersive-landing/types.ts @@ -99,7 +99,7 @@ export interface CookieConsentTexts { essential?: string; analytics?: string; - marketing: string; + marketing?: string; cancel?: string; save?: string; @@ -137,6 +137,8 @@ export interface FirefoxPWABannerTexts { export interface FirefoxPWABannerProps { extensionUrl?: string; storageKey?: string; + /** Delay before the banner appears. Defaults to 2000ms. */ + showDelayMs?: number; texts?: FirefoxPWABannerTexts; /** * The browser mark shown beside the text. Omit it and the banner renders diff --git a/src/components/input-otp/InputOTP.layout.tsx b/src/components/input-otp/InputOTP.layout.tsx index 63956dcc..898efcb0 100644 --- a/src/components/input-otp/InputOTP.layout.tsx +++ b/src/components/input-otp/InputOTP.layout.tsx @@ -117,6 +117,7 @@ const InputOTPRoot: Layout = () => { "onMouseDown", "onFocusOut", "aria-invalid", + "aria-label", "ref", ); @@ -358,6 +359,7 @@ const InputOTPRoot: Layout = () => { disabled={isDisabled()} aria-disabled={isDisabled() ? "true" : undefined} aria-invalid={isInvalid() ? "true" : undefined} + aria-label={local["aria-label"]} autocomplete="one-time-code" onFocus={() => { setIsFocused(true); @@ -404,7 +406,7 @@ const InputOTPSlot: Layout = () => { const char = () => context?.chars()[props.index] ?? ""; const isActive = () => Boolean(context?.isFocused()) && - !Boolean(context?.isDisabled()) && + !context?.isDisabled() && (context?.activeIndex() ?? 0) === props.index; const handleMouseDown: JSX.EventHandlerUnion = (event) => { diff --git a/src/index.ts b/src/index.ts index d4ccf7e6..2770998a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -679,3 +679,8 @@ export { glassTokensToCss, resolveGlassTokens, } from "./styles/glass"; +export { + componentFamilies, + type ComponentFamily, + type ComponentFamilyId, +} from "./component-families"; diff --git a/tests/ps-qa-headless/accordion.ron b/tests/ps-qa-headless/accordion.ron index d02891a3..e93e30bd 100644 --- a/tests/ps-qa-headless/accordion.ron +++ b/tests/ps-qa-headless/accordion.ron @@ -25,13 +25,23 @@ expect: Paints, ), ( - id: "accordion-paints", + id: "accordion-opens", group: "accordion", - what: "the Accordion reaches the renderer with a box", + what: "activating an accordion trigger reveals its panel", open: None, hover: None, - click: None, - subject: "Accordion", + click: Some("button:First section"), + subject: "heading:First panel", expect: Present, ), + ( + id: "accordion-reports", + group: "accordion", + what: "the accordion reports its controlled selection", + open: None, + hover: None, + click: Some("button:First section"), + subject: "heading:Accordion value:", + expect: NameChanges, + ), ] diff --git a/tests/ps-qa-headless/address.ron b/tests/ps-qa-headless/address.ron index 6abfd233..2d76d1de 100644 --- a/tests/ps-qa-headless/address.ron +++ b/tests/ps-qa-headless/address.ron @@ -25,13 +25,13 @@ expect: Paints, ), ( - id: "address-paints", + id: "address-copies", group: "address", - what: "the Address reaches the renderer with a box", + what: "copying an address reports the full value to its caller", open: None, hover: None, - click: None, - subject: "Address", - expect: Present, + click: Some("button:Copy address"), + subject: "heading:Address copied:", + expect: NameChanges, ), ] diff --git a/tests/ps-qa-headless/alert.ron b/tests/ps-qa-headless/alert.ron index 533fb887..98450517 100644 --- a/tests/ps-qa-headless/alert.ron +++ b/tests/ps-qa-headless/alert.ron @@ -25,13 +25,13 @@ expect: Paints, ), ( - id: "alert-paints", + id: "alert-dismisses", group: "alert", - what: "the Alert reaches the renderer with a box", + what: "the alert dismiss control invokes its owner", open: None, hover: None, - click: None, - subject: "Alert", + click: Some("button:Dismiss fixture alert"), + subject: "heading:Alert dismissed", expect: Present, ), ] diff --git a/tests/ps-qa-headless/auth-footer-links.ron b/tests/ps-qa-headless/auth-footer-links.ron index 3abd3251..b3c58011 100644 --- a/tests/ps-qa-headless/auth-footer-links.ron +++ b/tests/ps-qa-headless/auth-footer-links.ron @@ -25,13 +25,23 @@ expect: Paints, ), ( - id: "auth-footer-links-paints", + id: "auth-footer-links-follows-link", group: "auth-footer-links", - what: "the AuthFooterLinks reaches the renderer with a box", + what: "an auth footer link invokes its callback before navigation", open: None, hover: None, - click: None, - subject: "AuthFooterLinks", - expect: Present, + click: Some("link:Privacy fixture"), + subject: "heading:Auth footer action:", + expect: NameChanges, + ), + ( + id: "auth-footer-links-runs-action", + group: "auth-footer-links", + what: "an auth footer action remains a semantic button", + open: None, + hover: None, + click: Some("button:Help fixture"), + subject: "heading:Auth footer action:", + expect: NameChanges, ), ] diff --git a/tests/ps-qa-headless/auth-powered-by.ron b/tests/ps-qa-headless/auth-powered-by.ron index 5234a104..fd087d04 100644 --- a/tests/ps-qa-headless/auth-powered-by.ron +++ b/tests/ps-qa-headless/auth-powered-by.ron @@ -25,13 +25,13 @@ expect: Paints, ), ( - id: "auth-powered-by-paints", + id: "auth-powered-by-navigates", group: "auth-powered-by", - what: "the AuthPoweredBy reaches the renderer with a box", + what: "the powered-by attribution exposes an operable Honey link", open: None, hover: None, - click: None, - subject: "AuthPoweredBy", + click: Some("link:Secure Auth by Honey"), + subject: "heading:Honey link activated", expect: Present, ), ] diff --git a/tests/ps-qa-headless/breadcrumb.ron b/tests/ps-qa-headless/breadcrumb.ron index 98116377..fc699790 100644 --- a/tests/ps-qa-headless/breadcrumb.ron +++ b/tests/ps-qa-headless/breadcrumb.ron @@ -25,13 +25,13 @@ expect: Paints, ), ( - id: "breadcrumb-paints", + id: "breadcrumb-navigates", group: "breadcrumb", - what: "the Breadcrumb reaches the renderer with a box", + what: "a breadcrumb link remains operable inside the compound list", open: None, hover: None, - click: None, - subject: "Breadcrumb", + click: Some("link:Products fixture"), + subject: "heading:Breadcrumb activated", expect: Present, ), ] diff --git a/tests/ps-qa-headless/button-group.ron b/tests/ps-qa-headless/button-group.ron new file mode 100644 index 00000000..041e2d13 --- /dev/null +++ b/tests/ps-qa-headless/button-group.ron @@ -0,0 +1,38 @@ +// Generated from tests/qa-harness/components.ts by tests/qa-harness/generate-checks.ts. Do not edit. +// +// ButtonGroup, mounted alone on its own harness page. Outcomes for +// this component share one native host; idempotent preparation keeps each +// outcome reproducible by id against a fresh host as well. +[ + ( + id: "button-group-page-paints", + group: "button-group", + what: "the ButtonGroup page builds and paints", + open: None, + hover: None, + click: None, + subject: "heading:ButtonGroup", + expect: Present, + ), + ( + id: "button-group-renders", + group: "button-group", + what: "ButtonGroup renders a node of its own", + open: None, + hover: None, + click: None, + subject: "fixture", + expect: Paints, + ), + ( + id: "button-group-contains-actions", + group: "button-group", + what: "grouped buttons remain operable", + open: None, + hover: None, + click: Some("button:First grouped button"), + subject: "heading:ButtonGroup selected: first", + expect: Present, + covers: ["button:* grouped button"], + ), +] diff --git a/tests/ps-qa-headless/card.ron b/tests/ps-qa-headless/card.ron index 29270984..48b376df 100644 --- a/tests/ps-qa-headless/card.ron +++ b/tests/ps-qa-headless/card.ron @@ -25,13 +25,13 @@ expect: Paints, ), ( - id: "card-paints", + id: "card-activates", group: "card", - what: "the Card reaches the renderer with a box", + what: "an interactive card invokes its consumer callback", open: None, hover: None, - click: None, - subject: "Card", + click: Some("button:Interactive fixture card"), + subject: "heading:Card activated", expect: Present, ), ] diff --git a/tests/ps-qa-headless/checkbox-group.ron b/tests/ps-qa-headless/checkbox-group.ron new file mode 100644 index 00000000..1712a8fc --- /dev/null +++ b/tests/ps-qa-headless/checkbox-group.ron @@ -0,0 +1,47 @@ +// Generated from tests/qa-harness/components.ts by tests/qa-harness/generate-checks.ts. Do not edit. +// +// CheckboxGroup, mounted alone on its own harness page. Outcomes for +// this component share one native host; idempotent preparation keeps each +// outcome reproducible by id against a fresh host as well. +[ + ( + id: "checkbox-group-page-paints", + group: "checkbox-group", + what: "the CheckboxGroup page builds and paints", + open: None, + hover: None, + click: None, + subject: "heading:CheckboxGroup", + expect: Present, + ), + ( + id: "checkbox-group-renders", + group: "checkbox-group", + what: "CheckboxGroup renders a node of its own", + open: None, + hover: None, + click: None, + subject: "fixture", + expect: Paints, + ), + ( + id: "checkbox-group-selects", + group: "checkbox-group", + what: "selecting a grouped checkbox changes its controlled selection", + open: None, + hover: None, + click: Some("checkbox:Second choice"), + subject: "checkbox:Second choice", + expect: SelectionChanges, + ), + ( + id: "checkbox-group-reports", + group: "checkbox-group", + what: "selecting a grouped checkbox reports the new values", + open: None, + hover: None, + click: Some("checkbox:First choice"), + subject: "heading:CheckboxGroup value:", + expect: NameChanges, + ), +] diff --git a/tests/ps-qa-headless/checkbox.ron b/tests/ps-qa-headless/checkbox.ron index b74f9155..f4bbb446 100644 --- a/tests/ps-qa-headless/checkbox.ron +++ b/tests/ps-qa-headless/checkbox.ron @@ -30,8 +30,8 @@ what: "pressing the Checkbox changes what it reports", open: None, hover: None, - click: Some("checkbox:"), - subject: "checkbox:", + click: Some("checkbox:Checkbox"), + subject: "checkbox:Checkbox", expect: SelectionChanges, ), ( @@ -41,7 +41,7 @@ open: None, hover: None, settle_after_ms: 300, - click: Some("checkbox:"), + click: Some("checkbox:Checkbox"), subject: "heading:Callback ran", expect: Present, ), diff --git a/tests/ps-qa-headless/chip.ron b/tests/ps-qa-headless/chip.ron index 3257e009..5ec713f4 100644 --- a/tests/ps-qa-headless/chip.ron +++ b/tests/ps-qa-headless/chip.ron @@ -25,13 +25,13 @@ expect: Paints, ), ( - id: "chip-paints", + id: "chip-removes", group: "chip", - what: "the Chip reaches the renderer with a box", + what: "the removable chip invokes its owner", open: None, hover: None, - click: None, - subject: "Chip", + click: Some("button:Remove fixture chip"), + subject: "heading:Chip removed", expect: Present, ), ] diff --git a/tests/ps-qa-headless/color-area.ron b/tests/ps-qa-headless/color-area.ron new file mode 100644 index 00000000..262ed44d --- /dev/null +++ b/tests/ps-qa-headless/color-area.ron @@ -0,0 +1,62 @@ +// Generated from tests/qa-harness/components.ts by tests/qa-harness/generate-checks.ts. Do not edit. +// +// ColorArea, mounted alone on its own harness page. Outcomes for +// this component share one native host; idempotent preparation keeps each +// outcome reproducible by id against a fresh host as well. +[ + ( + id: "color-area-page-paints", + group: "color-area", + what: "the ColorArea page builds and paints", + open: None, + hover: None, + click: None, + subject: "heading:ColorArea", + expect: Present, + ), + ( + id: "color-area-renders", + group: "color-area", + what: "ColorArea renders a node of its own", + open: None, + hover: None, + click: None, + subject: "fixture", + expect: Paints, + ), + ( + id: "color-area-keyboard-changes", + group: "color-area", + what: "ArrowRight changes the controlled saturation", + open: None, + hover: None, + click: None, + subject: "slider:Color area", + expect: ValueChanges, + key: Some("ArrowRight"), + key_on: Some("slider:Color area"), + ), + ( + id: "color-area-reports", + group: "color-area", + what: "keyboard adjustment reports the new color area value", + open: None, + hover: None, + click: None, + subject: "heading:ColorArea changed", + expect: Present, + key: Some("ArrowRight"), + key_on: Some("slider:Color area"), + ), + ( + id: "color-area-pointer-changes", + group: "color-area", + what: "pointer dragging changes the controlled saturation", + open: None, + hover: None, + click: None, + subject: "slider:Color area", + expect: ValueChanges, + pointer_drag: Some((from: "slider:Color area", dx: -80.0, dy: 20.0, steps: 4)), + ), +] diff --git a/tests/ps-qa-headless/color-field.ron b/tests/ps-qa-headless/color-field.ron new file mode 100644 index 00000000..08332e0e --- /dev/null +++ b/tests/ps-qa-headless/color-field.ron @@ -0,0 +1,51 @@ +// Generated from tests/qa-harness/components.ts by tests/qa-harness/generate-checks.ts. Do not edit. +// +// ColorField, mounted alone on its own harness page. Outcomes for +// this component share one native host; idempotent preparation keeps each +// outcome reproducible by id against a fresh host as well. +[ + ( + id: "color-field-page-paints", + group: "color-field", + what: "the ColorField page builds and paints", + open: None, + hover: None, + click: None, + subject: "heading:ColorField", + expect: Present, + ), + ( + id: "color-field-renders", + group: "color-field", + what: "ColorField renders a node of its own", + open: None, + hover: None, + click: None, + subject: "fixture", + expect: Paints, + ), + ( + id: "color-field-accepts", + group: "color-field", + what: "typing a valid color changes the field value", + open: None, + hover: None, + click: None, + subject: "textbox:Color value", + expect: ValueChanges, + type_into: Some("textbox:Color value"), + text: Some("#112233"), + ), + ( + id: "color-field-reports", + group: "color-field", + what: "typing a valid color reports the normalized value", + open: None, + hover: None, + click: None, + subject: "heading:ColorField value: #112233", + expect: Present, + type_into: Some("textbox:Color value"), + text: Some("#112233"), + ), +] diff --git a/tests/ps-qa-headless/color-picker.ron b/tests/ps-qa-headless/color-picker.ron new file mode 100644 index 00000000..ed1d8c24 --- /dev/null +++ b/tests/ps-qa-headless/color-picker.ron @@ -0,0 +1,63 @@ +// Generated from tests/qa-harness/components.ts by tests/qa-harness/generate-checks.ts. Do not edit. +// +// ColorPicker, mounted alone on its own harness page. Outcomes for +// this component share one native host; idempotent preparation keeps each +// outcome reproducible by id against a fresh host as well. +[ + ( + id: "color-picker-page-paints", + group: "color-picker", + what: "the ColorPicker page builds and paints", + open: None, + hover: None, + click: None, + subject: "heading:ColorPicker", + expect: Present, + ), + ( + id: "color-picker-renders", + group: "color-picker", + what: "ColorPicker renders a node of its own", + open: None, + hover: None, + click: None, + subject: "fixture", + expect: Paints, + ), + ( + id: "color-picker-hue-changes", + group: "color-picker", + what: "the composed hue slider changes the controlled color", + open: None, + hover: None, + click: None, + subject: "slider:Hue", + expect: ValueChanges, + key: Some("ArrowRight"), + key_on: Some("slider:Hue"), + ), + ( + id: "color-picker-area-changes", + group: "color-picker", + what: "the composed color area changes the controlled color", + open: None, + hover: None, + click: None, + subject: "slider:Color area", + expect: ValueChanges, + key: Some("ArrowRight"), + key_on: Some("slider:Color area"), + ), + ( + id: "color-picker-field-reports", + group: "color-picker", + what: "the composed color field reports a typed literal", + open: None, + hover: None, + click: None, + subject: "heading:ColorPicker value:", + expect: NameChanges, + type_into: Some("textbox:Color value"), + text: Some("#112233"), + ), +] diff --git a/tests/ps-qa-headless/color-slider.ron b/tests/ps-qa-headless/color-slider.ron new file mode 100644 index 00000000..be8bb5e6 --- /dev/null +++ b/tests/ps-qa-headless/color-slider.ron @@ -0,0 +1,62 @@ +// Generated from tests/qa-harness/components.ts by tests/qa-harness/generate-checks.ts. Do not edit. +// +// ColorSlider, mounted alone on its own harness page. Outcomes for +// this component share one native host; idempotent preparation keeps each +// outcome reproducible by id against a fresh host as well. +[ + ( + id: "color-slider-page-paints", + group: "color-slider", + what: "the ColorSlider page builds and paints", + open: None, + hover: None, + click: None, + subject: "heading:ColorSlider", + expect: Present, + ), + ( + id: "color-slider-renders", + group: "color-slider", + what: "ColorSlider renders a node of its own", + open: None, + hover: None, + click: None, + subject: "fixture", + expect: Paints, + ), + ( + id: "color-slider-keyboard-changes", + group: "color-slider", + what: "ArrowRight changes the controlled hue", + open: None, + hover: None, + click: None, + subject: "slider:Hue", + expect: ValueChanges, + key: Some("ArrowRight"), + key_on: Some("slider:Hue"), + ), + ( + id: "color-slider-reports", + group: "color-slider", + what: "the hue slider reports the changed value", + open: None, + hover: None, + click: None, + subject: "heading:ColorSlider changed", + expect: Present, + key: Some("ArrowRight"), + key_on: Some("slider:Hue"), + ), + ( + id: "color-slider-pointer-changes", + group: "color-slider", + what: "pointer dragging changes the controlled hue", + open: None, + hover: None, + click: None, + subject: "slider:Hue", + expect: ValueChanges, + pointer_drag: Some((from: "slider:Hue", dx: 100.0, dy: 0.0, steps: 4)), + ), +] diff --git a/tests/ps-qa-headless/color-swatch-picker.ron b/tests/ps-qa-headless/color-swatch-picker.ron new file mode 100644 index 00000000..5c33da25 --- /dev/null +++ b/tests/ps-qa-headless/color-swatch-picker.ron @@ -0,0 +1,48 @@ +// Generated from tests/qa-harness/components.ts by tests/qa-harness/generate-checks.ts. Do not edit. +// +// ColorSwatchPicker, mounted alone on its own harness page. Outcomes for +// this component share one native host; idempotent preparation keeps each +// outcome reproducible by id against a fresh host as well. +[ + ( + id: "color-swatch-picker-page-paints", + group: "color-swatch-picker", + what: "the ColorSwatchPicker page builds and paints", + open: None, + hover: None, + click: None, + subject: "heading:ColorSwatchPicker", + expect: Present, + ), + ( + id: "color-swatch-picker-renders", + group: "color-swatch-picker", + what: "ColorSwatchPicker renders a node of its own", + open: None, + hover: None, + click: None, + subject: "fixture", + expect: Paints, + ), + ( + id: "color-swatch-picker-selects", + group: "color-swatch-picker", + what: "choosing another swatch changes its controlled selection", + open: None, + hover: None, + click: Some("radio:Blue swatch"), + subject: "radio:Blue swatch", + expect: SelectionChanges, + covers: ["radio:* swatch"], + ), + ( + id: "color-swatch-picker-reports", + group: "color-swatch-picker", + what: "choosing another swatch reports the color", + open: None, + hover: None, + click: Some("radio:Blue swatch"), + subject: "heading:ColorSwatchPicker value: #0000ff", + expect: Present, + ), +] diff --git a/tests/ps-qa-headless/color-swatch.ron b/tests/ps-qa-headless/color-swatch.ron index 6de15b5a..9bb8bbae 100644 --- a/tests/ps-qa-headless/color-swatch.ron +++ b/tests/ps-qa-headless/color-swatch.ron @@ -25,13 +25,13 @@ expect: Paints, ), ( - id: "color-swatch-paints", + id: "color-swatch-selects", group: "color-swatch", - what: "the ColorSwatch reaches the renderer with a box", + what: "a standalone swatch reports its color to its owner", open: None, hover: None, - click: None, - subject: "option:Color undefined", + click: Some("option:Fixture blue"), + subject: "heading:ColorSwatch selected: #0000ff", expect: Present, ), ] diff --git a/tests/ps-qa-headless/color-wheel-flower.ron b/tests/ps-qa-headless/color-wheel-flower.ron index e516b4c8..a2cb45fd 100644 --- a/tests/ps-qa-headless/color-wheel-flower.ron +++ b/tests/ps-qa-headless/color-wheel-flower.ron @@ -25,13 +25,24 @@ expect: Paints, ), ( - id: "color-wheel-flower-paints", + id: "color-wheel-flower-selects", group: "color-wheel-flower", - what: "the ColorWheelFlower reaches the renderer with a box", + what: "a flower petal changes the controlled color", open: None, hover: None, - click: None, - subject: "radio:Reset to neutral", + click: Some("radio:Theme color #DDA82C"), + subject: "radio:Theme color #DDA82C", + expect: SelectionChanges, + covers: ["radio:*"], + ), + ( + id: "color-wheel-flower-reports", + group: "color-wheel-flower", + what: "the standalone flower reports the selected color", + open: None, + hover: None, + click: Some("radio:Theme color #DD732C"), + subject: "heading:ColorWheelFlower changed", expect: Present, ), ] diff --git a/tests/ps-qa-headless/color-wheel.ron b/tests/ps-qa-headless/color-wheel.ron index 17b08bc7..a2ed9a2f 100644 --- a/tests/ps-qa-headless/color-wheel.ron +++ b/tests/ps-qa-headless/color-wheel.ron @@ -25,13 +25,24 @@ expect: Paints, ), ( - id: "color-wheel-paints", + id: "color-wheel-selects", group: "color-wheel", - what: "the ColorWheel reaches the renderer with a box", + what: "a color wheel petal changes the controlled selection", open: None, hover: None, - click: None, - subject: "ColorWheel", - expect: Present, + click: Some("radio:Theme color #DDA82C"), + subject: "radio:Theme color #DDA82C", + expect: SelectionChanges, + covers: ["radio:*"], + ), + ( + id: "color-wheel-reports", + group: "color-wheel", + what: "the color wheel reports the selected literal", + open: None, + hover: None, + click: Some("radio:Theme color #DD732C"), + subject: "heading:ColorWheel value:", + expect: NameChanges, ), ] diff --git a/tests/ps-qa-headless/combo-box.ron b/tests/ps-qa-headless/combo-box.ron new file mode 100644 index 00000000..5ee2610a --- /dev/null +++ b/tests/ps-qa-headless/combo-box.ron @@ -0,0 +1,73 @@ +// Generated from tests/qa-harness/components.ts by tests/qa-harness/generate-checks.ts. Do not edit. +// +// ComboBox, mounted alone on its own harness page. Outcomes for +// this component share one native host; idempotent preparation keeps each +// outcome reproducible by id against a fresh host as well. +[ + ( + id: "combo-box-page-paints", + group: "combo-box", + what: "the ComboBox page builds and paints", + open: None, + hover: None, + click: None, + subject: "heading:ComboBox", + expect: Present, + ), + ( + id: "combo-box-renders", + group: "combo-box", + what: "ComboBox renders a node of its own", + open: None, + hover: None, + click: None, + subject: "fixture", + expect: Paints, + ), + ( + id: "combo-box-opens", + group: "combo-box", + what: "the ComboBox opens an addressable listbox", + open: None, + hover: None, + click: Some("button:Toggle options"), + subject: "option:Beta", + expect: Present, + ), + ( + id: "combo-box-selects", + group: "combo-box", + what: "choosing an option changes the controlled input value", + open: None, + hover: None, + click: Some("option:Beta"), + subject: "combobox:Fixture combo box", + expect: ValueChanges, + prepare: Some("button:Toggle options"), + prepare_unless: Some("option:Beta"), + ), + ( + id: "combo-box-reports", + group: "combo-box", + what: "choosing another option reports the selected key", + open: None, + hover: None, + click: Some("option:Alpha"), + subject: "heading:ComboBox value:", + expect: NameChanges, + prepare: Some("button:Toggle options"), + prepare_unless: Some("option:Alpha"), + ), + ( + id: "combo-box-accepts-query", + group: "combo-box", + what: "typing a query clears the committed selection", + open: None, + hover: None, + click: None, + subject: "heading:ComboBox value:", + expect: NameChanges, + type_into: Some("combobox:Fixture combo box"), + text: Some("Gam"), + ), +] diff --git a/tests/ps-qa-headless/complex-color-wheel.ron b/tests/ps-qa-headless/complex-color-wheel.ron index ee02e1f2..3f05a72d 100644 --- a/tests/ps-qa-headless/complex-color-wheel.ron +++ b/tests/ps-qa-headless/complex-color-wheel.ron @@ -68,13 +68,25 @@ expect: Paints, ), ( - id: "complex-color-wheel-changes", + id: "complex-color-wheel-adjusts", group: "complex-color-wheel", - what: "activating a ComplexColorWheel adjustment changes its controlled selection", + what: "choosing an adjustment updates its controlled selection", open: None, hover: None, click: Some("button:Strength 20"), subject: "button:Strength 20", expect: SelectionChanges, + covers: ["button:Strength *"], + ), + ( + id: "complex-color-wheel-selects-color", + group: "complex-color-wheel", + what: "choosing a flower petal updates the controlled color", + open: None, + hover: None, + click: Some("radio:Theme color #DDA82C"), + subject: "radio:Theme color #DDA82C", + expect: SelectionChanges, + covers: ["radio:*"], ), ] diff --git a/tests/ps-qa-headless/composer.ron b/tests/ps-qa-headless/composer.ron index c15286c4..acb17ee6 100644 --- a/tests/ps-qa-headless/composer.ron +++ b/tests/ps-qa-headless/composer.ron @@ -25,23 +25,25 @@ expect: Paints, ), ( - id: "composer-paints", + id: "composer-accepts", group: "composer", - what: "the Composer control is on screen and addressable", + what: "typing into Composer updates its controlled draft", open: None, hover: None, click: None, - subject: "button:Send", + subject: "heading:Composer draft: QA message", expect: Present, + type_into: Some("textbox:Fixture message"), + text: Some("QA message"), ), ( - id: "composer-acts", + id: "composer-submits", group: "composer", - what: "activating Composer exposes the callback result", + what: "sending Composer reports its trimmed message", open: None, hover: None, click: Some("button:Send"), - subject: "heading:Action result: Composer complete", + subject: "heading:Composer submitted: QA message", expect: Present, ), ] diff --git a/tests/ps-qa-headless/connection-settings.ron b/tests/ps-qa-headless/connection-settings.ron index 5a83fd4a..fe93e7eb 100644 --- a/tests/ps-qa-headless/connection-settings.ron +++ b/tests/ps-qa-headless/connection-settings.ron @@ -95,4 +95,18 @@ subject: "heading:Reconnected: ws://qa-reconnected over ws://qa-reconnected", expect: Present, ), + ( + id: "connection-settings-resets-draft", + group: "connection-settings", + what: "resetting ConnectionSettings discards the draft and reports completion", + open: None, + hover: None, + prepare: Some("switch:Use a custom backend"), + prepare_unless: Some("textbox:API URL"), + setup_type_into: Some("textbox:API URL"), + setup_text: Some("ws://qa-reset-draft"), + click: Some("button:Reset"), + subject: "heading:Panel outcome: reset", + expect: Present, + ), ] diff --git a/tests/ps-qa-headless/cookie-consent.ron b/tests/ps-qa-headless/cookie-consent.ron index db13a241..1c389daf 100644 --- a/tests/ps-qa-headless/cookie-consent.ron +++ b/tests/ps-qa-headless/cookie-consent.ron @@ -25,13 +25,46 @@ expect: Paints, ), ( - id: "cookie-consent-paints", + id: "cookie-consent-manages", group: "cookie-consent", - what: "the CookieConsent reaches the renderer with a box", + what: "cookie consent opens its preference dialog", open: None, hover: None, - click: None, - subject: "CookieConsent", + click: Some("button:Manage M"), + subject: "heading:Manage M preferences", + expect: Present, + covers: ["button:Manage *"], + ), + ( + id: "cookie-consent-saves-custom", + group: "cookie-consent", + what: "saving managed preferences reports custom consent", + open: None, + hover: None, + click: Some("button:Save M"), + subject: "heading:Cookie consent M: custom", + expect: Present, + ), + ( + id: "cookie-consent-accepts-all", + group: "cookie-consent", + what: "accepting all cookies reports full consent", + open: None, + hover: None, + click: Some("button:Accept all A"), + subject: "heading:Cookie consent A: all", + expect: Present, + covers: ["button:Accept all *"], + ), + ( + id: "cookie-consent-declines", + group: "cookie-consent", + what: "declining optional cookies reports essential consent", + open: None, + hover: None, + click: Some("button:Decline D"), + subject: "heading:Cookie consent D: essential", expect: Present, + covers: ["button:Decline *"], ), ] diff --git a/tests/ps-qa-headless/data-grid.ron b/tests/ps-qa-headless/data-grid.ron index 60cd4f5d..afd59148 100644 --- a/tests/ps-qa-headless/data-grid.ron +++ b/tests/ps-qa-headless/data-grid.ron @@ -25,13 +25,56 @@ expect: Paints, ), ( - id: "data-grid-paints", + id: "data-grid-sorts", group: "data-grid", - what: "the DataGrid reaches the renderer with a box", + what: "a sortable grid column reports its next direction", + open: None, + hover: None, + click: Some("columnheader:Name"), + subject: "heading:Grid sort:", + expect: NameChanges, + ), + ( + id: "data-grid-selects-row", + group: "data-grid", + what: "a row checkbox reports the selected row identity", + open: None, + hover: None, + click: Some("checkbox:Select row"), + subject: "heading:Grid selection:", + expect: NameChanges, + covers: ["checkbox:Select row"], + ), + ( + id: "data-grid-selects-page", + group: "data-grid", + what: "the select-all checkbox selects every row on the current page", + open: None, + hover: None, + click: Some("checkbox:Select all rows"), + subject: "heading:Grid selection:", + expect: NameChanges, + ), + ( + id: "data-grid-pages", + group: "data-grid", + what: "the grid pager reports the next page", + open: None, + hover: None, + click: Some("button:Next page"), + subject: "heading:Grid page:", + expect: NameChanges, + ), + ( + id: "data-grid-filters", + group: "data-grid", + what: "the grid search field filters the source rows", open: None, hover: None, click: None, - subject: "DataGrid", - expect: Present, + subject: "heading:Grid first filtered row:", + expect: NameChanges, + type_into: Some("Search Name"), + text: Some("Gam"), ), ] diff --git a/tests/ps-qa-headless/date-field.ron b/tests/ps-qa-headless/date-field.ron new file mode 100644 index 00000000..7b873660 --- /dev/null +++ b/tests/ps-qa-headless/date-field.ron @@ -0,0 +1,51 @@ +// Generated from tests/qa-harness/components.ts by tests/qa-harness/generate-checks.ts. Do not edit. +// +// DateField, mounted alone on its own harness page. Outcomes for +// this component share one native host; idempotent preparation keeps each +// outcome reproducible by id against a fresh host as well. +[ + ( + id: "date-field-page-paints", + group: "date-field", + what: "the DateField page builds and paints", + open: None, + hover: None, + click: None, + subject: "heading:DateField", + expect: Present, + ), + ( + id: "date-field-renders", + group: "date-field", + what: "DateField renders a node of its own", + open: None, + hover: None, + click: None, + subject: "fixture", + expect: Paints, + ), + ( + id: "date-field-accepts", + group: "date-field", + what: "typing changes the date field value", + open: None, + hover: None, + click: None, + subject: "textbox:Date value", + expect: ValueChanges, + type_into: Some("textbox:Date value"), + text: Some("2025-06-24"), + ), + ( + id: "date-field-reports", + group: "date-field", + what: "typing reports the date value", + open: None, + hover: None, + click: None, + subject: "heading:DateField value: 2025-06-24", + expect: Present, + type_into: Some("textbox:Date value"), + text: Some("2025-06-24"), + ), +] diff --git a/tests/ps-qa-headless/date-picker.ron b/tests/ps-qa-headless/date-picker.ron new file mode 100644 index 00000000..0e49c229 --- /dev/null +++ b/tests/ps-qa-headless/date-picker.ron @@ -0,0 +1,49 @@ +// Generated from tests/qa-harness/components.ts by tests/qa-harness/generate-checks.ts. Do not edit. +// +// DatePicker, mounted alone on its own harness page. Outcomes for +// this component share one native host; idempotent preparation keeps each +// outcome reproducible by id against a fresh host as well. +[ + ( + id: "date-picker-page-paints", + group: "date-picker", + what: "the DatePicker page builds and paints", + open: None, + hover: None, + click: None, + subject: "heading:DatePicker", + expect: Present, + ), + ( + id: "date-picker-renders", + group: "date-picker", + what: "DatePicker renders a node of its own", + open: None, + hover: None, + click: None, + subject: "fixture", + expect: Paints, + ), + ( + id: "date-picker-opens", + group: "date-picker", + what: "the date picker opens its calendar dialog", + open: None, + hover: None, + click: Some("button:Jun 15, 2025"), + subject: "dialog:", + expect: Present, + ), + ( + id: "date-picker-selects", + group: "date-picker", + what: "choosing a date reports the controlled value", + open: None, + hover: None, + click: Some("gridcell:Tuesday, June 24, 2025"), + subject: "heading:DatePicker value: 2025-06-24", + expect: Present, + prepare: Some("button:Jun 15, 2025"), + prepare_unless: Some("dialog:"), + ), +] diff --git a/tests/ps-qa-headless/date-range-picker.ron b/tests/ps-qa-headless/date-range-picker.ron new file mode 100644 index 00000000..6ba12995 --- /dev/null +++ b/tests/ps-qa-headless/date-range-picker.ron @@ -0,0 +1,59 @@ +// Generated from tests/qa-harness/components.ts by tests/qa-harness/generate-checks.ts. Do not edit. +// +// DateRangePicker, mounted alone on its own harness page. Outcomes for +// this component share one native host; idempotent preparation keeps each +// outcome reproducible by id against a fresh host as well. +[ + ( + id: "date-range-picker-page-paints", + group: "date-range-picker", + what: "the DateRangePicker page builds and paints", + open: None, + hover: None, + click: None, + subject: "heading:DateRangePicker", + expect: Present, + ), + ( + id: "date-range-picker-renders", + group: "date-range-picker", + what: "DateRangePicker renders a node of its own", + open: None, + hover: None, + click: None, + subject: "fixture", + expect: Paints, + ), + ( + id: "date-range-picker-opens", + group: "date-range-picker", + what: "the date range picker opens its calendar dialog", + open: None, + hover: None, + click: Some("button:Jun 15, 2025 Jun 17, 2025"), + subject: "dialog:", + expect: Present, + ), + ( + id: "date-range-picker-starts", + group: "date-range-picker", + what: "choosing a date starts a pending range", + open: None, + hover: None, + click: Some("gridcell:Tuesday, June 24, 2025"), + subject: "gridcell:Tuesday, June 24, 2025", + expect: SelectionChanges, + prepare: Some("button:Jun 15, 2025 Jun 17, 2025"), + prepare_unless: Some("dialog:"), + ), + ( + id: "date-range-picker-completes", + group: "date-range-picker", + what: "choosing a second date completes the controlled range", + open: None, + hover: None, + click: Some("gridcell:Thursday, June 26, 2025"), + subject: "heading:DateRangePicker end: 2025-06-26", + expect: Present, + ), +] diff --git a/tests/ps-qa-headless/dock.ron b/tests/ps-qa-headless/dock.ron index 611f18bf..f2968d28 100644 --- a/tests/ps-qa-headless/dock.ron +++ b/tests/ps-qa-headless/dock.ron @@ -25,13 +25,14 @@ expect: Paints, ), ( - id: "dock-paints", + id: "dock-acts", group: "dock", - what: "the Dock reaches the renderer with a box", + what: "a dock item invokes its owner", open: None, hover: None, - click: None, - subject: "Dock", + click: Some("button:Search"), + subject: "heading:Dock selected: Search", expect: Present, + covers: ["button:Home", "button:Search", "button:Settings"], ), ] diff --git a/tests/ps-qa-headless/firefox-pwa-banner.ron b/tests/ps-qa-headless/firefox-pwa-banner.ron index 0c1f4739..1309fef5 100644 --- a/tests/ps-qa-headless/firefox-pwa-banner.ron +++ b/tests/ps-qa-headless/firefox-pwa-banner.ron @@ -25,13 +25,36 @@ expect: Paints, ), ( - id: "firefox-pwa-banner-paints", + id: "firefox-pwa-banner-installs", group: "firefox-pwa-banner", - what: "the FirefoxPWABanner reaches the renderer with a box", + what: "the Firefox extension action invokes its consumer callback", open: None, hover: None, - click: None, - subject: "FirefoxPWABanner", - expect: Present, + click: Some("button:Install extension A"), + subject: "heading:Firefox PWA outcome:", + expect: NameChanges, + covers: ["button:Install extension *"], + ), + ( + id: "firefox-pwa-banner-defers", + group: "firefox-pwa-banner", + what: "the Firefox later action dismisses the banner", + open: None, + hover: None, + click: Some("button:Maybe later B"), + subject: "heading:Firefox PWA outcome:", + expect: NameChanges, + covers: ["button:Maybe later *"], + ), + ( + id: "firefox-pwa-banner-closes", + group: "firefox-pwa-banner", + what: "the Firefox close action dismisses the banner", + open: None, + hover: None, + click: Some("button:Close Firefox C"), + subject: "heading:Firefox PWA outcome:", + expect: NameChanges, + covers: ["button:Close Firefox *"], ), ] diff --git a/tests/ps-qa-headless/flex-grid.ron b/tests/ps-qa-headless/flex-grid.ron new file mode 100644 index 00000000..82a6d458 --- /dev/null +++ b/tests/ps-qa-headless/flex-grid.ron @@ -0,0 +1,37 @@ +// Generated from tests/qa-harness/components.ts by tests/qa-harness/generate-checks.ts. Do not edit. +// +// FlexGrid, mounted alone on its own harness page. Outcomes for +// this component share one native host; idempotent preparation keeps each +// outcome reproducible by id against a fresh host as well. +[ + ( + id: "flex-grid-page-paints", + group: "flex-grid", + what: "the FlexGrid page builds and paints", + open: None, + hover: None, + click: None, + subject: "heading:FlexGrid", + expect: Present, + ), + ( + id: "flex-grid-renders", + group: "flex-grid", + what: "FlexGrid renders a node of its own", + open: None, + hover: None, + click: None, + subject: "fixture", + expect: Paints, + ), + ( + id: "flex-grid-reveals-more", + group: "flex-grid", + what: "the incremental grid reveals its next page when asked", + open: None, + hover: None, + click: Some("button:Load more rows"), + subject: "heading:Row Three", + expect: Present, + ), +] diff --git a/tests/ps-qa-headless/immersive-landing.ron b/tests/ps-qa-headless/immersive-landing.ron index 0b1387c7..028e3cb4 100644 --- a/tests/ps-qa-headless/immersive-landing.ron +++ b/tests/ps-qa-headless/immersive-landing.ron @@ -25,13 +25,14 @@ expect: Paints, ), ( - id: "immersive-landing-paints", + id: "immersive-landing-navigates", group: "immersive-landing", - what: "the ImmersiveLanding reaches the renderer with a box", + what: "landing navigation changes the active page and reports the route", open: None, hover: None, - click: None, - subject: "ImmersiveLanding", + click: Some("button:Go to page 2 of 2"), + subject: "heading:Landing navigation: first to second", expect: Present, + covers: ["button:Go to page 1 of 2", "button:Go to page 2 of 2", "button:Next page"], ), ] diff --git a/tests/ps-qa-headless/input-otp.ron b/tests/ps-qa-headless/input-otp.ron new file mode 100644 index 00000000..4bc7aea9 --- /dev/null +++ b/tests/ps-qa-headless/input-otp.ron @@ -0,0 +1,51 @@ +// Generated from tests/qa-harness/components.ts by tests/qa-harness/generate-checks.ts. Do not edit. +// +// InputOTP, mounted alone on its own harness page. Outcomes for +// this component share one native host; idempotent preparation keeps each +// outcome reproducible by id against a fresh host as well. +[ + ( + id: "input-otp-page-paints", + group: "input-otp", + what: "the InputOTP page builds and paints", + open: None, + hover: None, + click: None, + subject: "heading:InputOTP", + expect: Present, + ), + ( + id: "input-otp-renders", + group: "input-otp", + what: "InputOTP renders a node of its own", + open: None, + hover: None, + click: None, + subject: "fixture", + expect: Paints, + ), + ( + id: "input-otp-accepts", + group: "input-otp", + what: "typing fills the one-time password value", + open: None, + hover: None, + click: None, + subject: "textbox:Verification code", + expect: ValueChanges, + type_into: Some("textbox:Verification code"), + text: Some("123456"), + ), + ( + id: "input-otp-reports", + group: "input-otp", + what: "typing reports the one-time password", + open: None, + hover: None, + click: None, + subject: "heading:InputOTP value: 123456", + expect: Present, + type_into: Some("textbox:Verification code"), + text: Some("123456"), + ), +] diff --git a/tests/ps-qa-headless/join.ron b/tests/ps-qa-headless/join.ron new file mode 100644 index 00000000..6637a612 --- /dev/null +++ b/tests/ps-qa-headless/join.ron @@ -0,0 +1,38 @@ +// Generated from tests/qa-harness/components.ts by tests/qa-harness/generate-checks.ts. Do not edit. +// +// Join, mounted alone on its own harness page. Outcomes for +// this component share one native host; idempotent preparation keeps each +// outcome reproducible by id against a fresh host as well. +[ + ( + id: "join-page-paints", + group: "join", + what: "the Join page builds and paints", + open: None, + hover: None, + click: None, + subject: "heading:Join", + expect: Present, + ), + ( + id: "join-renders", + group: "join", + what: "Join renders a node of its own", + open: None, + hover: None, + click: None, + subject: "fixture", + expect: Paints, + ), + ( + id: "join-contains-actions", + group: "join", + what: "joined buttons remain operable", + open: None, + hover: None, + click: Some("button:First joined button"), + subject: "heading:Join selected: first", + expect: Present, + covers: ["button:* joined button"], + ), +] diff --git a/tests/ps-qa-headless/kbd.ron b/tests/ps-qa-headless/kbd.ron new file mode 100644 index 00000000..117053c0 --- /dev/null +++ b/tests/ps-qa-headless/kbd.ron @@ -0,0 +1,37 @@ +// Generated from tests/qa-harness/components.ts by tests/qa-harness/generate-checks.ts. Do not edit. +// +// Kbd, mounted alone on its own harness page. Outcomes for +// this component share one native host; idempotent preparation keeps each +// outcome reproducible by id against a fresh host as well. +[ + ( + id: "kbd-page-paints", + group: "kbd", + what: "the Kbd page builds and paints", + open: None, + hover: None, + click: None, + subject: "heading:Kbd", + expect: Present, + ), + ( + id: "kbd-renders", + group: "kbd", + what: "Kbd renders a node of its own", + open: None, + hover: None, + click: None, + subject: "fixture", + expect: Paints, + ), + ( + id: "kbd-paints", + group: "kbd", + what: "the Kbd reaches the renderer with a box", + open: None, + hover: None, + click: None, + subject: "Kbd", + expect: Present, + ), +] diff --git a/tests/ps-qa-headless/list-box.ron b/tests/ps-qa-headless/list-box.ron index 0aea6b5d..595c0e69 100644 --- a/tests/ps-qa-headless/list-box.ron +++ b/tests/ps-qa-headless/list-box.ron @@ -25,13 +25,23 @@ expect: Paints, ), ( - id: "list-box-paints", + id: "list-box-selects", group: "list-box", - what: "the ListBox reaches the renderer with a box", + what: "choosing a listbox item changes its controlled selection", open: None, hover: None, - click: None, - subject: "ListBox", - expect: Present, + click: Some("option:Second item"), + subject: "option:Second item", + expect: SelectionChanges, + ), + ( + id: "list-box-reports", + group: "list-box", + what: "the listbox reports the selected key", + open: None, + hover: None, + click: Some("option:First item"), + subject: "heading:ListBox value:", + expect: NameChanges, ), ] diff --git a/tests/ps-qa-headless/live-chat-panel.ron b/tests/ps-qa-headless/live-chat-panel.ron index cd2f1695..31d34182 100644 --- a/tests/ps-qa-headless/live-chat-panel.ron +++ b/tests/ps-qa-headless/live-chat-panel.ron @@ -25,19 +25,31 @@ expect: Paints, ), ( - id: "live-chat-panel-paints", + id: "live-chat-panel-accepts-message", group: "live-chat-panel", - what: "the LiveChatPanel control is on screen and addressable", + what: "the chat composer accepts a message", open: None, hover: None, click: None, - subject: "button:Close chat", + subject: "textbox:Message support...", + expect: ValueChanges, + type_into: Some("textbox:Message support..."), + text: Some("Hello support"), + ), + ( + id: "live-chat-panel-sends-message", + group: "live-chat-panel", + what: "the chat panel hands the message to its owner", + open: None, + hover: None, + click: Some("button:Send"), + subject: "heading:LiveChat sent: Hello support", expect: Present, ), ( - id: "live-chat-panel-acts", + id: "live-chat-panel-closes", group: "live-chat-panel", - what: "activating LiveChatPanel exposes the callback result", + what: "the chat panel close control invokes its owner", open: None, hover: None, click: Some("button:Close chat"), diff --git a/tests/ps-qa-headless/menu.ron b/tests/ps-qa-headless/menu.ron new file mode 100644 index 00000000..4869c145 --- /dev/null +++ b/tests/ps-qa-headless/menu.ron @@ -0,0 +1,48 @@ +// Generated from tests/qa-harness/components.ts by tests/qa-harness/generate-checks.ts. Do not edit. +// +// Menu, mounted alone on its own harness page. Outcomes for +// this component share one native host; idempotent preparation keeps each +// outcome reproducible by id against a fresh host as well. +[ + ( + id: "menu-page-paints", + group: "menu", + what: "the Menu page builds and paints", + open: None, + hover: None, + click: None, + subject: "heading:Menu", + expect: Present, + ), + ( + id: "menu-renders", + group: "menu", + what: "Menu renders a node of its own", + open: None, + hover: None, + click: None, + subject: "fixture", + expect: Paints, + ), + ( + id: "menu-selects", + group: "menu", + what: "activating a menu item changes its controlled selection", + open: None, + hover: None, + click: Some("menuitemradio:Beta action"), + subject: "menuitemradio:Beta action", + expect: SelectionChanges, + covers: ["menuitemradio:* action"], + ), + ( + id: "menu-reports", + group: "menu", + what: "activating a menu item reports its selected key", + open: None, + hover: None, + click: Some("menuitemradio:Beta action"), + subject: "heading:Menu value: b", + expect: Present, + ), +] diff --git a/tests/ps-qa-headless/meter.ron b/tests/ps-qa-headless/meter.ron new file mode 100644 index 00000000..8f7695ed --- /dev/null +++ b/tests/ps-qa-headless/meter.ron @@ -0,0 +1,37 @@ +// Generated from tests/qa-harness/components.ts by tests/qa-harness/generate-checks.ts. Do not edit. +// +// Meter, mounted alone on its own harness page. Outcomes for +// this component share one native host; idempotent preparation keeps each +// outcome reproducible by id against a fresh host as well. +[ + ( + id: "meter-page-paints", + group: "meter", + what: "the Meter page builds and paints", + open: None, + hover: None, + click: None, + subject: "heading:Meter", + expect: Present, + ), + ( + id: "meter-renders", + group: "meter", + what: "Meter renders a node of its own", + open: None, + hover: None, + click: None, + subject: "fixture", + expect: Paints, + ), + ( + id: "meter-paints", + group: "meter", + what: "the Meter reaches the renderer with a box", + open: None, + hover: None, + click: None, + subject: "Meter", + expect: Present, + ), +] diff --git a/tests/ps-qa-headless/noise-background.ron b/tests/ps-qa-headless/noise-background.ron new file mode 100644 index 00000000..9f43393c --- /dev/null +++ b/tests/ps-qa-headless/noise-background.ron @@ -0,0 +1,37 @@ +// Generated from tests/qa-harness/components.ts by tests/qa-harness/generate-checks.ts. Do not edit. +// +// NoiseBackground, mounted alone on its own harness page. Outcomes for +// this component share one native host; idempotent preparation keeps each +// outcome reproducible by id against a fresh host as well. +[ + ( + id: "noise-background-page-paints", + group: "noise-background", + what: "the NoiseBackground page builds and paints", + open: None, + hover: None, + click: None, + subject: "heading:NoiseBackground", + expect: Present, + ), + ( + id: "noise-background-renders", + group: "noise-background", + what: "NoiseBackground renders a node of its own", + open: None, + hover: None, + click: None, + subject: "fixture", + expect: Paints, + ), + ( + id: "noise-background-paints", + group: "noise-background", + what: "the NoiseBackground reaches the renderer with a box", + open: None, + hover: None, + click: None, + subject: "NoiseBackground", + expect: Present, + ), +] diff --git a/tests/ps-qa-headless/pagination.ron b/tests/ps-qa-headless/pagination.ron index d926745f..6acb6964 100644 --- a/tests/ps-qa-headless/pagination.ron +++ b/tests/ps-qa-headless/pagination.ron @@ -25,23 +25,14 @@ expect: Paints, ), ( - id: "pagination-paints", + id: "pagination-changes", group: "pagination", - what: "the Pagination control is on screen and addressable", - open: None, - hover: None, - click: None, - subject: "button:Go to next page", - expect: Present, - ), - ( - id: "pagination-acts", - group: "pagination", - what: "activating Pagination exposes the callback result", + what: "pagination reports the next page to its owner", open: None, hover: None, click: Some("button:Go to next page"), subject: "heading:Action result: Pagination complete", expect: Present, + covers: ["button:Go to *"], ), ] diff --git a/tests/ps-qa-headless/password-field.ron b/tests/ps-qa-headless/password-field.ron index 1059ab94..33b079f8 100644 --- a/tests/ps-qa-headless/password-field.ron +++ b/tests/ps-qa-headless/password-field.ron @@ -25,13 +25,25 @@ expect: Paints, ), ( - id: "password-field-paints", + id: "password-field-accepts", group: "password-field", - what: "the PasswordField reaches the renderer with a box", + what: "typing updates the controlled password value", open: None, hover: None, click: None, - subject: "PasswordField", + subject: "heading:Password value: secret", + expect: Present, + type_into: Some("textbox:Password"), + text: Some("secret"), + ), + ( + id: "password-field-reveals", + group: "password-field", + what: "the visibility control reports its pressed state", + open: None, + hover: None, + click: Some("button:Show password"), + subject: "button:Hide password", expect: Present, ), ] diff --git a/tests/ps-qa-headless/pwa-install-prompt.ron b/tests/ps-qa-headless/pwa-install-prompt.ron index c34d2d17..c351f05e 100644 --- a/tests/ps-qa-headless/pwa-install-prompt.ron +++ b/tests/ps-qa-headless/pwa-install-prompt.ron @@ -25,13 +25,36 @@ expect: Paints, ), ( - id: "pwa-install-prompt-paints", + id: "pwa-install-prompt-installs", group: "pwa-install-prompt", - what: "the PWAInstallPrompt reaches the renderer with a box", + what: "accepting the browser install prompt reports installation", open: None, hover: None, - click: None, - subject: "PWAInstallPrompt", - expect: Present, + click: Some("button:Install A"), + subject: "heading:PWA outcome:", + expect: NameChanges, + covers: ["button:Install *"], + ), + ( + id: "pwa-install-prompt-defers", + group: "pwa-install-prompt", + what: "the not-now action dismisses the prompt and reports deferral", + open: None, + hover: None, + click: Some("button:Not now B"), + subject: "heading:PWA outcome:", + expect: NameChanges, + covers: ["button:Not now *"], + ), + ( + id: "pwa-install-prompt-closes", + group: "pwa-install-prompt", + what: "the close action dismisses the prompt and reports closure", + open: None, + hover: None, + click: Some("button:Close C"), + subject: "heading:PWA outcome:", + expect: NameChanges, + covers: ["button:Close *"], ), ] diff --git a/tests/ps-qa-headless/radial-progress.ron b/tests/ps-qa-headless/radial-progress.ron new file mode 100644 index 00000000..0d0f8e79 --- /dev/null +++ b/tests/ps-qa-headless/radial-progress.ron @@ -0,0 +1,37 @@ +// Generated from tests/qa-harness/components.ts by tests/qa-harness/generate-checks.ts. Do not edit. +// +// RadialProgress, mounted alone on its own harness page. Outcomes for +// this component share one native host; idempotent preparation keeps each +// outcome reproducible by id against a fresh host as well. +[ + ( + id: "radial-progress-page-paints", + group: "radial-progress", + what: "the RadialProgress page builds and paints", + open: None, + hover: None, + click: None, + subject: "heading:RadialProgress", + expect: Present, + ), + ( + id: "radial-progress-renders", + group: "radial-progress", + what: "RadialProgress renders a node of its own", + open: None, + hover: None, + click: None, + subject: "fixture", + expect: Paints, + ), + ( + id: "radial-progress-paints", + group: "radial-progress", + what: "the RadialProgress reaches the renderer with a box", + open: None, + hover: None, + click: None, + subject: "RadialProgress", + expect: Present, + ), +] diff --git a/tests/ps-qa-headless/radio-group.ron b/tests/ps-qa-headless/radio-group.ron new file mode 100644 index 00000000..09171c4b --- /dev/null +++ b/tests/ps-qa-headless/radio-group.ron @@ -0,0 +1,48 @@ +// Generated from tests/qa-harness/components.ts by tests/qa-harness/generate-checks.ts. Do not edit. +// +// RadioGroup, mounted alone on its own harness page. Outcomes for +// this component share one native host; idempotent preparation keeps each +// outcome reproducible by id against a fresh host as well. +[ + ( + id: "radio-group-page-paints", + group: "radio-group", + what: "the RadioGroup page builds and paints", + open: None, + hover: None, + click: None, + subject: "heading:RadioGroup", + expect: Present, + ), + ( + id: "radio-group-renders", + group: "radio-group", + what: "RadioGroup renders a node of its own", + open: None, + hover: None, + click: None, + subject: "fixture", + expect: Paints, + ), + ( + id: "radio-group-selects", + group: "radio-group", + what: "choosing another grouped radio changes controlled selection", + open: None, + hover: None, + click: Some("radio:Second radio"), + subject: "radio:Second radio", + expect: SelectionChanges, + covers: ["radio:* radio"], + ), + ( + id: "radio-group-reports", + group: "radio-group", + what: "choosing another grouped radio reports its value", + open: None, + hover: None, + click: Some("radio:Second radio"), + subject: "heading:RadioGroup value: second", + expect: Present, + ), +] diff --git a/tests/ps-qa-headless/radio.ron b/tests/ps-qa-headless/radio.ron index f4e5e31b..22cd2202 100644 --- a/tests/ps-qa-headless/radio.ron +++ b/tests/ps-qa-headless/radio.ron @@ -30,8 +30,8 @@ what: "pressing the Radio changes what it reports", open: None, hover: None, - click: Some("radio:"), - subject: "radio:", + click: Some("radio:Radio"), + subject: "radio:Radio", expect: SelectionChanges, ), ( @@ -41,7 +41,7 @@ open: None, hover: None, settle_after_ms: 300, - click: Some("radio:"), + click: Some("radio:Radio"), subject: "heading:Callback ran", expect: Present, ), diff --git a/tests/ps-qa-headless/range-calendar.ron b/tests/ps-qa-headless/range-calendar.ron new file mode 100644 index 00000000..c1d6ceb2 --- /dev/null +++ b/tests/ps-qa-headless/range-calendar.ron @@ -0,0 +1,67 @@ +// Generated from tests/qa-harness/components.ts by tests/qa-harness/generate-checks.ts. Do not edit. +// +// RangeCalendar, mounted alone on its own harness page. Outcomes for +// this component share one native host; idempotent preparation keeps each +// outcome reproducible by id against a fresh host as well. +[ + ( + id: "range-calendar-page-paints", + group: "range-calendar", + what: "the RangeCalendar page builds and paints", + open: None, + hover: None, + click: None, + subject: "heading:RangeCalendar", + expect: Present, + ), + ( + id: "range-calendar-renders", + group: "range-calendar", + what: "RangeCalendar renders a node of its own", + open: None, + hover: None, + click: None, + subject: "fixture", + expect: Paints, + ), + ( + id: "range-calendar-starts", + group: "range-calendar", + what: "choosing a date starts a pending range", + open: None, + hover: None, + click: Some("gridcell:Tuesday, June 24, 2025"), + subject: "gridcell:Tuesday, June 24, 2025", + expect: SelectionChanges, + ), + ( + id: "range-calendar-completes", + group: "range-calendar", + what: "choosing a second date completes the controlled range", + open: None, + hover: None, + click: Some("gridcell:Thursday, June 26, 2025"), + subject: "heading:RangeCalendar end: 2025-06-26", + expect: Present, + ), + ( + id: "range-calendar-next-month", + group: "range-calendar", + what: "the next-month control advances the visible calendar", + open: None, + hover: None, + click: Some("button:Next month"), + subject: "heading:July 2025", + expect: Present, + ), + ( + id: "range-calendar-previous-month", + group: "range-calendar", + what: "the previous-month control returns to the prior calendar", + open: None, + hover: None, + click: Some("button:Previous month"), + subject: "heading:June 2025", + expect: Present, + ), +] diff --git a/tests/ps-qa-headless/size-picker.ron b/tests/ps-qa-headless/size-picker.ron new file mode 100644 index 00000000..ebb4aa49 --- /dev/null +++ b/tests/ps-qa-headless/size-picker.ron @@ -0,0 +1,48 @@ +// Generated from tests/qa-harness/components.ts by tests/qa-harness/generate-checks.ts. Do not edit. +// +// SizePicker, mounted alone on its own harness page. Outcomes for +// this component share one native host; idempotent preparation keeps each +// outcome reproducible by id against a fresh host as well. +[ + ( + id: "size-picker-page-paints", + group: "size-picker", + what: "the SizePicker page builds and paints", + open: None, + hover: None, + click: None, + subject: "heading:SizePicker", + expect: Present, + ), + ( + id: "size-picker-renders", + group: "size-picker", + what: "SizePicker renders a node of its own", + open: None, + hover: None, + click: None, + subject: "fixture", + expect: Paints, + ), + ( + id: "size-picker-selects", + group: "size-picker", + what: "choosing a size changes its selected radio", + open: None, + hover: None, + click: Some("radio:Size L"), + subject: "radio:Size L", + expect: SelectionChanges, + covers: ["radio:Size *"], + ), + ( + id: "size-picker-reports", + group: "size-picker", + what: "choosing a size reports the preset", + open: None, + hover: None, + click: Some("radio:Size L"), + subject: "heading:SizePicker value: L", + expect: Present, + ), +] diff --git a/tests/ps-qa-headless/switch.ron b/tests/ps-qa-headless/switch.ron index e2e65ad2..5e461f8a 100644 --- a/tests/ps-qa-headless/switch.ron +++ b/tests/ps-qa-headless/switch.ron @@ -30,8 +30,8 @@ what: "pressing the Switch changes what it reports", open: None, hover: None, - click: Some("switch:"), - subject: "switch:", + click: Some("switch:Switch"), + subject: "switch:Switch", expect: SelectionChanges, ), ( @@ -41,7 +41,7 @@ open: None, hover: None, settle_after_ms: 300, - click: Some("switch:"), + click: Some("switch:Switch"), subject: "heading:Callback ran", expect: Present, ), diff --git a/tests/ps-qa-headless/table.ron b/tests/ps-qa-headless/table.ron index 4a09b80c..aafbf8d3 100644 --- a/tests/ps-qa-headless/table.ron +++ b/tests/ps-qa-headless/table.ron @@ -25,13 +25,13 @@ expect: Paints, ), ( - id: "table-paints", + id: "table-sorts", group: "table", - what: "the Table reaches the renderer with a box", + what: "a sortable table column reports its next direction", open: None, hover: None, - click: None, - subject: "Table", - expect: Present, + click: Some("Name fixture"), + subject: "heading:Table sort:", + expect: NameChanges, ), ] diff --git a/tests/ps-qa-headless/tabs.ron b/tests/ps-qa-headless/tabs.ron index 5ce94f35..cd7a3759 100644 --- a/tests/ps-qa-headless/tabs.ron +++ b/tests/ps-qa-headless/tabs.ron @@ -27,21 +27,20 @@ ( id: "tabs-changes", group: "tabs", - what: "activating another Tabs tab changes selection", + what: "activating another tab changes controlled selection", open: None, hover: None, click: Some("tab:Second"), subject: "tab:Second", expect: SelectionChanges, + covers: ["tab:*"], ), ( id: "tabs-changes-panel", group: "tabs", - what: "the selected Tabs tab exposes its corresponding panel", + what: "the selected tab exposes its corresponding panel", open: None, hover: None, - prepare: Some("tab:Second"), - prepare_unless: Some("heading:Second panel"), click: None, subject: "heading:Second panel", expect: Present, diff --git a/tests/ps-qa-headless/theme-color-picker.ron b/tests/ps-qa-headless/theme-color-picker.ron index a6ac3cfc..0b29e6bf 100644 --- a/tests/ps-qa-headless/theme-color-picker.ron +++ b/tests/ps-qa-headless/theme-color-picker.ron @@ -25,13 +25,24 @@ expect: Paints, ), ( - id: "theme-color-picker-paints", + id: "theme-color-picker-opens", group: "theme-color-picker", - what: "the ThemeColorPicker reaches the renderer with a box", + what: "the theme color trigger opens its palette", open: None, hover: None, - click: None, - subject: "ThemeColorPicker", + click: Some("button:Change theme color"), + subject: "button:Black", + expect: Present, + ), + ( + id: "theme-color-picker-switches-theme", + group: "theme-color-picker", + what: "choosing a grayscale swatch reports the requested theme", + open: None, + hover: None, + click: Some("button:Black"), + subject: "heading:ThemeColorPicker theme: light", expect: Present, + covers: ["button:White", "button:Light gray", "button:Gray", "button:Dark gray", "button:Charcoal", "button:Black"], ), ] diff --git a/tests/ps-qa-headless/time-field.ron b/tests/ps-qa-headless/time-field.ron new file mode 100644 index 00000000..4abd5be0 --- /dev/null +++ b/tests/ps-qa-headless/time-field.ron @@ -0,0 +1,51 @@ +// Generated from tests/qa-harness/components.ts by tests/qa-harness/generate-checks.ts. Do not edit. +// +// TimeField, mounted alone on its own harness page. Outcomes for +// this component share one native host; idempotent preparation keeps each +// outcome reproducible by id against a fresh host as well. +[ + ( + id: "time-field-page-paints", + group: "time-field", + what: "the TimeField page builds and paints", + open: None, + hover: None, + click: None, + subject: "heading:TimeField", + expect: Present, + ), + ( + id: "time-field-renders", + group: "time-field", + what: "TimeField renders a node of its own", + open: None, + hover: None, + click: None, + subject: "fixture", + expect: Paints, + ), + ( + id: "time-field-accepts", + group: "time-field", + what: "typing changes the time field value", + open: None, + hover: None, + click: None, + subject: "textbox:Time value", + expect: ValueChanges, + type_into: Some("textbox:Time value"), + text: Some("12:34"), + ), + ( + id: "time-field-reports", + group: "time-field", + what: "typing reports the time value", + open: None, + hover: None, + click: None, + subject: "heading:TimeField value: 12:34", + expect: Present, + type_into: Some("textbox:Time value"), + text: Some("12:34"), + ), +] diff --git a/tests/ps-qa-headless/toast.ron b/tests/ps-qa-headless/toast.ron index e9c0502f..0af38158 100644 --- a/tests/ps-qa-headless/toast.ron +++ b/tests/ps-qa-headless/toast.ron @@ -25,13 +25,33 @@ expect: Paints, ), ( - id: "toast-paints", + id: "toast-opens", group: "toast", - what: "the Toast reaches the renderer with a box", + what: "requesting a toast paints its queued action", open: None, hover: None, - click: None, - subject: "Toast", + click: Some("button:Show fixture toast"), + subject: "button:Undo fixture", expect: Present, ), + ( + id: "toast-acts", + group: "toast", + what: "the toast action invokes its consumer callback", + open: None, + hover: None, + click: Some("button:Undo fixture"), + subject: "heading:Toast outcome:", + expect: NameChanges, + ), + ( + id: "toast-closes", + group: "toast", + what: "the toast close control removes the notification", + open: None, + hover: None, + click: Some("button:Dismiss notification"), + subject: "button:Dismiss notification", + expect: Vanishes, + ), ] diff --git a/tests/ps-qa-headless/toolbar.ron b/tests/ps-qa-headless/toolbar.ron new file mode 100644 index 00000000..358a02c7 --- /dev/null +++ b/tests/ps-qa-headless/toolbar.ron @@ -0,0 +1,40 @@ +// Generated from tests/qa-harness/components.ts by tests/qa-harness/generate-checks.ts. Do not edit. +// +// Toolbar, mounted alone on its own harness page. Outcomes for +// this component share one native host; idempotent preparation keeps each +// outcome reproducible by id against a fresh host as well. +[ + ( + id: "toolbar-page-paints", + group: "toolbar", + what: "the Toolbar page builds and paints", + open: None, + hover: None, + click: None, + subject: "heading:Toolbar", + expect: Present, + ), + ( + id: "toolbar-renders", + group: "toolbar", + what: "Toolbar renders a node of its own", + open: None, + hover: None, + click: None, + subject: "fixture", + expect: Paints, + ), + ( + id: "toolbar-moves-focus", + group: "toolbar", + what: "ArrowRight moves focus to the next toolbar control", + open: None, + hover: None, + click: None, + subject: "button:Second tool", + expect: FocusMoves, + prepare: Some("button:First tool"), + key: Some("ArrowRight"), + key_on: Some("button:First tool"), + ), +] diff --git a/tests/ps-qa-headless/tooltip.ron b/tests/ps-qa-headless/tooltip.ron index 8642b9b1..de0a5e3c 100644 --- a/tests/ps-qa-headless/tooltip.ron +++ b/tests/ps-qa-headless/tooltip.ron @@ -25,13 +25,13 @@ expect: Paints, ), ( - id: "tooltip-paints", + id: "tooltip-opens", group: "tooltip", - what: "the Tooltip reaches the renderer with a box", + what: "hovering the tooltip trigger reveals its content", open: None, - hover: None, + hover: Some("button:Tooltip target"), click: None, - subject: "Tooltip", + subject: "tooltip:Fixture tooltip", expect: Present, ), ] diff --git a/tests/ps-qa-headless/video-preview.ron b/tests/ps-qa-headless/video-preview.ron new file mode 100644 index 00000000..41ca477f --- /dev/null +++ b/tests/ps-qa-headless/video-preview.ron @@ -0,0 +1,37 @@ +// Generated from tests/qa-harness/components.ts by tests/qa-harness/generate-checks.ts. Do not edit. +// +// VideoPreview, mounted alone on its own harness page. Outcomes for +// this component share one native host; idempotent preparation keeps each +// outcome reproducible by id against a fresh host as well. +[ + ( + id: "video-preview-page-paints", + group: "video-preview", + what: "the VideoPreview page builds and paints", + open: None, + hover: None, + click: None, + subject: "heading:VideoPreview", + expect: Present, + ), + ( + id: "video-preview-renders", + group: "video-preview", + what: "VideoPreview renders a node of its own", + open: None, + hover: None, + click: None, + subject: "fixture", + expect: Paints, + ), + ( + id: "video-preview-paints", + group: "video-preview", + what: "the VideoPreview reaches the renderer with a box", + open: None, + hover: None, + click: None, + subject: "VideoPreview", + expect: Present, + ), +] diff --git a/tests/ps-qa/accordion.ron b/tests/ps-qa/accordion.ron index 6128e9c2..1f025225 100644 --- a/tests/ps-qa/accordion.ron +++ b/tests/ps-qa/accordion.ron @@ -25,13 +25,23 @@ expect: Paints, ), ( - id: "accordion-paints", + id: "accordion-opens", group: "accordion", - what: "the Accordion reaches the renderer with a box", + what: "activating an accordion trigger reveals its panel", open: None, hover: None, - click: None, - subject: "Accordion", - expect: Paints, + click: Some("button:First section"), + subject: "heading:First panel", + expect: PaintsNamed, + ), + ( + id: "accordion-reports", + group: "accordion", + what: "the accordion reports its controlled selection", + open: None, + hover: None, + click: Some("button:First section"), + subject: "heading:Accordion value:", + expect: NameChanges, ), ] diff --git a/tests/ps-qa/address.ron b/tests/ps-qa/address.ron index 61132164..d83e7835 100644 --- a/tests/ps-qa/address.ron +++ b/tests/ps-qa/address.ron @@ -25,13 +25,13 @@ expect: Paints, ), ( - id: "address-paints", + id: "address-copies", group: "address", - what: "the Address reaches the renderer with a box", + what: "copying an address reports the full value to its caller", open: None, hover: None, - click: None, - subject: "Address", - expect: Paints, + click: Some("button:Copy address"), + subject: "heading:Address copied:", + expect: NameChanges, ), ] diff --git a/tests/ps-qa/alert.ron b/tests/ps-qa/alert.ron index 0ff73598..15d5611c 100644 --- a/tests/ps-qa/alert.ron +++ b/tests/ps-qa/alert.ron @@ -25,13 +25,13 @@ expect: Paints, ), ( - id: "alert-paints", + id: "alert-dismisses", group: "alert", - what: "the Alert reaches the renderer with a box", + what: "the alert dismiss control invokes its owner", open: None, hover: None, - click: None, - subject: "Alert", - expect: Paints, + click: Some("button:Dismiss fixture alert"), + subject: "heading:Alert dismissed", + expect: PaintsNamed, ), ] diff --git a/tests/ps-qa/auth-footer-links.ron b/tests/ps-qa/auth-footer-links.ron index 4196d11f..ce4757e8 100644 --- a/tests/ps-qa/auth-footer-links.ron +++ b/tests/ps-qa/auth-footer-links.ron @@ -25,13 +25,23 @@ expect: Paints, ), ( - id: "auth-footer-links-paints", + id: "auth-footer-links-follows-link", group: "auth-footer-links", - what: "the AuthFooterLinks reaches the renderer with a box", + what: "an auth footer link invokes its callback before navigation", open: None, hover: None, - click: None, - subject: "AuthFooterLinks", - expect: Paints, + click: Some("link:Privacy fixture"), + subject: "heading:Auth footer action:", + expect: NameChanges, + ), + ( + id: "auth-footer-links-runs-action", + group: "auth-footer-links", + what: "an auth footer action remains a semantic button", + open: None, + hover: None, + click: Some("button:Help fixture"), + subject: "heading:Auth footer action:", + expect: NameChanges, ), ] diff --git a/tests/ps-qa/auth-powered-by.ron b/tests/ps-qa/auth-powered-by.ron index 0eeece3a..1b95ac96 100644 --- a/tests/ps-qa/auth-powered-by.ron +++ b/tests/ps-qa/auth-powered-by.ron @@ -25,13 +25,13 @@ expect: Paints, ), ( - id: "auth-powered-by-paints", + id: "auth-powered-by-navigates", group: "auth-powered-by", - what: "the AuthPoweredBy reaches the renderer with a box", + what: "the powered-by attribution exposes an operable Honey link", open: None, hover: None, - click: None, - subject: "AuthPoweredBy", - expect: Paints, + click: Some("link:Secure Auth by Honey"), + subject: "heading:Honey link activated", + expect: PaintsNamed, ), ] diff --git a/tests/ps-qa/breadcrumb.ron b/tests/ps-qa/breadcrumb.ron index 0cf5f4fa..1275b7b9 100644 --- a/tests/ps-qa/breadcrumb.ron +++ b/tests/ps-qa/breadcrumb.ron @@ -25,13 +25,13 @@ expect: Paints, ), ( - id: "breadcrumb-paints", + id: "breadcrumb-navigates", group: "breadcrumb", - what: "the Breadcrumb reaches the renderer with a box", + what: "a breadcrumb link remains operable inside the compound list", open: None, hover: None, - click: None, - subject: "Breadcrumb", - expect: Paints, + click: Some("link:Products fixture"), + subject: "heading:Breadcrumb activated", + expect: PaintsNamed, ), ] diff --git a/tests/ps-qa/button-group.ron b/tests/ps-qa/button-group.ron new file mode 100644 index 00000000..025ca6ba --- /dev/null +++ b/tests/ps-qa/button-group.ron @@ -0,0 +1,38 @@ +// Generated from tests/qa-harness/components.ts by tests/qa-harness/generate-checks.ts. Do not edit. +// +// ButtonGroup, mounted alone on its own harness page. Outcomes for +// this component share one native host; idempotent preparation keeps each +// outcome reproducible by id against a fresh host as well. +[ + ( + id: "button-group-page-paints", + group: "button-group", + what: "the ButtonGroup page builds and paints", + open: None, + hover: None, + click: None, + subject: "heading:ButtonGroup", + expect: PaintsNamed, + ), + ( + id: "button-group-renders", + group: "button-group", + what: "ButtonGroup renders a node of its own", + open: None, + hover: None, + click: None, + subject: "fixture", + expect: Paints, + ), + ( + id: "button-group-contains-actions", + group: "button-group", + what: "grouped buttons remain operable", + open: None, + hover: None, + click: Some("button:First grouped button"), + subject: "heading:ButtonGroup selected: first", + expect: PaintsNamed, + covers: ["button:* grouped button"], + ), +] diff --git a/tests/ps-qa/card.ron b/tests/ps-qa/card.ron index b7a07149..1ec451a6 100644 --- a/tests/ps-qa/card.ron +++ b/tests/ps-qa/card.ron @@ -25,13 +25,13 @@ expect: Paints, ), ( - id: "card-paints", + id: "card-activates", group: "card", - what: "the Card reaches the renderer with a box", + what: "an interactive card invokes its consumer callback", open: None, hover: None, - click: None, - subject: "Card", - expect: Paints, + click: Some("button:Interactive fixture card"), + subject: "heading:Card activated", + expect: PaintsNamed, ), ] diff --git a/tests/ps-qa/checkbox-group.ron b/tests/ps-qa/checkbox-group.ron new file mode 100644 index 00000000..aec26f91 --- /dev/null +++ b/tests/ps-qa/checkbox-group.ron @@ -0,0 +1,47 @@ +// Generated from tests/qa-harness/components.ts by tests/qa-harness/generate-checks.ts. Do not edit. +// +// CheckboxGroup, mounted alone on its own harness page. Outcomes for +// this component share one native host; idempotent preparation keeps each +// outcome reproducible by id against a fresh host as well. +[ + ( + id: "checkbox-group-page-paints", + group: "checkbox-group", + what: "the CheckboxGroup page builds and paints", + open: None, + hover: None, + click: None, + subject: "heading:CheckboxGroup", + expect: PaintsNamed, + ), + ( + id: "checkbox-group-renders", + group: "checkbox-group", + what: "CheckboxGroup renders a node of its own", + open: None, + hover: None, + click: None, + subject: "fixture", + expect: Paints, + ), + ( + id: "checkbox-group-selects", + group: "checkbox-group", + what: "selecting a grouped checkbox changes its controlled selection", + open: None, + hover: None, + click: Some("checkbox:Second choice"), + subject: "checkbox:Second choice", + expect: SelectionChanges, + ), + ( + id: "checkbox-group-reports", + group: "checkbox-group", + what: "selecting a grouped checkbox reports the new values", + open: None, + hover: None, + click: Some("checkbox:First choice"), + subject: "heading:CheckboxGroup value:", + expect: NameChanges, + ), +] diff --git a/tests/ps-qa/checkbox.ron b/tests/ps-qa/checkbox.ron index 11cc76a8..2deee8b6 100644 --- a/tests/ps-qa/checkbox.ron +++ b/tests/ps-qa/checkbox.ron @@ -30,8 +30,8 @@ what: "pressing the Checkbox changes what it reports", open: None, hover: None, - click: Some("checkbox:"), - subject: "checkbox:", + click: Some("checkbox:Checkbox"), + subject: "checkbox:Checkbox", expect: SelectionChanges, ), ( @@ -41,7 +41,7 @@ open: None, hover: None, settle_after_ms: 300, - click: Some("checkbox:"), + click: Some("checkbox:Checkbox"), subject: "heading:Callback ran", expect: PaintsNamed, ), diff --git a/tests/ps-qa/chip.ron b/tests/ps-qa/chip.ron index 4e64d1d6..3997a270 100644 --- a/tests/ps-qa/chip.ron +++ b/tests/ps-qa/chip.ron @@ -25,13 +25,13 @@ expect: Paints, ), ( - id: "chip-paints", + id: "chip-removes", group: "chip", - what: "the Chip reaches the renderer with a box", + what: "the removable chip invokes its owner", open: None, hover: None, - click: None, - subject: "Chip", - expect: Paints, + click: Some("button:Remove fixture chip"), + subject: "heading:Chip removed", + expect: PaintsNamed, ), ] diff --git a/tests/ps-qa/color-area.ron b/tests/ps-qa/color-area.ron new file mode 100644 index 00000000..6c697ba3 --- /dev/null +++ b/tests/ps-qa/color-area.ron @@ -0,0 +1,62 @@ +// Generated from tests/qa-harness/components.ts by tests/qa-harness/generate-checks.ts. Do not edit. +// +// ColorArea, mounted alone on its own harness page. Outcomes for +// this component share one native host; idempotent preparation keeps each +// outcome reproducible by id against a fresh host as well. +[ + ( + id: "color-area-page-paints", + group: "color-area", + what: "the ColorArea page builds and paints", + open: None, + hover: None, + click: None, + subject: "heading:ColorArea", + expect: PaintsNamed, + ), + ( + id: "color-area-renders", + group: "color-area", + what: "ColorArea renders a node of its own", + open: None, + hover: None, + click: None, + subject: "fixture", + expect: Paints, + ), + ( + id: "color-area-keyboard-changes", + group: "color-area", + what: "ArrowRight changes the controlled saturation", + open: None, + hover: None, + click: None, + subject: "slider:Color area", + expect: ValueChanges, + key: Some("ArrowRight"), + key_on: Some("slider:Color area"), + ), + ( + id: "color-area-reports", + group: "color-area", + what: "keyboard adjustment reports the new color area value", + open: None, + hover: None, + click: None, + subject: "heading:ColorArea changed", + expect: Present, + key: Some("ArrowRight"), + key_on: Some("slider:Color area"), + ), + ( + id: "color-area-pointer-changes", + group: "color-area", + what: "pointer dragging changes the controlled saturation", + open: None, + hover: None, + click: None, + subject: "slider:Color area", + expect: ValueChanges, + pointer_drag: Some((from: "slider:Color area", dx: -80.0, dy: 20.0, steps: 4)), + ), +] diff --git a/tests/ps-qa/color-field.ron b/tests/ps-qa/color-field.ron new file mode 100644 index 00000000..f00e8486 --- /dev/null +++ b/tests/ps-qa/color-field.ron @@ -0,0 +1,51 @@ +// Generated from tests/qa-harness/components.ts by tests/qa-harness/generate-checks.ts. Do not edit. +// +// ColorField, mounted alone on its own harness page. Outcomes for +// this component share one native host; idempotent preparation keeps each +// outcome reproducible by id against a fresh host as well. +[ + ( + id: "color-field-page-paints", + group: "color-field", + what: "the ColorField page builds and paints", + open: None, + hover: None, + click: None, + subject: "heading:ColorField", + expect: PaintsNamed, + ), + ( + id: "color-field-renders", + group: "color-field", + what: "ColorField renders a node of its own", + open: None, + hover: None, + click: None, + subject: "fixture", + expect: Paints, + ), + ( + id: "color-field-accepts", + group: "color-field", + what: "typing a valid color changes the field value", + open: None, + hover: None, + click: None, + subject: "textbox:Color value", + expect: ValueChanges, + type_into: Some("textbox:Color value"), + text: Some("#112233"), + ), + ( + id: "color-field-reports", + group: "color-field", + what: "typing a valid color reports the normalized value", + open: None, + hover: None, + click: None, + subject: "heading:ColorField value: #112233", + expect: Present, + type_into: Some("textbox:Color value"), + text: Some("#112233"), + ), +] diff --git a/tests/ps-qa/color-picker.ron b/tests/ps-qa/color-picker.ron new file mode 100644 index 00000000..a4554e1b --- /dev/null +++ b/tests/ps-qa/color-picker.ron @@ -0,0 +1,63 @@ +// Generated from tests/qa-harness/components.ts by tests/qa-harness/generate-checks.ts. Do not edit. +// +// ColorPicker, mounted alone on its own harness page. Outcomes for +// this component share one native host; idempotent preparation keeps each +// outcome reproducible by id against a fresh host as well. +[ + ( + id: "color-picker-page-paints", + group: "color-picker", + what: "the ColorPicker page builds and paints", + open: None, + hover: None, + click: None, + subject: "heading:ColorPicker", + expect: PaintsNamed, + ), + ( + id: "color-picker-renders", + group: "color-picker", + what: "ColorPicker renders a node of its own", + open: None, + hover: None, + click: None, + subject: "fixture", + expect: Paints, + ), + ( + id: "color-picker-hue-changes", + group: "color-picker", + what: "the composed hue slider changes the controlled color", + open: None, + hover: None, + click: None, + subject: "slider:Hue", + expect: ValueChanges, + key: Some("ArrowRight"), + key_on: Some("slider:Hue"), + ), + ( + id: "color-picker-area-changes", + group: "color-picker", + what: "the composed color area changes the controlled color", + open: None, + hover: None, + click: None, + subject: "slider:Color area", + expect: ValueChanges, + key: Some("ArrowRight"), + key_on: Some("slider:Color area"), + ), + ( + id: "color-picker-field-reports", + group: "color-picker", + what: "the composed color field reports a typed literal", + open: None, + hover: None, + click: None, + subject: "heading:ColorPicker value:", + expect: NameChanges, + type_into: Some("textbox:Color value"), + text: Some("#112233"), + ), +] diff --git a/tests/ps-qa/color-slider.ron b/tests/ps-qa/color-slider.ron new file mode 100644 index 00000000..89fa6d1e --- /dev/null +++ b/tests/ps-qa/color-slider.ron @@ -0,0 +1,62 @@ +// Generated from tests/qa-harness/components.ts by tests/qa-harness/generate-checks.ts. Do not edit. +// +// ColorSlider, mounted alone on its own harness page. Outcomes for +// this component share one native host; idempotent preparation keeps each +// outcome reproducible by id against a fresh host as well. +[ + ( + id: "color-slider-page-paints", + group: "color-slider", + what: "the ColorSlider page builds and paints", + open: None, + hover: None, + click: None, + subject: "heading:ColorSlider", + expect: PaintsNamed, + ), + ( + id: "color-slider-renders", + group: "color-slider", + what: "ColorSlider renders a node of its own", + open: None, + hover: None, + click: None, + subject: "fixture", + expect: Paints, + ), + ( + id: "color-slider-keyboard-changes", + group: "color-slider", + what: "ArrowRight changes the controlled hue", + open: None, + hover: None, + click: None, + subject: "slider:Hue", + expect: ValueChanges, + key: Some("ArrowRight"), + key_on: Some("slider:Hue"), + ), + ( + id: "color-slider-reports", + group: "color-slider", + what: "the hue slider reports the changed value", + open: None, + hover: None, + click: None, + subject: "heading:ColorSlider changed", + expect: Present, + key: Some("ArrowRight"), + key_on: Some("slider:Hue"), + ), + ( + id: "color-slider-pointer-changes", + group: "color-slider", + what: "pointer dragging changes the controlled hue", + open: None, + hover: None, + click: None, + subject: "slider:Hue", + expect: ValueChanges, + pointer_drag: Some((from: "slider:Hue", dx: 100.0, dy: 0.0, steps: 4)), + ), +] diff --git a/tests/ps-qa/color-swatch-picker.ron b/tests/ps-qa/color-swatch-picker.ron new file mode 100644 index 00000000..224feafd --- /dev/null +++ b/tests/ps-qa/color-swatch-picker.ron @@ -0,0 +1,48 @@ +// Generated from tests/qa-harness/components.ts by tests/qa-harness/generate-checks.ts. Do not edit. +// +// ColorSwatchPicker, mounted alone on its own harness page. Outcomes for +// this component share one native host; idempotent preparation keeps each +// outcome reproducible by id against a fresh host as well. +[ + ( + id: "color-swatch-picker-page-paints", + group: "color-swatch-picker", + what: "the ColorSwatchPicker page builds and paints", + open: None, + hover: None, + click: None, + subject: "heading:ColorSwatchPicker", + expect: PaintsNamed, + ), + ( + id: "color-swatch-picker-renders", + group: "color-swatch-picker", + what: "ColorSwatchPicker renders a node of its own", + open: None, + hover: None, + click: None, + subject: "fixture", + expect: Paints, + ), + ( + id: "color-swatch-picker-selects", + group: "color-swatch-picker", + what: "choosing another swatch changes its controlled selection", + open: None, + hover: None, + click: Some("radio:Blue swatch"), + subject: "radio:Blue swatch", + expect: SelectionChanges, + covers: ["radio:* swatch"], + ), + ( + id: "color-swatch-picker-reports", + group: "color-swatch-picker", + what: "choosing another swatch reports the color", + open: None, + hover: None, + click: Some("radio:Blue swatch"), + subject: "heading:ColorSwatchPicker value: #0000ff", + expect: Present, + ), +] diff --git a/tests/ps-qa/color-swatch.ron b/tests/ps-qa/color-swatch.ron index a165a941..0be322eb 100644 --- a/tests/ps-qa/color-swatch.ron +++ b/tests/ps-qa/color-swatch.ron @@ -25,13 +25,13 @@ expect: Paints, ), ( - id: "color-swatch-paints", + id: "color-swatch-selects", group: "color-swatch", - what: "the ColorSwatch reaches the renderer with a box", + what: "a standalone swatch reports its color to its owner", open: None, hover: None, - click: None, - subject: "option:Color undefined", + click: Some("option:Fixture blue"), + subject: "heading:ColorSwatch selected: #0000ff", expect: PaintsNamed, ), ] diff --git a/tests/ps-qa/color-wheel-flower.ron b/tests/ps-qa/color-wheel-flower.ron index c8c87bf9..3e81b46b 100644 --- a/tests/ps-qa/color-wheel-flower.ron +++ b/tests/ps-qa/color-wheel-flower.ron @@ -25,13 +25,24 @@ expect: Paints, ), ( - id: "color-wheel-flower-paints", + id: "color-wheel-flower-selects", group: "color-wheel-flower", - what: "the ColorWheelFlower reaches the renderer with a box", + what: "a flower petal changes the controlled color", open: None, hover: None, - click: None, - subject: "radio:Reset to neutral", + click: Some("radio:Theme color #DDA82C"), + subject: "radio:Theme color #DDA82C", + expect: SelectionChanges, + covers: ["radio:*"], + ), + ( + id: "color-wheel-flower-reports", + group: "color-wheel-flower", + what: "the standalone flower reports the selected color", + open: None, + hover: None, + click: Some("radio:Theme color #DD732C"), + subject: "heading:ColorWheelFlower changed", expect: PaintsNamed, ), ] diff --git a/tests/ps-qa/color-wheel.ron b/tests/ps-qa/color-wheel.ron index a42fe2ad..3fc116a1 100644 --- a/tests/ps-qa/color-wheel.ron +++ b/tests/ps-qa/color-wheel.ron @@ -25,13 +25,24 @@ expect: Paints, ), ( - id: "color-wheel-paints", + id: "color-wheel-selects", group: "color-wheel", - what: "the ColorWheel reaches the renderer with a box", + what: "a color wheel petal changes the controlled selection", open: None, hover: None, - click: None, - subject: "ColorWheel", - expect: Paints, + click: Some("radio:Theme color #DDA82C"), + subject: "radio:Theme color #DDA82C", + expect: SelectionChanges, + covers: ["radio:*"], + ), + ( + id: "color-wheel-reports", + group: "color-wheel", + what: "the color wheel reports the selected literal", + open: None, + hover: None, + click: Some("radio:Theme color #DD732C"), + subject: "heading:ColorWheel value:", + expect: NameChanges, ), ] diff --git a/tests/ps-qa/combo-box.ron b/tests/ps-qa/combo-box.ron new file mode 100644 index 00000000..7c6e367c --- /dev/null +++ b/tests/ps-qa/combo-box.ron @@ -0,0 +1,73 @@ +// Generated from tests/qa-harness/components.ts by tests/qa-harness/generate-checks.ts. Do not edit. +// +// ComboBox, mounted alone on its own harness page. Outcomes for +// this component share one native host; idempotent preparation keeps each +// outcome reproducible by id against a fresh host as well. +[ + ( + id: "combo-box-page-paints", + group: "combo-box", + what: "the ComboBox page builds and paints", + open: None, + hover: None, + click: None, + subject: "heading:ComboBox", + expect: PaintsNamed, + ), + ( + id: "combo-box-renders", + group: "combo-box", + what: "ComboBox renders a node of its own", + open: None, + hover: None, + click: None, + subject: "fixture", + expect: Paints, + ), + ( + id: "combo-box-opens", + group: "combo-box", + what: "the ComboBox opens an addressable listbox", + open: None, + hover: None, + click: Some("button:Toggle options"), + subject: "option:Beta", + expect: PaintsNamed, + ), + ( + id: "combo-box-selects", + group: "combo-box", + what: "choosing an option changes the controlled input value", + open: None, + hover: None, + click: Some("option:Beta"), + subject: "combobox:Fixture combo box", + expect: ValueChanges, + prepare: Some("button:Toggle options"), + prepare_unless: Some("option:Beta"), + ), + ( + id: "combo-box-reports", + group: "combo-box", + what: "choosing another option reports the selected key", + open: None, + hover: None, + click: Some("option:Alpha"), + subject: "heading:ComboBox value:", + expect: NameChanges, + prepare: Some("button:Toggle options"), + prepare_unless: Some("option:Alpha"), + ), + ( + id: "combo-box-accepts-query", + group: "combo-box", + what: "typing a query clears the committed selection", + open: None, + hover: None, + click: None, + subject: "heading:ComboBox value:", + expect: NameChanges, + type_into: Some("combobox:Fixture combo box"), + text: Some("Gam"), + ), +] diff --git a/tests/ps-qa/complex-color-wheel.ron b/tests/ps-qa/complex-color-wheel.ron index 0c62a193..28ba75c8 100644 --- a/tests/ps-qa/complex-color-wheel.ron +++ b/tests/ps-qa/complex-color-wheel.ron @@ -78,13 +78,25 @@ expect: Paints, ), ( - id: "complex-color-wheel-changes", + id: "complex-color-wheel-adjusts", group: "complex-color-wheel", - what: "activating a ComplexColorWheel adjustment changes its controlled selection", + what: "choosing an adjustment updates its controlled selection", open: None, hover: None, click: Some("button:Strength 20"), subject: "button:Strength 20", expect: SelectionChanges, + covers: ["button:Strength *"], + ), + ( + id: "complex-color-wheel-selects-color", + group: "complex-color-wheel", + what: "choosing a flower petal updates the controlled color", + open: None, + hover: None, + click: Some("radio:Theme color #DDA82C"), + subject: "radio:Theme color #DDA82C", + expect: SelectionChanges, + covers: ["radio:*"], ), ] diff --git a/tests/ps-qa/composer.ron b/tests/ps-qa/composer.ron index a02a0a7c..06e9f9e8 100644 --- a/tests/ps-qa/composer.ron +++ b/tests/ps-qa/composer.ron @@ -25,23 +25,25 @@ expect: Paints, ), ( - id: "composer-paints", + id: "composer-accepts", group: "composer", - what: "the Composer control is on screen and addressable", + what: "typing into Composer updates its controlled draft", open: None, hover: None, click: None, - subject: "button:Send", + subject: "heading:Composer draft: QA message", expect: PaintsNamed, + type_into: Some("textbox:Fixture message"), + text: Some("QA message"), ), ( - id: "composer-acts", + id: "composer-submits", group: "composer", - what: "activating Composer exposes the callback result", + what: "sending Composer reports its trimmed message", open: None, hover: None, click: Some("button:Send"), - subject: "heading:Action result: Composer complete", + subject: "heading:Composer submitted: QA message", expect: PaintsNamed, ), ] diff --git a/tests/ps-qa/connection-settings.ron b/tests/ps-qa/connection-settings.ron index ae705b27..2dce9d4c 100644 --- a/tests/ps-qa/connection-settings.ron +++ b/tests/ps-qa/connection-settings.ron @@ -95,4 +95,18 @@ subject: "heading:Reconnected: ws://qa-reconnected over ws://qa-reconnected", expect: PaintsNamed, ), + ( + id: "connection-settings-resets-draft", + group: "connection-settings", + what: "resetting ConnectionSettings discards the draft and reports completion", + open: None, + hover: None, + prepare: Some("switch:Use a custom backend"), + prepare_unless: Some("textbox:API URL"), + setup_type_into: Some("textbox:API URL"), + setup_text: Some("ws://qa-reset-draft"), + click: Some("button:Reset"), + subject: "heading:Panel outcome: reset", + expect: PaintsNamed, + ), ] diff --git a/tests/ps-qa/cookie-consent.ron b/tests/ps-qa/cookie-consent.ron index cd4253a5..109ede8d 100644 --- a/tests/ps-qa/cookie-consent.ron +++ b/tests/ps-qa/cookie-consent.ron @@ -25,13 +25,46 @@ expect: Paints, ), ( - id: "cookie-consent-paints", + id: "cookie-consent-manages", group: "cookie-consent", - what: "the CookieConsent reaches the renderer with a box", + what: "cookie consent opens its preference dialog", open: None, hover: None, - click: None, - subject: "CookieConsent", - expect: Paints, + click: Some("button:Manage M"), + subject: "heading:Manage M preferences", + expect: PaintsNamed, + covers: ["button:Manage *"], + ), + ( + id: "cookie-consent-saves-custom", + group: "cookie-consent", + what: "saving managed preferences reports custom consent", + open: None, + hover: None, + click: Some("button:Save M"), + subject: "heading:Cookie consent M: custom", + expect: PaintsNamed, + ), + ( + id: "cookie-consent-accepts-all", + group: "cookie-consent", + what: "accepting all cookies reports full consent", + open: None, + hover: None, + click: Some("button:Accept all A"), + subject: "heading:Cookie consent A: all", + expect: PaintsNamed, + covers: ["button:Accept all *"], + ), + ( + id: "cookie-consent-declines", + group: "cookie-consent", + what: "declining optional cookies reports essential consent", + open: None, + hover: None, + click: Some("button:Decline D"), + subject: "heading:Cookie consent D: essential", + expect: PaintsNamed, + covers: ["button:Decline *"], ), ] diff --git a/tests/ps-qa/data-grid.ron b/tests/ps-qa/data-grid.ron index 6dbbdc5e..5bac4f6a 100644 --- a/tests/ps-qa/data-grid.ron +++ b/tests/ps-qa/data-grid.ron @@ -25,13 +25,56 @@ expect: Paints, ), ( - id: "data-grid-paints", + id: "data-grid-sorts", group: "data-grid", - what: "the DataGrid reaches the renderer with a box", + what: "a sortable grid column reports its next direction", + open: None, + hover: None, + click: Some("columnheader:Name"), + subject: "heading:Grid sort:", + expect: NameChanges, + ), + ( + id: "data-grid-selects-row", + group: "data-grid", + what: "a row checkbox reports the selected row identity", + open: None, + hover: None, + click: Some("checkbox:Select row"), + subject: "heading:Grid selection:", + expect: NameChanges, + covers: ["checkbox:Select row"], + ), + ( + id: "data-grid-selects-page", + group: "data-grid", + what: "the select-all checkbox selects every row on the current page", + open: None, + hover: None, + click: Some("checkbox:Select all rows"), + subject: "heading:Grid selection:", + expect: NameChanges, + ), + ( + id: "data-grid-pages", + group: "data-grid", + what: "the grid pager reports the next page", + open: None, + hover: None, + click: Some("button:Next page"), + subject: "heading:Grid page:", + expect: NameChanges, + ), + ( + id: "data-grid-filters", + group: "data-grid", + what: "the grid search field filters the source rows", open: None, hover: None, click: None, - subject: "DataGrid", - expect: Paints, + subject: "heading:Grid first filtered row:", + expect: NameChanges, + type_into: Some("Search Name"), + text: Some("Gam"), ), ] diff --git a/tests/ps-qa/date-field.ron b/tests/ps-qa/date-field.ron new file mode 100644 index 00000000..bf8cf76d --- /dev/null +++ b/tests/ps-qa/date-field.ron @@ -0,0 +1,51 @@ +// Generated from tests/qa-harness/components.ts by tests/qa-harness/generate-checks.ts. Do not edit. +// +// DateField, mounted alone on its own harness page. Outcomes for +// this component share one native host; idempotent preparation keeps each +// outcome reproducible by id against a fresh host as well. +[ + ( + id: "date-field-page-paints", + group: "date-field", + what: "the DateField page builds and paints", + open: None, + hover: None, + click: None, + subject: "heading:DateField", + expect: PaintsNamed, + ), + ( + id: "date-field-renders", + group: "date-field", + what: "DateField renders a node of its own", + open: None, + hover: None, + click: None, + subject: "fixture", + expect: Paints, + ), + ( + id: "date-field-accepts", + group: "date-field", + what: "typing changes the date field value", + open: None, + hover: None, + click: None, + subject: "textbox:Date value", + expect: ValueChanges, + type_into: Some("textbox:Date value"), + text: Some("2025-06-24"), + ), + ( + id: "date-field-reports", + group: "date-field", + what: "typing reports the date value", + open: None, + hover: None, + click: None, + subject: "heading:DateField value: 2025-06-24", + expect: Present, + type_into: Some("textbox:Date value"), + text: Some("2025-06-24"), + ), +] diff --git a/tests/ps-qa/date-picker.ron b/tests/ps-qa/date-picker.ron new file mode 100644 index 00000000..c88b0b9f --- /dev/null +++ b/tests/ps-qa/date-picker.ron @@ -0,0 +1,49 @@ +// Generated from tests/qa-harness/components.ts by tests/qa-harness/generate-checks.ts. Do not edit. +// +// DatePicker, mounted alone on its own harness page. Outcomes for +// this component share one native host; idempotent preparation keeps each +// outcome reproducible by id against a fresh host as well. +[ + ( + id: "date-picker-page-paints", + group: "date-picker", + what: "the DatePicker page builds and paints", + open: None, + hover: None, + click: None, + subject: "heading:DatePicker", + expect: PaintsNamed, + ), + ( + id: "date-picker-renders", + group: "date-picker", + what: "DatePicker renders a node of its own", + open: None, + hover: None, + click: None, + subject: "fixture", + expect: Paints, + ), + ( + id: "date-picker-opens", + group: "date-picker", + what: "the date picker opens its calendar dialog", + open: None, + hover: None, + click: Some("button:Jun 15, 2025"), + subject: "dialog:", + expect: PaintsNamed, + ), + ( + id: "date-picker-selects", + group: "date-picker", + what: "choosing a date reports the controlled value", + open: None, + hover: None, + click: Some("gridcell:Tuesday, June 24, 2025"), + subject: "heading:DatePicker value: 2025-06-24", + expect: Present, + prepare: Some("button:Jun 15, 2025"), + prepare_unless: Some("dialog:"), + ), +] diff --git a/tests/ps-qa/date-range-picker.ron b/tests/ps-qa/date-range-picker.ron new file mode 100644 index 00000000..62d72d9b --- /dev/null +++ b/tests/ps-qa/date-range-picker.ron @@ -0,0 +1,59 @@ +// Generated from tests/qa-harness/components.ts by tests/qa-harness/generate-checks.ts. Do not edit. +// +// DateRangePicker, mounted alone on its own harness page. Outcomes for +// this component share one native host; idempotent preparation keeps each +// outcome reproducible by id against a fresh host as well. +[ + ( + id: "date-range-picker-page-paints", + group: "date-range-picker", + what: "the DateRangePicker page builds and paints", + open: None, + hover: None, + click: None, + subject: "heading:DateRangePicker", + expect: PaintsNamed, + ), + ( + id: "date-range-picker-renders", + group: "date-range-picker", + what: "DateRangePicker renders a node of its own", + open: None, + hover: None, + click: None, + subject: "fixture", + expect: Paints, + ), + ( + id: "date-range-picker-opens", + group: "date-range-picker", + what: "the date range picker opens its calendar dialog", + open: None, + hover: None, + click: Some("button:Jun 15, 2025 Jun 17, 2025"), + subject: "dialog:", + expect: PaintsNamed, + ), + ( + id: "date-range-picker-starts", + group: "date-range-picker", + what: "choosing a date starts a pending range", + open: None, + hover: None, + click: Some("gridcell:Tuesday, June 24, 2025"), + subject: "gridcell:Tuesday, June 24, 2025", + expect: SelectionChanges, + prepare: Some("button:Jun 15, 2025 Jun 17, 2025"), + prepare_unless: Some("dialog:"), + ), + ( + id: "date-range-picker-completes", + group: "date-range-picker", + what: "choosing a second date completes the controlled range", + open: None, + hover: None, + click: Some("gridcell:Thursday, June 26, 2025"), + subject: "heading:DateRangePicker end: 2025-06-26", + expect: Present, + ), +] diff --git a/tests/ps-qa/dock.ron b/tests/ps-qa/dock.ron index 01442338..6142b064 100644 --- a/tests/ps-qa/dock.ron +++ b/tests/ps-qa/dock.ron @@ -25,13 +25,14 @@ expect: Paints, ), ( - id: "dock-paints", + id: "dock-acts", group: "dock", - what: "the Dock reaches the renderer with a box", + what: "a dock item invokes its owner", open: None, hover: None, - click: None, - subject: "Dock", - expect: Paints, + click: Some("button:Search"), + subject: "heading:Dock selected: Search", + expect: PaintsNamed, + covers: ["button:Home", "button:Search", "button:Settings"], ), ] diff --git a/tests/ps-qa/firefox-pwa-banner.ron b/tests/ps-qa/firefox-pwa-banner.ron index a4e4ffd9..7eeb53b0 100644 --- a/tests/ps-qa/firefox-pwa-banner.ron +++ b/tests/ps-qa/firefox-pwa-banner.ron @@ -25,13 +25,36 @@ expect: Paints, ), ( - id: "firefox-pwa-banner-paints", + id: "firefox-pwa-banner-installs", group: "firefox-pwa-banner", - what: "the FirefoxPWABanner reaches the renderer with a box", + what: "the Firefox extension action invokes its consumer callback", open: None, hover: None, - click: None, - subject: "FirefoxPWABanner", - expect: Paints, + click: Some("button:Install extension A"), + subject: "heading:Firefox PWA outcome:", + expect: NameChanges, + covers: ["button:Install extension *"], + ), + ( + id: "firefox-pwa-banner-defers", + group: "firefox-pwa-banner", + what: "the Firefox later action dismisses the banner", + open: None, + hover: None, + click: Some("button:Maybe later B"), + subject: "heading:Firefox PWA outcome:", + expect: NameChanges, + covers: ["button:Maybe later *"], + ), + ( + id: "firefox-pwa-banner-closes", + group: "firefox-pwa-banner", + what: "the Firefox close action dismisses the banner", + open: None, + hover: None, + click: Some("button:Close Firefox C"), + subject: "heading:Firefox PWA outcome:", + expect: NameChanges, + covers: ["button:Close Firefox *"], ), ] diff --git a/tests/ps-qa/flex-grid.ron b/tests/ps-qa/flex-grid.ron new file mode 100644 index 00000000..6099fd84 --- /dev/null +++ b/tests/ps-qa/flex-grid.ron @@ -0,0 +1,37 @@ +// Generated from tests/qa-harness/components.ts by tests/qa-harness/generate-checks.ts. Do not edit. +// +// FlexGrid, mounted alone on its own harness page. Outcomes for +// this component share one native host; idempotent preparation keeps each +// outcome reproducible by id against a fresh host as well. +[ + ( + id: "flex-grid-page-paints", + group: "flex-grid", + what: "the FlexGrid page builds and paints", + open: None, + hover: None, + click: None, + subject: "heading:FlexGrid", + expect: PaintsNamed, + ), + ( + id: "flex-grid-renders", + group: "flex-grid", + what: "FlexGrid renders a node of its own", + open: None, + hover: None, + click: None, + subject: "fixture", + expect: Paints, + ), + ( + id: "flex-grid-reveals-more", + group: "flex-grid", + what: "the incremental grid reveals its next page when asked", + open: None, + hover: None, + click: Some("button:Load more rows"), + subject: "heading:Row Three", + expect: PaintsNamed, + ), +] diff --git a/tests/ps-qa/immersive-landing.ron b/tests/ps-qa/immersive-landing.ron index c847e699..a6a2107a 100644 --- a/tests/ps-qa/immersive-landing.ron +++ b/tests/ps-qa/immersive-landing.ron @@ -25,13 +25,14 @@ expect: Paints, ), ( - id: "immersive-landing-paints", + id: "immersive-landing-navigates", group: "immersive-landing", - what: "the ImmersiveLanding reaches the renderer with a box", + what: "landing navigation changes the active page and reports the route", open: None, hover: None, - click: None, - subject: "ImmersiveLanding", - expect: Paints, + click: Some("button:Go to page 2 of 2"), + subject: "heading:Landing navigation: first to second", + expect: PaintsNamed, + covers: ["button:Go to page 1 of 2", "button:Go to page 2 of 2", "button:Next page"], ), ] diff --git a/tests/ps-qa/input-otp.ron b/tests/ps-qa/input-otp.ron new file mode 100644 index 00000000..bf3301f5 --- /dev/null +++ b/tests/ps-qa/input-otp.ron @@ -0,0 +1,51 @@ +// Generated from tests/qa-harness/components.ts by tests/qa-harness/generate-checks.ts. Do not edit. +// +// InputOTP, mounted alone on its own harness page. Outcomes for +// this component share one native host; idempotent preparation keeps each +// outcome reproducible by id against a fresh host as well. +[ + ( + id: "input-otp-page-paints", + group: "input-otp", + what: "the InputOTP page builds and paints", + open: None, + hover: None, + click: None, + subject: "heading:InputOTP", + expect: PaintsNamed, + ), + ( + id: "input-otp-renders", + group: "input-otp", + what: "InputOTP renders a node of its own", + open: None, + hover: None, + click: None, + subject: "fixture", + expect: Paints, + ), + ( + id: "input-otp-accepts", + group: "input-otp", + what: "typing fills the one-time password value", + open: None, + hover: None, + click: None, + subject: "textbox:Verification code", + expect: ValueChanges, + type_into: Some("textbox:Verification code"), + text: Some("123456"), + ), + ( + id: "input-otp-reports", + group: "input-otp", + what: "typing reports the one-time password", + open: None, + hover: None, + click: None, + subject: "heading:InputOTP value: 123456", + expect: Present, + type_into: Some("textbox:Verification code"), + text: Some("123456"), + ), +] diff --git a/tests/ps-qa/join.ron b/tests/ps-qa/join.ron new file mode 100644 index 00000000..1b4b44ff --- /dev/null +++ b/tests/ps-qa/join.ron @@ -0,0 +1,38 @@ +// Generated from tests/qa-harness/components.ts by tests/qa-harness/generate-checks.ts. Do not edit. +// +// Join, mounted alone on its own harness page. Outcomes for +// this component share one native host; idempotent preparation keeps each +// outcome reproducible by id against a fresh host as well. +[ + ( + id: "join-page-paints", + group: "join", + what: "the Join page builds and paints", + open: None, + hover: None, + click: None, + subject: "heading:Join", + expect: PaintsNamed, + ), + ( + id: "join-renders", + group: "join", + what: "Join renders a node of its own", + open: None, + hover: None, + click: None, + subject: "fixture", + expect: Paints, + ), + ( + id: "join-contains-actions", + group: "join", + what: "joined buttons remain operable", + open: None, + hover: None, + click: Some("button:First joined button"), + subject: "heading:Join selected: first", + expect: PaintsNamed, + covers: ["button:* joined button"], + ), +] diff --git a/tests/ps-qa/kbd.ron b/tests/ps-qa/kbd.ron new file mode 100644 index 00000000..e8301b5c --- /dev/null +++ b/tests/ps-qa/kbd.ron @@ -0,0 +1,37 @@ +// Generated from tests/qa-harness/components.ts by tests/qa-harness/generate-checks.ts. Do not edit. +// +// Kbd, mounted alone on its own harness page. Outcomes for +// this component share one native host; idempotent preparation keeps each +// outcome reproducible by id against a fresh host as well. +[ + ( + id: "kbd-page-paints", + group: "kbd", + what: "the Kbd page builds and paints", + open: None, + hover: None, + click: None, + subject: "heading:Kbd", + expect: PaintsNamed, + ), + ( + id: "kbd-renders", + group: "kbd", + what: "Kbd renders a node of its own", + open: None, + hover: None, + click: None, + subject: "fixture", + expect: Paints, + ), + ( + id: "kbd-paints", + group: "kbd", + what: "the Kbd reaches the renderer with a box", + open: None, + hover: None, + click: None, + subject: "Kbd", + expect: Paints, + ), +] diff --git a/tests/ps-qa/list-box.ron b/tests/ps-qa/list-box.ron index 893e04a4..a5fe7b37 100644 --- a/tests/ps-qa/list-box.ron +++ b/tests/ps-qa/list-box.ron @@ -25,13 +25,23 @@ expect: Paints, ), ( - id: "list-box-paints", + id: "list-box-selects", group: "list-box", - what: "the ListBox reaches the renderer with a box", + what: "choosing a listbox item changes its controlled selection", open: None, hover: None, - click: None, - subject: "ListBox", - expect: Paints, + click: Some("option:Second item"), + subject: "option:Second item", + expect: SelectionChanges, + ), + ( + id: "list-box-reports", + group: "list-box", + what: "the listbox reports the selected key", + open: None, + hover: None, + click: Some("option:First item"), + subject: "heading:ListBox value:", + expect: NameChanges, ), ] diff --git a/tests/ps-qa/live-chat-panel.ron b/tests/ps-qa/live-chat-panel.ron index 021d3198..d93752b0 100644 --- a/tests/ps-qa/live-chat-panel.ron +++ b/tests/ps-qa/live-chat-panel.ron @@ -25,19 +25,31 @@ expect: Paints, ), ( - id: "live-chat-panel-paints", + id: "live-chat-panel-accepts-message", group: "live-chat-panel", - what: "the LiveChatPanel control is on screen and addressable", + what: "the chat composer accepts a message", open: None, hover: None, click: None, - subject: "button:Close chat", + subject: "textbox:Message support...", + expect: ValueChanges, + type_into: Some("textbox:Message support..."), + text: Some("Hello support"), + ), + ( + id: "live-chat-panel-sends-message", + group: "live-chat-panel", + what: "the chat panel hands the message to its owner", + open: None, + hover: None, + click: Some("button:Send"), + subject: "heading:LiveChat sent: Hello support", expect: PaintsNamed, ), ( - id: "live-chat-panel-acts", + id: "live-chat-panel-closes", group: "live-chat-panel", - what: "activating LiveChatPanel exposes the callback result", + what: "the chat panel close control invokes its owner", open: None, hover: None, click: Some("button:Close chat"), diff --git a/tests/ps-qa/menu.ron b/tests/ps-qa/menu.ron new file mode 100644 index 00000000..74dfe48a --- /dev/null +++ b/tests/ps-qa/menu.ron @@ -0,0 +1,48 @@ +// Generated from tests/qa-harness/components.ts by tests/qa-harness/generate-checks.ts. Do not edit. +// +// Menu, mounted alone on its own harness page. Outcomes for +// this component share one native host; idempotent preparation keeps each +// outcome reproducible by id against a fresh host as well. +[ + ( + id: "menu-page-paints", + group: "menu", + what: "the Menu page builds and paints", + open: None, + hover: None, + click: None, + subject: "heading:Menu", + expect: PaintsNamed, + ), + ( + id: "menu-renders", + group: "menu", + what: "Menu renders a node of its own", + open: None, + hover: None, + click: None, + subject: "fixture", + expect: Paints, + ), + ( + id: "menu-selects", + group: "menu", + what: "activating a menu item changes its controlled selection", + open: None, + hover: None, + click: Some("menuitemradio:Beta action"), + subject: "menuitemradio:Beta action", + expect: SelectionChanges, + covers: ["menuitemradio:* action"], + ), + ( + id: "menu-reports", + group: "menu", + what: "activating a menu item reports its selected key", + open: None, + hover: None, + click: Some("menuitemradio:Beta action"), + subject: "heading:Menu value: b", + expect: Present, + ), +] diff --git a/tests/ps-qa/meter.ron b/tests/ps-qa/meter.ron new file mode 100644 index 00000000..62ea1354 --- /dev/null +++ b/tests/ps-qa/meter.ron @@ -0,0 +1,37 @@ +// Generated from tests/qa-harness/components.ts by tests/qa-harness/generate-checks.ts. Do not edit. +// +// Meter, mounted alone on its own harness page. Outcomes for +// this component share one native host; idempotent preparation keeps each +// outcome reproducible by id against a fresh host as well. +[ + ( + id: "meter-page-paints", + group: "meter", + what: "the Meter page builds and paints", + open: None, + hover: None, + click: None, + subject: "heading:Meter", + expect: PaintsNamed, + ), + ( + id: "meter-renders", + group: "meter", + what: "Meter renders a node of its own", + open: None, + hover: None, + click: None, + subject: "fixture", + expect: Paints, + ), + ( + id: "meter-paints", + group: "meter", + what: "the Meter reaches the renderer with a box", + open: None, + hover: None, + click: None, + subject: "Meter", + expect: Paints, + ), +] diff --git a/tests/ps-qa/noise-background.ron b/tests/ps-qa/noise-background.ron new file mode 100644 index 00000000..26725ba1 --- /dev/null +++ b/tests/ps-qa/noise-background.ron @@ -0,0 +1,37 @@ +// Generated from tests/qa-harness/components.ts by tests/qa-harness/generate-checks.ts. Do not edit. +// +// NoiseBackground, mounted alone on its own harness page. Outcomes for +// this component share one native host; idempotent preparation keeps each +// outcome reproducible by id against a fresh host as well. +[ + ( + id: "noise-background-page-paints", + group: "noise-background", + what: "the NoiseBackground page builds and paints", + open: None, + hover: None, + click: None, + subject: "heading:NoiseBackground", + expect: PaintsNamed, + ), + ( + id: "noise-background-renders", + group: "noise-background", + what: "NoiseBackground renders a node of its own", + open: None, + hover: None, + click: None, + subject: "fixture", + expect: Paints, + ), + ( + id: "noise-background-paints", + group: "noise-background", + what: "the NoiseBackground reaches the renderer with a box", + open: None, + hover: None, + click: None, + subject: "NoiseBackground", + expect: Paints, + ), +] diff --git a/tests/ps-qa/pagination.ron b/tests/ps-qa/pagination.ron index f7583787..39f13c25 100644 --- a/tests/ps-qa/pagination.ron +++ b/tests/ps-qa/pagination.ron @@ -25,23 +25,14 @@ expect: Paints, ), ( - id: "pagination-paints", + id: "pagination-changes", group: "pagination", - what: "the Pagination control is on screen and addressable", - open: None, - hover: None, - click: None, - subject: "button:Go to next page", - expect: PaintsNamed, - ), - ( - id: "pagination-acts", - group: "pagination", - what: "activating Pagination exposes the callback result", + what: "pagination reports the next page to its owner", open: None, hover: None, click: Some("button:Go to next page"), subject: "heading:Action result: Pagination complete", expect: PaintsNamed, + covers: ["button:Go to *"], ), ] diff --git a/tests/ps-qa/password-field.ron b/tests/ps-qa/password-field.ron index 6a31d1a7..c3b8bc20 100644 --- a/tests/ps-qa/password-field.ron +++ b/tests/ps-qa/password-field.ron @@ -25,13 +25,25 @@ expect: Paints, ), ( - id: "password-field-paints", + id: "password-field-accepts", group: "password-field", - what: "the PasswordField reaches the renderer with a box", + what: "typing updates the controlled password value", open: None, hover: None, click: None, - subject: "PasswordField", - expect: Paints, + subject: "heading:Password value: secret", + expect: PaintsNamed, + type_into: Some("textbox:Password"), + text: Some("secret"), + ), + ( + id: "password-field-reveals", + group: "password-field", + what: "the visibility control reports its pressed state", + open: None, + hover: None, + click: Some("button:Show password"), + subject: "button:Hide password", + expect: PaintsNamed, ), ] diff --git a/tests/ps-qa/pwa-install-prompt.ron b/tests/ps-qa/pwa-install-prompt.ron index 467d3c1e..4bf1535f 100644 --- a/tests/ps-qa/pwa-install-prompt.ron +++ b/tests/ps-qa/pwa-install-prompt.ron @@ -25,13 +25,36 @@ expect: Paints, ), ( - id: "pwa-install-prompt-paints", + id: "pwa-install-prompt-installs", group: "pwa-install-prompt", - what: "the PWAInstallPrompt reaches the renderer with a box", + what: "accepting the browser install prompt reports installation", open: None, hover: None, - click: None, - subject: "PWAInstallPrompt", - expect: Paints, + click: Some("button:Install A"), + subject: "heading:PWA outcome:", + expect: NameChanges, + covers: ["button:Install *"], + ), + ( + id: "pwa-install-prompt-defers", + group: "pwa-install-prompt", + what: "the not-now action dismisses the prompt and reports deferral", + open: None, + hover: None, + click: Some("button:Not now B"), + subject: "heading:PWA outcome:", + expect: NameChanges, + covers: ["button:Not now *"], + ), + ( + id: "pwa-install-prompt-closes", + group: "pwa-install-prompt", + what: "the close action dismisses the prompt and reports closure", + open: None, + hover: None, + click: Some("button:Close C"), + subject: "heading:PWA outcome:", + expect: NameChanges, + covers: ["button:Close *"], ), ] diff --git a/tests/ps-qa/radial-progress.ron b/tests/ps-qa/radial-progress.ron new file mode 100644 index 00000000..16950a2a --- /dev/null +++ b/tests/ps-qa/radial-progress.ron @@ -0,0 +1,37 @@ +// Generated from tests/qa-harness/components.ts by tests/qa-harness/generate-checks.ts. Do not edit. +// +// RadialProgress, mounted alone on its own harness page. Outcomes for +// this component share one native host; idempotent preparation keeps each +// outcome reproducible by id against a fresh host as well. +[ + ( + id: "radial-progress-page-paints", + group: "radial-progress", + what: "the RadialProgress page builds and paints", + open: None, + hover: None, + click: None, + subject: "heading:RadialProgress", + expect: PaintsNamed, + ), + ( + id: "radial-progress-renders", + group: "radial-progress", + what: "RadialProgress renders a node of its own", + open: None, + hover: None, + click: None, + subject: "fixture", + expect: Paints, + ), + ( + id: "radial-progress-paints", + group: "radial-progress", + what: "the RadialProgress reaches the renderer with a box", + open: None, + hover: None, + click: None, + subject: "RadialProgress", + expect: Paints, + ), +] diff --git a/tests/ps-qa/radio-group.ron b/tests/ps-qa/radio-group.ron new file mode 100644 index 00000000..6a5bb37e --- /dev/null +++ b/tests/ps-qa/radio-group.ron @@ -0,0 +1,48 @@ +// Generated from tests/qa-harness/components.ts by tests/qa-harness/generate-checks.ts. Do not edit. +// +// RadioGroup, mounted alone on its own harness page. Outcomes for +// this component share one native host; idempotent preparation keeps each +// outcome reproducible by id against a fresh host as well. +[ + ( + id: "radio-group-page-paints", + group: "radio-group", + what: "the RadioGroup page builds and paints", + open: None, + hover: None, + click: None, + subject: "heading:RadioGroup", + expect: PaintsNamed, + ), + ( + id: "radio-group-renders", + group: "radio-group", + what: "RadioGroup renders a node of its own", + open: None, + hover: None, + click: None, + subject: "fixture", + expect: Paints, + ), + ( + id: "radio-group-selects", + group: "radio-group", + what: "choosing another grouped radio changes controlled selection", + open: None, + hover: None, + click: Some("radio:Second radio"), + subject: "radio:Second radio", + expect: SelectionChanges, + covers: ["radio:* radio"], + ), + ( + id: "radio-group-reports", + group: "radio-group", + what: "choosing another grouped radio reports its value", + open: None, + hover: None, + click: Some("radio:Second radio"), + subject: "heading:RadioGroup value: second", + expect: Present, + ), +] diff --git a/tests/ps-qa/radio.ron b/tests/ps-qa/radio.ron index ca48a63b..135d5df8 100644 --- a/tests/ps-qa/radio.ron +++ b/tests/ps-qa/radio.ron @@ -30,8 +30,8 @@ what: "pressing the Radio changes what it reports", open: None, hover: None, - click: Some("radio:"), - subject: "radio:", + click: Some("radio:Radio"), + subject: "radio:Radio", expect: SelectionChanges, ), ( @@ -41,7 +41,7 @@ open: None, hover: None, settle_after_ms: 300, - click: Some("radio:"), + click: Some("radio:Radio"), subject: "heading:Callback ran", expect: PaintsNamed, ), diff --git a/tests/ps-qa/range-calendar.ron b/tests/ps-qa/range-calendar.ron new file mode 100644 index 00000000..ef65fcb0 --- /dev/null +++ b/tests/ps-qa/range-calendar.ron @@ -0,0 +1,67 @@ +// Generated from tests/qa-harness/components.ts by tests/qa-harness/generate-checks.ts. Do not edit. +// +// RangeCalendar, mounted alone on its own harness page. Outcomes for +// this component share one native host; idempotent preparation keeps each +// outcome reproducible by id against a fresh host as well. +[ + ( + id: "range-calendar-page-paints", + group: "range-calendar", + what: "the RangeCalendar page builds and paints", + open: None, + hover: None, + click: None, + subject: "heading:RangeCalendar", + expect: PaintsNamed, + ), + ( + id: "range-calendar-renders", + group: "range-calendar", + what: "RangeCalendar renders a node of its own", + open: None, + hover: None, + click: None, + subject: "fixture", + expect: Paints, + ), + ( + id: "range-calendar-starts", + group: "range-calendar", + what: "choosing a date starts a pending range", + open: None, + hover: None, + click: Some("gridcell:Tuesday, June 24, 2025"), + subject: "gridcell:Tuesday, June 24, 2025", + expect: SelectionChanges, + ), + ( + id: "range-calendar-completes", + group: "range-calendar", + what: "choosing a second date completes the controlled range", + open: None, + hover: None, + click: Some("gridcell:Thursday, June 26, 2025"), + subject: "heading:RangeCalendar end: 2025-06-26", + expect: Present, + ), + ( + id: "range-calendar-next-month", + group: "range-calendar", + what: "the next-month control advances the visible calendar", + open: None, + hover: None, + click: Some("button:Next month"), + subject: "heading:July 2025", + expect: PaintsNamed, + ), + ( + id: "range-calendar-previous-month", + group: "range-calendar", + what: "the previous-month control returns to the prior calendar", + open: None, + hover: None, + click: Some("button:Previous month"), + subject: "heading:June 2025", + expect: PaintsNamed, + ), +] diff --git a/tests/ps-qa/size-picker.ron b/tests/ps-qa/size-picker.ron new file mode 100644 index 00000000..6441a23b --- /dev/null +++ b/tests/ps-qa/size-picker.ron @@ -0,0 +1,48 @@ +// Generated from tests/qa-harness/components.ts by tests/qa-harness/generate-checks.ts. Do not edit. +// +// SizePicker, mounted alone on its own harness page. Outcomes for +// this component share one native host; idempotent preparation keeps each +// outcome reproducible by id against a fresh host as well. +[ + ( + id: "size-picker-page-paints", + group: "size-picker", + what: "the SizePicker page builds and paints", + open: None, + hover: None, + click: None, + subject: "heading:SizePicker", + expect: PaintsNamed, + ), + ( + id: "size-picker-renders", + group: "size-picker", + what: "SizePicker renders a node of its own", + open: None, + hover: None, + click: None, + subject: "fixture", + expect: Paints, + ), + ( + id: "size-picker-selects", + group: "size-picker", + what: "choosing a size changes its selected radio", + open: None, + hover: None, + click: Some("radio:Size L"), + subject: "radio:Size L", + expect: SelectionChanges, + covers: ["radio:Size *"], + ), + ( + id: "size-picker-reports", + group: "size-picker", + what: "choosing a size reports the preset", + open: None, + hover: None, + click: Some("radio:Size L"), + subject: "heading:SizePicker value: L", + expect: Present, + ), +] diff --git a/tests/ps-qa/switch.ron b/tests/ps-qa/switch.ron index 010f0eb7..29140946 100644 --- a/tests/ps-qa/switch.ron +++ b/tests/ps-qa/switch.ron @@ -30,8 +30,8 @@ what: "pressing the Switch changes what it reports", open: None, hover: None, - click: Some("switch:"), - subject: "switch:", + click: Some("switch:Switch"), + subject: "switch:Switch", expect: SelectionChanges, ), ( @@ -41,7 +41,7 @@ open: None, hover: None, settle_after_ms: 300, - click: Some("switch:"), + click: Some("switch:Switch"), subject: "heading:Callback ran", expect: PaintsNamed, ), diff --git a/tests/ps-qa/table.ron b/tests/ps-qa/table.ron index 428a8e51..12b5e27a 100644 --- a/tests/ps-qa/table.ron +++ b/tests/ps-qa/table.ron @@ -25,13 +25,13 @@ expect: Paints, ), ( - id: "table-paints", + id: "table-sorts", group: "table", - what: "the Table reaches the renderer with a box", + what: "a sortable table column reports its next direction", open: None, hover: None, - click: None, - subject: "Table", - expect: Paints, + click: Some("Name fixture"), + subject: "heading:Table sort:", + expect: NameChanges, ), ] diff --git a/tests/ps-qa/tabs.ron b/tests/ps-qa/tabs.ron index ad55d8b6..a19be578 100644 --- a/tests/ps-qa/tabs.ron +++ b/tests/ps-qa/tabs.ron @@ -27,23 +27,22 @@ ( id: "tabs-changes", group: "tabs", - what: "activating another Tabs tab changes selection", + what: "activating another tab changes controlled selection", open: None, hover: None, click: Some("tab:Second"), subject: "tab:Second", expect: SelectionChanges, + covers: ["tab:*"], ), ( id: "tabs-changes-panel", group: "tabs", - what: "the selected Tabs tab exposes its corresponding panel", + what: "the selected tab exposes its corresponding panel", open: None, hover: None, - prepare: Some("tab:Second"), - prepare_unless: Some("heading:Second panel"), click: None, subject: "heading:Second panel", - expect: Paints, + expect: PaintsNamed, ), ] diff --git a/tests/ps-qa/theme-color-picker.ron b/tests/ps-qa/theme-color-picker.ron index aabc88ae..75aad417 100644 --- a/tests/ps-qa/theme-color-picker.ron +++ b/tests/ps-qa/theme-color-picker.ron @@ -25,13 +25,24 @@ expect: Paints, ), ( - id: "theme-color-picker-paints", + id: "theme-color-picker-opens", group: "theme-color-picker", - what: "the ThemeColorPicker reaches the renderer with a box", + what: "the theme color trigger opens its palette", open: None, hover: None, - click: None, - subject: "ThemeColorPicker", - expect: Paints, + click: Some("button:Change theme color"), + subject: "button:Black", + expect: PaintsNamed, + ), + ( + id: "theme-color-picker-switches-theme", + group: "theme-color-picker", + what: "choosing a grayscale swatch reports the requested theme", + open: None, + hover: None, + click: Some("button:Black"), + subject: "heading:ThemeColorPicker theme: light", + expect: PaintsNamed, + covers: ["button:White", "button:Light gray", "button:Gray", "button:Dark gray", "button:Charcoal", "button:Black"], ), ] diff --git a/tests/ps-qa/time-field.ron b/tests/ps-qa/time-field.ron new file mode 100644 index 00000000..1931e781 --- /dev/null +++ b/tests/ps-qa/time-field.ron @@ -0,0 +1,51 @@ +// Generated from tests/qa-harness/components.ts by tests/qa-harness/generate-checks.ts. Do not edit. +// +// TimeField, mounted alone on its own harness page. Outcomes for +// this component share one native host; idempotent preparation keeps each +// outcome reproducible by id against a fresh host as well. +[ + ( + id: "time-field-page-paints", + group: "time-field", + what: "the TimeField page builds and paints", + open: None, + hover: None, + click: None, + subject: "heading:TimeField", + expect: PaintsNamed, + ), + ( + id: "time-field-renders", + group: "time-field", + what: "TimeField renders a node of its own", + open: None, + hover: None, + click: None, + subject: "fixture", + expect: Paints, + ), + ( + id: "time-field-accepts", + group: "time-field", + what: "typing changes the time field value", + open: None, + hover: None, + click: None, + subject: "textbox:Time value", + expect: ValueChanges, + type_into: Some("textbox:Time value"), + text: Some("12:34"), + ), + ( + id: "time-field-reports", + group: "time-field", + what: "typing reports the time value", + open: None, + hover: None, + click: None, + subject: "heading:TimeField value: 12:34", + expect: Present, + type_into: Some("textbox:Time value"), + text: Some("12:34"), + ), +] diff --git a/tests/ps-qa/toast.ron b/tests/ps-qa/toast.ron index ca62bc9a..4bccef2f 100644 --- a/tests/ps-qa/toast.ron +++ b/tests/ps-qa/toast.ron @@ -25,13 +25,33 @@ expect: Paints, ), ( - id: "toast-paints", + id: "toast-opens", group: "toast", - what: "the Toast reaches the renderer with a box", + what: "requesting a toast paints its queued action", open: None, hover: None, - click: None, - subject: "Toast", - expect: Paints, + click: Some("button:Show fixture toast"), + subject: "button:Undo fixture", + expect: PaintsNamed, + ), + ( + id: "toast-acts", + group: "toast", + what: "the toast action invokes its consumer callback", + open: None, + hover: None, + click: Some("button:Undo fixture"), + subject: "heading:Toast outcome:", + expect: NameChanges, + ), + ( + id: "toast-closes", + group: "toast", + what: "the toast close control removes the notification", + open: None, + hover: None, + click: Some("button:Dismiss notification"), + subject: "button:Dismiss notification", + expect: Vanishes, ), ] diff --git a/tests/ps-qa/toolbar.ron b/tests/ps-qa/toolbar.ron new file mode 100644 index 00000000..5df69600 --- /dev/null +++ b/tests/ps-qa/toolbar.ron @@ -0,0 +1,40 @@ +// Generated from tests/qa-harness/components.ts by tests/qa-harness/generate-checks.ts. Do not edit. +// +// Toolbar, mounted alone on its own harness page. Outcomes for +// this component share one native host; idempotent preparation keeps each +// outcome reproducible by id against a fresh host as well. +[ + ( + id: "toolbar-page-paints", + group: "toolbar", + what: "the Toolbar page builds and paints", + open: None, + hover: None, + click: None, + subject: "heading:Toolbar", + expect: PaintsNamed, + ), + ( + id: "toolbar-renders", + group: "toolbar", + what: "Toolbar renders a node of its own", + open: None, + hover: None, + click: None, + subject: "fixture", + expect: Paints, + ), + ( + id: "toolbar-moves-focus", + group: "toolbar", + what: "ArrowRight moves focus to the next toolbar control", + open: None, + hover: None, + click: None, + subject: "button:Second tool", + expect: FocusMoves, + prepare: Some("button:First tool"), + key: Some("ArrowRight"), + key_on: Some("button:First tool"), + ), +] diff --git a/tests/ps-qa/tooltip.ron b/tests/ps-qa/tooltip.ron index 48074af6..630ce2b2 100644 --- a/tests/ps-qa/tooltip.ron +++ b/tests/ps-qa/tooltip.ron @@ -25,13 +25,13 @@ expect: Paints, ), ( - id: "tooltip-paints", + id: "tooltip-opens", group: "tooltip", - what: "the Tooltip reaches the renderer with a box", + what: "hovering the tooltip trigger reveals its content", open: None, - hover: None, + hover: Some("button:Tooltip target"), click: None, - subject: "Tooltip", - expect: Paints, + subject: "tooltip:Fixture tooltip", + expect: PaintsNamed, ), ] diff --git a/tests/ps-qa/video-preview.ron b/tests/ps-qa/video-preview.ron new file mode 100644 index 00000000..99410004 --- /dev/null +++ b/tests/ps-qa/video-preview.ron @@ -0,0 +1,37 @@ +// Generated from tests/qa-harness/components.ts by tests/qa-harness/generate-checks.ts. Do not edit. +// +// VideoPreview, mounted alone on its own harness page. Outcomes for +// this component share one native host; idempotent preparation keeps each +// outcome reproducible by id against a fresh host as well. +[ + ( + id: "video-preview-page-paints", + group: "video-preview", + what: "the VideoPreview page builds and paints", + open: None, + hover: None, + click: None, + subject: "heading:VideoPreview", + expect: PaintsNamed, + ), + ( + id: "video-preview-renders", + group: "video-preview", + what: "VideoPreview renders a node of its own", + open: None, + hover: None, + click: None, + subject: "fixture", + expect: Paints, + ), + ( + id: "video-preview-paints", + group: "video-preview", + what: "the VideoPreview reaches the renderer with a box", + open: None, + hover: None, + click: None, + subject: "VideoPreview", + expect: Paints, + ), +] diff --git a/tests/qa-harness/components.ts b/tests/qa-harness/components.ts index 1b552e14..f1b4486c 100644 --- a/tests/qa-harness/components.ts +++ b/tests/qa-harness/components.ts @@ -1,3 +1,5 @@ +import { componentFamilies } from "../../src/component-families"; + /* * Every component the harness can mount, and what a person can do to it. * @@ -60,6 +62,7 @@ export type ComponentKind = | "overlay" | "tabs" | "adjustment" + | "custom" | "display" /* * settings - the panel swaps to plain inputs behind a toggle and commits them @@ -130,21 +133,65 @@ export type ComponentSpec = { measure?: { subject: string; size: string }; /** Named painted family that must meet the native contrast floor. */ contrast?: string; + /** Component-specific native outcomes for behavior outside the shared kinds. */ + outcomes?: { + suffix: string; + what: string; + subject: string; + expect: string; + paint?: boolean; + hover?: string; + covers?: string[]; + click?: string; + prepare?: string; + prepareUnless?: string; + settleAfterMs?: number; + key?: string; + keyOn?: string; + typeInto?: string; + text?: string; + pointerDrag?: { from: string; dx: number; dy: number; steps: number }; + }[]; }; export const COMPONENTS: ComponentSpec[] = [ { id: "accordion", component: "Accordion", - kind: "display", + kind: "custom", + outcomes: [ + { suffix: "opens", what: "activating an accordion trigger reveals its panel", click: "button:First section", subject: "heading:First panel", expect: "PaintsNamed", paint: true }, + { suffix: "reports", what: "the accordion reports its controlled selection", click: "button:First section", subject: "heading:Accordion value:", expect: "NameChanges" }, + ], + }, + { + id: "address", component: "Address", kind: "custom", + outcomes: [ + { suffix: "copies", what: "copying an address reports the full value to its caller", click: "button:Copy address", subject: "heading:Address copied:", expect: "NameChanges" }, + ], + }, + { + id: "alert", component: "Alert", kind: "custom", + outcomes: [ + { suffix: "dismisses", what: "the alert dismiss control invokes its owner", click: "button:Dismiss fixture alert", subject: "heading:Alert dismissed", expect: "PaintsNamed", paint: true }, + ], }, - { id: "address", component: "Address", kind: "display" }, - { id: "alert", component: "Alert", kind: "display" }, { id: "auth-card", component: "AuthCard", kind: "display" }, { id: "auth-field-group", component: "AuthFieldGroup", kind: "display" }, - { id: "auth-footer-links", component: "AuthFooterLinks", kind: "display" }, + { + id: "auth-footer-links", component: "AuthFooterLinks", kind: "custom", + outcomes: [ + { suffix: "follows-link", what: "an auth footer link invokes its callback before navigation", click: "link:Privacy fixture", subject: "heading:Auth footer action:", expect: "NameChanges" }, + { suffix: "runs-action", what: "an auth footer action remains a semantic button", click: "button:Help fixture", subject: "heading:Auth footer action:", expect: "NameChanges" }, + ], + }, { id: "auth-message", component: "AuthMessage", kind: "display" }, - { id: "auth-powered-by", component: "AuthPoweredBy", kind: "display" }, + { + id: "auth-powered-by", component: "AuthPoweredBy", kind: "custom", + outcomes: [ + { suffix: "navigates", what: "the powered-by attribution exposes an operable Honey link", click: "link:Secure Auth by Honey", subject: "heading:Honey link activated", expect: "PaintsNamed", paint: true }, + ], + }, { id: "auth-submit-button", component: "AuthSubmitButton", @@ -154,7 +201,12 @@ export const COMPONENTS: ComponentSpec[] = [ }, { id: "avatar", component: "Avatar", kind: "display" }, { id: "badge", component: "Badge", kind: "display" }, - { id: "breadcrumb", component: "Breadcrumb", kind: "display" }, + { + id: "breadcrumb", component: "Breadcrumb", kind: "custom", + outcomes: [ + { suffix: "navigates", what: "a breadcrumb link remains operable inside the compound list", click: "link:Products fixture", subject: "heading:Breadcrumb activated", expect: "PaintsNamed", paint: true }, + ], + }, { id: "button", component: "Button", @@ -180,7 +232,12 @@ export const COMPONENTS: ComponentSpec[] = [ subject: "Tuesday, June 24, 2025", subjectRole: "gridcell", }, - { id: "card", component: "Card", kind: "display" }, + { + id: "card", component: "Card", kind: "custom", + outcomes: [ + { suffix: "activates", what: "an interactive card invokes its consumer callback", click: "button:Interactive fixture card", subject: "heading:Card activated", expect: "PaintsNamed", paint: true }, + ], + }, { id: "chat-bubble", component: "ChatBubble", kind: "display" }, { id: "close-button", @@ -193,10 +250,15 @@ export const COMPONENTS: ComponentSpec[] = [ id: "checkbox", component: "Checkbox", kind: "toggle", - // Measured: role `checkbox`, empty name, 1x1 at (79,116). + subject: "Checkbox", subjectRole: "checkbox", }, - { id: "chip", component: "Chip", kind: "display" }, + { + id: "chip", component: "Chip", kind: "custom", + outcomes: [ + { suffix: "removes", what: "the removable chip invokes its owner", click: "button:Remove fixture chip", subject: "heading:Chip removed", expect: "PaintsNamed", paint: true }, + ], + }, { id: "collapsible", component: "Collapsible", @@ -235,18 +297,10 @@ export const COMPONENTS: ComponentSpec[] = [ { id: "color-swatch", component: "ColorSwatch", - // Measured: role `option`, 32x32, named "Color undefined". Not a button, - // and not a menu, so `value` generated three checks against a control that - // does not exist. - // - // The name is a real defect rather than a fixture artefact: the component - // interpolates a colour prop into its accessible name without checking it - // is set, so a swatch with no colour announces itself as "Color undefined" - // to anyone using assistive technology. Left asserted as measured, so the - // check goes green only once that is fixed and the name changes. - kind: "display", - subject: "Color undefined", - subjectRole: "option", + kind: "custom", + outcomes: [ + { suffix: "selects", what: "a standalone swatch reports its color to its owner", click: "option:Fixture blue", subject: "heading:ColorSwatch selected: #0000ff", expect: "PaintsNamed", paint: true }, + ], }, /* * The flower on its own, with no `ThemeColorPicker` around it. @@ -265,7 +319,7 @@ export const COMPONENTS: ComponentSpec[] = [ { id: "color-wheel-flower", component: "ColorWheelFlower", - kind: "display", + kind: "custom", /* * The centre petal, by name. * @@ -277,16 +331,24 @@ export const COMPONENTS: ComponentSpec[] = [ */ subject: "Reset to neutral", subjectRole: "radio", + outcomes: [ + { suffix: "selects", what: "a flower petal changes the controlled color", click: "radio:Theme color #DDA82C", subject: "radio:Theme color #DDA82C", expect: "SelectionChanges", covers: ["radio:*"] }, + { suffix: "reports", what: "the standalone flower reports the selected color", click: "radio:Theme color #DD732C", subject: "heading:ColorWheelFlower changed", expect: "PaintsNamed", paint: true }, + ], }, { id: "color-wheel", component: "ColorWheel", - kind: "display", + kind: "custom", + outcomes: [ + { suffix: "selects", what: "a color wheel petal changes the controlled selection", click: "radio:Theme color #DDA82C", subject: "radio:Theme color #DDA82C", expect: "SelectionChanges", covers: ["radio:*"] }, + { suffix: "reports", what: "the color wheel reports the selected literal", click: "radio:Theme color #DD732C", subject: "heading:ColorWheel value:", expect: "NameChanges" }, + ], }, { id: "complex-color-wheel", component: "ComplexColorWheel", - kind: "adjustment", + kind: "custom", subject: "Strength 20", subjectRole: "button", geometry: { @@ -311,25 +373,42 @@ export const COMPONENTS: ComponentSpec[] = [ }, }, contrast: "Theme color ", + outcomes: [ + { suffix: "adjusts", what: "choosing an adjustment updates its controlled selection", click: "button:Strength 20", subject: "button:Strength 20", expect: "SelectionChanges", covers: ["button:Strength *"] }, + { suffix: "selects-color", what: "choosing a flower petal updates the controlled color", click: "radio:Theme color #DDA82C", subject: "radio:Theme color #DDA82C", expect: "SelectionChanges", covers: ["radio:*"] }, + ], }, { id: "composer", component: "Composer", - // Measured: role `textbox` (empty name) and `button:Send`, disabled until - // there is something to send. - kind: "action", - subject: "Send", - subjectRole: "button", + kind: "custom", + outcomes: [ + { suffix: "accepts", what: "typing into Composer updates its controlled draft", typeInto: "textbox:Fixture message", text: "QA message", subject: "heading:Composer draft: QA message", expect: "PaintsNamed", paint: true }, + { suffix: "submits", what: "sending Composer reports its trimmed message", click: "button:Send", subject: "heading:Composer submitted: QA message", expect: "PaintsNamed", paint: true }, + ], }, { id: "cookie-consent", component: "CookieConsent", - kind: "display", + kind: "custom", + outcomes: [ + { suffix: "manages", what: "cookie consent opens its preference dialog", click: "button:Manage M", subject: "heading:Manage M preferences", expect: "PaintsNamed", paint: true, covers: ["button:Manage *"] }, + { suffix: "saves-custom", what: "saving managed preferences reports custom consent", click: "button:Save M", subject: "heading:Cookie consent M: custom", expect: "PaintsNamed", paint: true }, + { suffix: "accepts-all", what: "accepting all cookies reports full consent", click: "button:Accept all A", subject: "heading:Cookie consent A: all", expect: "PaintsNamed", paint: true, covers: ["button:Accept all *"] }, + { suffix: "declines", what: "declining optional cookies reports essential consent", click: "button:Decline D", subject: "heading:Cookie consent D: essential", expect: "PaintsNamed", paint: true, covers: ["button:Decline *"] }, + ], }, { id: "data-grid", component: "DataGrid", - kind: "display", + kind: "custom", + outcomes: [ + { suffix: "sorts", what: "a sortable grid column reports its next direction", click: "columnheader:Name", subject: "heading:Grid sort:", expect: "NameChanges" }, + { suffix: "selects-row", what: "a row checkbox reports the selected row identity", click: "checkbox:Select row", subject: "heading:Grid selection:", expect: "NameChanges", covers: ["checkbox:Select row"] }, + { suffix: "selects-page", what: "the select-all checkbox selects every row on the current page", click: "checkbox:Select all rows", subject: "heading:Grid selection:", expect: "NameChanges" }, + { suffix: "pages", what: "the grid pager reports the next page", click: "button:Next page", subject: "heading:Grid page:", expect: "NameChanges" }, + { suffix: "filters", what: "the grid search field filters the source rows", typeInto: "Search Name", text: "Gam", subject: "heading:Grid first filtered row:", expect: "NameChanges" }, + ], }, { id: "dialog", @@ -339,7 +418,12 @@ export const COMPONENTS: ComponentSpec[] = [ subjectRole: "button", opens: "heading:Dialog outcome", }, - { id: "dock", component: "Dock", kind: "display" }, + { + id: "dock", component: "Dock", kind: "custom", + outcomes: [ + { suffix: "acts", what: "a dock item invokes its owner", click: "button:Search", subject: "heading:Dock selected: Search", expect: "PaintsNamed", paint: true, covers: ["button:Home", "button:Search", "button:Settings"] }, + ], + }, { id: "drawer", component: "Drawer", @@ -372,7 +456,12 @@ export const COMPONENTS: ComponentSpec[] = [ { id: "firefox-pwa-banner", component: "FirefoxPWABanner", - kind: "display", + kind: "custom", + outcomes: [ + { suffix: "installs", what: "the Firefox extension action invokes its consumer callback", click: "button:Install extension A", subject: "heading:Firefox PWA outcome:", expect: "NameChanges", covers: ["button:Install extension *"] }, + { suffix: "defers", what: "the Firefox later action dismisses the banner", click: "button:Maybe later B", subject: "heading:Firefox PWA outcome:", expect: "NameChanges", covers: ["button:Maybe later *"] }, + { suffix: "closes", what: "the Firefox close action dismisses the banner", click: "button:Close Firefox C", subject: "heading:Firefox PWA outcome:", expect: "NameChanges", covers: ["button:Close Firefox *"] }, + ], }, { id: "flex", component: "Flex", kind: "display" }, { id: "footer", component: "Footer", kind: "display" }, @@ -381,7 +470,12 @@ export const COMPONENTS: ComponentSpec[] = [ { id: "grid", component: "Grid", kind: "display" }, { id: "header", component: "Header", kind: "display" }, { id: "icon", component: "Icon", kind: "display" }, - { id: "immersive-landing", component: "ImmersiveLanding", kind: "display" }, + { + id: "immersive-landing", component: "ImmersiveLanding", kind: "custom", + outcomes: [ + { suffix: "navigates", what: "landing navigation changes the active page and reports the route", click: "button:Go to page 2 of 2", subject: "heading:Landing navigation: first to second", expect: "PaintsNamed", paint: true, covers: ["button:Go to page 1 of 2", "button:Go to page 2 of 2", "button:Next page"] }, + ], + }, { id: "inline-edit", component: "InlineEdit", @@ -429,7 +523,11 @@ export const COMPONENTS: ComponentSpec[] = [ { id: "list-box", component: "ListBox", - kind: "display", + kind: "custom", + outcomes: [ + { suffix: "selects", what: "choosing a listbox item changes its controlled selection", click: "option:Second item", subject: "option:Second item", expect: "SelectionChanges" }, + { suffix: "reports", what: "the listbox reports the selected key", click: "option:First item", subject: "heading:ListBox value:", expect: "NameChanges" }, + ], }, { id: "live-chat-bubble", @@ -455,25 +553,34 @@ export const COMPONENTS: ComponentSpec[] = [ * of that name exists at all. Closing is the bubble's contract, not this * component's. */ - kind: "action", - subject: "Close chat", - subjectRole: "button", + kind: "custom", + outcomes: [ + { suffix: "accepts-message", what: "the chat composer accepts a message", typeInto: "textbox:Message support...", text: "Hello support", subject: "textbox:Message support...", expect: "ValueChanges" }, + { suffix: "sends-message", what: "the chat panel hands the message to its owner", click: "button:Send", subject: "heading:LiveChat sent: Hello support", expect: "PaintsNamed", paint: true }, + { suffix: "closes", what: "the chat panel close control invokes its owner", click: "button:Close chat", subject: "heading:Action result: LiveChatPanel complete", expect: "PaintsNamed", paint: true }, + ], }, { id: "metal-border", component: "MetalBorder", kind: "display" }, { id: "navbar", component: "Navbar", kind: "display" }, { id: "pwa-install-prompt", component: "PWAInstallPrompt", - kind: "display", + kind: "custom", + outcomes: [ + { suffix: "installs", what: "accepting the browser install prompt reports installation", click: "button:Install A", subject: "heading:PWA outcome:", expect: "NameChanges", covers: ["button:Install *"] }, + { suffix: "defers", what: "the not-now action dismisses the prompt and reports deferral", click: "button:Not now B", subject: "heading:PWA outcome:", expect: "NameChanges", covers: ["button:Not now *"] }, + { suffix: "closes", what: "the close action dismisses the prompt and reports closure", click: "button:Close C", subject: "heading:PWA outcome:", expect: "NameChanges", covers: ["button:Close *"] }, + ], }, { id: "pagination", component: "Pagination", // Measured: `navigation:pagination`, with named previous/next controls. The // fixture has two controlled pages, so Next must call onChange. - kind: "action", - subject: "Go to next page", - subjectRole: "button", + kind: "custom", + outcomes: [ + { suffix: "changes", what: "pagination reports the next page to its owner", click: "button:Go to next page", subject: "heading:Action result: Pagination complete", expect: "PaintsNamed", paint: true, covers: ["button:Go to *"] }, + ], }, { id: "panel-toggle", @@ -486,10 +593,11 @@ export const COMPONENTS: ComponentSpec[] = [ { id: "password-field", component: "PasswordField", - // Measured: role `textbox` (empty name) plus an unnamed `button` at 28x28, - // which is the reveal control and has no accessible name at all. - kind: "display", - subjectRole: "textbox", + kind: "custom", + outcomes: [ + { suffix: "accepts", what: "typing updates the controlled password value", typeInto: "textbox:Password", text: "secret", subject: "heading:Password value: secret", expect: "PaintsNamed", paint: true }, + { suffix: "reveals", what: "the visibility control reports its pressed state", click: "button:Show password", subject: "button:Hide password", expect: "PaintsNamed", paint: true }, + ], }, { id: "password-requirements", @@ -509,7 +617,7 @@ export const COMPONENTS: ComponentSpec[] = [ id: "radio", component: "Radio", kind: "toggle", - // Measured: role `radio`, empty name, 1x1 at (79,103). + subject: "Radio", subjectRole: "radio", }, { id: "scroll-area", component: "ScrollArea", kind: "display" }, @@ -545,24 +653,25 @@ export const COMPONENTS: ComponentSpec[] = [ id: "switch", component: "Switch", kind: "toggle", - // Measured: role `switch`, empty name, 1x1 at (79,116). The visible control - // is a styled sibling; this is the real input. It had `checkbox:Switch`, - // which is wrong in both halves. + subject: "Switch", subjectRole: "switch", }, { id: "table", component: "Table", - kind: "display", + kind: "custom", + outcomes: [ + { suffix: "sorts", what: "a sortable table column reports its next direction", click: "Name fixture", subject: "heading:Table sort:", expect: "NameChanges" }, + ], }, { id: "tabs", component: "Tabs", - kind: "tabs", - subject: "First", - subjectRole: "tab", - activate: "tab:Second", - opens: "heading:Second panel", + kind: "custom", + outcomes: [ + { suffix: "changes", what: "activating another tab changes controlled selection", click: "tab:Second", subject: "tab:Second", expect: "SelectionChanges", covers: ["tab:*"] }, + { suffix: "changes-panel", what: "the selected tab exposes its corresponding panel", subject: "heading:Second panel", expect: "PaintsNamed", paint: true }, + ], }, { id: "text", component: "Text", kind: "display" }, { @@ -575,18 +684,222 @@ export const COMPONENTS: ComponentSpec[] = [ { id: "theme-color-picker", component: "ThemeColorPicker", - kind: "display", + kind: "custom", + outcomes: [ + { suffix: "opens", what: "the theme color trigger opens its palette", click: "button:Change theme color", subject: "button:Black", expect: "PaintsNamed", paint: true }, + { suffix: "switches-theme", what: "choosing a grayscale swatch reports the requested theme", click: "button:Black", subject: "heading:ThemeColorPicker theme: light", expect: "PaintsNamed", paint: true, covers: ["button:White", "button:Light gray", "button:Gray", "button:Dark gray", "button:Charcoal", "button:Black"] }, + ], }, { id: "toast", component: "Toast", - kind: "display", + kind: "custom", + outcomes: [ + { suffix: "opens", what: "requesting a toast paints its queued action", click: "button:Show fixture toast", subject: "button:Undo fixture", expect: "PaintsNamed", paint: true }, + { suffix: "acts", what: "the toast action invokes its consumer callback", click: "button:Undo fixture", subject: "heading:Toast outcome:", expect: "NameChanges" }, + { suffix: "closes", what: "the toast close control removes the notification", click: "button:Dismiss notification", subject: "button:Dismiss notification", expect: "Vanishes" }, + ], }, { id: "tooltip", component: "Tooltip", - kind: "display", + kind: "custom", + outcomes: [ + { suffix: "opens", what: "hovering the tooltip trigger reveals its content", hover: "button:Tooltip target", subject: "tooltip:Fixture tooltip", expect: "PaintsNamed", paint: true }, + ], }, + { + id: "button-group", component: "ButtonGroup", kind: "custom", + outcomes: [ + { suffix: "contains-actions", what: "grouped buttons remain operable", click: "button:First grouped button", subject: "heading:ButtonGroup selected: first", expect: "PaintsNamed", paint: true, covers: ["button:* grouped button"] }, + ], + }, + { + id: "checkbox-group", + component: "CheckboxGroup", + kind: "custom", + outcomes: [ + { suffix: "selects", what: "selecting a grouped checkbox changes its controlled selection", click: "checkbox:Second choice", subject: "checkbox:Second choice", expect: "SelectionChanges" }, + { suffix: "reports", what: "selecting a grouped checkbox reports the new values", click: "checkbox:First choice", subject: "heading:CheckboxGroup value:", expect: "NameChanges" }, + ], + }, + { + id: "color-area", + component: "ColorArea", + kind: "custom", + outcomes: [ + { suffix: "keyboard-changes", what: "ArrowRight changes the controlled saturation", key: "ArrowRight", keyOn: "slider:Color area", subject: "slider:Color area", expect: "ValueChanges" }, + { suffix: "reports", what: "keyboard adjustment reports the new color area value", key: "ArrowRight", keyOn: "slider:Color area", subject: "heading:ColorArea changed", expect: "Present" }, + { suffix: "pointer-changes", what: "pointer dragging changes the controlled saturation", pointerDrag: { from: "slider:Color area", dx: -80, dy: 20, steps: 4 }, subject: "slider:Color area", expect: "ValueChanges" }, + ], + }, + { + id: "color-field", + component: "ColorField", + kind: "custom", + outcomes: [ + { suffix: "accepts", what: "typing a valid color changes the field value", typeInto: "textbox:Color value", text: "#112233", subject: "textbox:Color value", expect: "ValueChanges" }, + { suffix: "reports", what: "typing a valid color reports the normalized value", typeInto: "textbox:Color value", text: "#112233", subject: "heading:ColorField value: #112233", expect: "Present" }, + ], + }, + { + id: "color-picker", + component: "ColorPicker", + kind: "custom", + outcomes: [ + { suffix: "hue-changes", what: "the composed hue slider changes the controlled color", key: "ArrowRight", keyOn: "slider:Hue", subject: "slider:Hue", expect: "ValueChanges" }, + { suffix: "area-changes", what: "the composed color area changes the controlled color", key: "ArrowRight", keyOn: "slider:Color area", subject: "slider:Color area", expect: "ValueChanges" }, + { suffix: "field-reports", what: "the composed color field reports a typed literal", typeInto: "textbox:Color value", text: "#112233", subject: "heading:ColorPicker value:", expect: "NameChanges" }, + ], + }, + { + id: "color-slider", + component: "ColorSlider", + kind: "custom", + outcomes: [ + { suffix: "keyboard-changes", what: "ArrowRight changes the controlled hue", key: "ArrowRight", keyOn: "slider:Hue", subject: "slider:Hue", expect: "ValueChanges" }, + { suffix: "reports", what: "the hue slider reports the changed value", key: "ArrowRight", keyOn: "slider:Hue", subject: "heading:ColorSlider changed", expect: "Present" }, + { suffix: "pointer-changes", what: "pointer dragging changes the controlled hue", pointerDrag: { from: "slider:Hue", dx: 100, dy: 0, steps: 4 }, subject: "slider:Hue", expect: "ValueChanges" }, + ], + }, + { + id: "color-swatch-picker", + component: "ColorSwatchPicker", + kind: "custom", + outcomes: [ + { suffix: "selects", what: "choosing another swatch changes its controlled selection", click: "radio:Blue swatch", subject: "radio:Blue swatch", expect: "SelectionChanges", covers: ["radio:* swatch"] }, + { suffix: "reports", what: "choosing another swatch reports the color", click: "radio:Blue swatch", subject: "heading:ColorSwatchPicker value: #0000ff", expect: "Present" }, + ], + }, + { + id: "combo-box", + component: "ComboBox", + kind: "custom", + outcomes: [ + { suffix: "opens", what: "the ComboBox opens an addressable listbox", click: "button:Toggle options", subject: "option:Beta", expect: "PaintsNamed", paint: true }, + { suffix: "selects", what: "choosing an option changes the controlled input value", prepare: "button:Toggle options", prepareUnless: "option:Beta", click: "option:Beta", subject: "combobox:Fixture combo box", expect: "ValueChanges" }, + { suffix: "reports", what: "choosing another option reports the selected key", prepare: "button:Toggle options", prepareUnless: "option:Alpha", click: "option:Alpha", subject: "heading:ComboBox value:", expect: "NameChanges" }, + { suffix: "accepts-query", what: "typing a query clears the committed selection", typeInto: "combobox:Fixture combo box", text: "Gam", subject: "heading:ComboBox value:", expect: "NameChanges" }, + ], + }, + { + id: "date-field", + component: "DateField", + kind: "custom", + outcomes: [ + { suffix: "accepts", what: "typing changes the date field value", typeInto: "textbox:Date value", text: "2025-06-24", subject: "textbox:Date value", expect: "ValueChanges" }, + { suffix: "reports", what: "typing reports the date value", typeInto: "textbox:Date value", text: "2025-06-24", subject: "heading:DateField value: 2025-06-24", expect: "Present" }, + ], + }, + { + id: "date-picker", + component: "DatePicker", + kind: "custom", + outcomes: [ + { suffix: "opens", what: "the date picker opens its calendar dialog", click: "button:Jun 15, 2025", subject: "dialog:", expect: "PaintsNamed", paint: true }, + { suffix: "selects", what: "choosing a date reports the controlled value", prepare: "button:Jun 15, 2025", prepareUnless: "dialog:", click: "gridcell:Tuesday, June 24, 2025", subject: "heading:DatePicker value: 2025-06-24", expect: "Present" }, + ], + }, + { + id: "date-range-picker", + component: "DateRangePicker", + kind: "custom", + outcomes: [ + { suffix: "opens", what: "the date range picker opens its calendar dialog", click: "button:Jun 15, 2025 Jun 17, 2025", subject: "dialog:", expect: "PaintsNamed", paint: true }, + { suffix: "starts", what: "choosing a date starts a pending range", prepare: "button:Jun 15, 2025 Jun 17, 2025", prepareUnless: "dialog:", click: "gridcell:Tuesday, June 24, 2025", subject: "gridcell:Tuesday, June 24, 2025", expect: "SelectionChanges" }, + { suffix: "completes", what: "choosing a second date completes the controlled range", click: "gridcell:Thursday, June 26, 2025", subject: "heading:DateRangePicker end: 2025-06-26", expect: "Present" }, + ], + }, + { + id: "flex-grid", + component: "FlexGrid", + kind: "custom", + outcomes: [ + { + suffix: "reveals-more", + what: "the incremental grid reveals its next page when asked", + click: "button:Load more rows", + subject: "heading:Row Three", + expect: "PaintsNamed", + paint: true, + }, + ], + }, + { + id: "input-otp", + component: "InputOTP", + kind: "custom", + outcomes: [ + { suffix: "accepts", what: "typing fills the one-time password value", typeInto: "textbox:Verification code", text: "123456", subject: "textbox:Verification code", expect: "ValueChanges" }, + { suffix: "reports", what: "typing reports the one-time password", typeInto: "textbox:Verification code", text: "123456", subject: "heading:InputOTP value: 123456", expect: "Present" }, + ], + }, + { + id: "join", component: "Join", kind: "custom", + outcomes: [ + { suffix: "contains-actions", what: "joined buttons remain operable", click: "button:First joined button", subject: "heading:Join selected: first", expect: "PaintsNamed", paint: true, covers: ["button:* joined button"] }, + ], + }, + { id: "kbd", component: "Kbd", kind: "display" }, + { + id: "menu", + component: "Menu", + kind: "custom", + outcomes: [ + { suffix: "selects", what: "activating a menu item changes its controlled selection", click: "menuitemradio:Beta action", subject: "menuitemradio:Beta action", expect: "SelectionChanges", covers: ["menuitemradio:* action"] }, + { suffix: "reports", what: "activating a menu item reports its selected key", click: "menuitemradio:Beta action", subject: "heading:Menu value: b", expect: "Present" }, + ], + }, + { id: "meter", component: "Meter", kind: "display" }, + { id: "noise-background", component: "NoiseBackground", kind: "display" }, + { id: "radial-progress", component: "RadialProgress", kind: "display" }, + { + id: "radio-group", + component: "RadioGroup", + kind: "custom", + outcomes: [ + { suffix: "selects", what: "choosing another grouped radio changes controlled selection", click: "radio:Second radio", subject: "radio:Second radio", expect: "SelectionChanges", covers: ["radio:* radio"] }, + { suffix: "reports", what: "choosing another grouped radio reports its value", click: "radio:Second radio", subject: "heading:RadioGroup value: second", expect: "Present" }, + ], + }, + { + id: "range-calendar", + component: "RangeCalendar", + kind: "custom", + outcomes: [ + { suffix: "starts", what: "choosing a date starts a pending range", click: "gridcell:Tuesday, June 24, 2025", subject: "gridcell:Tuesday, June 24, 2025", expect: "SelectionChanges" }, + { suffix: "completes", what: "choosing a second date completes the controlled range", click: "gridcell:Thursday, June 26, 2025", subject: "heading:RangeCalendar end: 2025-06-26", expect: "Present" }, + { suffix: "next-month", what: "the next-month control advances the visible calendar", click: "button:Next month", subject: "heading:July 2025", expect: "PaintsNamed", paint: true }, + { suffix: "previous-month", what: "the previous-month control returns to the prior calendar", click: "button:Previous month", subject: "heading:June 2025", expect: "PaintsNamed", paint: true }, + ], + }, + { + id: "size-picker", + component: "SizePicker", + kind: "custom", + outcomes: [ + { suffix: "selects", what: "choosing a size changes its selected radio", click: "radio:Size L", subject: "radio:Size L", expect: "SelectionChanges", covers: ["radio:Size *"] }, + { suffix: "reports", what: "choosing a size reports the preset", click: "radio:Size L", subject: "heading:SizePicker value: L", expect: "Present" }, + ], + }, + { + id: "time-field", + component: "TimeField", + kind: "custom", + outcomes: [ + { suffix: "accepts", what: "typing changes the time field value", typeInto: "textbox:Time value", text: "12:34", subject: "textbox:Time value", expect: "ValueChanges" }, + { suffix: "reports", what: "typing reports the time value", typeInto: "textbox:Time value", text: "12:34", subject: "heading:TimeField value: 12:34", expect: "Present" }, + ], + }, + { + id: "toolbar", + component: "Toolbar", + kind: "custom", + outcomes: [ + { suffix: "moves-focus", what: "ArrowRight moves focus to the next toolbar control", prepare: "button:First tool", key: "ArrowRight", keyOn: "button:First tool", subject: "button:Second tool", expect: "FocusMoves" }, + ], + }, + { id: "video-preview", component: "VideoPreview", kind: "display" }, ]; /** Refuse an inventory whose generated pages could overwrite or under-specify one another. */ @@ -611,6 +924,13 @@ export function validateComponentSpecs(): void { continue; } + if (spec.kind === "custom") { + if (!spec.outcomes?.length) { + throw new Error(`${spec.component}: custom QA requires outcomes`); + } + continue; + } + if (spec.kind !== "display" && (!spec.subject || !spec.subjectRole)) { throw new Error( `${spec.component}: ${spec.kind} QA requires subject and subjectRole`, @@ -633,4 +953,19 @@ export function validateComponentSpecs(): void { throw new Error(`${spec.component}: ${spec.kind} QA requires opens`); } } + + const declared = new Map(componentFamilies.map((family) => [family.id, family.name])); + const missing = COMPONENTS.filter((spec) => declared.get(spec.id) !== spec.component); + const stale = componentFamilies.filter( + (family) => !COMPONENTS.some((spec) => spec.id === family.id && spec.component === family.name), + ); + if (missing.length > 0 || stale.length > 0) { + throw new Error( + `component family manifest and native QA registry differ; missing/mismatched: ${missing + .map((spec) => `${spec.id}:${spec.component}`) + .join(", ") || "none"}; stale: ${stale + .map((family) => `${family.id}:${family.name}`) + .join(", ") || "none"}`, + ); + } } diff --git a/tests/qa-harness/generate-checks.ts b/tests/qa-harness/generate-checks.ts index cffb576d..33d9a88a 100644 --- a/tests/qa-harness/generate-checks.ts +++ b/tests/qa-harness/generate-checks.ts @@ -330,6 +330,7 @@ function checksFor(spec: ComponentSpec, profile: Profile): string { if ( spec.kind !== "display" && spec.kind !== "toggle" && + spec.kind !== "custom" && (!spec.subject || !spec.subjectRole) ) { throw new Error( @@ -550,6 +551,22 @@ function checksFor(spec: ComponentSpec, profile: Profile): string { expect: profile.paints("PaintsNamed"), }), ); + records.push( + check({ + id: `"${spec.id}-resets-draft"`, + group: `"${spec.id}"`, + what: `"resetting ${spec.component} discards the draft and reports completion"`, + open: surface, + hover: "None", + prepare: `Some("${spec.subjectRole}:${spec.subject}")`, + prepare_unless: `Some("${spec.opens}")`, + setup_type_into: `Some("${spec.opens}")`, + setup_text: `Some("ws://qa-reset-draft")`, + click: `Some("button:Reset")`, + subject: `"heading:Panel outcome: reset"`, + expect: profile.paints("PaintsNamed"), + }), + ); } if (spec.kind === "overlay") { @@ -631,6 +648,7 @@ function checksFor(spec: ComponentSpec, profile: Profile): string { } if (spec.kind === "toggle") { + const toggleSubject = `${spec.subjectRole}:${spec.subject ?? ""}`; /* * A toggle's whole contract: pressing it flips the state the tree reports. * @@ -651,8 +669,8 @@ function checksFor(spec: ComponentSpec, profile: Profile): string { what: `"pressing the ${spec.component} changes what it reports"`, open: surface, hover: "None", - click: `Some("${spec.subjectRole}:")`, - subject: `"${spec.subjectRole}:"`, + click: `Some("${toggleSubject}")`, + subject: `"${toggleSubject}"`, expect: "SelectionChanges", }), ); @@ -674,7 +692,7 @@ function checksFor(spec: ComponentSpec, profile: Profile): string { open: surface, hover: "None", settle_after_ms: "300", - click: `Some("${spec.subjectRole}:")`, + click: `Some("${toggleSubject}")`, /* * A latched marker, because the checks in a group share one host: * `-toggles` has already pressed this control by the time this runs, so @@ -860,6 +878,39 @@ function checksFor(spec: ComponentSpec, profile: Profile): string { ); } + if (spec.kind === "custom") { + for (const outcome of spec.outcomes ?? []) { + const fields: Record = { + id: JSON.stringify(`${spec.id}-${outcome.suffix}`), + group: JSON.stringify(spec.id), + what: JSON.stringify(outcome.what), + open: surface, + hover: outcome.hover ? `Some(${JSON.stringify(outcome.hover)})` : "None", + click: outcome.click ? `Some(${JSON.stringify(outcome.click)})` : "None", + subject: JSON.stringify(outcome.subject), + expect: outcome.paint + ? profile.paints(outcome.expect as "Paints" | "PaintsNamed") + : outcome.expect, + }; + + if (outcome.prepare) fields.prepare = `Some(${JSON.stringify(outcome.prepare)})`; + if (outcome.covers?.length) { + fields.covers = `[${outcome.covers.map((selector) => JSON.stringify(selector)).join(", ")}]`; + } + if (outcome.prepareUnless) fields.prepare_unless = `Some(${JSON.stringify(outcome.prepareUnless)})`; + if (outcome.settleAfterMs !== undefined) fields.settle_after_ms = String(outcome.settleAfterMs); + if (outcome.key) fields.key = `Some(${JSON.stringify(outcome.key)})`; + if (outcome.keyOn) fields.key_on = `Some(${JSON.stringify(outcome.keyOn)})`; + if (outcome.typeInto) fields.type_into = `Some(${JSON.stringify(outcome.typeInto)})`; + if (outcome.text) fields.text = `Some(${JSON.stringify(outcome.text)})`; + if (outcome.pointerDrag) { + const drag = outcome.pointerDrag; + fields.pointer_drag = `Some((from: ${JSON.stringify(drag.from)}, dx: ${drag.dx}.0, dy: ${drag.dy}.0, steps: ${drag.steps}))`; + } + records.push(check(fields)); + } + } + if (spec.kind === "action") { records.push( check({ diff --git a/tests/qa-harness/generate-entries.ts b/tests/qa-harness/generate-entries.ts index d6bedd6b..a5de5e29 100644 --- a/tests/qa-harness/generate-entries.ts +++ b/tests/qa-harness/generate-entries.ts @@ -21,7 +21,7 @@ * Run: bun run qa:entries (or qa:build, which does it first) */ import { COMPONENTS, validateComponentSpecs } from "./components"; -import { mkdirSync, readdirSync, unlinkSync, writeFileSync } from "node:fs"; +import { mkdirSync, readdirSync, statSync, unlinkSync, writeFileSync } from "node:fs"; import { join } from "node:path"; /* @@ -65,6 +65,7 @@ const IMPORT_FORM: Record = { "Breadcrumb": "named", "BreadcrumbItem": "named", "Button": "default", + "ButtonGroup": "default", "Calendar": "default", "CardRoot": "named", "CardBody": "named", @@ -73,14 +74,21 @@ const IMPORT_FORM: Record = { "Card": "default", "ChatBubble": "default", "Checkbox": "default", + "CheckboxGroup": "default", "Chip": "default", "CloseButton": "default", + "ColorArea": "default", + "ColorField": "default", + "ColorPicker": "default", + "ColorSlider": "default", + "ColorSwatchPicker": "default", "Collapsible": "default", "ConnectionSettings": "named", "ColorSwatch": "default", "ColorWheel": "named", "ColorWheelFlower": "named", "ComplexColorWheel": "named", + "ComboBox": "default", "autosize": "named", "boundsFromRows": "named", "Composer": "default", @@ -88,6 +96,9 @@ const IMPORT_FORM: Record = { "shouldSubmit": "named", "createDataGrid": "named", "DataGrid": "default", + "DateField": "default", + "DatePicker": "default", + "DateRangePicker": "default", "DialogBackdrop": "named", "DialogBody": "named", "DialogCloseTrigger": "named", @@ -138,6 +149,8 @@ const IMPORT_FORM: Record = { "useImmersiveLandingContext": "named", "InlineEdit": "default", "Input": "default", + "Join": "default", + "Kbd": "default", "InputOTP": "default", "InputOTPGroup": "named", "InputOTPSeparator": "named", @@ -160,18 +173,24 @@ const IMPORT_FORM: Record = { "LiveChatBubble": "named", "LiveChatPanel": "named", "MetalBorder": "named", + "Menu": "default", + "Meter": "default", "Navbar": "default", + "NoiseBackground": "default", "Pagination": "default", "PanelToggle": "named", "PasswordField": "named", "PasswordRequirements": "named", "Popover": "default", "Progress": "default", + "RadialProgress": "default", "Radio": "default", + "RangeCalendar": "default", "RadioGroup": "named", "ScrollArea": "default", "Select": "default", "Separator": "default", + "SizePicker": "named", "Skeleton": "default", "Slider": "default", "Spinner": "default", @@ -191,6 +210,8 @@ const IMPORT_FORM: Record = { "getDefaultHueShiftStore": "named", "resetHueShift": "named", "ThemeColorPicker": "named", + "TimeField": "default", + "Toolbar": "default", "DEFAULT_TOAST_GAP": "named", "DEFAULT_MAX_VISIBLE_TOAST": "named", "DEFAULT_TOAST_SCALE_FACTOR": "named", @@ -208,6 +229,7 @@ const IMPORT_FORM: Record = { "toast": "named", "toastQueue": "named", "Tooltip": "default", + "VideoPreview": "named", "TooltipArrow": "named", "TooltipContent": "named", "TooltipTrigger": "named", @@ -250,21 +272,32 @@ const MODULE_PATHS: Record = { "badge": "components/badge", "breadcrumb": "components/breadcrumb", "button": "components/button", + "button-group": "components/button-group", "calendar": "components/calendar", "card": "components/card", "chat-bubble": "components/chatbubble", "checkbox": "components/checkbox", + "checkbox-group": "components/checkbox-group", "chip": "components/chip", "close-button": "components/close-button", + "color-area": "components/color-area", + "color-field": "components/color-field", + "color-picker": "components/color-picker", + "color-slider": "components/color-slider", + "color-swatch-picker": "components/color-swatch-picker", "collapsible": "components/collapsible", "connection-settings": "components/connection-settings", "color-swatch": "components/color-swatch", "color-wheel": "components/color-wheel", "color-wheel-flower": "components/color-wheel-flower", "complex-color-wheel": "components/color-wheel", + "combo-box": "components/combo-box", "composer": "components/composer", "cookie-consent": "components/immersive-landing", "data-grid": "components/data-grid", + "date-field": "components/date-field", + "date-picker": "components/date-picker", + "date-range-picker": "components/date-range-picker", "dialog": "components/dialog", "dock": "components/dock", "drawer": "components/drawer", @@ -274,6 +307,7 @@ const MODULE_PATHS: Record = { "fieldset": "components/fieldset", "firefox-pwa-banner": "components/immersive-landing", "flex": "components/flex", + "flex-grid": "components/flex-grid", "footer": "components/footer", "form": "components/form", "glow-card": "components/glow-card", @@ -283,6 +317,9 @@ const MODULE_PATHS: Record = { "immersive-landing": "components/immersive-landing", "inline-edit": "components/inline-edit", "input": "components/input", + "input-otp": "components/input-otp", + "join": "components/join", + "kbd": "components/kbd", "label": "components/label", "language-switcher": "components/language-switcher", "link": "components/link", @@ -290,7 +327,10 @@ const MODULE_PATHS: Record = { "live-chat-bubble": "components/live-chat", "live-chat-panel": "components/live-chat", "metal-border": "components/metal-border", + "menu": "components/menu", + "meter": "components/meter", "navbar": "components/navbar", + "noise-background": "components/noise-background", "pwa-install-prompt": "components/immersive-landing", "pagination": "components/pagination", "panel-toggle": "components/panel-toggle", @@ -298,10 +338,14 @@ const MODULE_PATHS: Record = { "password-requirements": "components/password-requirements", "popover": "components/popover", "progress": "components/progress", + "radial-progress": "components/radial-progress", "radio": "components/radio", + "radio-group": "components/radio-group", + "range-calendar": "components/range-calendar", "scroll-area": "components/scroll-area", "select": "components/select", "separator": "components/separator", + "size-picker": "components/size-picker", "skeleton": "components/skeleton", "slider": "components/slider", "spinner": "components/spinner", @@ -311,8 +355,11 @@ const MODULE_PATHS: Record = { "text": "components/text", "textarea": "components/textarea", "theme-color-picker": "components/theme-color-picker", + "time-field": "components/time-field", + "toolbar": "components/toolbar", "toast": "components/toast", - "tooltip": "components/tooltip" + "tooltip": "components/tooltip", + "video-preview": "components/video-preview" }; const outputDir = join(import.meta.dir, "entries"); @@ -320,6 +367,35 @@ mkdirSync(outputDir, { recursive: true }); validateComponentSpecs(); +/* + * Coverage is a source-tree invariant, not a number copied into a README. + * Every visual component family gets a dedicated harness surface. Aliases are + * explicit so a newly added directory cannot disappear between a package + * export and the native QA matrix without breaking qa:entries. + */ +const SOURCE_FAMILY_TO_HARNESS: Record = { + chatbubble: ["chat-bubble"], + "live-chat": ["live-chat-bubble", "live-chat-panel"], +}; +const NON_VISUAL_SOURCE_FAMILIES = new Set(["_shared", "status"]); +const harnessIds = new Set(COMPONENTS.map((spec) => spec.id)); +const sourceComponentsDir = join(import.meta.dir, "../../src/components"); +for (const family of readdirSync(sourceComponentsDir)) { + if ( + NON_VISUAL_SOURCE_FAMILIES.has(family) || + !statSync(join(sourceComponentsDir, family)).isDirectory() + ) { + continue; + } + const requiredIds = SOURCE_FAMILY_TO_HARNESS[family] ?? [family]; + const missing = requiredIds.filter((id) => !harnessIds.has(id)); + if (missing.length > 0) { + throw new Error( + `${family}: source component family has no QA harness page (${missing.join(", ")})`, + ); + } +} + const expectedFiles = new Set(COMPONENTS.map((spec) => `${spec.id}.tsx`)); for (const file of readdirSync(outputDir)) { if (file.endsWith(".tsx") && !expectedFiles.has(file)) { diff --git a/tests/qa-harness/mount.tsx b/tests/qa-harness/mount.tsx index 4fab8705..63300457 100644 --- a/tests/qa-harness/mount.tsx +++ b/tests/qa-harness/mount.tsx @@ -25,9 +25,30 @@ import Collapsible, { CollapsibleContent, CollapsibleTrigger, } from "@pathscale/ui/components/collapsible"; +import Accordion, { + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@pathscale/ui/components/accordion"; +import Address from "@pathscale/ui/components/address"; +import Alert from "@pathscale/ui/components/alert"; +import Chip from "@pathscale/ui/components/chip"; +import Icon from "@pathscale/ui/components/icon"; +import { AuthPoweredBy } from "@pathscale/ui/components/auth-powered-by"; +import { AuthFooterLinks } from "@pathscale/ui/components/auth-footer-links"; +import { Breadcrumb } from "@pathscale/ui/components/breadcrumb"; +import Card from "@pathscale/ui/components/card"; +import DataGrid, { createDataGrid } from "@pathscale/ui/components/data-grid"; +import { PasswordField } from "@pathscale/ui/components/password-field"; +import ImmersiveLanding, { + CookieConsent, + FirefoxPWABanner, + PWAInstallPrompt, +} from "@pathscale/ui/components/immersive-landing"; import { ConnectionSettings } from "@pathscale/ui/components/connection-settings"; import { createConnectionSettings } from "@pathscale/ui/hooks/connection"; -import { ComplexColorWheel } from "@pathscale/ui/components/color-wheel"; +import { ColorWheel, ComplexColorWheel } from "@pathscale/ui/components/color-wheel"; +import { ColorWheelFlower } from "@pathscale/ui/components/color-wheel-flower"; import { createI18n, LanguageSwitcher } from "@pathscale/ui/components/language-switcher"; import Dialog from "@pathscale/ui/components/dialog"; import Drawer from "@pathscale/ui/components/drawer"; @@ -42,7 +63,40 @@ import CloseButton from "@pathscale/ui/components/close-button"; import { Form } from "@pathscale/ui/components/form"; import Input from "@pathscale/ui/components/input"; import { createForm } from "@pathscale/ui/hooks/form"; -import { createErrorBoundary, createSignal, For, Show } from "solid-js"; +import ButtonGroup from "@pathscale/ui/components/button-group"; +import Checkbox from "@pathscale/ui/components/checkbox"; +import CheckboxGroup from "@pathscale/ui/components/checkbox-group"; +import ColorArea, { type ColorAreaValue } from "@pathscale/ui/components/color-area"; +import ColorField from "@pathscale/ui/components/color-field"; +import ColorPicker from "@pathscale/ui/components/color-picker"; +import ColorSlider from "@pathscale/ui/components/color-slider"; +import ColorSwatch from "@pathscale/ui/components/color-swatch"; +import ColorSwatchPicker from "@pathscale/ui/components/color-swatch-picker"; +import ComboBox from "@pathscale/ui/components/combo-box"; +import DateField from "@pathscale/ui/components/date-field"; +import DatePicker from "@pathscale/ui/components/date-picker"; +import DateRangePicker, { type DateRangeValue } from "@pathscale/ui/components/date-range-picker"; +import FlexGrid from "@pathscale/ui/components/flex-grid"; +import InputOTP from "@pathscale/ui/components/input-otp"; +import Join from "@pathscale/ui/components/join"; +import Kbd from "@pathscale/ui/components/kbd"; +import Menu from "@pathscale/ui/components/menu"; +import ListBox, { ListBoxItem } from "@pathscale/ui/components/list-box"; +import Meter from "@pathscale/ui/components/meter"; +import NoiseBackground from "@pathscale/ui/components/noise-background"; +import RadialProgress from "@pathscale/ui/components/radial-progress"; +import Radio from "@pathscale/ui/components/radio"; +import RadioGroup from "@pathscale/ui/components/radio-group"; +import RangeCalendar, { type RangeCalendarValue } from "@pathscale/ui/components/range-calendar"; +import { SizePicker } from "@pathscale/ui/components/size-picker"; +import TimeField from "@pathscale/ui/components/time-field"; +import Toolbar from "@pathscale/ui/components/toolbar"; +import Tooltip from "@pathscale/ui/components/tooltip"; +import Table from "@pathscale/ui/components/table"; +import Toast, { toast } from "@pathscale/ui/components/toast"; +import { ThemeColorPicker } from "@pathscale/ui/components/theme-color-picker"; +import { VideoPreview } from "@pathscale/ui/components/video-preview"; +import { createErrorBoundary, createSignal, For, onCleanup, Show } from "solid-js"; import { Dynamic, type JSX, render } from "@solidjs/web"; import { COMPONENTS, type ComponentSpec } from "./components"; @@ -90,6 +144,240 @@ function installMemoryStorage(): void { }); } +function AccordionFixture() { + const [value, setValue] = createSignal([]); + return ( + <> + + + First section +

First panel

+ + +

Accordion value: {value().join(",") || "none"}

+ + ); +} + +function AddressFixture() { + const [copied, setCopied] = createSignal("none"); + try { + Object.defineProperty(globalThis.navigator, "clipboard", { + configurable: true, + value: { writeText: async () => undefined }, + }); + } catch { + // A host-supplied clipboard is already sufficient. + } + return ( + <> +
+

Address copied: {copied()}

+ + ); +} + +function AlertFixture() { + const [dismissed, setDismissed] = createSignal(false); + return ( + Alert dismissed}> + setDismissed(true)} dismissLabel="Dismiss fixture alert"> + Fixture alert + + + ); +} + +function AuthPoweredByFixture() { + const [activated, setActivated] = createSignal(false); + const observeHoneyLink = (event: MouseEvent) => { + if ((event.target as Element | null)?.closest("a[href='#honey']")) { + setActivated(true); + } + }; + document.addEventListener("click", observeHoneyLink); + onCleanup(() => document.removeEventListener("click", observeHoneyLink)); + return ( +
+ +

Honey link activated

+
+ ); +} + +function AuthFooterLinksFixture() { + const [activated, setActivated] = createSignal("none"); + return ( + <> + setActivated("privacy"), + }, + { + key: "help", + label: "Help fixture", + onClick: () => setActivated("help"), + }, + { + key: "disabled", + label: "Disabled fixture", + disabled: true, + }, + ]} + /> +

Auth footer action: {activated()}

+ + ); +} + +function BreadcrumbFixture() { + const [activated, setActivated] = createSignal(false); + return ( + <> + + setActivated(true)} + > + Products fixture + + Current fixture + +

Breadcrumb activated

+ + ); +} + +function CardFixture() { + const [activated, setActivated] = createSignal(false); + return ( + <> + setActivated(true)}> + Interactive fixture card + +

Card activated

+ + ); +} + +function PasswordFieldFixture() { + const [value, setValue] = createSignal(""); + return ( + <> + +

Password value: {value()}

+ + ); +} + +function ChipFixture() { + const [removed, setRemoved] = createSignal(false); + return ( + Chip removed}> + setRemoved(true)} + removeButtonLabel="Remove fixture chip" + endIcon={} + > + Fixture chip + + + ); +} + +function ColorSwatchFixture() { + const [selected, setSelected] = createSignal("none"); + return ( + <> + +

ColorSwatch selected: {selected()}

+ + ); +} + +function ListBoxFixture() { + const [selected, setSelected] = createSignal(new Set()); + return ( + <> + + First item + Second item + +

ListBox value: {[...selected()].join(",") || "none"}

+ + ); +} + +function TooltipFixture() { + return ( + + + Fixture tooltip + + ); +} + +function ThemeColorPickerFixture() { + const [theme, setTheme] = createSignal("none"); + return ( + <> + +

ThemeColorPicker theme: {theme()}

+ + ); +} + +function CookieConsentFixture() { + const [managed, setManaged] = createSignal("none"); + const [accepted, setAccepted] = createSignal("none"); + const [declined, setDeclined] = createSignal("none"); + const consent = (prefix: string) => ({ + consentKey: `qa-cookie-${prefix}-consent`, + analyticsKey: `qa-cookie-${prefix}-analytics`, + marketingKey: `qa-cookie-${prefix}-marketing`, + }); + return ( + <> + setManaged(type)} + /> + setAccepted(type)} + /> + setDeclined(type)} + /> +

Cookie consent M: {managed()}

+

Cookie consent A: {accepted()}

+

Cookie consent D: {declined()}

+ + ); +} + function DropdownFixture(props: { spec: ComponentSpec }) { const options = () => props.spec.options ?? []; const [value, setValue] = createSignal(options()[1]?.value ?? ""); @@ -253,7 +541,8 @@ function CloseButtonFixture(props: { } function ComposerFixture(props: { spec: ComponentSpec; under?: unknown }) { - const [complete, setComplete] = createSignal(false); + const [value, setValue] = createSignal(""); + const [submitted, setSubmitted] = createSignal("none"); return ( setComplete(true)} - /> - +

Composer draft: {value()}

+

Composer submitted: {submitted()}

)}
@@ -357,6 +646,7 @@ function TabsFixture() { function ComplexColorWheelFixture() { const [strength, setStrength] = createSignal(10); + const [value, setValue] = createSignal("#ffffff"); return (
{ @@ -366,8 +656,8 @@ function ComplexColorWheelFixture() { }} > {}} + value={value()} + onChange={setValue} aria-label="Fixture colour" adjustments={[ { @@ -385,6 +675,200 @@ function ComplexColorWheelFixture() { ); } +function ColorWheelFlowerFixture() { + const [value, setValue] = createSignal("#ffffff"); + const [changed, setChanged] = createSignal(false); + return ( + <> + { + setValue(next.hex); + setChanged(true); + }} + /> +

ColorWheelFlower changed

+ + ); +} + +function ColorWheelFixture() { + const [value, setValue] = createSignal("#ffffff"); + return ( + <> + +

ColorWheel value: {value()}

+ + ); +} + +function DataGridFixture() { + const grid = createDataGrid<{ id: string; name: string }>({ + pageSize: 2, + selection: "multiple", + }); + grid.addColumn("name", "Name", "string", { + sortable: true, + searchable: true, + }); + grid.setRows([ + { id: "alpha", name: "Alpha" }, + { id: "beta", name: "Beta" }, + { id: "gamma", name: "Gamma" }, + ]); + + const [sort, setSort] = createSignal("none"); + const [page, setPage] = createSignal(0); + const [selection, setSelection] = createSignal("none"); + + return ( + <> + setSort(next ? `${next.column} ${next.direction}` : "none")} + onPageChange={setPage} + onSelectionChange={(ids) => setSelection([...ids].sort().join(",") || "none")} + /> +

Grid sort: {sort()}

+

Grid page: {page()}

+

Grid selection: {selection()}

+

Grid first filtered row: {grid.filteredRows()[0]?.name ?? "none"}

+ + ); +} + +function ImmersiveLandingFixture() { + const [navigation, setNavigation] = createSignal("none"); + return ( + <> + setNavigation(`${from} to ${to}`)} + > +

First landing page

+

Second landing page

+
+

Landing navigation: {navigation()}

+ + ); +} + +function PWAInstallPromptFixture() { + const [outcome, setOutcome] = createSignal("none"); + setTimeout(() => { + const installEvent = new Event("beforeinstallprompt", { cancelable: true }); + Object.defineProperties(installEvent, { + prompt: { value: () => undefined }, + userChoice: { value: Promise.resolve({ outcome: "accepted", platform: "web" }) }, + }); + window.dispatchEvent(installEvent); + }, 0); + + return ( + <> + setOutcome("installed")} + /> + setOutcome("later")} + /> + setOutcome("closed")} + /> +

PWA outcome: {outcome()}

+ + ); +} + +function FirefoxPWABannerFixture() { + const [outcome, setOutcome] = createSignal("none"); + const userAgentDescriptor = Object.getOwnPropertyDescriptor(navigator, "userAgent"); + Object.defineProperty(navigator, "userAgent", { + configurable: true, + value: "Mozilla/5.0 Firefox/130.0", + }); + const originalOpen = globalThis.open; + globalThis.open = (() => null) as typeof globalThis.open; + onCleanup(() => { + if (userAgentDescriptor) Object.defineProperty(navigator, "userAgent", userAgentDescriptor); + globalThis.open = originalOpen; + }); + + return ( + <> + setOutcome("installed")} + /> + setOutcome("later")} + /> + setOutcome("closed")} + /> +

Firefox PWA outcome: {outcome()}

+ + ); +} + +function TableFixture() { + const [direction, setDirection] = createSignal<"ascending" | "descending">("descending"); + return ( + <> + + setDirection(next.direction)} + > + Name fixture + Alpha + +
+

Table sort: {direction()}

+ + ); +} + +function ToastFixture() { + const [outcome, setOutcome] = createSignal("none"); + toast.clear(); + return ( + <> + + +

Toast outcome: {outcome()}

+ + ); +} + function LiveChatBubbleFixture(props: { spec: ComponentSpec; under?: unknown; @@ -417,6 +901,7 @@ function LiveChatBubbleFixture(props: { function LiveChatPanelFixture(props: { spec: ComponentSpec; under?: unknown }) { const [complete, setComplete] = createSignal(false); + const [sent, setSent] = createSignal("none"); return ( setComplete(true)} + onSendMessage={async ({ message }: { message: string }) => { + setSent(message); + return { messageId: "qa-message", timestamp: Date.now() }; + }} /> +

LiveChat sent: {sent()}

setOutcome("saved")} + onResetDone={() => setOutcome("reset")} onSaveFailed={(error: unknown) => setOutcome( `failed ${error instanceof Error ? error.message : String(error)}`, @@ -664,6 +1156,8 @@ function ConnectionSettingsFixture() {

Save outcome: {outcome()}

Reconnected: {reconnect()}

+

Panel outcome: {outcome()}

+ ); } @@ -751,6 +1245,7 @@ function ToggleFixtureWithReport(props: { * in `components.ts` the way a string or a number can. */ function DockFixture(props: { spec: ComponentSpec; under?: unknown }) { + const [selected, setSelected] = createSignal("none"); return ( {props.spec.component} is not exported} > {(Component) => ( - H }, - { title: "Search", icon: S }, - { title: "Settings", icon: G }, - ]} - /> + <> + , onClick: () => setSelected("Home") }, + { title: "Search", icon: , onClick: () => setSelected("Search") }, + { title: "Settings", icon: , onClick: () => setSelected("Settings") }, + ]} + showMobile={false} + /> +

Dock selected: {selected()}

+ )}
); @@ -818,6 +1317,250 @@ function CalendarFixture() { ); } +const isoDate = (value?: Date) => value + ? `${value.getFullYear()}-${String(value.getMonth() + 1).padStart(2, "0")}-${String(value.getDate()).padStart(2, "0")}` + : "none"; + +function ButtonGroupFixture() { + const [selected, setSelected] = createSignal("none"); + return <>

ButtonGroup selected: {selected()}

; +} + +function CheckboxGroupFixture() { + const [value, setValue] = createSignal([]); + return ( + <> + + First choice + Second choice + +

CheckboxGroup value: {value().join(",") || "none"}

+ + ); +} + +function ColorAreaFixture() { + const [value, setValue] = createSignal({ h: 240, s: 50, v: 50 }); + const [changed, setChanged] = createSignal(false); + return ( + <> + { setValue(next); setChanged(true); }} /> +

ColorArea changed

+ + ); +} + +function ColorFieldFixture() { + const [value, setValue] = createSignal("#FFFFFF"); + return ( + <> + +

ColorField value: {value()}

+ + ); +} + +function ColorPickerFixture() { + const [value, setValue] = createSignal("#6366F1"); + const [changed, setChanged] = createSignal(false); + return ( + <> + { setValue(next); setChanged(true); }}> + + + + +

ColorPicker changed

+

ColorPicker value: {value()}

+ + ); +} + +function ColorSliderFixture() { + const [value, setValue] = createSignal(180); + const [changed, setChanged] = createSignal(false); + return ( + <> + { setValue(next); setChanged(true); }} /> +

ColorSlider changed

+ + ); +} + +function ColorSwatchPickerFixture() { + const [value, setValue] = createSignal("#ff0000"); + return ( + <> + + + + +

ColorSwatchPicker value: {value()}

+ + ); +} + +function ComboBoxFixture() { + const [selected, setSelected] = createSignal(null); + return ( + <> + + + + + + + +

ComboBox value: {selected() ?? "none"}

+ + ); +} + +function DateFieldFixture() { + const [value, setValue] = createSignal(""); + return ( + <> + + + +

DateField value: {value()}

+ + ); +} + +function DatePickerFixture() { + const [value, setValue] = createSignal(new Date(2025, 5, 15)); + return ( + <> + +

DatePicker value: {isoDate(value())}

+ + ); +} + +function DateRangePickerFixture() { + const [value, setValue] = createSignal({ start: new Date(2025, 5, 15), end: new Date(2025, 5, 17) }); + return ( + <> + +

DateRangePicker start: {isoDate(value().start)}

+

DateRangePicker end: {isoDate(value().end)}

+ + ); +} + +function FlexGridFixture() { + return ( + } + > + {(row) =>

Row {row}

} +
+ ); +} + +function InputOTPFixture() { + const [value, setValue] = createSignal(""); + return ( + <> + +

InputOTP value: {value()}

+ + ); +} + +function JoinFixture() { + const [selected, setSelected] = createSignal("none"); + return <>

Join selected: {selected()}

; +} + +function KbdFixture() { + return K; +} + +function MenuFixture() { + const [selected, setSelected] = createSignal(new Set(["a"])); + return ( + <> + + Alpha action + Beta action + +

Menu value: {[...selected()].join(",") || "none"}

+ + ); +} + +function MeterFixture() { return ; } +function NoiseBackgroundFixture() { return Noise background content; } +function RadialProgressFixture() { return ; } + +function RadioGroupFixture() { + const [value, setValue] = createSignal("first"); + return ( + <> + + First radio + Second radio + +

RadioGroup value: {value()}

+ + ); +} + +function RangeCalendarFixture() { + const [value, setValue] = createSignal({ start: new Date(2025, 5, 15), end: new Date(2025, 5, 17) }); + return ( + <> + +

RangeCalendar start: {isoDate(value().start)}

+

RangeCalendar end: {isoDate(value().end)}

+ + ); +} + +function SizePickerFixture() { + const [value, setValue] = createSignal("M"); + return <>

SizePicker value: {value()}

; +} + +function TimeFieldFixture() { + const [value, setValue] = createSignal(""); + return ( + <> + + + +

TimeField value: {value()}

+ + ); +} + +function ToolbarFixture() { + const [focused, setFocused] = createSignal("none"); + return ( + <> + + + + +

Toolbar focus: {focused()}

+ + ); +} + +function VideoPreviewFixture() { + const stream = () => ({ getTracks: () => [] } as unknown as MediaStream); + return ; +} + /** Ids with a hand-written fixture; everything else mounts generically. */ const FIXTURES: Record< string, @@ -827,35 +1570,82 @@ const FIXTURES: Record< // components and needs it. (props: { spec: ComponentSpec; under?: unknown }) => JSX.Element > = { + accordion: AccordionFixture, + address: AddressFixture, + alert: AlertFixture, + "auth-footer-links": AuthFooterLinksFixture, + "auth-powered-by": AuthPoweredByFixture, "auth-submit-button": ActionFixture, button: ActionFixture, + "button-group": ButtonGroupFixture, + breadcrumb: BreadcrumbFixture, + card: CardFixture, calendar: CalendarFixture, checkbox: ToggleFixtureWithReport, + "checkbox-group": CheckboxGroupFixture, + chip: ChipFixture, "close-button": CloseButtonFixture, + "color-area": ColorAreaFixture, + "color-field": ColorFieldFixture, + "color-picker": ColorPickerFixture, + "color-slider": ColorSliderFixture, + "color-swatch": ColorSwatchFixture, + "color-swatch-picker": ColorSwatchPickerFixture, + "color-wheel": ColorWheelFixture, + "color-wheel-flower": ColorWheelFlowerFixture, collapsible: CollapsibleFixture, "connection-settings": ConnectionSettingsFixture, + "cookie-consent": CookieConsentFixture, + "data-grid": DataGridFixture, "complex-color-wheel": ComplexColorWheelFixture, + "combo-box": ComboBoxFixture, composer: ComposerFixture, dialog: DialogFixture, + "date-field": DateFieldFixture, + "date-picker": DatePickerFixture, + "date-range-picker": DateRangePickerFixture, dock: DockFixture, drawer: DrawerFixture, dropdown: DropdownFixture, form: FormFixture, + "flex-grid": FlexGridFixture, + "firefox-pwa-banner": FirefoxPWABannerFixture, "inline-edit": InlineEditFixture, input: FieldFixture, + "immersive-landing": ImmersiveLandingFixture, + "input-otp": InputOTPFixture, + join: JoinFixture, + kbd: KbdFixture, "language-switcher": LanguageSwitcherFixture, link: ActionFixture, + "list-box": ListBoxFixture, "live-chat-bubble": LiveChatBubbleFixture, "live-chat-panel": LiveChatPanelFixture, + menu: MenuFixture, + meter: MeterFixture, + "noise-background": NoiseBackgroundFixture, pagination: PaginationFixture, "panel-toggle": PanelToggleFixture, + "password-field": PasswordFieldFixture, popover: PopoverFixture, + "pwa-install-prompt": PWAInstallPromptFixture, + "radial-progress": RadialProgressFixture, radio: ToggleFixtureWithReport, + "radio-group": RadioGroupFixture, + "range-calendar": RangeCalendarFixture, select: SelectFixture, + "size-picker": SizePickerFixture, slider: SliderFixture, switch: ToggleFixtureWithReport, + table: TableFixture, tabs: TabsFixture, textarea: FieldFixture, + "time-field": TimeFieldFixture, + toolbar: ToolbarFixture, + "theme-color-picker": ThemeColorPickerFixture, + tooltip: TooltipFixture, + toast: ToastFixture, + "video-preview": VideoPreviewFixture, }; /* From 6d4c14b0935248f7f42d760b255359756aa33870 Mon Sep 17 00:00:00 2001 From: meh Date: Sat, 12 Sep 2026 16:59:03 +0700 Subject: [PATCH 8/9] fix(components): preserve stable control identity --- README.md | 2 +- docs/release-readiness-2026-09-12.md | 8 ++++---- src/components/address/Address.layout.tsx | 2 ++ .../auth-footer-links/AuthFooterLinks.layout.tsx | 2 ++ src/components/auth-powered-by/AuthPoweredBy.layout.tsx | 1 + src/components/breadcrumb/Breadcrumb.layout.tsx | 5 ++++- src/components/color-wheel/ComplexColorWheel.layout.tsx | 2 +- src/components/composer/Composer.layout.tsx | 2 ++ .../connection-settings/ConnectionSettings.layout.tsx | 7 ++++++- src/components/date-picker/DatePicker.layout.tsx | 6 +++++- .../date-range-picker/DateRangePicker.layout.tsx | 6 +++++- src/components/input-otp/InputOTP.layout.tsx | 1 + src/components/menu/Menu.layout.tsx | 1 + src/components/menu/MenuItem.layout.tsx | 1 + src/components/menu/context.ts | 1 + src/components/pagination/Pagination.layout.tsx | 3 +++ src/components/password-field/PasswordField.layout.tsx | 1 + src/components/popover/Popover.layout.tsx | 8 ++++++-- src/components/range-calendar/RangeCalendar.layout.tsx | 1 + src/components/size-picker/SizePicker.layout.tsx | 1 + src/components/switch/Switch.layout.tsx | 2 ++ .../theme-color-picker/ThemeColorPicker.layout.tsx | 8 +++++++- src/components/vocabulary.ts | 6 ++++++ tests/interactive-stable-ids.test.ts | 2 +- 24 files changed, 65 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index caaeac0f..15116c33 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ Highly opinionated SolidJS component library — batteries and kitchen sink included, but optimized and shiny. -**103 components.** HeroUI-parity API, daisyUI-style theming, Tailwind v4 tokens, light and +**101 component families.** HeroUI-parity API, daisyUI-style theming, Tailwind v4 tokens, light and dark themes built in. **[→ Browse every component, live](https://js.software)** diff --git a/docs/release-readiness-2026-09-12.md b/docs/release-readiness-2026-09-12.md index 5bbde8d9..eb6c2dc3 100644 --- a/docs/release-readiness-2026-09-12.md +++ b/docs/release-readiness-2026-09-12.md @@ -61,7 +61,7 @@ Exact packed-candidate consumer runs also pass: | NoFilter | 132/132 | Its separate documentation placeholder was corrected on PR #340; rebuilt output is clean | | Pays | 45/45 | Expanded application-id refusal passes; production build has no phantom UI Iconify warning | | Honey public surface | 13/13 | Production build has no phantom UI Iconify warning | -| JS Software | 555/555 across 13 groups | Exact packed UI candidate; all 101 public families are demonstrated, every declared site outcome passes in one host, Slider and Color Picker keyboard and pointer outcomes pass, and the phantom UI Iconify warning is gone | +| JS Software | 599/599 across 13 groups | Exact packed UI candidate; all 101 public families are demonstrated, all 286 discovered controls have an attributed outcome and a stable unique identity, every declared site outcome passes in one host, Slider and Color Picker keyboard and pointer outcomes pass, and the phantom UI Iconify warning is gone | ## Harness patch @@ -77,8 +77,8 @@ document-height root or `
` as the physical window. The engine branch at `9d131c27` passes formatting, 96/96 DOM tests, 6/6 fragment-navigation tests, and the complete script suite. The harness review tree passes formatting, -80/80 protocol tests with capture enabled, 158/158 ps-qa tests, and the CLI -tests. Its coordinated JS Software run passes 555/555 with every new pointer +80/80 protocol tests with capture enabled, 161/161 ps-qa tests, and the CLI +tests. Its coordinated JS Software run passes 599/599 with every new pointer coordinate inside the renderer-reported viewport. Its required publication order is: @@ -113,7 +113,7 @@ uncovered product workflow works. | [web3.trading #18](https://github.com/pathscale/web3.trading/pull/18) | 103/103 | Public, auth validation, theme/carousel, and guest chat are covered. Authenticated trading is not yet end-to-end proven. | | [pays.online #166](https://github.com/pathscale/pays.online/pull/166) | Typecheck, lint, build, 45/45 against UI #292 | Code review can proceed. Deployment is blocked by an obsolete production Honey UUID, no known production Pays registration, and no matching deployed backend. The frontend now refuses the invalid id locally and explains the problem. | | [honey.id #332](https://github.com/pathscale/honey.id/pull/332) | 196 defined native checks across five roles; deployed dev 193/196; coordinated local app lifecycle 19/19; recovery runner 33/33 | UI is review-ready. Dev's three failures expose the backend's empty regenerated API key. TOTP confirmation and Telegram enrollment/login remain unproved. | -| [js.software #54](https://github.com/pathscale/js.software/pull/54) | Typecheck, lint, build, and 555/555 across 13 native groups against the exact packed UI #292 candidate. The showcase maps all 101 public UI families, drives every demonstrated Slider and Color Picker path with keyboard or viewport-bounded pointer input, and exercises every landing-page and header action. | Review-ready; refresh the UI lock after 3.2.1 publishes. Additional product bugs reported later should receive their own reproductions and outcomes. | +| [js.software #54](https://github.com/pathscale/js.software/pull/54) | Typecheck, lint, build, and 599/599 across 13 native groups against the exact packed UI #292 candidate. The showcase maps all 101 public UI families; the inventory finds 286/286 controls with attributed outcomes and zero missing, unstable, or duplicate IDs. Slider and Color Picker paths use keyboard or viewport-bounded pointer input, and every landing-page and header action is exercised. | Review-ready; refresh the UI lock after 3.2.1 publishes. Additional product bugs reported later should receive their own reproductions and outcomes. | | [nofilter.io #340](https://github.com/pathscale/nofilter.io/pull/340) | Lint, build, 132/132 | Public/auth validation is covered. A real two-participant WebRTC studio session remains unproved. | | [24x.ai #11](https://github.com/pathscale/24x.ai/pull/11) | Lint, build, desktop 141/141, phone 20/20 | Session UI uses a Honey application identity workaround. 24x has a dev registration, but no working callback backend for it. | | [kard.vip #8](https://github.com/pathscale/kard.vip/pull/8) | 223/223 | Demo behavior is covered; this is not real payment evidence. | diff --git a/src/components/address/Address.layout.tsx b/src/components/address/Address.layout.tsx index 92c88ea1..8a4cda3e 100644 --- a/src/components/address/Address.layout.tsx +++ b/src/components/address/Address.layout.tsx @@ -88,6 +88,7 @@ export const AddressLayout: Layout = () => {