v1 shipped click-based state cycles. v2 adds URL-seeded initial state so
the runner can reach any state the component exposes without walking a
click sequence, and runs every instrumented demo TWICE in CI (LTR + RTL)
to catch direction-specific regressions like the slider thumb transform
that landed this week.
## New test-route infrastructure
- `src/routes/test/soma/_perms.svelte.ts` — parses `?perm.X=Y` query
params into a typed `Perms` record (naive coercion: `'true'` → boolean,
digits → number, else string). Exposed via `Perms.getOr({})` context.
Underscore prefix so SvelteKit does not route it.
- `+layout.svelte` — parses perms once at mount, sets context, honours
`?perm.dir=rtl` and `?perm.locale=en` by routing through
`App.setDir` / `App.setLocale` so the existing control strip stays in
sync.
## Demo opt-in
Three demos seed initial state from URL params (v1 + v2 combined):
- `dialog` — `?perm.open=true` lands open.
- `tabs` — `?perm.value=tab-2` lands on that tab.
- `toolbar` — `?perm.format=bold,italic` + `?perm.align=center` preselect
toggles.
## Runner extension
`scripts/permutation-check.ts` now runs each instrumented demo under a
matrix of URL variants (currently `ltr` + `rtl`), validates morfo at the
URL-seeded state BEFORE any clicks (logged as `[initial seed]`), then
runs the v1 click cycle. Output format updated to show per-variant
blocks.
Result: 3 instrumented demos × 2 variants = 22 step validations per CI
run. RTL seeding reproduces the specific state machine the slider
`translate(50%, -50%)` bug needed to fail — had v2 been in place that
day, the regression would have fired before shipping.
## Docs
`src/uix/morfo/PERMUTATION_RUNNER.md` gains the v2 section (layout
plumbing, demo opt-in, URL variant table, `[initial seed]` step
semantics) and marks v1 + v2 as shipped 2026-04-22. v3 (morfo-inferred
per-component axis matrix) and v4 (MutationObserver ordering for Sema)
remain on the roadmap.
## Verification
- `npm run check`: 0 errors in soma/morfo/test-route scope.
- `npm run smoke`: 65/65 routes.
- `npm run morfo:check`: 66/66 morfos.
- `npm run perm:check`: 3/3 demos · 22 steps across 2 variants.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
semantuix
parent
61e4ba6d26
commit
575748f079
@ -0,0 +1,69 @@
|
||||
/**
|
||||
* Test-infra: URL-driven permutations (permutation-runner v2).
|
||||
*
|
||||
* The soma demo pages opt into **seed-from-URL** initial state via
|
||||
* `?perm.X=Y` query params. The test-route layout parses them into a
|
||||
* typed `Perms` context; each demo reads its specific keys with sensible
|
||||
* defaults.
|
||||
*
|
||||
* Why: click-driven permutation sequences (v1) can't reach every state
|
||||
* combination — e.g. "Dialog open AND dir=rtl AND variant=alertdialog"
|
||||
* would need a careful 3-step dance. URL-seed lets the permutation runner
|
||||
* navigate directly to any state the component's morfo can express,
|
||||
* validate morfo, and move on.
|
||||
*
|
||||
* Convention:
|
||||
* /test/soma/dialog?perm.open=true&perm.dir=rtl&perm.variant=alertdialog
|
||||
* /test/soma/tabs?perm.value=tab-2
|
||||
* /test/soma/toolbar?perm.dir=rtl&perm.disabled=true
|
||||
*
|
||||
* Type coercion is naive: `'true'`/`'false'` → boolean, bare digits →
|
||||
* number, everything else stays a string. Demos can cast further when
|
||||
* reading: `(perms?.variant as 'default' | 'alertdialog') ?? 'default'`.
|
||||
*
|
||||
* Prefixed with `_` so SvelteKit does not route it.
|
||||
*/
|
||||
|
||||
import { page } from '$app/state';
|
||||
import { context } from '$soma/provider';
|
||||
|
||||
export type PermValue = string | number | boolean;
|
||||
export type Perms = Record<string, PermValue>;
|
||||
|
||||
const PREFIX = 'perm.';
|
||||
|
||||
function coerce(raw: string): PermValue {
|
||||
if (raw === 'true') return true;
|
||||
if (raw === 'false') return false;
|
||||
if (raw !== '' && !Number.isNaN(Number(raw)) && /^-?\d+(\.\d+)?$/.test(raw)) {
|
||||
return Number(raw);
|
||||
}
|
||||
return raw;
|
||||
}
|
||||
|
||||
/** Parse `?perm.X=Y` params from the current URL. Re-runs on navigation. */
|
||||
export function parsePerms(): Perms {
|
||||
const out: Perms = {};
|
||||
const params = page.url.searchParams;
|
||||
for (const [key, value] of params.entries()) {
|
||||
if (!key.startsWith(PREFIX)) continue;
|
||||
out[key.slice(PREFIX.length)] = coerce(value);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Context for the parsed `Perms` object. Demos read with `Perms.getOr({})`
|
||||
* and fall back to their own defaults:
|
||||
*
|
||||
* ```ts
|
||||
* const perms = Perms.getOr({});
|
||||
* let open = $state((perms.open as boolean) ?? false);
|
||||
* let dir = $state((perms.dir as 'ltr' | 'rtl') ?? 'ltr');
|
||||
* ```
|
||||
*
|
||||
* Use `getOr({})` (not `get()`) so a demo rendered outside the test-route
|
||||
* layout doesn't throw — the empty record falls through to the demo's own
|
||||
* defaults.
|
||||
*/
|
||||
export const Perms = context<Perms>('soma-test-perms');
|
||||
Loading…
Reference in new issue