@motion-proto/live-tokens 0.77.0 → 0.79.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/skills/live-tokens-check-compliance/SKILL.md +60 -29
- package/.claude/skills/live-tokens-create-component/SKILL.md +23 -28
- package/.claude/skills/live-tokens-create-component/references/contract-tests.md +61 -0
- package/.claude/skills/live-tokens-create-component/references/sketch-mode.md +2 -2
- package/.claude/skills/live-tokens-create-component/references/token-naming.md +6 -15
- package/.claude/skills/live-tokens-create-page/SKILL.md +9 -6
- package/.claude/skills/live-tokens-create-theme/references/design-directions.md +1 -1
- package/.claude/skills/live-tokens-pick-component/SKILL.md +3 -3
- package/.claude/skills/live-tokens-set-colors/references/color-anchors.md +1 -1
- package/.claude/skills/live-tokens-set-geometry/SKILL.md +1 -1
- package/.claude/skills/live-tokens-set-geometry/references/geometry-anchors.md +1 -1
- package/CHANGELOG.md +284 -0
- package/README.md +9 -15
- package/bin/check-component.mjs +152 -551
- package/bin/check-page.mjs +56 -420
- package/bin/cli.mjs +83 -15
- package/bin/contractRunner.mjs +380 -120
- package/bin/lib/catalogue.mjs +161 -34
- package/bin/lib/componentSource.mjs +152 -0
- package/bin/lib/cssValues.mjs +9 -0
- package/bin/lib/dataDir.mjs +126 -0
- package/bin/lib/findings.mjs +103 -12
- package/bin/lib/fixers.mjs +64 -0
- package/bin/lib/geometry.mjs +92 -0
- package/bin/lib/pageRoutes.mjs +309 -0
- package/bin/lib/pageSource.mjs +230 -0
- package/bin/lib/report.mjs +57 -59
- package/bin/lib/tokenVocabulary.mjs +104 -34
- package/bin/rules/componentStructure.mjs +344 -0
- package/bin/rules/componentUse.mjs +313 -0
- package/bin/rules/importsAndRoutes.mjs +136 -0
- package/bin/rules/testRuns.mjs +122 -0
- package/bin/rules/tokens.mjs +363 -0
- package/bin/setup-claude.mjs +1 -2
- package/dist-plugin/{chunk-PDNL4NC5.js → chunk-D4WRIKEZ.js} +7 -2
- package/dist-plugin/{chunk-SWXRVZKT.js → chunk-REBHE3ZM.js} +414 -1
- package/dist-plugin/index.cjs +432 -15
- package/dist-plugin/index.js +8 -9
- package/dist-plugin/migrateData/index.cjs +422 -4
- package/dist-plugin/migrateData/index.js +2 -2
- package/dist-plugin/setColors/index.cjs +414 -1
- package/dist-plugin/setColors/index.d.cts +1 -1
- package/dist-plugin/setColors/index.d.ts +1 -1
- package/dist-plugin/setColors/index.js +1 -1
- package/dist-plugin/setGeometry/index.cjs +423 -13
- package/dist-plugin/setGeometry/index.d.cts +3 -3
- package/dist-plugin/setGeometry/index.d.ts +3 -3
- package/dist-plugin/setGeometry/index.js +10 -13
- package/dist-plugin/setType/index.d.cts +1 -1
- package/dist-plugin/setType/index.d.ts +1 -1
- package/dist-plugin/{themeTypes-BxRtuN5V.d.cts → themeTypes-B8_Idrp4.d.cts} +2 -2
- package/dist-plugin/{themeTypes-BxRtuN5V.d.ts → themeTypes-B8_Idrp4.d.ts} +2 -2
- package/package.json +6 -4
- package/src/editor/component-editor/CalloutEditor.svelte +2 -2
- package/src/editor/component-editor/CollapsibleSectionEditor.svelte +14 -15
- package/src/editor/component-editor/CornerBadgeEditor.svelte +14 -14
- package/src/editor/component-editor/DialogEditor.svelte +5 -5
- package/src/editor/component-editor/InlineEditActionsEditor.svelte +2 -2
- package/src/editor/component-editor/RadioButtonEditor.svelte +21 -21
- package/src/editor/component-editor/SectionDividerEditor.svelte +7 -7
- package/src/editor/component-editor/SegmentedControlEditor.svelte +5 -5
- package/src/editor/component-editor/SideNavigationEditor.svelte +25 -25
- package/src/editor/component-editor/TabBarEditor.svelte +6 -6
- package/src/editor/component-editor/TableEditor.svelte +6 -6
- package/src/editor/component-editor/ToggleEditor.svelte +2 -2
- package/src/editor/component-editor/index.ts +3 -0
- package/src/editor/component-editor/registry.ts +57 -1
- package/src/editor/component-editor/scaffolding/TokenLayout.svelte +14 -14
- package/src/editor/component-editor/scaffolding/VariantGroup.svelte +3 -0
- package/src/editor/component-editor/scaffolding/types.ts +15 -0
- package/src/editor/core/components/adjustAliases.ts +4 -4
- package/src/editor/core/components/aliasKinds.ts +20 -19
- package/src/editor/core/sketch/sketchLayer.ts +3 -3
- package/src/editor/core/themes/migrateComponentConfig.ts +14 -6
- package/src/editor/core/themes/migrations/2026-09-13-badge-brand.ts +42 -0
- package/src/editor/core/themes/migrations/2026-09-13-collapsiblesection-open.ts +33 -0
- package/src/editor/core/themes/migrations/2026-09-13-cornerbadge-prefix.ts +70 -0
- package/src/editor/core/themes/migrations/2026-09-13-hairline.ts +92 -0
- package/src/editor/core/themes/migrations/2026-09-13-indicator.ts +54 -0
- package/src/editor/core/themes/migrations/2026-09-13-sectiondivider-surface.ts +36 -0
- package/src/editor/core/themes/migrations/2026-09-13-selected-state.ts +124 -0
- package/src/editor/core/themes/migrations/2026-09-13-toggle-label.ts +31 -0
- package/src/editor/core/themes/migrations/index.ts +16 -0
- package/src/editor/core/themes/themeService.ts +3 -3
- package/src/editor/core/themes/themeTypes.ts +13 -15
- package/src/editor/docs/Docs.svelte +1 -1
- package/src/editor/docs/content/light-and-dark.md +3 -3
- package/src/editor/docs/content.generated.ts +1 -1
- package/src/editor/index.ts +1 -0
- package/src/editor/overlay/ColumnsOverlay.svelte +1 -1
- package/src/editor/overlay/LiveEditorOverlay.svelte +16 -0
- package/src/editor/overlay/LiveTokensRouter.svelte +1 -0
- package/src/editor/skill-atlas/SkillAtlas.svelte +4 -4
- package/src/editor/skill-atlas/TreeNodeCard.svelte +4 -4
- package/src/editor/skill-atlas/skillSources.generated.ts +11 -14
- package/src/editor/skill-atlas/skillTrees.ts +1 -3
- package/src/editor/skill-atlas/trees/check-compliance.ts +258 -93
- package/src/editor/skill-atlas/trees/create-component.ts +73 -62
- package/src/editor/skill-atlas/trees/create-page.ts +54 -33
- package/src/editor/skill-atlas/trees/pick-component.ts +3 -3
- package/src/editor/skill-atlas/trees/set-geometry.ts +1 -1
- package/src/editor/ui/UIPaletteSelector.svelte +11 -15
- package/src/editor/ui/variantScales.ts +8 -8
- package/src/live-tokens/data/themes/autumn.json +188 -188
- package/src/live-tokens/data/themes/halloween.json +188 -188
- package/src/live-tokens/data/themes/midnight-study.json +245 -245
- package/src/live-tokens/data/themes/ocean.json +188 -188
- package/src/live-tokens/data/themes/royal-velvet.json +188 -188
- package/src/live-tokens/data/themes/sketchy.json +188 -188
- package/src/live-tokens/data/themes/spring-meadow.json +188 -188
- package/src/live-tokens/data/themes/sunset.json +188 -188
- package/src/system/components/Badge.svelte +27 -23
- package/src/system/components/Button.svelte +13 -7
- package/src/system/components/Callout.svelte +16 -11
- package/src/system/components/Card.svelte +22 -15
- package/src/system/components/CodeSnippet.svelte +10 -6
- package/src/system/components/CollapsibleSection.svelte +68 -63
- package/src/system/components/CornerBadge.svelte +78 -72
- package/src/system/components/Dialog.svelte +21 -18
- package/src/system/components/IconButton.svelte +13 -9
- package/src/system/components/Image.svelte +14 -8
- package/src/system/components/ImageLightbox.svelte +10 -6
- package/src/system/components/InlineEditActions.svelte +17 -14
- package/src/system/components/Input.svelte +13 -7
- package/src/system/components/MenuSelect.svelte +13 -7
- package/src/system/components/Notification.svelte +17 -11
- package/src/system/components/Panel.svelte +13 -6
- package/src/system/components/ProgressBar.svelte +10 -5
- package/src/system/components/RadioButton.svelte +33 -30
- package/src/system/components/SectionDivider.svelte +28 -21
- package/src/system/components/SegmentedControl.svelte +27 -23
- package/src/system/components/SideNavigation.svelte +175 -171
- package/src/system/components/Slider.svelte +11 -7
- package/src/system/components/TabBar.svelte +53 -49
- package/src/system/components/Table.svelte +20 -15
- package/src/system/components/Toggle.svelte +14 -10
- package/src/system/components/Tooltip.svelte +10 -6
- package/src/system/styles/CONVENTIONS.md +3 -4
- package/src/testing-js/chunk-3UKGXCDL.js +48 -0
- package/src/testing-js/chunk-3UKGXCDL.js.map +1 -0
- package/src/testing-js/{chunk-ZMSX6CXR.js → chunk-NB3NZRBM.js} +13 -2
- package/src/testing-js/chunk-NB3NZRBM.js.map +1 -0
- package/src/testing-js/{chunk-FAFOAWYL.js → chunk-Q3YIAAG3.js} +17 -3
- package/src/testing-js/chunk-Q3YIAAG3.js.map +1 -0
- package/src/testing-js/{chunk-7TI7Z6Y6.js → chunk-U7OJE5DU.js} +19 -19
- package/src/testing-js/chunk-U7OJE5DU.js.map +1 -0
- package/src/testing-js/{chunk-L73N4NSO.js → chunk-WIZ6W7UT.js} +2 -2
- package/src/testing-js/{chunk-4JQX6WWL.js → chunk-ZMZQZ33J.js} +474 -166
- package/src/testing-js/chunk-ZMZQZ33J.js.map +1 -0
- package/src/testing-js/component-alias.contract.js +2 -2
- package/src/testing-js/component-behavior.contract.js +155 -0
- package/src/testing-js/component-behavior.contract.js.map +1 -0
- package/src/testing-js/component-editor.contract.js +7 -5
- package/src/testing-js/component-editor.contract.js.map +1 -1
- package/src/testing-js/component-render.contract.js +15 -11
- package/src/testing-js/component-render.contract.js.map +1 -1
- package/src/testing-js/index.d.ts +50 -4
- package/src/testing-js/index.js +32 -8
- package/src/testing-js/index.js.map +1 -1
- package/src/testing-js/page-compliance.contract.js +867 -0
- package/src/testing-js/page-compliance.contract.js.map +1 -0
- package/src/testing-js/registry.contract.js +5 -3
- package/src/testing-js/registry.contract.js.map +1 -1
- package/src/testing-js/{vitest-BE6uGF31.d.ts → vitest-BMLIbDs2.d.ts} +29 -3
- package/src/testing-js/vitest.d.ts +1 -1
- package/src/testing-js/vitest.js +3 -2
- package/template/README.md +16 -9
- package/template/package.json +2 -2
- package/template/src/pages/Home.svelte +13 -0
- package/.claude/skills/live-tokens-create-component/SKILL copy.md +0 -196
- package/.claude/skills/live-tokens-fix-findings/SKILL.md +0 -98
- package/src/editor/skill-atlas/trees/fix-findings.ts +0 -469
- package/src/testing-js/chunk-4JQX6WWL.js.map +0 -1
- package/src/testing-js/chunk-7TI7Z6Y6.js.map +0 -1
- package/src/testing-js/chunk-FAFOAWYL.js.map +0 -1
- package/src/testing-js/chunk-ZMSX6CXR.js.map +0 -1
- /package/src/testing-js/{chunk-L73N4NSO.js.map → chunk-WIZ6W7UT.js.map} +0 -0
package/bin/contractRunner.mjs
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
// component contract suites under Playwright for one component or every
|
|
3
3
|
// authored one, and maps their results onto findings by rule.
|
|
4
4
|
//
|
|
5
|
+
// `check-page --tests` (`runPageTests`, below `runContractTests`) shares this
|
|
6
|
+
// file's tool detection, data isolation, generated Playwright config, spawn
|
|
7
|
+
// deadline, report reading, and infrastructure classification, running the
|
|
8
|
+
// suite's `page` project instead of `contract` and skipping the registry
|
|
9
|
+
// (Vitest) half pages have no equivalent of.
|
|
10
|
+
//
|
|
5
11
|
// Spawns the two tools as child processes rather than importing their APIs, so
|
|
6
12
|
// this module needs neither `@playwright/test` nor `vitest` at its own module
|
|
7
13
|
// top — it is loaded lazily, only when `--tests` is passed (see cli.mjs).
|
|
@@ -22,13 +28,31 @@ import { tmpdir } from 'node:os';
|
|
|
22
28
|
import { basename, dirname, join, relative, resolve } from 'node:path';
|
|
23
29
|
import { fileURLToPath } from 'node:url';
|
|
24
30
|
import { discoverComponents, resolveComponentPaths } from './check-component.mjs';
|
|
31
|
+
import { resolveSourceDataDir, scrapeSettingsField, settingsFilePath } from './lib/dataDir.mjs';
|
|
25
32
|
import { lineOf } from './lib/findings.mjs';
|
|
33
|
+
import { settingsPageViewports } from './lib/pageRoutes.mjs';
|
|
26
34
|
|
|
27
35
|
const PKG_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
28
36
|
|
|
29
37
|
const TEST_DATA_DIR_ENV = 'LIVE_TOKENS_TEST_DATA_DIR';
|
|
30
38
|
const DATA_DIR_ENV = 'LIVE_TOKENS_DATA_DIR';
|
|
31
39
|
const COMPONENT_ENV = 'LIVE_TOKENS_COMPONENT';
|
|
40
|
+
// Mirrors src/testing/config.ts's own PAGES_ENV: the page targets a run opens,
|
|
41
|
+
// as JSON. Duplicated rather than imported for the same reason as
|
|
42
|
+
// SESSION_FILES below.
|
|
43
|
+
const PAGES_ENV = 'LIVE_TOKENS_PAGES';
|
|
44
|
+
|
|
45
|
+
/** Milliseconds a spawned tool gets before this file sends it SIGINT, then
|
|
46
|
+
* SIGKILL five seconds later. A hung dev server or worker can no longer hold
|
|
47
|
+
* a caller open past this. */
|
|
48
|
+
const DEFAULT_TESTS_TIMEOUT_MS = 15 * 60_000;
|
|
49
|
+
const TESTS_TIMEOUT_ENV = 'LIVE_TOKENS_TESTS_TIMEOUT';
|
|
50
|
+
|
|
51
|
+
function testsTimeoutMs() {
|
|
52
|
+
const raw = process.env[TESTS_TIMEOUT_ENV];
|
|
53
|
+
const ms = raw ? Number(raw) : NaN;
|
|
54
|
+
return Number.isFinite(ms) && ms > 0 ? ms : DEFAULT_TESTS_TIMEOUT_MS;
|
|
55
|
+
}
|
|
32
56
|
|
|
33
57
|
// Mirrors src/testing/isolation.ts's own list. Duplicated rather than shared:
|
|
34
58
|
// that module compiles into src/testing-js, which does not exist until
|
|
@@ -51,6 +75,7 @@ const HARD_FAILURE_RULES = new Set(['tests-not-installed', 'tests-setup', 'tests
|
|
|
51
75
|
* coverage against. */
|
|
52
76
|
const ALL_CONTRACT_RULES = [
|
|
53
77
|
'contract-registry',
|
|
78
|
+
'contract-behavior',
|
|
54
79
|
'contract-listed',
|
|
55
80
|
'contract-alias',
|
|
56
81
|
'contract-states',
|
|
@@ -160,100 +185,10 @@ function resolveTestingEntry(name) {
|
|
|
160
185
|
);
|
|
161
186
|
}
|
|
162
187
|
|
|
163
|
-
function settingsFilePath(root) {
|
|
164
|
-
for (const name of ['live-tokens.testing.ts', 'live-tokens.testing.mts', 'live-tokens.testing.js', 'live-tokens.testing.mjs']) {
|
|
165
|
-
const path = join(root, name);
|
|
166
|
-
if (existsSync(path)) return path;
|
|
167
|
-
}
|
|
168
|
-
return null;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* A generated config's own extensionless relative imports (Vite/Playwright's
|
|
173
|
-
* loaders resolve those, matching how this repo's own `vite.config.ts` and
|
|
174
|
-
* `live-tokens.testing.ts` are written) only get that treatment for a
|
|
175
|
-
* *static* import. A dynamic `import()` of the same path, called after a Vite
|
|
176
|
-
* config finishes loading, runs through plain Node resolution instead and
|
|
177
|
-
* fails on the same files: verified against `vitest.contract.config.ts` with
|
|
178
|
-
* both a `file://` URL and a plain absolute path as the dynamic specifier,
|
|
179
|
-
* identical `ERR_MODULE_NOT_FOUND` both times. So neither `settings.viteConfig`
|
|
180
|
-
* nor `settings.dataDir` (the resolved values `resolveTestingConfig` computes)
|
|
181
|
-
* has a reader here on purpose — reading either would mean importing the
|
|
182
|
-
* settings file dynamically first, which reintroduces exactly this failure.
|
|
183
|
-
* Regexing the settings file's *source text* for a field, imprecise as that
|
|
184
|
-
* is, is what stays inside the static-import constraint: a wrong guess still
|
|
185
|
-
* fails loudly (a bad `viteConfig` throws on its own static import in the
|
|
186
|
-
* generated file; a bad `dataDir` throws "no data directory at ..." below),
|
|
187
|
-
* never as a silent pass. A settings-level `dataDir` this cannot see at all
|
|
188
|
-
* is worse than one resolved this imprecisely, since a project that names its
|
|
189
|
-
* data directory only in `live-tokens.testing.ts` would otherwise have its
|
|
190
|
-
* contracts checked against whatever happens to sit at the default path.
|
|
191
|
-
*/
|
|
192
|
-
function stripComments(text) {
|
|
193
|
-
// Block comments first: a `//` inside one (`/* // note */`) must not seed a
|
|
194
|
-
// second, overlapping strip.
|
|
195
|
-
return text.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/.*$/gm, '');
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
/** Measured across eleven settings-file shapes: a commented-out `dataDir:`
|
|
199
|
-
* above the real one won the regex, because the regex only sees text
|
|
200
|
-
* position, never comment syntax, and a comment naming the field with
|
|
201
|
-
* nothing else present invented a setting out of prose. Comments are
|
|
202
|
-
* stripped before the field ever gets a chance to match. A field present in
|
|
203
|
-
* live code as a template literal, a computed value, or an import throws
|
|
204
|
-
* instead of falling through to a default silently: the two guesses this
|
|
205
|
-
* feeds, `viteConfig` and `dataDir`, can each resolve to a real path that
|
|
206
|
-
* simply names the wrong tree, which then reads as a clean run. */
|
|
207
|
-
function scrapeSettingsField(settingsPath, fieldName) {
|
|
208
|
-
if (!settingsPath) return null;
|
|
209
|
-
let text;
|
|
210
|
-
try {
|
|
211
|
-
text = readFileSync(settingsPath, 'utf8');
|
|
212
|
-
} catch {
|
|
213
|
-
return null;
|
|
214
|
-
}
|
|
215
|
-
const live = stripComments(text);
|
|
216
|
-
const literal = new RegExp(`\\b${fieldName}\\s*:\\s*['"]([^'"]+)['"]`).exec(live);
|
|
217
|
-
if (literal) return literal[1];
|
|
218
|
-
if (new RegExp(`\\b${fieldName}\\s*:`).test(live)) {
|
|
219
|
-
throw new Error(
|
|
220
|
-
`"${fieldName}" in ${settingsPath} is set to something other than a plain string literal, ` +
|
|
221
|
-
`so --tests cannot read it statically. Use a literal string, or remove the key to fall back to the default.`,
|
|
222
|
-
);
|
|
223
|
-
}
|
|
224
|
-
return null;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
188
|
function guessViteConfigPath(root, settingsPath) {
|
|
228
189
|
return resolve(root, scrapeSettingsField(settingsPath, 'viteConfig') ?? 'vite.config.ts');
|
|
229
190
|
}
|
|
230
191
|
|
|
231
|
-
/**
|
|
232
|
-
* `dataDir` as the plugin resolves it: a `dataDir` field scraped from the
|
|
233
|
-
* settings file (see above), else `live-tokens.config.json`'s own key, else
|
|
234
|
-
* the default. Mirrors `resolveTestingConfig`'s own `configuredDataDir`
|
|
235
|
-
* fallback (`src/testing/config.ts`) rather than importing it: that module
|
|
236
|
-
* compiles into `src/testing-js`, which does not exist until `build:testing`
|
|
237
|
-
* runs, and importing the *source* `.ts` module to reach it hits the same
|
|
238
|
-
* extensionless-import failure documented above — measured with
|
|
239
|
-
* `src/testing-js` moved aside, `resolveTestingEntry`'s `.ts` fallback threw
|
|
240
|
-
* exactly that trying to load `vitest.ts`. Duplicating this small a resolver
|
|
241
|
-
* has precedent in this file already, at `SESSION_FILES`.
|
|
242
|
-
*/
|
|
243
|
-
export function resolveSourceDataDir(root, settingsPath) {
|
|
244
|
-
const scraped = scrapeSettingsField(settingsPath, 'dataDir');
|
|
245
|
-
if (scraped) return resolve(root, scraped);
|
|
246
|
-
try {
|
|
247
|
-
const parsed = JSON.parse(readFileSync(join(root, 'live-tokens.config.json'), 'utf8'));
|
|
248
|
-
if (parsed && typeof parsed === 'object' && typeof parsed.dataDir === 'string') {
|
|
249
|
-
return resolve(root, parsed.dataDir);
|
|
250
|
-
}
|
|
251
|
-
} catch {
|
|
252
|
-
// Missing or unparseable reads as absent, matching the plugin's own resolver.
|
|
253
|
-
}
|
|
254
|
-
return resolve(root, 'src/live-tokens/data');
|
|
255
|
-
}
|
|
256
|
-
|
|
257
192
|
// ─── data isolation ─────────────────────────────────────────────────────────
|
|
258
193
|
|
|
259
194
|
function copyIsolatedDataDir(sourceDataDir) {
|
|
@@ -332,27 +267,41 @@ function withCleanup(paths) {
|
|
|
332
267
|
|
|
333
268
|
// ─── generated tool configs ─────────────────────────────────────────────────
|
|
334
269
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
270
|
+
// `configDir` sits under the OS temp directory, which has no ancestor
|
|
271
|
+
// `package.json`. Without one naming `"type": "module"` here, Node treats
|
|
272
|
+
// these configs as CommonJS by default, and `createPlaywrightConfig`'s
|
|
273
|
+
// `import.meta.url` throws "Cannot use 'import.meta' outside a module".
|
|
274
|
+
export function writeConfigPackageJson(configDir) {
|
|
340
275
|
writeFileSync(join(configDir, 'package.json'), JSON.stringify({ type: 'module' }));
|
|
276
|
+
}
|
|
341
277
|
|
|
278
|
+
/** A source fragment for the generated files below, not an actual settings
|
|
279
|
+
* object: this module never imports the consumer's settings file itself (see
|
|
280
|
+
* the static-import note above), so it only ever sees this as text.
|
|
281
|
+
* `settingsModule` is already the file's default export (a default import
|
|
282
|
+
* unwraps it), not a module namespace object — it has no `.default` of its
|
|
283
|
+
* own. */
|
|
284
|
+
function settingsFragments(root) {
|
|
342
285
|
const settingsPath = settingsFilePath(root);
|
|
343
|
-
const testingIndex = resolveTestingEntry('index');
|
|
344
|
-
const testingVitest = resolveTestingEntry('vitest');
|
|
345
|
-
const viteConfigPath = guessViteConfigPath(root, settingsPath);
|
|
346
|
-
|
|
347
286
|
const settingsImport = settingsPath ? `import settingsModule from ${JSON.stringify(settingsPath)};\n` : '';
|
|
348
|
-
// A source fragment for the generated files below, not an actual settings
|
|
349
|
-
// object: this module never imports the consumer's settings file itself
|
|
350
|
-
// (see the static-import note above), so it only ever sees this as text.
|
|
351
|
-
// `settingsModule` is already the file's default export (a default import
|
|
352
|
-
// unwraps it), not a module namespace object — it has no `.default` of its
|
|
353
|
-
// own.
|
|
354
287
|
const settingsExpr = settingsPath ? 'settingsModule' : '{}';
|
|
288
|
+
return { settingsPath, settingsImport, settingsExpr };
|
|
289
|
+
}
|
|
355
290
|
|
|
291
|
+
/**
|
|
292
|
+
* The `page` project alone, config `globalTimeout` set to `timeoutMs`: a
|
|
293
|
+
* `check-page --tests` run must never also carry the whole `contract` project
|
|
294
|
+
* (every component's editor cycle, against no `LIVE_TOKENS_COMPONENT`), and a
|
|
295
|
+
* page suite that hangs reports `timedOut` tests in a real JSON report before
|
|
296
|
+
* this file's own SIGINT/SIGKILL watchdog (`runCli`) ever has to act.
|
|
297
|
+
* `globalTimeout` is `createPlaywrightConfig`'s own option (`src/testing/
|
|
298
|
+
* playwright.ts`), passed through rather than patched onto the resolved
|
|
299
|
+
* config afterward, so the bound holds for every caller of that factory, not
|
|
300
|
+
* only the CLI's generated configs.
|
|
301
|
+
*/
|
|
302
|
+
export function writePlaywrightConfig({ configDir, root, timeoutMs = DEFAULT_TESTS_TIMEOUT_MS } = {}) {
|
|
303
|
+
const { settingsImport, settingsExpr } = settingsFragments(root);
|
|
304
|
+
const testingIndex = resolveTestingEntry('index');
|
|
356
305
|
const playwrightConfigPath = join(configDir, 'playwright.config.ts');
|
|
357
306
|
writeFileSync(
|
|
358
307
|
playwrightConfigPath,
|
|
@@ -361,9 +310,20 @@ export function writeGeneratedConfigs({ configDir, root }) {
|
|
|
361
310
|
export default createPlaywrightConfig({
|
|
362
311
|
...${settingsExpr},
|
|
363
312
|
root: ${JSON.stringify(root)},
|
|
313
|
+
globalTimeout: ${JSON.stringify(timeoutMs)},
|
|
364
314
|
});
|
|
365
315
|
`,
|
|
366
316
|
);
|
|
317
|
+
return playwrightConfigPath;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
export function writeGeneratedConfigs({ configDir, root, timeoutMs = DEFAULT_TESTS_TIMEOUT_MS } = {}) {
|
|
321
|
+
writeConfigPackageJson(configDir);
|
|
322
|
+
const playwrightConfigPath = writePlaywrightConfig({ configDir, root, timeoutMs });
|
|
323
|
+
|
|
324
|
+
const { settingsPath, settingsImport, settingsExpr } = settingsFragments(root);
|
|
325
|
+
const testingVitest = resolveTestingEntry('vitest');
|
|
326
|
+
const viteConfigPath = guessViteConfigPath(root, settingsPath);
|
|
367
327
|
|
|
368
328
|
const vitestConfigPath = join(configDir, 'vitest.config.ts');
|
|
369
329
|
writeFileSync(
|
|
@@ -375,6 +335,7 @@ const settings = resolveTestingConfig(${settingsExpr}, ${JSON.stringify(root)});
|
|
|
375
335
|
|
|
376
336
|
export default createVitestConfig(viteConfigModule.default ?? viteConfigModule, {
|
|
377
337
|
registrySetup: settings.registrySetup,
|
|
338
|
+
contractsModule: settings.contractsModule,
|
|
378
339
|
});
|
|
379
340
|
`,
|
|
380
341
|
);
|
|
@@ -384,17 +345,30 @@ export default createVitestConfig(viteConfigModule.default ?? viteConfigModule,
|
|
|
384
345
|
|
|
385
346
|
// ─── subprocess execution ───────────────────────────────────────────────────
|
|
386
347
|
|
|
387
|
-
|
|
348
|
+
/** Spawns `command`, and at `timeoutMs` sends the child SIGINT (Playwright's
|
|
349
|
+
* own graceful-stop signal, per `withCleanup`'s own note above), then
|
|
350
|
+
* SIGKILL five seconds later if it has not exited. `result.timedOut` tells
|
|
351
|
+
* the caller the run never finished on its own, whatever the child's own
|
|
352
|
+
* exit code or report ends up saying. */
|
|
353
|
+
function runCli(command, args, { cwd, env, timeoutMs = DEFAULT_TESTS_TIMEOUT_MS } = {}) {
|
|
388
354
|
return new Promise((resolveRun) => {
|
|
389
355
|
const child = spawn(command, args, { cwd, env, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
390
356
|
activeChild = child;
|
|
391
357
|
let stdout = '';
|
|
392
358
|
let stderr = '';
|
|
359
|
+
let timedOut = false;
|
|
393
360
|
child.stdout.on('data', (chunk) => (stdout += chunk));
|
|
394
361
|
child.stderr.on('data', (chunk) => (stderr += chunk));
|
|
362
|
+
const deadline = setTimeout(() => {
|
|
363
|
+
timedOut = true;
|
|
364
|
+
const killer = setTimeout(() => child.kill('SIGKILL'), 5_000);
|
|
365
|
+
child.once('exit', () => clearTimeout(killer));
|
|
366
|
+
child.kill('SIGINT');
|
|
367
|
+
}, timeoutMs);
|
|
395
368
|
const finish = (result) => {
|
|
369
|
+
clearTimeout(deadline);
|
|
396
370
|
if (activeChild === child) activeChild = null;
|
|
397
|
-
resolveRun(result);
|
|
371
|
+
resolveRun({ ...result, timedOut });
|
|
398
372
|
};
|
|
399
373
|
child.on('close', (code) => finish({ code, stdout, stderr }));
|
|
400
374
|
child.on('error', (error) => finish({ code: -1, stdout, stderr: `${stderr}\n${error.message}` }));
|
|
@@ -410,6 +384,17 @@ function setupFinding(tool, code, stdout, stderr) {
|
|
|
410
384
|
};
|
|
411
385
|
}
|
|
412
386
|
|
|
387
|
+
/** `tests-incomplete`, not `tests-setup`: the tool itself never got the
|
|
388
|
+
* chance to explain the failure, this file's own watchdog cut it off. */
|
|
389
|
+
export function timeoutFinding(tool, timeoutMs) {
|
|
390
|
+
return {
|
|
391
|
+
rule: 'tests-incomplete',
|
|
392
|
+
file: 'package.json',
|
|
393
|
+
line: 1,
|
|
394
|
+
message: `${tool} did not finish within ${Math.round(timeoutMs / 60_000)} minute(s) and was interrupted. Set LIVE_TOKENS_TESTS_TIMEOUT (milliseconds) to change the bound.`,
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
|
|
413
398
|
/** Reads a JSON report or explains, as a `tests-setup` finding, why there
|
|
414
399
|
* isn't one. Split from the process-spawning around it so a malformed or
|
|
415
400
|
* absent report is testable without a subprocess. */
|
|
@@ -422,25 +407,29 @@ export function readReportOrSetupFinding(tool, reportPath, code, stdout, stderr)
|
|
|
422
407
|
}
|
|
423
408
|
}
|
|
424
409
|
|
|
425
|
-
export async function runPlaywrightSuite({ root, configDir, playwrightConfigPath }) {
|
|
410
|
+
export async function runPlaywrightSuite({ root, configDir, playwrightConfigPath, project, timeoutMs = DEFAULT_TESTS_TIMEOUT_MS }) {
|
|
426
411
|
const reportPath = join(configDir, 'playwright-report.json');
|
|
427
412
|
const bin = peerBin(root, '@playwright/test', 'cli.js');
|
|
428
|
-
const
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
413
|
+
const args = [bin, 'test', '-c', playwrightConfigPath, '--reporter=json'];
|
|
414
|
+
if (project) args.push('--project', project);
|
|
415
|
+
const { code, stdout, stderr, timedOut } = await runCli(process.execPath, args, {
|
|
416
|
+
cwd: root,
|
|
417
|
+
env: { ...process.env, PLAYWRIGHT_JSON_OUTPUT_FILE: reportPath },
|
|
418
|
+
timeoutMs,
|
|
419
|
+
});
|
|
420
|
+
if (timedOut) return { setupFinding: timeoutFinding('Playwright', timeoutMs) };
|
|
433
421
|
return readReportOrSetupFinding('Playwright', reportPath, code, stdout, stderr);
|
|
434
422
|
}
|
|
435
423
|
|
|
436
|
-
export async function runRegistrySuite({ root, configDir, vitestConfigPath }) {
|
|
424
|
+
export async function runRegistrySuite({ root, configDir, vitestConfigPath, timeoutMs = DEFAULT_TESTS_TIMEOUT_MS }) {
|
|
437
425
|
const reportPath = join(configDir, 'vitest-report.json');
|
|
438
426
|
const bin = peerBin(root, 'vitest', 'vitest.mjs');
|
|
439
|
-
const { code, stdout, stderr } = await runCli(
|
|
427
|
+
const { code, stdout, stderr, timedOut } = await runCli(
|
|
440
428
|
process.execPath,
|
|
441
429
|
[bin, 'run', '--config', vitestConfigPath, '--reporter=json', '--outputFile', reportPath],
|
|
442
|
-
{ cwd: root, env: process.env },
|
|
430
|
+
{ cwd: root, env: process.env, timeoutMs },
|
|
443
431
|
);
|
|
432
|
+
if (timedOut) return { setupFinding: timeoutFinding('Vitest', timeoutMs) };
|
|
444
433
|
const outcome = readReportOrSetupFinding('Vitest', reportPath, code, stdout, stderr);
|
|
445
434
|
// Kept alongside a successfully-parsed report too: a file that failed to
|
|
446
435
|
// collect a single test still carries a report, and `mapVitestResults`
|
|
@@ -712,6 +701,136 @@ export function mapPlaywrightResults(report, { root, sourceDataDir, knownIds })
|
|
|
712
701
|
return { findings, coverage };
|
|
713
702
|
}
|
|
714
703
|
|
|
704
|
+
// ─── result mapping: page rules ─────────────────────────────────────────────
|
|
705
|
+
|
|
706
|
+
/** The plan's Runtime rule ids, fixed rather than read off a page's own
|
|
707
|
+
* results: `reconcileCoverage`'s expected set, same reasoning as
|
|
708
|
+
* `ALL_CONTRACT_RULES` above. */
|
|
709
|
+
const PAGE_RUNTIME_RULES = ['page-component-paint', 'page-text-style', 'page-contrast', 'page-grid', 'page-overflow'];
|
|
710
|
+
|
|
711
|
+
/** `page-compliance.contract.ts` titles each test `${rule} | ${source} |
|
|
712
|
+
* ${width}x${height}`, so the rule, the page, and the viewport are read off
|
|
713
|
+
* the title rather than off describe-block position (there is none — every
|
|
714
|
+
* page test is a sibling at the suite's top level). */
|
|
715
|
+
const PAGE_TEST_TITLE_RE = /^(page-[a-z-]+) \| (.+) \| (\d+x\d+)$/;
|
|
716
|
+
|
|
717
|
+
/** A `PageViolation`'s own `[rule] source:line: message`, read back out of
|
|
718
|
+
* Playwright's `Error.prototype.toString()` (`${name}: ${message}`), the
|
|
719
|
+
* same convention `VIOLATION_RE` above reads `ContractViolation` through. */
|
|
720
|
+
const PAGE_VIOLATION_RE = /^PageViolation:\s*\[([a-z-]+)\]\s+(.+):(\d+):\s*([\s\S]*)$/;
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* `PageViolation` to a finding at the page file and line; a timeout or an
|
|
724
|
+
* interruption to `tests-incomplete`; a `test.skip(...)` call — the mechanism
|
|
725
|
+
* `PageHarness.assert*` uses to report a rule inapplicable — to `inapplicable`
|
|
726
|
+
* coverage carrying the skip's own description, never a finding. Structurally
|
|
727
|
+
* the Playwright half of `mapPlaywrightResults` above, with no component,
|
|
728
|
+
* describe-block, or `ContractViolation` concept to lean on: every triple's
|
|
729
|
+
* identity comes off the test's own title.
|
|
730
|
+
*/
|
|
731
|
+
export function mapPageResults(report) {
|
|
732
|
+
const zeroCollected = (report.suites ?? []).length === 0;
|
|
733
|
+
if (zeroCollected || (report.errors ?? []).length > 0) {
|
|
734
|
+
const detail = (report.errors ?? []).map((e) => e.message).join('\n') || 'the run collected no tests';
|
|
735
|
+
return {
|
|
736
|
+
findings: [{ rule: 'tests-incomplete', file: 'package.json', line: 1, message: `Playwright collected nothing to check: ${detail}` }],
|
|
737
|
+
coverage: {},
|
|
738
|
+
explained: true,
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
const tests = readPlaywrightTests(report);
|
|
743
|
+
|
|
744
|
+
for (const t of tests) {
|
|
745
|
+
if (t.status !== 'unexpected') continue;
|
|
746
|
+
const infra = classifyInfrastructureError(t.lastResult?.errors?.[0]?.message);
|
|
747
|
+
if (infra?.rule === 'tests-not-installed') {
|
|
748
|
+
return { findings: [{ rule: infra.rule, file: 'package.json', line: 1, message: infra.message }], coverage: {}, explained: true };
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
const findings = [];
|
|
753
|
+
const coverage = {};
|
|
754
|
+
// The outer key carries the page and the viewport, the inner key the bare
|
|
755
|
+
// rule id — never the reverse: `applyCoverageSeverity` and `--off` (`bin/
|
|
756
|
+
// lib/findings.mjs`) resolve a coverage entry's severity by looking up its
|
|
757
|
+
// inner key straight in `PAGE_RULES`, so a composite `rule@viewport` inner
|
|
758
|
+
// key would never match a plain rule id and `--off`/`checks.rules` would
|
|
759
|
+
// silently stop reaching page coverage.
|
|
760
|
+
const markCoverage = (pageAtViewport, rule, status, reason) => {
|
|
761
|
+
if (!pageAtViewport || !rule) return;
|
|
762
|
+
coverage[pageAtViewport] ??= {};
|
|
763
|
+
const existing = coverage[pageAtViewport][rule];
|
|
764
|
+
if (!existing || COVERAGE_PRIORITY[status] >= COVERAGE_PRIORITY[existing.status]) {
|
|
765
|
+
coverage[pageAtViewport][rule] = reason ? { status, reason } : { status };
|
|
766
|
+
}
|
|
767
|
+
};
|
|
768
|
+
|
|
769
|
+
for (const t of tests) {
|
|
770
|
+
const [, titleRule, titleSource, titleViewport] = PAGE_TEST_TITLE_RE.exec(t.specTitle) ?? [];
|
|
771
|
+
const pageAtViewport = titleSource && titleViewport ? `${titleSource}@${titleViewport}` : null;
|
|
772
|
+
|
|
773
|
+
if (t.status === 'expected' || t.status === 'flaky') {
|
|
774
|
+
markCoverage(pageAtViewport, titleRule, t.status === 'flaky' ? 'flaky' : 'passed');
|
|
775
|
+
continue;
|
|
776
|
+
}
|
|
777
|
+
if (t.status === 'skipped') {
|
|
778
|
+
// Playwright's own `skip` annotation, from `test.skip(condition,
|
|
779
|
+
// description)` inside the test body — decision 10's "inapplicable is a
|
|
780
|
+
// status with a reason", never a silent pass.
|
|
781
|
+
const reason = t.annotations.find((a) => a.type === 'skip')?.description || 'inapplicable';
|
|
782
|
+
markCoverage(pageAtViewport, titleRule, 'inapplicable', reason);
|
|
783
|
+
continue;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
const last = t.lastResult;
|
|
787
|
+
if (last?.status === 'timedOut' || last?.status === 'interrupted') {
|
|
788
|
+
findings.push({
|
|
789
|
+
rule: 'tests-incomplete',
|
|
790
|
+
file: 'package.json',
|
|
791
|
+
line: 1,
|
|
792
|
+
message: `"${t.specTitle}" did not finish (${last.status})`,
|
|
793
|
+
context: { suite: 'playwright', title: t.specTitle, suiteFile: t.specFile, suiteLine: t.specLine },
|
|
794
|
+
});
|
|
795
|
+
markCoverage(pageAtViewport, titleRule, 'failed');
|
|
796
|
+
continue;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
const rawErrors = last?.errors?.length ? last.errors : [{ message: `${t.status}: ${t.specTitle}` }];
|
|
800
|
+
for (const error of rawErrors) {
|
|
801
|
+
const infra = classifyInfrastructureError(error.message);
|
|
802
|
+
if (infra) {
|
|
803
|
+
findings.push({
|
|
804
|
+
rule: infra.rule,
|
|
805
|
+
file: 'package.json',
|
|
806
|
+
line: 1,
|
|
807
|
+
message: infra.message,
|
|
808
|
+
context: { suite: 'playwright', title: t.specTitle },
|
|
809
|
+
});
|
|
810
|
+
markCoverage(pageAtViewport, titleRule, 'failed');
|
|
811
|
+
continue;
|
|
812
|
+
}
|
|
813
|
+
const block = messageBlock(error.message);
|
|
814
|
+
const violation = PAGE_VIOLATION_RE.exec(block);
|
|
815
|
+
findings.push({
|
|
816
|
+
rule: violation?.[1] ?? titleRule ?? 'tests-setup',
|
|
817
|
+
file: violation?.[2] ?? titleSource ?? 'package.json',
|
|
818
|
+
line: violation ? Number(violation[3]) : 1,
|
|
819
|
+
message: violation?.[4] ?? block,
|
|
820
|
+
context: {
|
|
821
|
+
suite: 'playwright',
|
|
822
|
+
title: t.specTitle,
|
|
823
|
+
suiteFile: t.specFile,
|
|
824
|
+
suiteLine: t.specLine,
|
|
825
|
+
attachments: (last?.attachments ?? []).map((a) => a.path).filter(Boolean),
|
|
826
|
+
},
|
|
827
|
+
});
|
|
828
|
+
markCoverage(pageAtViewport, titleRule, 'failed');
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
return { findings, coverage };
|
|
832
|
+
}
|
|
833
|
+
|
|
715
834
|
// ─── result mapping: Vitest (registry contract) ─────────────────────────────
|
|
716
835
|
|
|
717
836
|
/** `checkRegistryEntry`'s violation strings, read back out of Vitest's
|
|
@@ -739,6 +858,12 @@ export function mapRegistryViolation(root, sourceDataDir, componentId, text) {
|
|
|
739
858
|
return { file: relative(root, target), line: findTokenLine(target, token) };
|
|
740
859
|
}
|
|
741
860
|
|
|
861
|
+
/** `component-behavior.contract.ts` shares the same `describe(id) > it(case)`
|
|
862
|
+
* shape `registry.contract.ts` uses (see `structuralRule`'s Playwright
|
|
863
|
+
* analogue), so the file name — not the describe titles — is what tells the
|
|
864
|
+
* two suites' assertions apart. */
|
|
865
|
+
const BEHAVIOR_FILE_PREFIX = 'component-behavior.contract';
|
|
866
|
+
|
|
742
867
|
/** Mirrors the Playwright side's zero-collection check: a file that failed
|
|
743
868
|
* before it ran a single assertion (`assertionResults` empty, `status`
|
|
744
869
|
* 'failed') carries its own `message` — measured against a bad
|
|
@@ -773,16 +898,43 @@ export function mapVitestResults(report, { root, sourceDataDir, stderr } = {}) {
|
|
|
773
898
|
const coverage = {};
|
|
774
899
|
for (const file of files) {
|
|
775
900
|
if (collectionFailures.includes(file)) continue;
|
|
901
|
+
const isBehaviorFile = basename(file.name ?? '').startsWith(BEHAVIOR_FILE_PREFIX);
|
|
902
|
+
const passRule = isBehaviorFile ? 'contract-behavior' : 'contract-registry';
|
|
776
903
|
for (const assertion of file.assertionResults ?? []) {
|
|
777
904
|
const componentId = assertion.ancestorTitles.length >= 2 ? assertion.ancestorTitles[1] : null;
|
|
778
905
|
if (assertion.status !== 'failed') {
|
|
779
906
|
if (assertion.status === 'passed' && componentId) {
|
|
780
907
|
coverage[componentId] ??= {};
|
|
781
|
-
coverage[componentId][
|
|
908
|
+
coverage[componentId][passRule] = { status: 'passed' };
|
|
782
909
|
}
|
|
783
910
|
continue;
|
|
784
911
|
}
|
|
785
912
|
const text = (assertion.failureMessages ?? []).join('\n');
|
|
913
|
+
// `component-behavior.contract.ts` throws `ContractViolation` directly
|
|
914
|
+
// rather than a registry-style violation array, and Vitest's own stack
|
|
915
|
+
// frames follow the message on their own line with no blank line
|
|
916
|
+
// between (measured; unlike Playwright's ANSI code frame), so only the
|
|
917
|
+
// first line is the message `messageBlock`'s blank-line split expects.
|
|
918
|
+
if (isBehaviorFile) {
|
|
919
|
+
const violation = VIOLATION_RE.exec(text.split('\n')[0].trim());
|
|
920
|
+
const finalRule = violation?.[1] ?? 'contract-behavior';
|
|
921
|
+
const id = violation?.[2] ?? componentId;
|
|
922
|
+
const message = violation?.[3] ?? text.split('\n')[0].trim();
|
|
923
|
+
const token = extractToken(message);
|
|
924
|
+
const { file: f, line } = artifactForContractRule(root, sourceDataDir, finalRule, id, token);
|
|
925
|
+
findings.push({
|
|
926
|
+
rule: finalRule,
|
|
927
|
+
file: f,
|
|
928
|
+
line,
|
|
929
|
+
message: id ? `${id}: ${message}` : message,
|
|
930
|
+
context: { suite: 'vitest', title: assertion.fullName },
|
|
931
|
+
});
|
|
932
|
+
if (id) {
|
|
933
|
+
coverage[id] ??= {};
|
|
934
|
+
coverage[id][finalRule] = { status: 'failed' };
|
|
935
|
+
}
|
|
936
|
+
continue;
|
|
937
|
+
}
|
|
786
938
|
const violations = componentId ? extractViolationArray(text) : [];
|
|
787
939
|
if (violations.length > 0) {
|
|
788
940
|
for (const violation of violations) {
|
|
@@ -898,7 +1050,8 @@ export async function runContractTests(id, { root = process.cwd(), dataDir: expl
|
|
|
898
1050
|
const cleanup = withCleanup([dataDir, configDir]);
|
|
899
1051
|
|
|
900
1052
|
try {
|
|
901
|
-
const
|
|
1053
|
+
const timeoutMs = testsTimeoutMs();
|
|
1054
|
+
const { playwrightConfigPath, vitestConfigPath } = writeGeneratedConfigs({ configDir, root, timeoutMs });
|
|
902
1055
|
process.env[TEST_DATA_DIR_ENV] = dataDir;
|
|
903
1056
|
process.env[DATA_DIR_ENV] = dataDir;
|
|
904
1057
|
if (id) process.env[COMPONENT_ENV] = id;
|
|
@@ -910,8 +1063,8 @@ export async function runContractTests(id, { root = process.cwd(), dataDir: expl
|
|
|
910
1063
|
// Sequential: both share the one isolated data directory, and the
|
|
911
1064
|
// registry run's own Vite instance and the Playwright run's dev server
|
|
912
1065
|
// would otherwise regenerate the same derived files concurrently.
|
|
913
|
-
const registryOutcome = await runRegistrySuite({ root, configDir, vitestConfigPath });
|
|
914
|
-
const playwrightOutcome = await runPlaywrightSuite({ root, configDir, playwrightConfigPath });
|
|
1066
|
+
const registryOutcome = await runRegistrySuite({ root, configDir, vitestConfigPath, timeoutMs });
|
|
1067
|
+
const playwrightOutcome = await runPlaywrightSuite({ root, configDir, playwrightConfigPath, project: 'contract', timeoutMs });
|
|
915
1068
|
|
|
916
1069
|
const registryMapped = registryOutcome.setupFinding
|
|
917
1070
|
? { findings: [registryOutcome.setupFinding], coverage: {}, explained: true }
|
|
@@ -945,6 +1098,113 @@ export async function runContractTests(id, { root = process.cwd(), dataDir: expl
|
|
|
945
1098
|
}
|
|
946
1099
|
}
|
|
947
1100
|
|
|
1101
|
+
/** One expected id per (page, viewport) pair — `mapPageResults`'s own outer
|
|
1102
|
+
* coverage key. Reads the viewport list off the project's own settings
|
|
1103
|
+
* (`settingsPageViewports`, decision 4's "the settings file may replace the
|
|
1104
|
+
* list") rather than a shipped default, so a page checked at a replaced
|
|
1105
|
+
* viewport reconciles against the sizes the generated Playwright config
|
|
1106
|
+
* actually resolved for that same run, not a stale expectation. */
|
|
1107
|
+
export function pageExpectedIds(routed, root) {
|
|
1108
|
+
const viewports = settingsPageViewports(root);
|
|
1109
|
+
return routed.flatMap((t) => viewports.map((v) => `${t.source}@${v.width}x${v.height}`));
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
/**
|
|
1113
|
+
* `check-page --tests`'s own entry point: the Playwright half of
|
|
1114
|
+
* `runContractTests` above, minus the registry (Vitest) suite pages have no
|
|
1115
|
+
* equivalent of (decision 11) and minus the `contract` project (`project:
|
|
1116
|
+
* 'page'` below, so a page run never opens every component's editor cycle).
|
|
1117
|
+
*
|
|
1118
|
+
* `targets` is `resolvePageTargets`'s own return shape (`bin/lib/
|
|
1119
|
+
* pageRoutes.mjs`): a page with no route carries `route: null` and a
|
|
1120
|
+
* `reason`, reported as its own `tests-setup` finding rather than passed to
|
|
1121
|
+
* the suite, which cannot open a page it has no URL for (decision 3).
|
|
1122
|
+
*/
|
|
1123
|
+
export async function runPageTests(targets, { root = process.cwd(), dataDir: explicitDataDir } = {}) {
|
|
1124
|
+
const toolFindings = missingToolFindings(root);
|
|
1125
|
+
if (toolFindings.length > 0) return { findings: toolFindings, coverage: {} };
|
|
1126
|
+
|
|
1127
|
+
if (targets.length === 0) {
|
|
1128
|
+
return {
|
|
1129
|
+
findings: [
|
|
1130
|
+
{
|
|
1131
|
+
rule: 'tests-setup',
|
|
1132
|
+
file: 'package.json',
|
|
1133
|
+
line: 1,
|
|
1134
|
+
message: 'no page renders through a route yet; nothing for --tests to run',
|
|
1135
|
+
},
|
|
1136
|
+
],
|
|
1137
|
+
coverage: {},
|
|
1138
|
+
};
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
const unrouted = targets.filter((t) => !t.route);
|
|
1142
|
+
const routed = targets.filter((t) => t.route);
|
|
1143
|
+
const unroutedFindings = unrouted.map((t) => ({
|
|
1144
|
+
rule: 'tests-setup',
|
|
1145
|
+
file: t.source,
|
|
1146
|
+
line: 1,
|
|
1147
|
+
message: t.reason ?? 'no route renders this page',
|
|
1148
|
+
}));
|
|
1149
|
+
|
|
1150
|
+
if (routed.length === 0) {
|
|
1151
|
+
return { findings: unroutedFindings, coverage: {} };
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
let sourceDataDir;
|
|
1155
|
+
let dataDir;
|
|
1156
|
+
let configDir;
|
|
1157
|
+
try {
|
|
1158
|
+
sourceDataDir = explicitDataDir ?? resolveSourceDataDir(root, settingsFilePath(root));
|
|
1159
|
+
dataDir = copyIsolatedDataDir(sourceDataDir);
|
|
1160
|
+
configDir = mkdtempSync(join(tmpdir(), 'live-tokens-check-cfg-'));
|
|
1161
|
+
} catch (error) {
|
|
1162
|
+
if (dataDir) rmSync(dataDir, { recursive: true, force: true });
|
|
1163
|
+
if (configDir) rmSync(configDir, { recursive: true, force: true });
|
|
1164
|
+
return {
|
|
1165
|
+
findings: [...unroutedFindings, { rule: 'tests-setup', file: 'package.json', line: 1, message: `could not prepare an isolated run: ${error.message}` }],
|
|
1166
|
+
coverage: {},
|
|
1167
|
+
};
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
const cleanup = withCleanup([dataDir, configDir]);
|
|
1171
|
+
|
|
1172
|
+
try {
|
|
1173
|
+
const timeoutMs = testsTimeoutMs();
|
|
1174
|
+
writeConfigPackageJson(configDir);
|
|
1175
|
+
const playwrightConfigPath = writePlaywrightConfig({ configDir, root, timeoutMs });
|
|
1176
|
+
process.env[TEST_DATA_DIR_ENV] = dataDir;
|
|
1177
|
+
process.env[DATA_DIR_ENV] = dataDir;
|
|
1178
|
+
process.env[PAGES_ENV] = JSON.stringify(routed);
|
|
1179
|
+
delete process.env[COMPONENT_ENV];
|
|
1180
|
+
|
|
1181
|
+
const playwrightOutcome = await runPlaywrightSuite({ root, configDir, playwrightConfigPath, project: 'page', timeoutMs });
|
|
1182
|
+
const mapped = playwrightOutcome.setupFinding
|
|
1183
|
+
? { findings: [playwrightOutcome.setupFinding], coverage: {}, explained: true }
|
|
1184
|
+
: mapPageResults(playwrightOutcome.report);
|
|
1185
|
+
|
|
1186
|
+
const explainedGlobally =
|
|
1187
|
+
mapped.explained || mapped.findings.some((f) => f.rule === 'tests-setup' || f.rule === 'tests-not-installed');
|
|
1188
|
+
const reconciled = reconcileCoverage(mapped.coverage, pageExpectedIds(routed, root), {
|
|
1189
|
+
expectedRules: PAGE_RUNTIME_RULES,
|
|
1190
|
+
explainedGlobally,
|
|
1191
|
+
});
|
|
1192
|
+
|
|
1193
|
+
return {
|
|
1194
|
+
findings: [...unroutedFindings, ...mapped.findings, ...reconciled.findings],
|
|
1195
|
+
coverage: reconciled.coverage,
|
|
1196
|
+
};
|
|
1197
|
+
} catch (error) {
|
|
1198
|
+
return {
|
|
1199
|
+
findings: [...unroutedFindings, { rule: 'tests-setup', file: 'package.json', line: 1, message: `check-page --tests crashed: ${error.message}` }],
|
|
1200
|
+
coverage: {},
|
|
1201
|
+
};
|
|
1202
|
+
} finally {
|
|
1203
|
+
cleanup();
|
|
1204
|
+
delete process.env[PAGES_ENV];
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
|
|
948
1208
|
export function hasHardFailure(findings) {
|
|
949
1209
|
return findings.some((f) => HARD_FAILURE_RULES.has(f.rule));
|
|
950
1210
|
}
|