|
|
|
|
@ -2,7 +2,7 @@
|
|
|
|
|
* Morfo DOM-validation check.
|
|
|
|
|
*
|
|
|
|
|
* For each morfo at `src/uix/morfo/components/*.ts`:
|
|
|
|
|
* 1. Navigate to the matching demo route at `/test/soma/{kebab}`.
|
|
|
|
|
* 1. Navigate to the matching routed demo at `/uix/components/{kebab}`.
|
|
|
|
|
* 2. For every `public` part declared in the morfo, query the DOM for
|
|
|
|
|
* `[data-{component}-{part}]` (or `[data-{component}]` for root).
|
|
|
|
|
* 3. For each matching element, verify:
|
|
|
|
|
@ -13,15 +13,16 @@
|
|
|
|
|
* the morfo (strict mode — excludes `data-_*` privates).
|
|
|
|
|
*
|
|
|
|
|
* Runs after `npm run smoke` passes. Requires `npm run dev` running.
|
|
|
|
|
* Morfos without a current routed demo are reported as SKIP.
|
|
|
|
|
*
|
|
|
|
|
* Exit codes:
|
|
|
|
|
* 0 — all morfos validate against their rendered DOM.
|
|
|
|
|
* 0 — all routed morfos validate against their rendered DOM.
|
|
|
|
|
* 1 — at least one morfo-vs-DOM discrepancy.
|
|
|
|
|
* 2 — no dev server / couldn't load morfos.
|
|
|
|
|
* 2 — no dev server / couldn't load morfos / no routed morfos.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
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';
|
|
|
|
|
@ -29,6 +30,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.MORFO_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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function probePort(start: number, end: number): Promise<string | null> {
|
|
|
|
|
for (let port = start; port <= end; port++) {
|
|
|
|
|
@ -186,9 +210,18 @@ console.error(`Loaded ${morfos.length} morfo${morfos.length === 1 ? '' : 's'}`);
|
|
|
|
|
const browser = await chromium.launch();
|
|
|
|
|
const ctx = await browser.newContext();
|
|
|
|
|
const failures: Array<{ morfo: string; issues: Issue[] }> = [];
|
|
|
|
|
const skipped: string[] = [];
|
|
|
|
|
let checked = 0;
|
|
|
|
|
|
|
|
|
|
for (const morfo of morfos) {
|
|
|
|
|
const route = `/test/soma/${morfo.kebab}`;
|
|
|
|
|
const route = routeForMorfo(morfo);
|
|
|
|
|
if (!route) {
|
|
|
|
|
skipped.push(morfo.kebab);
|
|
|
|
|
console.log(`SKIP ${morfo.kebab.padEnd(24)} no routed demo under ${routePrefix}`);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
checked += 1;
|
|
|
|
|
const page = await ctx.newPage();
|
|
|
|
|
try {
|
|
|
|
|
await page.goto(BASE + route, { waitUntil: 'networkidle', timeout: 20000 });
|
|
|
|
|
@ -215,13 +248,24 @@ for (const morfo of morfos) {
|
|
|
|
|
await browser.close();
|
|
|
|
|
|
|
|
|
|
console.log('');
|
|
|
|
|
if (failures.length === 0) {
|
|
|
|
|
console.log(`All ${morfos.length} morfo${morfos.length === 1 ? '' : 's'} validate against their demo DOM.`);
|
|
|
|
|
if (checked === 0) {
|
|
|
|
|
console.log(`No routed morfos found under ${routePrefix}.`);
|
|
|
|
|
process.exit(2);
|
|
|
|
|
} else if (failures.length === 0) {
|
|
|
|
|
console.log(
|
|
|
|
|
`All ${checked} routed morfo${checked === 1 ? '' : 's'} validate against their demo DOM.`
|
|
|
|
|
);
|
|
|
|
|
if (skipped.length > 0) {
|
|
|
|
|
console.log(`${skipped.length}/${morfos.length} morfos skipped without routed demo.`);
|
|
|
|
|
}
|
|
|
|
|
process.exit(0);
|
|
|
|
|
} else {
|
|
|
|
|
const totalIssues = failures.reduce((sum, f) => sum + f.issues.length, 0);
|
|
|
|
|
console.log(
|
|
|
|
|
`${failures.length}/${morfos.length} morfos failed (${totalIssues} total issues): ${failures.map((f) => f.morfo).join(', ')}`
|
|
|
|
|
`${failures.length}/${checked} routed morfos failed (${totalIssues} total issues): ${failures.map((f) => f.morfo).join(', ')}`
|
|
|
|
|
);
|
|
|
|
|
if (skipped.length > 0) {
|
|
|
|
|
console.log(`${skipped.length}/${morfos.length} morfos skipped without routed demo.`);
|
|
|
|
|
}
|
|
|
|
|
process.exit(1);
|
|
|
|
|
}
|
|
|
|
|
|