Reconnect permutation runner routes

active-uix
dev 5 months ago
parent cf7c83a6d3
commit c04b6a241a

@ -232,6 +232,17 @@ Actualizacion smoke 2026-05-16:
- Pendiente deliberado: `scripts/permutation-check.ts` sigue siendo runner
historico del arbol `/test/soma`; no usarlo como release gate hasta
reconectarlo a una route map real del docs shell actual.
- `scripts/permutation-check.ts` reconectado al shell actual:
- usa `/uix/components/{kebab}` por defecto;
- admite `PERM_ROUTE_PREFIX` para otros shells;
- salta morfos sin ruta como `SKIP no route`;
- salta rutas sin `data-perm-step` ni `perm-axes` como
`SKIP no perm instrumentation`;
- si todavia no hay demos instrumentadas, sale limpio con
"No instrumented routed demos found".
- `src/uix/morfo/PERMUTATION_RUNNER.md` actualizado: la antigua infraestructura
`_perms.svelte.ts` queda marcada como retirada; las paginas actuales deben
leer `?perm.*` desde su ruta si optan a permutations.
Actualizacion Eidos visual wrappers 2026-05-16:

@ -11,14 +11,17 @@
* each settle. See `src/uix/morfo/PERMUTATION_RUNNER.md` for the full
* authoring convention.
*
* Current route map: `/uix/components/{kebab}`. Morfos without a routed demo
* or without `data-perm-*` instrumentation are skipped.
*
* Exit codes:
* 0 — every demo with `data-perm-step` passes all permutations.
* 0 — every instrumented routed demo passes all permutations.
* 1 — at least one permutation failed.
* 2 — no dev server / no morfos found.
*/
import { chromium, type Page } from 'playwright';
import { readdirSync } from 'node:fs';
import { existsSync, readdirSync } from 'node:fs';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { dirname, join } from 'node:path';
import type { Morfo, MorfoPart, MorfoData } from '../src/uix/morfo/types';
@ -26,6 +29,29 @@ import { validateMorfo } from '../src/uix/morfo/schema';
const __dirname = dirname(fileURLToPath(import.meta.url));
const MORFOS_DIR = join(__dirname, '..', 'src', 'uix', 'morfo', 'components');
const ROUTES_DIR = join(__dirname, '..', 'web', 'routes');
const DEFAULT_ROUTE_PREFIX = '/uix/components';
const routePrefix = normaliseRoutePrefix(process.env.PERM_ROUTE_PREFIX ?? DEFAULT_ROUTE_PREFIX);
function normaliseRoutePrefix(value: string): string {
const trimmed = value.trim().replace(/\/+$/, '');
if (!trimmed || trimmed === '/') return '';
return trimmed.startsWith('/') ? trimmed : `/${trimmed}`;
}
function routeToPageFile(route: string): string {
const segments = route
.split('/')
.filter(Boolean)
.filter((segment) => !(segment.startsWith('(') && segment.endsWith(')')));
return join(ROUTES_DIR, ...segments, '+page.svelte');
}
function routeForMorfo(morfo: Morfo): string | null {
const route = `${routePrefix}/${morfo.kebab}`.replace(/\/+/g, '/');
return existsSync(routeToPageFile(route)) ? route : null;
}
// ── Shared with morfo-check.ts ──────────────────────────────────────────────
@ -329,7 +355,8 @@ console.error(`Loaded ${morfos.length} morfo${morfos.length === 1 ? '' : 's'}`);
const browser = await chromium.launch();
const ctx = await browser.newContext();
const results: RouteResult[] = [];
const skipped: string[] = [];
const skippedNoRoute: string[] = [];
const skippedNotInstrumented: string[] = [];
/** Run one URL variant: load, cycle click-driven steps, report. */
async function runVariant(
@ -472,11 +499,14 @@ async function runVariant(
*/
async function probeDemo(
base: string,
morfoKebab: string
): Promise<{ axes: string[]; hasSteps: boolean } | 'no-demo'> {
morfo: Morfo
): Promise<{ route: string; axes: string[]; hasSteps: boolean } | 'no-route' | 'no-demo'> {
const route = routeForMorfo(morfo);
if (!route) return 'no-route';
const page = await ctx.newPage();
try {
const res = await page.goto(`${base}/test/soma/${morfoKebab}`, {
const res = await page.goto(`${base}${route}`, {
waitUntil: 'networkidle',
timeout: 20000
});
@ -484,7 +514,7 @@ async function probeDemo(
await page.waitForTimeout(200);
const axes = await readDeclaredAxes(page);
const hasSteps = (await collectSteps(page)).length > 0;
return { axes, hasSteps };
return { route, axes, hasSteps };
} catch {
return 'no-demo';
} finally {
@ -495,9 +525,9 @@ async function probeDemo(
for (const morfo of morfos) {
const variantResults: VariantResult[] = [];
const probe = await probeDemo(BASE, morfo.kebab);
if (probe === 'no-demo') {
skipped.push(morfo.kebab);
const probe = await probeDemo(BASE, morfo);
if (probe === 'no-route' || probe === 'no-demo') {
skippedNoRoute.push(morfo.kebab);
continue;
}
@ -506,21 +536,21 @@ for (const morfo of morfos) {
// validation at each URL variant; step-only demos get the v1 click cycle
// at baseline; declaring both is the full v1+v2+v3 treatment.
if (probe.axes.length === 0 && !probe.hasSteps) {
skipped.push(morfo.kebab);
skippedNotInstrumented.push(morfo.kebab);
continue;
}
const variants = buildVariants(probe.axes);
for (const v of variants) {
const url = `/test/soma/${morfo.kebab}${v.suffix}`;
const url = `${probe.route}${v.suffix}`;
const r = await runVariant(BASE, url, v.label, morfo);
if (r === 'no-steps') continue;
variantResults.push(r);
}
if (variantResults.length === 0) {
skipped.push(morfo.kebab);
skippedNotInstrumented.push(morfo.kebab);
continue;
}
@ -570,20 +600,27 @@ for (const r of results) {
}
console.log('');
if (skipped.length > 0) {
if (skippedNoRoute.length > 0) {
console.log(
`SKIPPED no route (${skippedNoRoute.length}): ${skippedNoRoute.slice(0, 10).join(', ')}${skippedNoRoute.length > 10 ? ', …' : ''}`
);
}
if (skippedNotInstrumented.length > 0) {
console.log(
`SKIPPED (${skipped.length}): ${skipped.slice(0, 10).join(', ')}${skipped.length > 10 ? ', …' : ''}`
`SKIPPED no perm instrumentation (${skippedNotInstrumented.length}): ${skippedNotInstrumented.slice(0, 10).join(', ')}${skippedNotInstrumented.length > 10 ? ', …' : ''}`
);
console.log(' (demo has no `data-perm-step` annotations yet — see src/uix/morfo/PERMUTATION_RUNNER.md)');
console.log(' (demo has no `data-perm-step` / `perm-axes` opt-in yet — see src/uix/morfo/PERMUTATION_RUNNER.md)');
}
console.log('');
const totalVariants = results.reduce((acc, r) => acc + r.variants.length, 0);
const summary =
failedRoutes === 0
? `All ${passedRoutes} instrumented demo${passedRoutes === 1 ? '' : 's'} passed. (${totalSteps} step${totalSteps === 1 ? '' : 's'} across ${totalVariants} URL variant${totalVariants === 1 ? '' : 's'}.)`
: `${failedRoutes} demo${failedRoutes === 1 ? '' : 's'} failed (${totalFailed}/${totalSteps} steps): ${results
.filter((r) => r.variants.some((v) => v.steps.some((s) => s.kind !== 'pass')))
.map((r) => r.morfo)
.join(', ')}`;
results.length === 0
? `No instrumented routed demos found under ${routePrefix}.`
: failedRoutes === 0
? `All ${passedRoutes} instrumented demo${passedRoutes === 1 ? '' : 's'} passed. (${totalSteps} step${totalSteps === 1 ? '' : 's'} across ${totalVariants} URL variant${totalVariants === 1 ? '' : 's'}.)`
: `${failedRoutes} demo${failedRoutes === 1 ? '' : 's'} failed (${totalFailed}/${totalSteps} steps): ${results
.filter((r) => r.variants.some((v) => v.steps.some((s) => s.kind !== 'pass')))
.map((r) => r.morfo)
.join(', ')}`;
console.log(summary);
process.exit(failedRoutes === 0 ? 0 : 1);

@ -123,13 +123,21 @@ FAIL toolbar 3 permutations, 1 failed
npm run perm:check
```
The current docs-shell route map is `/uix/components/{kebab}`. Components
without a routed page are reported as `SKIP no route`; routed pages without
`data-perm-step` or `perm-axes` opt-in are reported as
`SKIP no perm instrumentation`. Override the prefix with
`PERM_ROUTE_PREFIX=/some/path` when testing another shell.
Exit codes:
- `0` — every demo with `data-perm-step` passes all permutations.
- `0` — every instrumented routed demo passes all permutations. If no routed
demo is instrumented yet, the runner exits cleanly and says so.
- `1` — at least one permutation failed.
- `2` — no dev server / no morfos found.
Demos without `data-perm-step` are skipped with a `SKIP` line (they haven't opted in yet).
Demos without `data-perm-step` / `perm-axes` are skipped with a `SKIP` line
(they haven't opted in yet).
---
@ -139,9 +147,12 @@ v1 covers "click through a state sequence and validate after each step." v2 adds
### Layout plumbing
`web/routes/+layout.svelte` parses `?perm.*` params once at mount, exposes them via a context at `web/routes/_perms.svelte.ts`, and also honours `?perm.dir=ltr|rtl` and `?perm.locale=es|en` by routing through the existing `App.setDir` / `App.setLocale` path (so the control strip stays in sync).
> **Note (2026-05-08):** routes were relocated from `src/routes/test/soma/` to `web/routes/` during the active-uix cleanup. The `_perms.svelte.ts` context wiring referenced above predates that move and may need re-implementation when permutation testing is re-enabled on the new demo tree.
The old `/test/soma` shell used a shared `_perms.svelte.ts` context. That
shell was retired; the current runner only provides the URL variants and
expects each routed page under `web/routes` to read `?perm.*` params directly
or through a new shell-level helper when we decide to add one. Until a page
does that and declares `perm-axes` / `data-perm-step`, it is skipped rather
than counted as coverage.
### Demo opt-in
@ -149,14 +160,13 @@ Read the perms and use them as **initial** state (not live-bound — the user ca
```svelte
<script lang="ts">
import { Perms } from '../_perms.svelte';
const perms = Perms.getOr({} as Record<string, string | number | boolean>);
import { page } from '$app/state';
const params = page.url.searchParams;
// Perm-runner v2 seed — `?perm.open=true` / `?perm.variant=alertdialog`
// land directly in that state without walking the click sequence.
let open = $state((perms.open as boolean) ?? false);
let variant = $state((perms.variant as 'default' | 'alertdialog') ?? 'default');
let open = $state(params.get('perm.open') === 'true');
let variant = $state((params.get('perm.variant') as 'default' | 'alertdialog') ?? 'default');
</script>
```
@ -197,13 +207,13 @@ Demos opt in by listing the axes they honour. The runner skips meta-less demos a
```svelte
<script lang="ts">
import { Perms } from '../_perms.svelte';
const perms = Perms.getOr({} as Record<string, string | number | boolean>);
import { page } from '$app/state';
const params = page.url.searchParams;
let orientation = $state<'horizontal' | 'vertical'>(
(perms.orientation as 'horizontal' | 'vertical') ?? 'horizontal'
(params.get('perm.orientation') as 'horizontal' | 'vertical') ?? 'horizontal'
);
let disabled = $state((perms.disabled as boolean) ?? false);
let disabled = $state(params.get('perm.disabled') === 'true');
</script>
```

Loading…
Cancel
Save

Powered by TurnKey Linux.