@motion-proto/live-tokens 0.97.0 → 0.98.2
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 +5 -0
- package/.claude/skills/live-tokens-create-component/SKILL.md +4 -3
- package/.claude/skills/live-tokens-create-component/references/contract-tests.md +71 -11
- package/CHANGELOG.md +272 -0
- package/README.md +2 -2
- package/bin/check-component.mjs +8 -0
- package/bin/check-page.mjs +6 -0
- package/bin/cli.mjs +36 -6
- package/bin/contractRunner.mjs +142 -17
- package/bin/lib/findings.mjs +20 -4
- package/bin/rules/a11y.mjs +53 -0
- package/dist-plugin/{chunk-A4TBP4MM.js → chunk-SLICH4IH.js} +2 -2
- package/dist-plugin/index.cjs +148 -3
- package/dist-plugin/index.js +250 -105
- package/dist-plugin/setColors/index.js +1 -1
- package/package.json +47 -2
- package/src/demo/LabeledSelect.svelte +1 -1
- package/src/editor/bootstrap.ts +3 -1
- package/src/editor/component-editor/BadgeEditor.svelte +13 -0
- package/src/editor/component-editor/ButtonEditor.svelte +33 -2
- package/src/editor/component-editor/CalloutEditor.svelte +13 -0
- package/src/editor/component-editor/CardEditor.svelte +12 -2
- package/src/editor/component-editor/CodeSnippetEditor.svelte +11 -1
- package/src/editor/component-editor/CollapsibleSectionEditor.svelte +18 -1
- package/src/editor/component-editor/CornerBadgeEditor.svelte +13 -0
- package/src/editor/component-editor/DialogEditor.svelte +15 -0
- package/src/editor/component-editor/IconButtonEditor.svelte +29 -1
- package/src/editor/component-editor/ImageEditor.svelte +20 -9
- package/src/editor/component-editor/ImageLightboxEditor.svelte +44 -0
- package/src/editor/component-editor/InlineEditActionsEditor.svelte +13 -1
- package/src/editor/component-editor/InputEditor.svelte +23 -0
- package/src/editor/component-editor/MenuSelectEditor.svelte +27 -1
- package/src/editor/component-editor/NotificationEditor.svelte +15 -0
- package/src/editor/component-editor/PanelEditor.svelte +1 -1
- package/src/editor/component-editor/ProgressBarEditor.svelte +13 -0
- package/src/editor/component-editor/RadioButtonEditor.svelte +20 -1
- package/src/editor/component-editor/SectionDividerEditor.svelte +41 -13
- package/src/editor/component-editor/SegmentedControlEditor.svelte +29 -1
- package/src/editor/component-editor/SideNavigationEditor.svelte +40 -1
- package/src/editor/component-editor/SlidePagerEditor.svelte +20 -0
- package/src/editor/component-editor/SliderEditor.svelte +33 -0
- package/src/editor/component-editor/TabBarEditor.svelte +23 -2
- package/src/editor/component-editor/TableEditor.svelte +14 -0
- package/src/editor/component-editor/ToggleEditor.svelte +17 -0
- package/src/editor/component-editor/TooltipEditor.svelte +11 -2
- package/src/editor/component-editor/VideoFrameEditor.svelte +8 -0
- package/src/editor/component-editor/VideoLightboxEditor.svelte +10 -0
- package/src/editor/component-editor/registration.ts +2 -1
- package/src/editor/component-editor/registry.ts +69 -27
- package/src/editor/component-editor/scaffolding/DemoHeader.svelte +2 -0
- package/src/editor/component-editor/scaffolding/StateBlock.svelte +19 -8
- package/src/editor/component-editor/scaffolding/TokenLayout.svelte +3 -0
- package/src/editor/component-editor/scaffolding/VariantGroup.svelte +70 -3
- package/src/editor/component-editor/scaffolding/editorContext.ts +7 -0
- package/src/editor/core/a11y/a11yReport.ts +101 -0
- package/src/editor/core/a11y/a11yReportTypes.ts +69 -0
- package/src/editor/core/a11y/contrastFailures.ts +22 -0
- package/src/editor/core/components/contrastPairRegistry.ts +18 -0
- package/src/editor/core/palettes/contrast.ts +44 -0
- package/src/editor/core/palettes/contrastPairs.ts +79 -0
- package/src/editor/core/palettes/resolveColor.ts +77 -0
- package/src/editor/core/store/editorStore.ts +4 -0
- package/src/editor/core/store/editorViewStore.ts +2 -2
- package/src/editor/core/store/unsavedEdits.ts +10 -0
- package/src/editor/core/themes/liveStateStream.ts +7 -1
- package/src/editor/core/themes/parsers/gradient.ts +5 -2
- package/src/editor/docs/chapters.ts +1 -0
- package/src/editor/docs/content/accessibility.md +362 -0
- package/src/editor/docs/content/editing-tokens.md +28 -1
- package/src/editor/docs/content.generated.ts +2 -1
- package/src/editor/pages/ComponentEditorPage.svelte +10 -1
- package/src/editor/pages/EditorShell.svelte +96 -10
- package/src/editor/skill-atlas/skillSources.generated.ts +3 -3
- package/src/editor/skill-atlas/trees/check-compliance.ts +3 -3
- package/src/editor/skill-atlas/trees/create-component.ts +12 -12
- package/src/editor/ui/EditorViewSwitcher.svelte +15 -2
- package/src/editor/ui/GradientEditor.svelte +9 -1
- package/src/editor/ui/ThemePanel.svelte +4 -4
- package/src/editor/ui/UIContrastReadout.svelte +186 -0
- package/src/editor/ui/UIInfoPopover.svelte +16 -2
- package/src/editor/ui/UIPaletteSelector.svelte +9 -0
- package/src/editor/ui/UITokenSelector.svelte +26 -2
- package/src/editor/ui/a11y/A11yComponentIssues.svelte +103 -0
- package/src/editor/ui/a11y/A11yFindingRow.svelte +57 -0
- package/src/editor/ui/a11y/A11yIssueRow.svelte +121 -0
- package/src/editor/ui/a11y/A11yOverview.svelte +242 -0
- package/src/editor/ui/a11y/A11yPanel.svelte +128 -0
- package/src/editor/ui/a11y/A11yPropertyMark.svelte +84 -0
- package/src/editor/ui/a11y/A11yRailMark.svelte +34 -0
- package/src/editor/ui/a11y/AccessibilityTab.svelte +334 -0
- package/src/editor/ui/a11y/a11yView.ts +218 -0
- package/src/editor/ui/contrastReadout.ts +45 -0
- package/src/live-tokens/data/themes/autumn.json +20 -20
- package/src/live-tokens/data/themes/halloween.json +20 -20
- package/src/live-tokens/data/themes/midnight-study.json +20 -20
- package/src/live-tokens/data/themes/ocean.json +20 -20
- package/src/live-tokens/data/themes/royal-velvet.json +20 -20
- package/src/live-tokens/data/themes/sketchy.json +20 -20
- package/src/live-tokens/data/themes/spring-meadow.json +20 -20
- package/src/live-tokens/data/themes/sunset.json +20 -20
- package/src/system/components/Badge.svelte +3 -3
- package/src/system/components/Button.svelte +9 -3
- package/src/system/components/CodeSnippet.svelte +8 -1
- package/src/system/components/CornerBadge.svelte +3 -3
- package/src/system/components/Dialog.svelte +43 -14
- package/src/system/components/IconButton.svelte +1 -1
- package/src/system/components/Image.svelte +7 -0
- package/src/system/components/ImageLightbox.svelte +18 -25
- package/src/system/components/Input.svelte +8 -1
- package/src/system/components/MenuSelect.svelte +30 -2
- package/src/system/components/Notification.svelte +1 -1
- package/src/system/components/ProgressBar.svelte +6 -0
- package/src/system/components/RadioButton.svelte +36 -3
- package/src/system/components/SegmentedControl.svelte +36 -2
- package/src/system/components/SlidePager.svelte +2 -2
- package/src/system/components/Slider.svelte +13 -9
- package/src/system/components/TabBar.svelte +33 -3
- package/src/system/components/Toggle.svelte +2 -2
- package/src/system/components/VideoFrame.svelte +1 -1
- package/src/system/components/VideoLightbox.svelte +2 -2
- package/src/system/internal/focusTrap.ts +35 -0
- package/src/testing-js/__aria__/badge.yml +40 -0
- package/src/testing-js/__aria__/button.yml +88 -0
- package/src/testing-js/__aria__/callout.yml +12 -0
- package/src/testing-js/__aria__/card.yml +8 -0
- package/src/testing-js/__aria__/codesnippet.yml +21 -0
- package/src/testing-js/__aria__/collapsiblesection.yml +23 -0
- package/src/testing-js/__aria__/cornerbadge.yml +24 -0
- package/src/testing-js/__aria__/dialog.yml +42 -0
- package/src/testing-js/__aria__/iconbutton.yml +57 -0
- package/src/testing-js/__aria__/image.yml +10 -0
- package/src/testing-js/__aria__/imagelightbox.yml +50 -0
- package/src/testing-js/__aria__/inlineeditactions.yml +16 -0
- package/src/testing-js/__aria__/input.yml +61 -0
- package/src/testing-js/__aria__/menuselect.yml +30 -0
- package/src/testing-js/__aria__/notification.yml +30 -0
- package/src/testing-js/__aria__/panel.yml +2 -0
- package/src/testing-js/__aria__/progressbar.yml +2 -0
- package/src/testing-js/__aria__/radiobutton.yml +20 -0
- package/src/testing-js/__aria__/sectiondivider.yml +9 -0
- package/src/testing-js/__aria__/segmentedcontrol.yml +35 -0
- package/src/testing-js/__aria__/sidenavigation.yml +304 -0
- package/src/testing-js/__aria__/slidepager.yml +72 -0
- package/src/testing-js/__aria__/slider.yml +28 -0
- package/src/testing-js/__aria__/tabbar.yml +35 -0
- package/src/testing-js/__aria__/table.yml +144 -0
- package/src/testing-js/__aria__/toggle.yml +12 -0
- package/src/testing-js/__aria__/tooltip.yml +2 -0
- package/src/testing-js/__aria__/videoframe.yml +6 -0
- package/src/testing-js/__aria__/videolightbox.yml +12 -0
- package/src/testing-js/{chunk-WIZ6W7UT.js → chunk-3WCLMOM7.js} +2 -2
- package/src/testing-js/{chunk-SHBYZUPD.js → chunk-5HNZHIHG.js} +211 -14
- package/src/testing-js/chunk-5HNZHIHG.js.map +1 -0
- package/src/testing-js/{chunk-NB3NZRBM.js → chunk-7CM5D7PF.js} +5 -1
- package/src/testing-js/chunk-7CM5D7PF.js.map +1 -0
- package/src/testing-js/chunk-EWSGE27V.js +168 -0
- package/src/testing-js/chunk-EWSGE27V.js.map +1 -0
- package/src/testing-js/{chunk-GKO27NGN.js → chunk-R54I7YL6.js} +2 -2
- package/src/testing-js/{chunk-X6KHEVIG.js → chunk-RWH46IU5.js} +97 -29
- package/src/testing-js/chunk-RWH46IU5.js.map +1 -0
- package/src/testing-js/chunk-SHLPDNAQ.js +658 -0
- package/src/testing-js/chunk-SHLPDNAQ.js.map +1 -0
- package/src/testing-js/{chunk-3UKGXCDL.js → chunk-YVVR3GJZ.js} +1 -1
- package/src/testing-js/chunk-YVVR3GJZ.js.map +1 -0
- package/src/testing-js/component-a11y.contract.js +283 -0
- package/src/testing-js/component-a11y.contract.js.map +1 -0
- package/src/testing-js/component-alias.contract.js +2 -2
- package/src/testing-js/component-behavior.contract.js +2 -2
- package/src/testing-js/component-editor.contract.js +6 -5
- package/src/testing-js/component-editor.contract.js.map +1 -1
- package/src/testing-js/component-render.contract.js +10 -6
- package/src/testing-js/component-render.contract.js.map +1 -1
- package/src/testing-js/index.d.ts +44 -16
- package/src/testing-js/index.js +9 -9
- package/src/testing-js/page-compliance.contract.js +132 -13
- package/src/testing-js/page-compliance.contract.js.map +1 -1
- package/src/testing-js/vitest.js +3 -3
- package/template/package.json +9 -0
- package/src/testing-js/chunk-3UKGXCDL.js.map +0 -1
- package/src/testing-js/chunk-NB3NZRBM.js.map +0 -1
- package/src/testing-js/chunk-SHBYZUPD.js.map +0 -1
- package/src/testing-js/chunk-X6KHEVIG.js.map +0 -1
- /package/src/testing-js/{chunk-WIZ6W7UT.js.map → chunk-3WCLMOM7.js.map} +0 -0
- /package/src/testing-js/{chunk-GKO27NGN.js.map → chunk-R54I7YL6.js.map} +0 -0
|
@@ -9,6 +9,8 @@ var DEFAULT_COMPONENTS_PATH = "/live-tokens/components";
|
|
|
9
9
|
var TESTING_CONFIG_FILE = "live-tokens.testing.ts";
|
|
10
10
|
var COMPONENTS_PATH_ENV = "LIVE_TOKENS_COMPONENTS_PATH";
|
|
11
11
|
var COMPONENT_ENV = "LIVE_TOKENS_COMPONENT";
|
|
12
|
+
var A11Y_THEMES_ENV = "LIVE_TOKENS_A11Y_THEMES";
|
|
13
|
+
var UPDATE_ARIA_ENV = "LIVE_TOKENS_UPDATE_ARIA";
|
|
12
14
|
var PAGES_ENV = "LIVE_TOKENS_PAGES";
|
|
13
15
|
var PAGE_VIEWPORTS_ENV = "LIVE_TOKENS_PAGE_VIEWPORTS";
|
|
14
16
|
var DEFAULT_PAGE_VIEWPORTS = [
|
|
@@ -54,6 +56,8 @@ export {
|
|
|
54
56
|
TESTING_CONFIG_FILE,
|
|
55
57
|
COMPONENTS_PATH_ENV,
|
|
56
58
|
COMPONENT_ENV,
|
|
59
|
+
A11Y_THEMES_ENV,
|
|
60
|
+
UPDATE_ARIA_ENV,
|
|
57
61
|
PAGES_ENV,
|
|
58
62
|
PAGE_VIEWPORTS_ENV,
|
|
59
63
|
DEFAULT_PAGE_VIEWPORTS,
|
|
@@ -61,4 +65,4 @@ export {
|
|
|
61
65
|
resolveTestingConfig,
|
|
62
66
|
devServerCommand
|
|
63
67
|
};
|
|
64
|
-
//# sourceMappingURL=chunk-
|
|
68
|
+
//# sourceMappingURL=chunk-7CM5D7PF.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../testing/config.ts","../editor/core/routing/ownedRoutes.ts"],"sourcesContent":["import fs from 'node:fs';\nimport path from 'node:path';\nimport { DEFAULT_COMPONENTS_PATH } from '../editor/core/routing/ownedRoutes';\n\n/** The file a project puts its shared test settings in, at the project root. */\nexport const TESTING_CONFIG_FILE = 'live-tokens.testing.ts';\n\n/** The components route the run drives, published from the settings so the\n * suites need no argument. */\nexport const COMPONENTS_PATH_ENV = 'LIVE_TOKENS_COMPONENTS_PATH';\n\n/** Narrows a run to one component. */\nexport const COMPONENT_ENV = 'LIVE_TOKENS_COMPONENT';\n\n/** `all` repeats each component accessibility scan under the eight preset\n * themes. The repository's own contract run sets it; a consumer run scans\n * the active theme alone. */\nexport const A11Y_THEMES_ENV = 'LIVE_TOKENS_A11Y_THEMES';\n\n/** Rewrites each component's recorded accessibility tree. */\nexport const UPDATE_ARIA_ENV = 'LIVE_TOKENS_UPDATE_ARIA';\n\n/** The page targets a run opens, as JSON: `[{ source, route }]`. */\nexport const PAGES_ENV = 'LIVE_TOKENS_PAGES';\n\n/** The viewports every page rule runs at, as JSON: `[{ width, height }]`. */\nexport const PAGE_VIEWPORTS_ENV = 'LIVE_TOKENS_PAGE_VIEWPORTS';\n\nexport interface PageViewport {\n width: number;\n height: number;\n}\n\n/** The contract project's own desktop size, and the phone the text styles\n * carry media overrides for. Nothing here is derived from a page. */\nexport const DEFAULT_PAGE_VIEWPORTS: PageViewport[] = [\n { width: 1280, height: 900 },\n { width: 390, height: 844 },\n];\n\n/** What `resolveDataDirs` falls back to, restated because that resolver is\n * plugin-side and the tarball ships the plugin built rather than as source.\n * `vite-plugin/testingConfig.test.ts` pins the two together. */\nconst DEFAULT_DATA_DIR = 'src/live-tokens/data';\n\nconst DEFAULT_DEV_COMMAND = 'npm run dev -- --host {host} --port {port}';\n\nexport interface LiveTokensTestingConfig {\n /** Vite config a generated Vitest config merges. Default `vite.config.ts`. */\n viteConfig?: string;\n /** Command that starts the dev server. `{host}` and `{port}` are substituted.\n * Default `npm run dev -- --host {host} --port {port}`. */\n devCommand?: string;\n /** Fixed port for the dev server. Omitted, the run takes a free one. */\n port?: number;\n /** The components route, when `editorRoutes` moved it off the owned default. */\n componentsPath?: string;\n /** The plugin's data directory. Default: `dataDir` from\n * `live-tokens.config.json`, else `src/live-tokens/data`. The run never uses\n * it directly; it is the tree the isolated copy is made from. */\n dataDir?: string;\n /** Module that registers the project's components without mounting the app.\n * The registry contract imports it before it selects entries. */\n registrySetup?: string;\n /** Module exporting the `ComponentContract[]` a custom component adds to the\n * shipped list (default export or a named `contracts` export). The suite\n * files are static Playwright entry points, so an env var naming this path\n * plus a dynamic import inside `selectedContracts()` is the only way a\n * custom contract reaches them. */\n contractsModule?: string;\n /** Page source path to the concrete URL that renders it, for a route the\n * `pages` object cannot express: one served by `resolve()`, or one whose\n * parameters only the project knows. Page paths are relative to the\n * project root. */\n pageRoutes?: Record<string, string>;\n /** Replaces the two sizes every page rule runs at. */\n pageViewports?: PageViewport[];\n}\n\nexport interface ResolvedTestingConfig {\n root: string;\n viteConfig: string;\n devCommand: string;\n port?: number;\n componentsPath: string;\n dataDir: string;\n registrySetup?: string;\n contractsModule?: string;\n pageRoutes: Record<string, string>;\n pageViewports: PageViewport[];\n}\n\n/** Types the settings file without importing the interface by hand. */\nexport function defineTestingConfig(config: LiveTokensTestingConfig): LiveTokensTestingConfig {\n return config;\n}\n\n/** `dataDir` as the plugin resolves it, from the same file the plugin reads. */\nfunction configuredDataDir(root: string): string {\n try {\n const parsed = JSON.parse(fs.readFileSync(path.join(root, 'live-tokens.config.json'), 'utf-8'));\n if (parsed && typeof parsed === 'object' && typeof parsed.dataDir === 'string') {\n return path.resolve(root, parsed.dataDir);\n }\n } catch {\n // Missing or unparseable reads as absent, matching `readLiveTokensConfig`.\n }\n return path.resolve(root, DEFAULT_DATA_DIR);\n}\n\nexport function resolveTestingConfig(\n config: LiveTokensTestingConfig = {},\n root: string = process.cwd(),\n): ResolvedTestingConfig {\n const projectRoot = path.resolve(root);\n return {\n root: projectRoot,\n viteConfig: path.resolve(projectRoot, config.viteConfig ?? 'vite.config.ts'),\n devCommand: config.devCommand ?? DEFAULT_DEV_COMMAND,\n port: config.port,\n componentsPath: config.componentsPath ?? DEFAULT_COMPONENTS_PATH,\n dataDir: config.dataDir ? path.resolve(projectRoot, config.dataDir) : configuredDataDir(projectRoot),\n registrySetup: config.registrySetup\n ? path.resolve(projectRoot, config.registrySetup)\n : undefined,\n contractsModule: config.contractsModule\n ? path.resolve(projectRoot, config.contractsModule)\n : undefined,\n pageRoutes: config.pageRoutes ?? {},\n pageViewports: config.pageViewports?.length ? config.pageViewports : DEFAULT_PAGE_VIEWPORTS,\n };\n}\n\nexport function devServerCommand(command: string, host: string, port: number): string {\n return command.replaceAll('{host}', host).replaceAll('{port}', String(port));\n}\n","// The package's dev-only editor surfaces live under a reserved `/live-tokens/*`\n// namespace so they can never collide with a consumer's own routes — the\n// consumer owns the rest of the URL space. Keeping the defaults in one place\n// is the guard against the nav rail and the dispatcher drifting onto different\n// paths (the collision/shadow class these constants exist to prevent).\n//\n// A consumer relocates or disables each surface via the `editorRoutes` prop on\n// <LiveTokensRouter>; these are only the defaults.\nexport const DEFAULT_EDITOR_PATH = '/live-tokens/editor';\nexport const DEFAULT_COMPONENTS_PATH = '/live-tokens/components';\nexport const DEFAULT_COLORS_PATH = '/live-tokens/colors';\nexport const DEFAULT_DOCS_PATH = '/live-tokens/docs';\n"],"mappings":";AAAA,OAAO,QAAQ;AACf,OAAO,UAAU;;;ACQV,IAAM,0BAA0B;;;ADJhC,IAAM,sBAAsB;AAI5B,IAAM,sBAAsB;AAG5B,IAAM,gBAAgB;AAKtB,IAAM,kBAAkB;AAGxB,IAAM,kBAAkB;AAGxB,IAAM,YAAY;AAGlB,IAAM,qBAAqB;AAS3B,IAAM,yBAAyC;AAAA,EACpD,EAAE,OAAO,MAAM,QAAQ,IAAI;AAAA,EAC3B,EAAE,OAAO,KAAK,QAAQ,IAAI;AAC5B;AAKA,IAAM,mBAAmB;AAEzB,IAAM,sBAAsB;AAgDrB,SAAS,oBAAoB,QAA0D;AAC5F,SAAO;AACT;AAGA,SAAS,kBAAkB,MAAsB;AAC/C,MAAI;AACF,UAAM,SAAS,KAAK,MAAM,GAAG,aAAa,KAAK,KAAK,MAAM,yBAAyB,GAAG,OAAO,CAAC;AAC9F,QAAI,UAAU,OAAO,WAAW,YAAY,OAAO,OAAO,YAAY,UAAU;AAC9E,aAAO,KAAK,QAAQ,MAAM,OAAO,OAAO;AAAA,IAC1C;AAAA,EACF,QAAQ;AAAA,EAER;AACA,SAAO,KAAK,QAAQ,MAAM,gBAAgB;AAC5C;AAEO,SAAS,qBACd,SAAkC,CAAC,GACnC,OAAe,QAAQ,IAAI,GACJ;AACvB,QAAM,cAAc,KAAK,QAAQ,IAAI;AACrC,SAAO;AAAA,IACL,MAAM;AAAA,IACN,YAAY,KAAK,QAAQ,aAAa,OAAO,cAAc,gBAAgB;AAAA,IAC3E,YAAY,OAAO,cAAc;AAAA,IACjC,MAAM,OAAO;AAAA,IACb,gBAAgB,OAAO,kBAAkB;AAAA,IACzC,SAAS,OAAO,UAAU,KAAK,QAAQ,aAAa,OAAO,OAAO,IAAI,kBAAkB,WAAW;AAAA,IACnG,eAAe,OAAO,gBAClB,KAAK,QAAQ,aAAa,OAAO,aAAa,IAC9C;AAAA,IACJ,iBAAiB,OAAO,kBACpB,KAAK,QAAQ,aAAa,OAAO,eAAe,IAChD;AAAA,IACJ,YAAY,OAAO,cAAc,CAAC;AAAA,IAClC,eAAe,OAAO,eAAe,SAAS,OAAO,gBAAgB;AAAA,EACvE;AACF;AAEO,SAAS,iBAAiB,SAAiB,MAAc,MAAsB;AACpF,SAAO,QAAQ,WAAW,UAAU,IAAI,EAAE,WAAW,UAAU,OAAO,IAAI,CAAC;AAC7E;","names":[]}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// src/testing/support/a11y.ts
|
|
2
|
+
import { test } from "@playwright/test";
|
|
3
|
+
var A11Y_ANNOTATION = "a11y";
|
|
4
|
+
var AXE_TAGS = ["wcag2a", "wcag2aa", "wcag21aa", "wcag22aa"];
|
|
5
|
+
var SCOPE_ATTR = "data-lt-a11y-scope";
|
|
6
|
+
var EXCLUDE_ATTR = "data-lt-a11y-exclude";
|
|
7
|
+
var HTML_LIMIT = 300;
|
|
8
|
+
var EngineMissing = class extends Error {
|
|
9
|
+
};
|
|
10
|
+
async function importEngine(packageName, load) {
|
|
11
|
+
try {
|
|
12
|
+
return await load();
|
|
13
|
+
} catch (error) {
|
|
14
|
+
if (error.code === "ERR_MODULE_NOT_FOUND") throw new EngineMissing(`${packageName} is not installed`);
|
|
15
|
+
throw error;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
var axeModules = null;
|
|
19
|
+
async function importAxe() {
|
|
20
|
+
await importEngine("axe-core", () => import("axe-core"));
|
|
21
|
+
const { AxeBuilder } = await importEngine("@axe-core/playwright", () => import("@axe-core/playwright"));
|
|
22
|
+
return { AxeBuilder };
|
|
23
|
+
}
|
|
24
|
+
var loadAxe = () => axeModules ??= importAxe();
|
|
25
|
+
var alfaModules = null;
|
|
26
|
+
async function importAlfa() {
|
|
27
|
+
const { Audit, Outcome } = await importEngine("@siteimprove/alfa-act", () => import("@siteimprove/alfa-act"));
|
|
28
|
+
const { Rules } = await importEngine("@siteimprove/alfa-rules", () => import("@siteimprove/alfa-rules"));
|
|
29
|
+
const { Node } = await importEngine("@siteimprove/alfa-dom", () => import("@siteimprove/alfa-dom"));
|
|
30
|
+
const { Native: DomNative } = await importEngine("@siteimprove/alfa-dom", () => import("@siteimprove/alfa-dom/native"));
|
|
31
|
+
const { Device } = await importEngine("@siteimprove/alfa-device", () => import("@siteimprove/alfa-device"));
|
|
32
|
+
const { Native: DeviceNative } = await importEngine("@siteimprove/alfa-device", () => import("@siteimprove/alfa-device/native"));
|
|
33
|
+
const { Page: AlfaPage } = await importEngine("@siteimprove/alfa-web", () => import("@siteimprove/alfa-web"));
|
|
34
|
+
const http = await importEngine("@siteimprove/alfa-http", () => import("@siteimprove/alfa-http"));
|
|
35
|
+
const { URL } = await importEngine("@siteimprove/alfa-url", () => import("@siteimprove/alfa-url"));
|
|
36
|
+
return {
|
|
37
|
+
Audit,
|
|
38
|
+
Outcome,
|
|
39
|
+
R65: Rules.get("R65").getUnsafe(),
|
|
40
|
+
Node,
|
|
41
|
+
DomNative,
|
|
42
|
+
Device,
|
|
43
|
+
DeviceNative,
|
|
44
|
+
AlfaPage,
|
|
45
|
+
http,
|
|
46
|
+
URL
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
var loadAlfa = () => alfaModules ??= importAlfa();
|
|
50
|
+
async function missingEngine(engines) {
|
|
51
|
+
try {
|
|
52
|
+
for (const name of engines) await (name === "axe" ? loadAxe() : loadAlfa());
|
|
53
|
+
return null;
|
|
54
|
+
} catch (error) {
|
|
55
|
+
if (error instanceof EngineMissing) return error.message;
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function axeResults(rule, results, withReasons) {
|
|
60
|
+
return results.flatMap(
|
|
61
|
+
(result) => result.nodes.map((node) => {
|
|
62
|
+
const reasons = withReasons ? [...new Set([...node.any, ...node.all, ...node.none].map((check) => check.message).filter(Boolean))] : [];
|
|
63
|
+
return {
|
|
64
|
+
rule,
|
|
65
|
+
engineRule: result.id,
|
|
66
|
+
impact: node.impact ?? result.impact ?? null,
|
|
67
|
+
target: node.target.map((part) => Array.isArray(part) ? part.join(" >>> ") : String(part)).join(" | "),
|
|
68
|
+
html: node.html.slice(0, HTML_LIMIT),
|
|
69
|
+
help: [result.help, ...reasons].join(". "),
|
|
70
|
+
helpUrl: result.helpUrl
|
|
71
|
+
};
|
|
72
|
+
})
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
async function scanAxe(page, scope, exclude, rule, only) {
|
|
76
|
+
const { AxeBuilder } = await loadAxe();
|
|
77
|
+
let builder = new AxeBuilder({ page }).include(scope);
|
|
78
|
+
builder = only ? builder.withRules(only) : builder.withTags(AXE_TAGS);
|
|
79
|
+
for (const selector of exclude) builder = builder.exclude(selector);
|
|
80
|
+
if (rule === "a11y-page") builder = builder.disableRules(["color-contrast"]);
|
|
81
|
+
const result = await builder.analyze();
|
|
82
|
+
return [
|
|
83
|
+
...axeResults(rule, result.violations, false),
|
|
84
|
+
...axeResults("a11y-review", result.incomplete, true)
|
|
85
|
+
];
|
|
86
|
+
}
|
|
87
|
+
function pruneToScope(node) {
|
|
88
|
+
if (node.type === "element" && node.attributes?.some((a) => a.name === SCOPE_ATTR)) return true;
|
|
89
|
+
if (!node.children) return false;
|
|
90
|
+
if (node.type === "element" && node.name === "head") return true;
|
|
91
|
+
node.children = node.children.filter((child) => child.type === "type" || pruneToScope(child));
|
|
92
|
+
return node.type === "document" || node.children.some((child) => child.type === "element");
|
|
93
|
+
}
|
|
94
|
+
var FOCUS_HELP = {
|
|
95
|
+
failed: "Focusing the element changes nothing visible",
|
|
96
|
+
cantTell: "Focusing the element changes no style on it, its descendants, or its ancestors"
|
|
97
|
+
};
|
|
98
|
+
async function scanFocusVisible(page, scope, exclude) {
|
|
99
|
+
const alfa = await loadAlfa();
|
|
100
|
+
await page.evaluate(({ scope: scope2, exclude: exclude2, scopeAttr, excludeAttr }) => {
|
|
101
|
+
for (const element of document.querySelectorAll(scope2)) element.setAttribute(scopeAttr, "");
|
|
102
|
+
for (const selector of exclude2) {
|
|
103
|
+
for (const element of document.querySelectorAll(selector)) element.setAttribute(excludeAttr, "");
|
|
104
|
+
}
|
|
105
|
+
}, { scope, exclude, scopeAttr: SCOPE_ATTR, excludeAttr: EXCLUDE_ATTR });
|
|
106
|
+
let json;
|
|
107
|
+
try {
|
|
108
|
+
const handle = await page.evaluateHandle(() => document);
|
|
109
|
+
json = await handle.evaluate(alfa.DomNative.fromNode);
|
|
110
|
+
await handle.dispose();
|
|
111
|
+
} finally {
|
|
112
|
+
await page.evaluate((attrs) => {
|
|
113
|
+
for (const attr of attrs) for (const element of document.querySelectorAll(`[${attr}]`)) element.removeAttribute(attr);
|
|
114
|
+
}, [SCOPE_ATTR, EXCLUDE_ATTR]);
|
|
115
|
+
}
|
|
116
|
+
pruneToScope(json);
|
|
117
|
+
const fromWindow = alfa.DeviceNative.fromWindow;
|
|
118
|
+
const device = alfa.Device.from(await page.evaluate(fromWindow));
|
|
119
|
+
const url = alfa.URL.parse(page.url()).getUnsafe();
|
|
120
|
+
const { Request, Response, Headers, Header } = alfa.http;
|
|
121
|
+
const alfaPage = alfa.AlfaPage.of(
|
|
122
|
+
Request.of("GET", url),
|
|
123
|
+
Response.of(url, 200, Headers.of([Header.of("Content-Type", "text/html")])),
|
|
124
|
+
alfa.Node.from(json, device),
|
|
125
|
+
device
|
|
126
|
+
);
|
|
127
|
+
const results = [];
|
|
128
|
+
for (const outcome of await alfa.Audit.of(alfaPage, [alfa.R65]).evaluate()) {
|
|
129
|
+
const kind = alfa.Outcome.isFailed(outcome) ? "failed" : alfa.Outcome.isCantTell(outcome) ? "cantTell" : null;
|
|
130
|
+
if (!kind) continue;
|
|
131
|
+
const target = outcome.target;
|
|
132
|
+
if (!inScope(target)) continue;
|
|
133
|
+
results.push({
|
|
134
|
+
rule: "a11y-focus-visible",
|
|
135
|
+
engineRule: "SIA-R65",
|
|
136
|
+
impact: null,
|
|
137
|
+
target: elementLabel(target),
|
|
138
|
+
html: target.toString().replace(/\s+/g, " ").slice(0, HTML_LIMIT),
|
|
139
|
+
help: FOCUS_HELP[kind],
|
|
140
|
+
helpUrl: alfa.R65.uri
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
return results;
|
|
144
|
+
}
|
|
145
|
+
function inScope(target) {
|
|
146
|
+
for (const node of target.inclusiveAncestors()) {
|
|
147
|
+
if (typeof node.attribute !== "function") continue;
|
|
148
|
+
if (node.attribute(EXCLUDE_ATTR).isSome()) return false;
|
|
149
|
+
if (node.attribute(SCOPE_ATTR).isSome()) return true;
|
|
150
|
+
}
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
function elementLabel(target) {
|
|
154
|
+
const id = target.id.isSome() ? `#${target.id.get()}` : "";
|
|
155
|
+
return `${target.name}${id}${[...target.classes].map((name) => `.${name}`).join("")}`;
|
|
156
|
+
}
|
|
157
|
+
function recordA11y(results) {
|
|
158
|
+
if (results.length === 0) return;
|
|
159
|
+
test.info().annotations.push({ type: A11Y_ANNOTATION, description: JSON.stringify(results) });
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export {
|
|
163
|
+
missingEngine,
|
|
164
|
+
scanAxe,
|
|
165
|
+
scanFocusVisible,
|
|
166
|
+
recordA11y
|
|
167
|
+
};
|
|
168
|
+
//# sourceMappingURL=chunk-EWSGE27V.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../testing/support/a11y.ts"],"sourcesContent":["import { test, type Page } from '@playwright/test';\n\nexport type A11yRule =\n | 'a11y-component'\n | 'a11y-page'\n | 'a11y-review'\n | 'a11y-focus-visible'\n | 'a11y-text-spacing'\n | 'a11y-reduced-motion'\n | 'a11y-keyboard';\n\n/** One element one engine flagged. `line` is the page file line, which only\n * the page suite can resolve; a component finding anchors on its runtime.\n * `view` names the variants, states, and themes the element failed in.\n * `variables` names the component properties the result traced to. */\nexport interface A11yResult {\n rule: A11yRule;\n engineRule: string;\n impact: string | null;\n target: string;\n html: string;\n help: string;\n helpUrl: string;\n line?: number;\n view?: string;\n variables?: string[];\n}\n\nconst A11Y_ANNOTATION = 'a11y';\n\nexport type A11yEngine = 'axe' | 'alfa';\n\nconst AXE_TAGS = ['wcag2a', 'wcag2aa', 'wcag21aa', 'wcag22aa'];\nconst SCOPE_ATTR = 'data-lt-a11y-scope';\nconst EXCLUDE_ATTR = 'data-lt-a11y-exclude';\nconst HTML_LIMIT = 300;\n\nclass EngineMissing extends Error {}\n\nexport async function importEngine<T>(packageName: string, load: () => Promise<T>): Promise<T> {\n try {\n return await load();\n } catch (error) {\n if ((error as { code?: string }).code === 'ERR_MODULE_NOT_FOUND') throw new EngineMissing(`${packageName} is not installed`);\n throw error;\n }\n}\n\nlet axeModules: ReturnType<typeof importAxe> | null = null;\nasync function importAxe() {\n await importEngine('axe-core', () => import('axe-core'));\n const { AxeBuilder } = await importEngine('@axe-core/playwright', () => import('@axe-core/playwright'));\n return { AxeBuilder };\n}\nconst loadAxe = () => (axeModules ??= importAxe());\n\nlet alfaModules: ReturnType<typeof importAlfa> | null = null;\nasync function importAlfa() {\n const { Audit, Outcome } = await importEngine('@siteimprove/alfa-act', () => import('@siteimprove/alfa-act'));\n const { Rules } = await importEngine('@siteimprove/alfa-rules', () => import('@siteimprove/alfa-rules'));\n const { Node } = await importEngine('@siteimprove/alfa-dom', () => import('@siteimprove/alfa-dom'));\n const { Native: DomNative } = await importEngine('@siteimprove/alfa-dom', () => import('@siteimprove/alfa-dom/native'));\n const { Device } = await importEngine('@siteimprove/alfa-device', () => import('@siteimprove/alfa-device'));\n const { Native: DeviceNative } = await importEngine('@siteimprove/alfa-device', () => import('@siteimprove/alfa-device/native'));\n const { Page: AlfaPage } = await importEngine('@siteimprove/alfa-web', () => import('@siteimprove/alfa-web'));\n const http = await importEngine('@siteimprove/alfa-http', () => import('@siteimprove/alfa-http'));\n const { URL } = await importEngine('@siteimprove/alfa-url', () => import('@siteimprove/alfa-url'));\n return {\n Audit, Outcome, R65: Rules.get('R65').getUnsafe(), Node, DomNative, Device, DeviceNative, AlfaPage, http, URL,\n };\n}\nconst loadAlfa = () => (alfaModules ??= importAlfa());\n\n/** The reason to annotate a test `inapplicable` with when one of `engines`\n * cannot load, or null when all of them load. */\nexport async function missingEngine(engines: A11yEngine[]): Promise<string | null> {\n try {\n for (const name of engines) await (name === 'axe' ? loadAxe() : loadAlfa());\n return null;\n } catch (error) {\n if (error instanceof EngineMissing) return error.message;\n throw error;\n }\n}\n\ninterface AxeNode {\n target: unknown[];\n html: string;\n impact?: string | null;\n any: { message: string }[];\n all: { message: string }[];\n none: { message: string }[];\n}\n\ninterface AxeRuleResult {\n id: string;\n impact?: string | null;\n help: string;\n helpUrl: string;\n nodes: AxeNode[];\n}\n\nfunction axeResults(rule: A11yRule, results: AxeRuleResult[], withReasons: boolean): A11yResult[] {\n return results.flatMap((result) =>\n result.nodes.map((node) => {\n const reasons = withReasons\n ? [...new Set([...node.any, ...node.all, ...node.none].map((check) => check.message).filter(Boolean))]\n : [];\n return {\n rule,\n engineRule: result.id,\n impact: node.impact ?? result.impact ?? null,\n target: node.target.map((part) => (Array.isArray(part) ? part.join(' >>> ') : String(part))).join(' | '),\n html: node.html.slice(0, HTML_LIMIT),\n help: [result.help, ...reasons].join('. '),\n helpUrl: result.helpUrl,\n };\n }),\n );\n}\n\n/** axe over `scope` (a selector), minus `exclude`. Violations report under\n * `rule`, and the results axe cannot decide under `a11y-review`. A page scan\n * leaves contrast to `page-contrast` (decision 8). `only` narrows the scan\n * from the WCAG tags to the axe rules it names. */\nexport async function scanAxe(\n page: Page,\n scope: string,\n exclude: string[],\n rule: 'a11y-component' | 'a11y-page',\n only?: string[],\n): Promise<A11yResult[]> {\n const { AxeBuilder } = await loadAxe();\n let builder = new AxeBuilder({ page }).include(scope);\n builder = only ? builder.withRules(only) : builder.withTags(AXE_TAGS);\n for (const selector of exclude) builder = builder.exclude(selector);\n if (rule === 'a11y-page') builder = builder.disableRules(['color-contrast']);\n const result = await builder.analyze();\n return [\n ...axeResults(rule, result.violations as AxeRuleResult[], false),\n ...axeResults('a11y-review', result.incomplete as AxeRuleResult[], true),\n ];\n}\n\ninterface SerialNode {\n type: string;\n name?: string;\n attributes?: { name: string }[];\n children?: SerialNode[];\n}\n\n/** Keeps the head, every scope's ancestor chain, and each scope's subtree\n * whole. The style sheets travel on the document node, so the cascade stays\n * intact, and SIA-R65 skips the editor's own controls: 0.3 to 0.8 s a scan\n * against 2.5 s for the whole document, with the same outcomes. */\nfunction pruneToScope(node: SerialNode): boolean {\n if (node.type === 'element' && node.attributes?.some((a) => a.name === SCOPE_ATTR)) return true;\n if (!node.children) return false;\n if (node.type === 'element' && node.name === 'head') return true;\n node.children = node.children.filter((child) => child.type === 'type' || pruneToScope(child));\n return node.type === 'document' || node.children.some((child) => child.type === 'element');\n}\n\nconst FOCUS_HELP = {\n failed: 'Focusing the element changes nothing visible',\n cantTell: 'Focusing the element changes no style on it, its descendants, or its ancestors',\n};\n\n/** Alfa's SIA-R65 over `scope` (a selector), minus `exclude`. A cantTell\n * outcome means Alfa found no focus style at all, so it reports beside a\n * failure under `a11y-focus-visible`. */\nexport async function scanFocusVisible(page: Page, scope: string, exclude: string[]): Promise<A11yResult[]> {\n const alfa = await loadAlfa();\n await page.evaluate(({ scope, exclude, scopeAttr, excludeAttr }) => {\n for (const element of document.querySelectorAll(scope)) element.setAttribute(scopeAttr, '');\n for (const selector of exclude) {\n for (const element of document.querySelectorAll(selector)) element.setAttribute(excludeAttr, '');\n }\n }, { scope, exclude, scopeAttr: SCOPE_ATTR, excludeAttr: EXCLUDE_ATTR });\n let json: SerialNode;\n try {\n const handle = await page.evaluateHandle(() => document);\n json = (await handle.evaluate(alfa.DomNative.fromNode as (node: Document) => Promise<unknown>)) as SerialNode;\n await handle.dispose();\n } finally {\n await page.evaluate((attrs) => {\n for (const attr of attrs) for (const element of document.querySelectorAll(`[${attr}]`)) element.removeAttribute(attr);\n }, [SCOPE_ATTR, EXCLUDE_ATTR]);\n }\n pruneToScope(json);\n\n const fromWindow = alfa.DeviceNative.fromWindow as () => ReturnType<typeof alfa.DeviceNative.fromWindow>;\n const device = alfa.Device.from(await page.evaluate(fromWindow));\n const url = alfa.URL.parse(page.url()).getUnsafe();\n const { Request, Response, Headers, Header } = alfa.http;\n const alfaPage = alfa.AlfaPage.of(\n Request.of('GET', url),\n Response.of(url, 200, Headers.of([Header.of('Content-Type', 'text/html')])),\n alfa.Node.from(json as Parameters<typeof alfa.Node.from>[0], device) as Parameters<typeof alfa.AlfaPage.of>[2],\n device,\n );\n\n const results: A11yResult[] = [];\n for (const outcome of await alfa.Audit.of(alfaPage, [alfa.R65]).evaluate()) {\n const kind = alfa.Outcome.isFailed(outcome) ? 'failed' : alfa.Outcome.isCantTell(outcome) ? 'cantTell' : null;\n if (!kind) continue;\n const target = outcome.target as unknown as AlfaElement;\n if (!inScope(target)) continue;\n results.push({\n rule: 'a11y-focus-visible',\n engineRule: 'SIA-R65',\n impact: null,\n target: elementLabel(target),\n html: target.toString().replace(/\\s+/g, ' ').slice(0, HTML_LIMIT),\n help: FOCUS_HELP[kind],\n helpUrl: alfa.R65.uri,\n });\n }\n return results;\n}\n\ninterface AlfaElement {\n name: string;\n classes: Iterable<string>;\n id: { isSome(): boolean; get(): string };\n attribute?: (name: string) => { isSome(): boolean };\n inclusiveAncestors(): Iterable<AlfaElement>;\n toString(): string;\n}\n\nfunction inScope(target: AlfaElement): boolean {\n for (const node of target.inclusiveAncestors()) {\n if (typeof node.attribute !== 'function') continue;\n if (node.attribute(EXCLUDE_ATTR).isSome()) return false;\n if (node.attribute(SCOPE_ATTR).isSome()) return true;\n }\n return false;\n}\n\nfunction elementLabel(target: AlfaElement): string {\n const id = target.id.isSome() ? `#${target.id.get()}` : '';\n return `${target.name}${id}${[...target.classes].map((name) => `.${name}`).join('')}`;\n}\n\n/** Records `results` on the running test as the annotation the runner turns\n * into findings (decision 3). The test itself passes. */\nexport function recordA11y(results: A11yResult[]): void {\n if (results.length === 0) return;\n test.info().annotations.push({ type: A11Y_ANNOTATION, description: JSON.stringify(results) });\n}\n"],"mappings":"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","names":["scope","exclude"]}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
CONTRACTS_MODULE_ENV
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-5HNZHIHG.js";
|
|
4
4
|
|
|
5
5
|
// src/testing/vitest.ts
|
|
6
6
|
import path from "path";
|
|
@@ -54,4 +54,4 @@ function createVitestConfig(viteConfig, options = {}) {
|
|
|
54
54
|
export {
|
|
55
55
|
createVitestConfig
|
|
56
56
|
};
|
|
57
|
-
//# sourceMappingURL=chunk-
|
|
57
|
+
//# sourceMappingURL=chunk-R54I7YL6.js.map
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
2
|
openComponentsEditor
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-3WCLMOM7.js";
|
|
4
|
+
import {
|
|
5
|
+
recordA11y
|
|
6
|
+
} from "./chunk-EWSGE27V.js";
|
|
4
7
|
import {
|
|
5
8
|
ContractViolation,
|
|
6
9
|
isInapplicable,
|
|
7
10
|
partLocator,
|
|
8
11
|
requiresInteraction
|
|
9
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-YVVR3GJZ.js";
|
|
10
13
|
|
|
11
14
|
// src/testing/support/contractHarness.ts
|
|
12
15
|
import fs from "fs";
|
|
@@ -14,6 +17,8 @@ import path from "path";
|
|
|
14
17
|
var STAGE_GROUP = ".variant-group";
|
|
15
18
|
var STAGE_SCOPE = ".sketch-scope";
|
|
16
19
|
var STAGE = `${STAGE_GROUP}:visible ${STAGE_SCOPE}`;
|
|
20
|
+
var THEME_PICKER = '[role="dialog"][aria-label="Theme Picker"]';
|
|
21
|
+
var KEYBOARD_PATTERNS_URL = "https://www.w3.org/WAI/ARIA/apg/patterns/";
|
|
17
22
|
var SERVER_WRITE_TIMEOUT_MS = 3e4;
|
|
18
23
|
var settle = (page) => page.evaluate(async () => {
|
|
19
24
|
const frame = () => new Promise((resolve) => requestAnimationFrame(() => resolve()));
|
|
@@ -158,6 +163,19 @@ var ContractHarness = class _ContractHarness {
|
|
|
158
163
|
this.fail("contract-listed", `rail lists "${entry.label}" under ${group}, contract declares ${this.contract.origin}`);
|
|
159
164
|
}
|
|
160
165
|
}
|
|
166
|
+
/** Every variable a property row of the component can set: its schema
|
|
167
|
+
* tokens and its intrinsics. */
|
|
168
|
+
async propertyVariables() {
|
|
169
|
+
const names = await this.page.evaluate((id) => {
|
|
170
|
+
const entry = window.__liveTokensEditor?.getComponentRegistryEntries().find((e) => e.id === id);
|
|
171
|
+
if (!entry) throw new Error(`no registry entry for "${id}"`);
|
|
172
|
+
return [
|
|
173
|
+
...entry.schema.map((token) => token.variable),
|
|
174
|
+
...(entry.intrinsics ?? []).flatMap((spec) => spec.variants.map((variant) => spec.variable(variant)))
|
|
175
|
+
];
|
|
176
|
+
}, this.contract.id);
|
|
177
|
+
return new Set(names);
|
|
178
|
+
}
|
|
161
179
|
// ── 2. Property projection ───────────────────────────────────────────────
|
|
162
180
|
/**
|
|
163
181
|
* Drive one design token to a probe value and require the declared part's
|
|
@@ -372,7 +390,7 @@ ${missing.join("\n")}`);
|
|
|
372
390
|
}
|
|
373
391
|
async performAction(action) {
|
|
374
392
|
if (action.kind === "press") {
|
|
375
|
-
await this.locator(action.part).focus().catch(() => void 0);
|
|
393
|
+
if (action.part) await this.locator(action.part).focus().catch(() => void 0);
|
|
376
394
|
await this.page.keyboard.press(action.key);
|
|
377
395
|
return;
|
|
378
396
|
}
|
|
@@ -398,49 +416,68 @@ ${missing.join("\n")}`);
|
|
|
398
416
|
}
|
|
399
417
|
async runInteraction(testCase) {
|
|
400
418
|
await this.selectView(testCase);
|
|
419
|
+
const gap = await this.interactionGap(testCase);
|
|
420
|
+
if (gap === null) return;
|
|
421
|
+
if (testCase.rule !== "a11y-keyboard") this.fail("contract-interaction", `${testCase.name}: ${gap}`);
|
|
422
|
+
const element = this.locator(testCase.expect.part);
|
|
423
|
+
const merged = { ...this.contract.view, ...testCase };
|
|
424
|
+
const view = [merged.variant, merged.state].filter(Boolean).join(" / ");
|
|
425
|
+
recordA11y([{
|
|
426
|
+
rule: "a11y-keyboard",
|
|
427
|
+
engineRule: testCase.name,
|
|
428
|
+
impact: null,
|
|
429
|
+
target: testCase.expect.part,
|
|
430
|
+
html: await element.count() ? await element.evaluate((node) => node.outerHTML.slice(0, 300)) : "",
|
|
431
|
+
help: gap,
|
|
432
|
+
helpUrl: KEYBOARD_PATTERNS_URL,
|
|
433
|
+
...view ? { view } : {}
|
|
434
|
+
}]);
|
|
435
|
+
}
|
|
436
|
+
/** What the case observed against what it expects, or null when they agree. */
|
|
437
|
+
async interactionGap(testCase) {
|
|
401
438
|
const outcome = testCase.expect;
|
|
402
439
|
const tracksValue = outcome.kind === "valueMoves" || outcome.kind === "valueBecomes" || outcome.kind === "valueChanges" || outcome.kind === "valueHolds";
|
|
403
440
|
const before = tracksValue ? await this.valueOf(outcome.part) : "";
|
|
441
|
+
if (outcome.kind === "activated") {
|
|
442
|
+
await this.locator(outcome.part).evaluate((node) => {
|
|
443
|
+
const marked = node;
|
|
444
|
+
marked.__contractActivated = false;
|
|
445
|
+
node.addEventListener("click", () => {
|
|
446
|
+
marked.__contractActivated = true;
|
|
447
|
+
}, { once: true });
|
|
448
|
+
});
|
|
449
|
+
}
|
|
404
450
|
await this.performAction(testCase.action);
|
|
405
451
|
await settle(this.page);
|
|
406
452
|
if (outcome.kind === "attribute") {
|
|
407
453
|
const actual = await this.locator(outcome.part).evaluate((node, name) => node.getAttribute(name), outcome.name);
|
|
408
|
-
|
|
409
|
-
this.fail("contract-interaction", `${testCase.name}: ${outcome.part}[${outcome.name}] is "${actual}", expected "${outcome.value}"`);
|
|
410
|
-
}
|
|
411
|
-
return;
|
|
454
|
+
return actual === outcome.value ? null : `${outcome.part}[${outcome.name}] is "${actual}", expected "${outcome.value}"`;
|
|
412
455
|
}
|
|
413
456
|
if (outcome.kind === "focused") {
|
|
414
457
|
const focused = await this.isFocused(outcome.part);
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
458
|
+
return focused === outcome.value ? null : `${outcome.part} is ${focused ? "" : "not "}focused, expected ${outcome.value ? "" : "not "}focused`;
|
|
459
|
+
}
|
|
460
|
+
if (outcome.kind === "activated") {
|
|
461
|
+
const activated = await this.locator(outcome.part).evaluate((node) => node.__contractActivated === true);
|
|
462
|
+
return activated ? null : `${outcome.part} received no click, expected the key to activate it`;
|
|
463
|
+
}
|
|
464
|
+
if (outcome.kind === "present") {
|
|
465
|
+
const present = await this.locator(outcome.part).count() > 0;
|
|
466
|
+
return present === outcome.value ? null : `${outcome.part} is ${present ? "still" : "not"} in the document, expected it ${outcome.value ? "present" : "gone"}`;
|
|
419
467
|
}
|
|
420
468
|
const after = await this.valueOf(outcome.part);
|
|
421
469
|
if (outcome.kind === "valueHolds") {
|
|
422
|
-
|
|
423
|
-
this.fail("contract-interaction", `${testCase.name}: value moved from ${before} to ${after}`);
|
|
424
|
-
}
|
|
425
|
-
return;
|
|
470
|
+
return after === before ? null : `value moved from ${before} to ${after}`;
|
|
426
471
|
}
|
|
427
472
|
if (outcome.kind === "valueChanges") {
|
|
428
|
-
|
|
429
|
-
this.fail("contract-interaction", `${testCase.name}: value held at ${before}`);
|
|
430
|
-
}
|
|
431
|
-
return;
|
|
473
|
+
return after === before ? `value held at ${before}` : null;
|
|
432
474
|
}
|
|
433
475
|
if (outcome.kind === "valueBecomes") {
|
|
434
|
-
|
|
435
|
-
this.fail("contract-interaction", `${testCase.name}: value is ${after}, expected ${outcome.value}`);
|
|
436
|
-
}
|
|
437
|
-
return;
|
|
476
|
+
return after === outcome.value ? null : `value is ${after}, expected ${outcome.value}`;
|
|
438
477
|
}
|
|
439
478
|
const moved = Number(after) - Number(before);
|
|
440
479
|
const wanted = outcome.direction === "up" ? moved > 0 : moved < 0;
|
|
441
|
-
|
|
442
|
-
this.fail("contract-interaction", `${testCase.name}: value went ${before} -> ${after}, expected to move ${outcome.direction}`);
|
|
443
|
-
}
|
|
480
|
+
return wanted ? null : `value went ${before} -> ${after}, expected to move ${outcome.direction}`;
|
|
444
481
|
}
|
|
445
482
|
// ── 5. Persistence and reset ─────────────────────────────────────────────
|
|
446
483
|
/** Every alias and config value the component holds, for restoring the buffer. */
|
|
@@ -646,7 +683,7 @@ ${missing.join("\n")}`);
|
|
|
646
683
|
* follows lands after the repaint. */
|
|
647
684
|
async previewTheme(slug, settleOn) {
|
|
648
685
|
await this.page.locator(".theme-name-trigger").click();
|
|
649
|
-
await this.page.locator(
|
|
686
|
+
await this.page.locator(THEME_PICKER).waitFor();
|
|
650
687
|
const row = this.page.locator(`.load-item[data-file-name="theme:${slug}"] .load-name-btn`);
|
|
651
688
|
if (await row.count() === 0) this.fail("contract-theme", `the Theme Picker has no theme "${slug}"`);
|
|
652
689
|
await Promise.all([
|
|
@@ -660,12 +697,43 @@ ${missing.join("\n")}`);
|
|
|
660
697
|
if (settleOn) await this.awaitRootChange(settleOn.variable, settleOn.from);
|
|
661
698
|
}
|
|
662
699
|
async cancelThemePreview(settleOn) {
|
|
663
|
-
const dialog = this.page.locator(
|
|
700
|
+
const dialog = this.page.locator(THEME_PICKER);
|
|
664
701
|
await dialog.getByRole("button", { name: "Cancel", exact: true }).click();
|
|
665
702
|
await dialog.waitFor({ state: "detached" });
|
|
666
703
|
await settle(this.page);
|
|
667
704
|
if (settleOn) await this.awaitRootChange(settleOn.variable, settleOn.from);
|
|
668
705
|
}
|
|
706
|
+
/** A theme preview that leaves the stage in view, for a check that reads the
|
|
707
|
+
* stage's own paint: the Theme Picker's backdrop covers it while the
|
|
708
|
+
* preview lasts. Each click is dispatched on its element, because a
|
|
709
|
+
* portaled overlay such as ImageLightbox's sits over the picker trigger. */
|
|
710
|
+
async previewThemeUncovered(slug) {
|
|
711
|
+
await this.page.locator(".theme-name-trigger").dispatchEvent("click");
|
|
712
|
+
await this.page.locator(THEME_PICKER).waitFor();
|
|
713
|
+
const row = this.page.locator(`.load-item[data-file-name="theme:${slug}"] .load-name-btn`);
|
|
714
|
+
if (await row.count() === 0) this.fail("contract-theme", `the Theme Picker has no theme "${slug}"`);
|
|
715
|
+
await Promise.all([
|
|
716
|
+
this.page.waitForResponse(
|
|
717
|
+
(response) => response.url().includes("/themes/") && response.ok(),
|
|
718
|
+
{ timeout: SERVER_WRITE_TIMEOUT_MS }
|
|
719
|
+
),
|
|
720
|
+
row.dispatchEvent("click")
|
|
721
|
+
]);
|
|
722
|
+
await settle(this.page);
|
|
723
|
+
await this.showThemePicker(false);
|
|
724
|
+
}
|
|
725
|
+
async cancelUncoveredPreview() {
|
|
726
|
+
await this.showThemePicker(true);
|
|
727
|
+
const dialog = this.page.locator(THEME_PICKER);
|
|
728
|
+
await dialog.getByRole("button", { name: "Cancel", exact: true }).dispatchEvent("click");
|
|
729
|
+
await dialog.waitFor({ state: "detached" });
|
|
730
|
+
await settle(this.page);
|
|
731
|
+
}
|
|
732
|
+
async showThemePicker(shown) {
|
|
733
|
+
await this.page.locator(THEME_PICKER).evaluate((dialog, visible) => {
|
|
734
|
+
dialog.closest(".ui-dialog-backdrop").style.visibility = visible ? "" : "hidden";
|
|
735
|
+
}, shown);
|
|
736
|
+
}
|
|
669
737
|
async rootValues(names) {
|
|
670
738
|
return this.page.evaluate((variables) => {
|
|
671
739
|
const style = getComputedStyle(document.documentElement);
|
|
@@ -804,4 +872,4 @@ function probeValueFor(css) {
|
|
|
804
872
|
export {
|
|
805
873
|
ContractHarness
|
|
806
874
|
};
|
|
807
|
-
//# sourceMappingURL=chunk-
|
|
875
|
+
//# sourceMappingURL=chunk-RWH46IU5.js.map
|