mirror of
https://github.com/rommapp/romm.git
synced 2026-10-11 22:40:04 +00:00
test(frontend): run Storybook stories and a11y checks in Chromium via addon-vitest (#5153)
This commit is contained in:
@@ -158,7 +158,7 @@ Run from `frontend/`:
|
||||
|
||||
1. `npm run typecheck`: zero errors (`vue-tsc --build`, the app and the Node tooling).
|
||||
2. `trunk check`: ESLint clean on the changed files.
|
||||
3. `npm run test`: zero failures (Vitest + happy-dom; runs unit tests **and** story `play()` functions via `composeStories`).
|
||||
3. `npm run test`: zero failures (unit tests in happy-dom, and every story's `play()` and axe check in headless Chromium; needs `npx playwright install chromium` once).
|
||||
4. `npm run build`: zero failures (CI sanity check).
|
||||
|
||||
**If you touched the backend API:** start the backend, run `npm run generate`, then re-`typecheck`.
|
||||
|
||||
@@ -33,7 +33,12 @@ jobs:
|
||||
- name: Set up the frontend
|
||||
uses: ./.github/actions/setup-frontend
|
||||
|
||||
- name: Run tests (vitest, includes Storybook play() tests)
|
||||
# Stories run in browser mode, which needs only the headless shell.
|
||||
- name: Install Playwright browser
|
||||
run: npx playwright install --with-deps --only-shell chromium
|
||||
working-directory: frontend
|
||||
|
||||
- name: Run tests (vitest, includes Storybook play() and a11y tests)
|
||||
run: npm test
|
||||
working-directory: frontend
|
||||
|
||||
|
||||
+3
-1
@@ -185,9 +185,11 @@ Component docs and visual QA for `frontend/src/v2/` (port 6006):
|
||||
|
||||
```sh
|
||||
npm run storybook
|
||||
npm run storybook:test # composeStories + play() + a11y (every v2 story)
|
||||
npm run storybook:test # play() + axe on every v2 story, in headless Chromium
|
||||
```
|
||||
|
||||
`storybook:test` (and `npm run test`, which includes it) needs Playwright's Chromium: run `npx playwright install chromium` once.
|
||||
|
||||
For responsive layouts, use the viewport toolbar presets from `.storybook/rommViewports.ts`.
|
||||
|
||||
#### - Optional: local frontend against a remote RomM
|
||||
|
||||
@@ -7,6 +7,7 @@ const config: StorybookConfig = {
|
||||
"@storybook/addon-docs",
|
||||
"@storybook/addon-a11y",
|
||||
"@storybook/addon-themes",
|
||||
"@storybook/addon-vitest",
|
||||
],
|
||||
framework: {
|
||||
name: "@storybook/vue3-vite",
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ npm install # install (Node 24)
|
||||
npm run dev # dev server :3000
|
||||
npm run typecheck # vue-tsc --build: the app, the Node tooling and the e2e suite
|
||||
npm run typecheck:scripts # Node only: scripts/, eslint-plugin-romm/, tool configs, the e2e suite
|
||||
npm run test # vitest (+ Storybook play() tests)
|
||||
npm run test # vitest (+ Storybook play() and a11y tests in Chromium)
|
||||
npm run test:e2e # playwright, see e2e/README.md
|
||||
npm run build # production build
|
||||
npm run generate # regenerate types from backend OpenAPI (backend must be running)
|
||||
|
||||
Generated
+116
-3
@@ -45,6 +45,7 @@
|
||||
"@storybook/addon-a11y": "^10.2.19",
|
||||
"@storybook/addon-docs": "^10.2.19",
|
||||
"@storybook/addon-themes": "^10.2.19",
|
||||
"@storybook/addon-vitest": "^10.6.0",
|
||||
"@storybook/vue3-vite": "^10.2.19",
|
||||
"@tailwindcss/vite": "^4.3.1",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
@@ -54,6 +55,7 @@
|
||||
"@types/semver": "^7.7.0",
|
||||
"@types/spark-md5": "^3.0.5",
|
||||
"@vitejs/plugin-vue": "^6.0.7",
|
||||
"@vitest/browser-playwright": "^5.0.2",
|
||||
"@vitest/ui": "^5.0.1",
|
||||
"@vue/eslint-config-typescript": "^14.9.0",
|
||||
"@vue/test-utils": "^2.4.9",
|
||||
@@ -1705,6 +1707,13 @@
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@blazediff/core": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@blazediff/core/-/core-1.10.0.tgz",
|
||||
"integrity": "sha512-AOQff0zgR7cGsZL+4E7hVkmujoPUpm0J9xzWGWZj5wCjd3gmxESXAPfKyuzs93VdpQNFhHlBhfOjrcZ+XTERtQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@cacheable/memory": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.2.0.tgz",
|
||||
@@ -4754,6 +4763,42 @@
|
||||
"storybook": "^10.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/addon-vitest": {
|
||||
"version": "10.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/addon-vitest/-/addon-vitest-10.6.0.tgz",
|
||||
"integrity": "sha512-GY2SwSXLNgyMqjQ59J7YTkEQTHdEbBMggOvbbuhuPUBhWo97OGSDCmn1UW3nXDZvkoWyWhF9T9628m1uei7FbA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@storybook/global": "^5.0.0",
|
||||
"@storybook/icons": "^2.0.2"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/storybook"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@vitest/browser": "^3.0.0 || ^4.0.0",
|
||||
"@vitest/browser-playwright": "^4.0.0",
|
||||
"@vitest/runner": "^3.0.0 || ^4.0.0",
|
||||
"storybook": "^10.6.0",
|
||||
"vitest": "^3.0.0 || ^4.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@vitest/browser": {
|
||||
"optional": true
|
||||
},
|
||||
"@vitest/browser-playwright": {
|
||||
"optional": true
|
||||
},
|
||||
"@vitest/runner": {
|
||||
"optional": true
|
||||
},
|
||||
"vitest": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/builder-vite": {
|
||||
"version": "10.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@storybook/builder-vite/-/builder-vite-10.6.0.tgz",
|
||||
@@ -5456,9 +5501,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "19.2.17",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz",
|
||||
"integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==",
|
||||
"version": "19.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz",
|
||||
"integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
@@ -6125,6 +6170,74 @@
|
||||
"vue": "^3.2.25"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/browser": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-5.0.2.tgz",
|
||||
"integrity": "sha512-nX9xXZ/j8YfU7KTeFa+tpSP331SbI81qBQTASRlcopYeaTgBerNmJryzHXHrFOW0NYldd3Dn5gw651zGnmaQzw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@blazediff/core": "1.10.0",
|
||||
"@vitest/mocker": "5.0.2",
|
||||
"@vitest/ui": "5.0.2",
|
||||
"@vitest/utils": "5.0.2",
|
||||
"magic-string": "^1.2.3",
|
||||
"pngjs": "^7.0.0",
|
||||
"sirv": "^3.0.2",
|
||||
"tinyrainbow": "^3.1.1",
|
||||
"ws": "^8.21.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vitest": "5.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/browser-playwright": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/browser-playwright/-/browser-playwright-5.0.2.tgz",
|
||||
"integrity": "sha512-wU0B1Zy9+a55fdUT7JBCcnnjpTx0HHvUlQT6yFnuwCSY244BkPPwabNX+9PAljc96gRELcfm5APtnsn/3qiaPw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vitest/browser": "5.0.2",
|
||||
"@vitest/mocker": "5.0.2",
|
||||
"tinyrainbow": "^3.1.1"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"playwright": "*",
|
||||
"vitest": "5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"playwright": {
|
||||
"optional": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/browser/node_modules/magic-string": {
|
||||
"version": "1.4.2",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.2.tgz",
|
||||
"integrity": "sha512-vG+rjFRj1PqdIBozIxAGMjPlOhaVe+GXpbttY/iSK7rGcJRMlwNJO7dcUwmUqkymsFLJiNGI06t4D7Fr7yRC9g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/browser/node_modules/pngjs": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz",
|
||||
"integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.19.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/expect": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz",
|
||||
|
||||
+10
-2
@@ -34,7 +34,7 @@
|
||||
"test:ui": "vitest --ui",
|
||||
"storybook": "storybook dev -p 6006",
|
||||
"storybook:build": "storybook build",
|
||||
"storybook:test": "vitest run test/storybook.test.ts",
|
||||
"storybook:test": "vitest run --project storybook",
|
||||
"test:e2e": "playwright test",
|
||||
"test:e2e:ui": "playwright test --ui",
|
||||
"test:e2e:report": "playwright show-report e2e/.output/specs/report"
|
||||
@@ -76,6 +76,7 @@
|
||||
"@storybook/addon-a11y": "^10.2.19",
|
||||
"@storybook/addon-docs": "^10.2.19",
|
||||
"@storybook/addon-themes": "^10.2.19",
|
||||
"@storybook/addon-vitest": "^10.6.0",
|
||||
"@storybook/vue3-vite": "^10.2.19",
|
||||
"@tailwindcss/vite": "^4.3.1",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
@@ -85,6 +86,7 @@
|
||||
"@types/semver": "^7.7.0",
|
||||
"@types/spark-md5": "^3.0.5",
|
||||
"@vitejs/plugin-vue": "^6.0.7",
|
||||
"@vitest/browser-playwright": "^5.0.2",
|
||||
"@vitest/ui": "^5.0.1",
|
||||
"@vue/eslint-config-typescript": "^14.9.0",
|
||||
"@vue/test-utils": "^2.4.9",
|
||||
@@ -116,7 +118,13 @@
|
||||
"overrides": {
|
||||
"rolldown": "1.2.11",
|
||||
"esbuild": "^0.28.1",
|
||||
"form-data": "^4.0.6"
|
||||
"form-data": "^4.0.6",
|
||||
"@storybook/addon-vitest": {
|
||||
"vitest": "$vitest",
|
||||
"@vitest/browser": "$vitest",
|
||||
"@vitest/runner": "$vitest",
|
||||
"@vitest/browser-playwright": "$@vitest/browser-playwright"
|
||||
}
|
||||
},
|
||||
"engines": {
|
||||
"node": "24",
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
downloadButtons,
|
||||
pickSaveDataSubtab,
|
||||
} from "@/v2/utils/saveStates.plays";
|
||||
import { CONTRAST_TODO_PARAMETERS } from "@/v2/utils/storyA11y";
|
||||
import SaveDataTab from "./SaveDataTab.vue";
|
||||
|
||||
type Subtab = "saves" | "states";
|
||||
@@ -46,9 +47,7 @@ async function waitForSubtab(root: HTMLElement, subtab: Subtab) {
|
||||
const meta: Meta<StoryArgs> = {
|
||||
title: "GameDetails/SaveDataTab",
|
||||
component: SaveDataTab,
|
||||
parameters: {
|
||||
layout: "fullscreen",
|
||||
},
|
||||
parameters: { ...CONTRAST_TODO_PARAMETERS, layout: "fullscreen" },
|
||||
args: {
|
||||
subtab: "saves",
|
||||
rom: storyDetailedRom(),
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { SliderBtnGroupItem } from "@v2/lib/primitives/RSliderBtnGroup/type
|
||||
import { computed, ref } from "vue";
|
||||
import type { SaveSchema, StateSchema } from "@/__generated__";
|
||||
import { makeSave, manyStates } from "@/v2/utils/saveStates.fixtures";
|
||||
import { CONTRAST_TODO_PARAMETERS } from "@/v2/utils/storyA11y";
|
||||
import AssetList from "../shared/AssetList.vue";
|
||||
import AssetStrip from "../shared/AssetStrip.vue";
|
||||
import AssetPreview from "./AssetPreview.vue";
|
||||
@@ -36,6 +37,7 @@ interface Args {
|
||||
|
||||
const meta: Meta<Args> = {
|
||||
title: "Player/ResumePanel (composition)",
|
||||
parameters: CONTRAST_TODO_PARAMETERS,
|
||||
decorators: [
|
||||
() => ({
|
||||
template: `
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import type { Meta, StoryObj } from "@storybook/vue3-vite";
|
||||
import { makeSave } from "@/v2/utils/saveStates.fixtures";
|
||||
import { CONTRAST_TODO_PARAMETERS } from "@/v2/utils/storyA11y";
|
||||
import AssetChips from "./AssetChips.vue";
|
||||
|
||||
// Chip content per prop is covered by AssetChips.test.ts.
|
||||
const meta: Meta<typeof AssetChips> = {
|
||||
title: "Shared/AssetChips",
|
||||
component: AssetChips,
|
||||
parameters: CONTRAST_TODO_PARAMETERS,
|
||||
decorators: [
|
||||
() => ({
|
||||
template: `
|
||||
|
||||
@@ -18,11 +18,13 @@ import {
|
||||
downloadButtons,
|
||||
selectableItems,
|
||||
} from "@/v2/utils/saveStates.plays";
|
||||
import { CONTRAST_TODO_PARAMETERS } from "@/v2/utils/storyA11y";
|
||||
import AssetList from "./AssetList.vue";
|
||||
|
||||
const meta: Meta<typeof AssetList> = {
|
||||
title: "Shared/AssetList",
|
||||
component: AssetList,
|
||||
parameters: CONTRAST_TODO_PARAMETERS,
|
||||
decorators: [
|
||||
() => ({
|
||||
template: `
|
||||
|
||||
@@ -11,11 +11,13 @@ import {
|
||||
screenshotFixture,
|
||||
} from "@/v2/utils/saveStates.fixtures";
|
||||
import { downloadButtons, selectableItems } from "@/v2/utils/saveStates.plays";
|
||||
import { CONTRAST_TODO_PARAMETERS } from "@/v2/utils/storyA11y";
|
||||
import AssetStrip from "./AssetStrip.vue";
|
||||
|
||||
const meta: Meta<typeof AssetStrip> = {
|
||||
title: "Shared/AssetStrip",
|
||||
component: AssetStrip,
|
||||
parameters: CONTRAST_TODO_PARAMETERS,
|
||||
decorators: [
|
||||
() => ({
|
||||
template: `
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
// footer pinned to the bottom edge.
|
||||
import type { Meta, StoryObj } from "@storybook/vue3-vite";
|
||||
import { RBtn } from "@v2/lib";
|
||||
import { CONTRAST_TODO_PARAMETERS } from "@/v2/utils/storyA11y";
|
||||
import MetadataProviderCard from "./MetadataProviderCard.vue";
|
||||
import type { ProviderCardStatus } from "./types";
|
||||
|
||||
@@ -36,6 +37,7 @@ const checking: ProviderCardStatus = {
|
||||
const meta: Meta<typeof MetadataProviderCard> = {
|
||||
title: "Shared/MetadataProviderCard",
|
||||
component: MetadataProviderCard,
|
||||
parameters: CONTRAST_TODO_PARAMETERS,
|
||||
// The canvas body is not theme-painted; give the cards the app's
|
||||
// background so the light theme reads correctly.
|
||||
decorators: [
|
||||
|
||||
@@ -3,6 +3,7 @@ import { expect, userEvent, within, waitFor } from "storybook/test";
|
||||
import { ref } from "vue";
|
||||
import type { Platform } from "@/stores/platforms";
|
||||
import { platformFixture } from "@/utils/platform.fixtures";
|
||||
import { a11yTodoRules, CONTRAST_TODO_PARAMETERS } from "@/v2/utils/storyA11y";
|
||||
import PlatformSelect from "./PlatformSelect.vue";
|
||||
import {
|
||||
formatPlatformRomCount,
|
||||
@@ -122,14 +123,18 @@ async function openMenu(canvasElement: HTMLElement) {
|
||||
});
|
||||
}
|
||||
|
||||
// The open panel's option list scrolls but can't take keyboard focus.
|
||||
const OPEN_PANEL_A11Y_TODO = a11yTodoRules(
|
||||
"color-contrast",
|
||||
"scrollable-region-focusable",
|
||||
);
|
||||
|
||||
// Generic components can't fill Storybook's `component` slot, so it's cast;
|
||||
// the instantiation types the stories' args.
|
||||
const meta: Meta<typeof PlatformSelect<number | null>> = {
|
||||
title: "Shared/PlatformSelect",
|
||||
component: PlatformSelect as never,
|
||||
parameters: {
|
||||
layout: "padded",
|
||||
},
|
||||
parameters: { ...CONTRAST_TODO_PARAMETERS, layout: "padded" },
|
||||
decorators: [
|
||||
() => ({
|
||||
template: `<div style="width:min(360px,100%);padding-top:8px"><story /></div>`,
|
||||
@@ -160,6 +165,7 @@ export const PromotedOnPage: Story = {
|
||||
|
||||
export const PromotedOpenMenu: Story = {
|
||||
name: "Promotion on — open menu, do not type",
|
||||
parameters: OPEN_PANEL_A11Y_TODO,
|
||||
render: promoteFilledRender(),
|
||||
play: async ({ canvasElement, step }) => {
|
||||
await step("open menu (do not type in search)", async () => {
|
||||
@@ -252,6 +258,7 @@ export const PromotedTypingInSearch: Story = {
|
||||
|
||||
export const PromotedSearchDisabled: Story = {
|
||||
name: "Promotion on — search field disabled",
|
||||
parameters: OPEN_PANEL_A11Y_TODO,
|
||||
render: () => ({
|
||||
components: { PlatformSelect },
|
||||
setup() {
|
||||
@@ -297,6 +304,7 @@ export const PromotedAllLibrariesEmpty: Story = {
|
||||
|
||||
export const ScanPagePicker: Story = {
|
||||
name: "Scan page picker, promotion on",
|
||||
parameters: OPEN_PANEL_A11Y_TODO,
|
||||
render: () => ({
|
||||
components: { PlatformSelect },
|
||||
setup() {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Meta, StoryObj } from "@storybook/vue3-vite";
|
||||
import { defineComponent, h } from "vue";
|
||||
import { CONTRAST_TODO_PARAMETERS } from "@/v2/utils/storyA11y";
|
||||
|
||||
// Intro / design-token preview story. Exists to verify the Storybook pipeline
|
||||
// end-to-end (tokens + theme switcher) before any R-components land.
|
||||
@@ -122,9 +123,7 @@ const VisualLanguage = defineComponent({
|
||||
const meta: Meta<typeof VisualLanguage> = {
|
||||
title: "Visual Language",
|
||||
component: VisualLanguage,
|
||||
parameters: {
|
||||
layout: "fullscreen",
|
||||
},
|
||||
parameters: { ...CONTRAST_TODO_PARAMETERS, layout: "fullscreen" },
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
@@ -2,11 +2,13 @@ import type { Meta, StoryObj } from "@storybook/vue3-vite";
|
||||
import { RBtn, RForm } from "@v2/lib";
|
||||
import { expect, userEvent } from "storybook/test";
|
||||
import { ref } from "vue";
|
||||
import { CONTRAST_TODO_PARAMETERS } from "@/v2/utils/storyA11y";
|
||||
import RComboboxField from "./RComboboxField.vue";
|
||||
|
||||
const meta: Meta<typeof RComboboxField> = {
|
||||
title: "Forms/RComboboxField",
|
||||
component: RComboboxField,
|
||||
parameters: CONTRAST_TODO_PARAMETERS,
|
||||
argTypes: {
|
||||
prefixLabel: {
|
||||
control: "inline-radio",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Meta, StoryObj } from "@storybook/vue3-vite";
|
||||
import { ref } from "vue";
|
||||
import { CONTRAST_TODO_PARAMETERS } from "@/v2/utils/storyA11y";
|
||||
import RBtn from "../../primitives/RBtn/RBtn.vue";
|
||||
import RSelect from "../RSelect/RSelect.vue";
|
||||
import RTextField from "../RTextField/RTextField.vue";
|
||||
@@ -8,6 +9,7 @@ import RForm from "./RForm.vue";
|
||||
const meta: Meta<typeof RForm> = {
|
||||
title: "Forms/RForm",
|
||||
component: RForm,
|
||||
parameters: CONTRAST_TODO_PARAMETERS,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
@@ -5,6 +5,7 @@ import RMenu from "@/v2/lib/menus/RMenu/RMenu.vue";
|
||||
import RMenuItem from "@/v2/lib/menus/RMenuItem/RMenuItem.vue";
|
||||
import RBtn from "@/v2/lib/primitives/RBtn/RBtn.vue";
|
||||
import RIcon from "@/v2/lib/primitives/RIcon/RIcon.vue";
|
||||
import { CONTRAST_TODO_PARAMETERS } from "@/v2/utils/storyA11y";
|
||||
import RSelect from "./RSelect.vue";
|
||||
|
||||
const ROLES = [
|
||||
@@ -33,6 +34,7 @@ const PLATFORMS = [
|
||||
const meta: Meta<typeof RSelect<unknown, unknown>> = {
|
||||
title: "Forms/RSelect",
|
||||
component: RSelect as never,
|
||||
parameters: CONTRAST_TODO_PARAMETERS,
|
||||
argTypes: {
|
||||
variant: {
|
||||
control: "inline-radio",
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import type { Meta, StoryObj } from "@storybook/vue3-vite";
|
||||
import { ref } from "vue";
|
||||
import { CONTRAST_TODO_PARAMETERS } from "@/v2/utils/storyA11y";
|
||||
import RSlider from "./RSlider.vue";
|
||||
|
||||
const meta: Meta<typeof RSlider> = {
|
||||
title: "Forms/RSlider",
|
||||
component: RSlider,
|
||||
parameters: CONTRAST_TODO_PARAMETERS,
|
||||
// RSlider has no visible label, so every instance needs `ariaLabel`.
|
||||
args: { ariaLabel: "Value" },
|
||||
argTypes: {
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import type { Meta, StoryObj } from "@storybook/vue3-vite";
|
||||
import { ref } from "vue";
|
||||
import RBtn from "@/v2/lib/primitives/RBtn/RBtn.vue";
|
||||
import { CONTRAST_TODO_PARAMETERS } from "@/v2/utils/storyA11y";
|
||||
import RTextField from "./RTextField.vue";
|
||||
|
||||
const meta: Meta<typeof RTextField> = {
|
||||
title: "Forms/RTextField",
|
||||
component: RTextField,
|
||||
parameters: CONTRAST_TODO_PARAMETERS,
|
||||
argTypes: {
|
||||
variant: {
|
||||
control: "inline-radio",
|
||||
|
||||
@@ -116,6 +116,9 @@ export const NewTabGesture: Story = {
|
||||
const canvas = within(canvasElement);
|
||||
const link = canvas.getByText("Open in new tab").closest("a");
|
||||
if (!link) throw new Error("expected the item to render as an <a>");
|
||||
// In a real browser the Ctrl-click would open the link and tear down the
|
||||
// test page; cancel it after the item's own handler has seen the event.
|
||||
canvasElement.addEventListener("click", (event) => event.preventDefault());
|
||||
|
||||
await step("plain click activates the item", async () => {
|
||||
link.dispatchEvent(
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { Meta, StoryObj } from "@storybook/vue3-vite";
|
||||
import { expect, fn, userEvent, within } from "storybook/test";
|
||||
import { type Component, ref } from "vue";
|
||||
import RBtn from "@/v2/lib/primitives/RBtn/RBtn.vue";
|
||||
import { CONTRAST_TODO_PARAMETERS } from "@/v2/utils/storyA11y";
|
||||
import RCarousel from "./RCarousel.vue";
|
||||
|
||||
// RCarousel is generic (`<T>`), so its component-typed shape doesn't fit
|
||||
@@ -26,6 +27,7 @@ const SAMPLES = [
|
||||
const meta: Meta = {
|
||||
title: "Overlays/RCarousel",
|
||||
component: RCarousel as never,
|
||||
parameters: CONTRAST_TODO_PARAMETERS,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
@@ -2,11 +2,13 @@ import type { Meta, StoryObj } from "@storybook/vue3-vite";
|
||||
import { ref } from "vue";
|
||||
import RTextField from "@/v2/lib/forms/RTextField/RTextField.vue";
|
||||
import RBtn from "@/v2/lib/primitives/RBtn/RBtn.vue";
|
||||
import { a11yTodoRules, CONTRAST_TODO_PARAMETERS } from "@/v2/utils/storyA11y";
|
||||
import RDrawer from "./RDrawer.vue";
|
||||
|
||||
const meta: Meta<typeof RDrawer> = {
|
||||
title: "Overlays/RDrawer",
|
||||
component: RDrawer,
|
||||
parameters: CONTRAST_TODO_PARAMETERS,
|
||||
argTypes: {
|
||||
side: { control: "inline-radio", options: ["left", "right"] },
|
||||
width: { control: "text" },
|
||||
@@ -73,10 +75,14 @@ export const LeftSide: Story = {
|
||||
|
||||
export const TallContent: Story = {
|
||||
name: "Tall content (scrolling body)",
|
||||
// The scrolling body can't take keyboard focus.
|
||||
args: { side: "right", width: 380, icon: "mdi-format-list-bulleted" },
|
||||
// Opens on mount, so the docs page renders it in its own iframe to keep
|
||||
// the fixed drawer from covering the page.
|
||||
parameters: { docs: { story: { inline: false, height: "560px" } } },
|
||||
parameters: {
|
||||
...a11yTodoRules("color-contrast", "scrollable-region-focusable"),
|
||||
docs: { story: { inline: false, height: "560px" } },
|
||||
},
|
||||
render: (args) => ({
|
||||
components: { RDrawer, RBtn, RTextField },
|
||||
setup: () => ({
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Meta, StoryObj } from "@storybook/vue3-vite";
|
||||
import { ref } from "vue";
|
||||
import { CONTRAST_TODO_PARAMETERS } from "@/v2/utils/storyA11y";
|
||||
import RBtn from "../RBtn/RBtn.vue";
|
||||
import "./RAlert.stories.css";
|
||||
import RAlert from "./RAlert.vue";
|
||||
@@ -7,6 +8,7 @@ import RAlert from "./RAlert.vue";
|
||||
const meta: Meta<typeof RAlert> = {
|
||||
title: "Primitives/RAlert",
|
||||
component: RAlert,
|
||||
parameters: CONTRAST_TODO_PARAMETERS,
|
||||
argTypes: {
|
||||
type: {
|
||||
control: "select",
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import type { Meta, StoryObj } from "@storybook/vue3-vite";
|
||||
import { CONTRAST_TODO_PARAMETERS } from "@/v2/utils/storyA11y";
|
||||
import RAvatar from "./RAvatar.vue";
|
||||
|
||||
const meta: Meta<typeof RAvatar> = {
|
||||
title: "Primitives/RAvatar",
|
||||
component: RAvatar,
|
||||
parameters: CONTRAST_TODO_PARAMETERS,
|
||||
argTypes: {
|
||||
size: { control: "text" },
|
||||
color: { control: "text" },
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Meta, StoryObj } from "@storybook/vue3-vite";
|
||||
import { ref } from "vue";
|
||||
import { CONTRAST_TODO_PARAMETERS } from "@/v2/utils/storyA11y";
|
||||
import RAvatar from "../RAvatar/RAvatar.vue";
|
||||
import RIcon from "../RIcon/RIcon.vue";
|
||||
import RBadge from "./RBadge.vue";
|
||||
@@ -7,6 +8,7 @@ import RBadge from "./RBadge.vue";
|
||||
const meta: Meta<typeof RBadge> = {
|
||||
title: "Primitives/RBadge",
|
||||
component: RBadge,
|
||||
parameters: CONTRAST_TODO_PARAMETERS,
|
||||
argTypes: {
|
||||
content: { control: "text" },
|
||||
color: { control: "text" },
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import type { Meta, StoryObj } from "@storybook/vue3-vite";
|
||||
import { expect, userEvent, within } from "storybook/test";
|
||||
import { ref } from "vue";
|
||||
import { CONTRAST_TODO_PARAMETERS } from "@/v2/utils/storyA11y";
|
||||
import RBtn from "./RBtn.vue";
|
||||
|
||||
const meta: Meta<typeof RBtn> = {
|
||||
title: "Primitives/RBtn",
|
||||
component: RBtn,
|
||||
parameters: CONTRAST_TODO_PARAMETERS,
|
||||
argTypes: {
|
||||
color: { control: "text" },
|
||||
size: {
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import type { Meta, StoryObj } from "@storybook/vue3-vite";
|
||||
import { CONTRAST_TODO_PARAMETERS } from "@/v2/utils/storyA11y";
|
||||
import RCard from "./RCard.vue";
|
||||
|
||||
const meta: Meta<typeof RCard> = {
|
||||
title: "Primitives/RCard",
|
||||
component: RCard,
|
||||
parameters: CONTRAST_TODO_PARAMETERS,
|
||||
argTypes: {
|
||||
color: { control: "text" },
|
||||
elevation: { control: "number" },
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { Meta, StoryObj } from "@storybook/vue3-vite";
|
||||
import { ref } from "vue";
|
||||
import { CONTRAST_TODO_PARAMETERS } from "@/v2/utils/storyA11y";
|
||||
import RChip from "./RChip.vue";
|
||||
|
||||
/** Strings that expose descenders, ascenders, and mixed scripts in tight pills. */
|
||||
@@ -18,6 +19,7 @@ const CHIP_SIZES = ["x-small", "small", "default", "large", "x-large"] as const;
|
||||
const meta: Meta<typeof RChip> = {
|
||||
title: "Primitives/RChip",
|
||||
component: RChip,
|
||||
parameters: CONTRAST_TODO_PARAMETERS,
|
||||
argTypes: {
|
||||
size: {
|
||||
control: "select",
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import type { Meta, StoryObj } from "@storybook/vue3-vite";
|
||||
import { CONTRAST_TODO_PARAMETERS } from "@/v2/utils/storyA11y";
|
||||
import RDivider from "./RDivider.vue";
|
||||
|
||||
const meta: Meta<typeof RDivider> = {
|
||||
title: "Primitives/RDivider",
|
||||
component: RDivider,
|
||||
parameters: CONTRAST_TODO_PARAMETERS,
|
||||
argTypes: {
|
||||
thickness: { control: "text" },
|
||||
},
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import type { Meta, StoryObj } from "@storybook/vue3-vite";
|
||||
import { onMounted, onUnmounted, ref } from "vue";
|
||||
import { CONTRAST_TODO_PARAMETERS } from "@/v2/utils/storyA11y";
|
||||
import "./RImg.stories.css";
|
||||
import RImg from "./RImg.vue";
|
||||
|
||||
const meta: Meta<typeof RImg> = {
|
||||
title: "Primitives/RImg",
|
||||
component: RImg,
|
||||
parameters: CONTRAST_TODO_PARAMETERS,
|
||||
argTypes: {
|
||||
width: { control: "text" },
|
||||
height: { control: "text" },
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import type { Meta, StoryObj } from "@storybook/vue3-vite";
|
||||
import { ref } from "vue";
|
||||
import { CONTRAST_TODO_PARAMETERS } from "@/v2/utils/storyA11y";
|
||||
import RBtn from "../RBtn/RBtn.vue";
|
||||
import RSteps from "./RSteps.vue";
|
||||
|
||||
const meta: Meta<typeof RSteps> = {
|
||||
title: "Primitives/RSteps",
|
||||
component: RSteps,
|
||||
parameters: CONTRAST_TODO_PARAMETERS,
|
||||
argTypes: {
|
||||
current: { control: { type: "number", min: 1, max: 5 } },
|
||||
total: { control: { type: "number", min: 2, max: 6 } },
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Meta, StoryObj } from "@storybook/vue3-vite";
|
||||
import { expect, userEvent, within } from "storybook/test";
|
||||
import { ref } from "vue";
|
||||
import { CONTRAST_TODO_PARAMETERS } from "@/v2/utils/storyA11y";
|
||||
import RTabNav from "./RTabNav.vue";
|
||||
|
||||
// Generic components can't fill Storybook's `component` slot, so it's cast;
|
||||
@@ -8,6 +9,7 @@ import RTabNav from "./RTabNav.vue";
|
||||
const meta: Meta<typeof RTabNav<string>> = {
|
||||
title: "Primitives/RTabNav",
|
||||
component: RTabNav as never,
|
||||
parameters: CONTRAST_TODO_PARAMETERS,
|
||||
argTypes: {
|
||||
size: {
|
||||
control: "select",
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import type { Meta, StoryObj } from "@storybook/vue3-vite";
|
||||
import { CONTRAST_TODO_PARAMETERS } from "@/v2/utils/storyA11y";
|
||||
import RTag from "./RTag.vue";
|
||||
|
||||
const meta: Meta<typeof RTag> = {
|
||||
title: "Primitives/RTag",
|
||||
component: RTag,
|
||||
parameters: CONTRAST_TODO_PARAMETERS,
|
||||
argTypes: {
|
||||
text: { control: "text" },
|
||||
size: {
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import type { Meta, StoryObj } from "@storybook/vue3-vite";
|
||||
import { ref } from "vue";
|
||||
import RTag from "@/v2/lib/primitives/RTag/RTag.vue";
|
||||
import { CONTRAST_TODO_PARAMETERS } from "@/v2/utils/storyA11y";
|
||||
import RCollapsible from "./RCollapsible.vue";
|
||||
|
||||
const meta: Meta<typeof RCollapsible> = {
|
||||
title: "Structural/RCollapsible",
|
||||
component: RCollapsible,
|
||||
parameters: CONTRAST_TODO_PARAMETERS,
|
||||
render: (args) => ({
|
||||
components: { RCollapsible },
|
||||
setup: () => {
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import type { Meta, StoryObj } from "@storybook/vue3-vite";
|
||||
import { CONTRAST_TODO_PARAMETERS } from "@/v2/utils/storyA11y";
|
||||
import RListItem from "../RListItem/RListItem.vue";
|
||||
import RList from "./RList.vue";
|
||||
|
||||
const meta: Meta<typeof RList> = {
|
||||
title: "Structural/RList",
|
||||
component: RList,
|
||||
parameters: CONTRAST_TODO_PARAMETERS,
|
||||
argTypes: {
|
||||
density: {
|
||||
control: "select",
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import type { Meta, StoryObj } from "@storybook/vue3-vite";
|
||||
import { ref } from "vue";
|
||||
import { CONTRAST_TODO_PARAMETERS } from "@/v2/utils/storyA11y";
|
||||
import RList from "../RList/RList.vue";
|
||||
import RListItem from "./RListItem.vue";
|
||||
|
||||
const meta: Meta<typeof RListItem> = {
|
||||
title: "Structural/RListItem",
|
||||
component: RListItem,
|
||||
parameters: CONTRAST_TODO_PARAMETERS,
|
||||
argTypes: {
|
||||
rounded: { control: "text" },
|
||||
},
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import type { Meta, StoryObj } from "@storybook/vue3-vite";
|
||||
import RBtn from "@/v2/lib/primitives/RBtn/RBtn.vue";
|
||||
import RIcon from "@/v2/lib/primitives/RIcon/RIcon.vue";
|
||||
import { CONTRAST_TODO_PARAMETERS } from "@/v2/utils/storyA11y";
|
||||
import RToolbar from "./RToolbar.vue";
|
||||
|
||||
const meta: Meta<typeof RToolbar> = {
|
||||
title: "Structural/RToolbar",
|
||||
component: RToolbar,
|
||||
parameters: CONTRAST_TODO_PARAMETERS,
|
||||
argTypes: {
|
||||
color: { control: "text" },
|
||||
density: {
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import type { Meta, StoryObj } from "@storybook/vue3-vite";
|
||||
import { a11yTodoRules } from "@/v2/utils/storyA11y";
|
||||
import RVirtualScroller from "./RVirtualScroller.vue";
|
||||
|
||||
const meta: Meta<typeof RVirtualScroller> = {
|
||||
title: "Structural/RVirtualScroller",
|
||||
component: RVirtualScroller,
|
||||
// The scroll viewport isn't focusable, so keyboard users can't scroll it.
|
||||
parameters: a11yTodoRules("scrollable-region-focusable"),
|
||||
argTypes: {
|
||||
// An object control would print all 5000 rows into the docs table.
|
||||
items: { control: false },
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
type AxeRuleId = "color-contrast" | "scrollable-region-focusable";
|
||||
|
||||
// Storybook replaces arrays when merging parameters, so a story lists every
|
||||
// rule it skips, including those its meta skips.
|
||||
export function a11yTodoRules(...ids: AxeRuleId[]) {
|
||||
return {
|
||||
a11y: { config: { rules: ids.map((id) => ({ id, enabled: false })) } },
|
||||
};
|
||||
}
|
||||
|
||||
// The brand-purple and on-brand text tokens fall short of 4.5:1. Remove from a
|
||||
// story file once its contrast passes, so the rule guards it from then on.
|
||||
export const CONTRAST_TODO_PARAMETERS = a11yTodoRules("color-contrast");
|
||||
@@ -1,127 +0,0 @@
|
||||
import { composeStories, setProjectAnnotations } from "@storybook/vue3-vite";
|
||||
import axe from "axe-core";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import * as previewAnnotations from "../.storybook/preview";
|
||||
|
||||
type StoryModule = Record<string, unknown>;
|
||||
|
||||
// Per-story addon-a11y config. `test`: "error" fails on violations, "todo"
|
||||
// warns (ratchet escape hatch), "off" skips. `options` forwards to axe.run.
|
||||
type A11yParameters = {
|
||||
test?: "error" | "todo" | "off";
|
||||
options?: axe.RunOptions;
|
||||
};
|
||||
|
||||
type ComposedStory = {
|
||||
run?: (context?: { canvasElement?: HTMLElement }) => Promise<void>;
|
||||
parameters?: { a11y?: A11yParameters };
|
||||
};
|
||||
|
||||
// axe over a full composite takes about a second locally and several times
|
||||
// that on a shared CI runner, past Vitest's 5s default.
|
||||
const STORY_TIMEOUT_MS = 20_000;
|
||||
|
||||
// color-contrast needs real layout, which happy-dom lacks; disable it here
|
||||
// (covered by the a11y panel / a future browser-mode run).
|
||||
const DEFAULT_AXE_OPTIONS: axe.RunOptions = {
|
||||
rules: { "color-contrast": { enabled: false } },
|
||||
};
|
||||
|
||||
function formatViolations(violations: axe.Result[]): string {
|
||||
return violations
|
||||
.map((v) => {
|
||||
const nodes = v.nodes
|
||||
.map((n) => ` ${n.html}\n ${n.failureSummary ?? ""}`)
|
||||
.join("\n");
|
||||
return ` [${v.impact ?? "n/a"}] ${v.id}: ${v.help}\n${nodes}\n ${v.helpUrl}`;
|
||||
})
|
||||
.join("\n\n");
|
||||
}
|
||||
|
||||
async function checkA11y(
|
||||
elements: HTMLElement[],
|
||||
params: A11yParameters,
|
||||
storyName: string,
|
||||
): Promise<void> {
|
||||
const mode = params.test ?? "error";
|
||||
if (mode === "off") return;
|
||||
|
||||
const options: axe.RunOptions = {
|
||||
...DEFAULT_AXE_OPTIONS,
|
||||
...params.options,
|
||||
rules: { ...DEFAULT_AXE_OPTIONS.rules, ...params.options?.rules },
|
||||
};
|
||||
|
||||
const results = await axe.run({ include: elements }, options);
|
||||
if (results.violations.length === 0) return;
|
||||
|
||||
const report = formatViolations(results.violations);
|
||||
if (mode === "todo") {
|
||||
console.warn(
|
||||
`a11y (todo) "${storyName}" has ${results.violations.length} violation(s):\n${report}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
expect(
|
||||
results.violations,
|
||||
`Accessibility violations in "${storyName}":\n${report}`,
|
||||
).toHaveLength(0);
|
||||
}
|
||||
|
||||
// Kept out of vitest.setup.ts so only the story test loads full Vuetify.
|
||||
setProjectAnnotations([
|
||||
previewAnnotations as Parameters<typeof setProjectAnnotations>[0][number],
|
||||
]);
|
||||
|
||||
const storyModules = import.meta.glob<StoryModule>(
|
||||
"../src/v2/**/*.stories.ts",
|
||||
{
|
||||
eager: true,
|
||||
},
|
||||
);
|
||||
|
||||
for (const [path, module] of Object.entries(storyModules)) {
|
||||
const composed = composeStories(module);
|
||||
describe(path.replace(/^\.\.\//, ""), () => {
|
||||
for (const [name, story] of Object.entries(composed)) {
|
||||
const s = story as ComposedStory;
|
||||
it(
|
||||
name,
|
||||
async () => {
|
||||
if (typeof s.run !== "function") {
|
||||
throw new Error(
|
||||
`Story "${name}" has no run() helper — composeStories incompatible.`,
|
||||
);
|
||||
}
|
||||
// Render into an owned canvas so axe scans exactly this story's
|
||||
// subtree, then clean it up so stories stay isolated.
|
||||
const canvasElement = document.createElement("div");
|
||||
const before = new Set(document.body.children);
|
||||
document.body.appendChild(canvasElement);
|
||||
// Overlays teleport to <body>, so whatever the story left there is
|
||||
// its own, scanned with the canvas and removed after it.
|
||||
const teleported = () =>
|
||||
[...document.body.children].filter(
|
||||
(el): el is HTMLElement =>
|
||||
el instanceof HTMLElement &&
|
||||
el !== canvasElement &&
|
||||
!before.has(el),
|
||||
);
|
||||
try {
|
||||
await s.run({ canvasElement });
|
||||
await checkA11y(
|
||||
[canvasElement, ...teleported()],
|
||||
s.parameters?.a11y ?? {},
|
||||
`${path.replace(/^\.\.\//, "")} > ${name}`,
|
||||
);
|
||||
} finally {
|
||||
canvasElement.remove();
|
||||
for (const el of teleported()) el.remove();
|
||||
}
|
||||
},
|
||||
STORY_TIMEOUT_MS,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1,6 +1,11 @@
|
||||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||
"include": ["src/**/*.ts", "src/**/*.vue", "src/**/*.d.ts"],
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.vue",
|
||||
"src/**/*.d.ts",
|
||||
".storybook/**/*.ts"
|
||||
],
|
||||
"exclude": ["node_modules"],
|
||||
"compilerOptions": {
|
||||
"incremental": true,
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { storybookTest } from "@storybook/addon-vitest/vitest-plugin";
|
||||
import vue from "@vitejs/plugin-vue";
|
||||
import { playwright } from "@vitest/browser-playwright";
|
||||
import vuetify from "vite-plugin-vuetify";
|
||||
import { defineConfig } from "vitest/config";
|
||||
import { appAliases } from "./scripts/aliases";
|
||||
@@ -32,6 +34,30 @@ export default defineConfig({
|
||||
include: ["src/**/*.{test,spec}.ts", "test/**/*.{test,spec}.ts"],
|
||||
},
|
||||
},
|
||||
// Every story renders in real Chromium, where addon-a11y runs axe on it
|
||||
// with layout (so color-contrast is checked) and honours `parameters.a11y`.
|
||||
{
|
||||
extends: true,
|
||||
plugins: [storybookTest({ configDir: ".storybook" })],
|
||||
// A dependency found mid-run reloads the page and silently drops the
|
||||
// tests in flight, so pre-bundle everything stories reach up front.
|
||||
optimizeDeps: {
|
||||
include: ["vuetify/components/*", "country-flag-emoji-polyfill"],
|
||||
},
|
||||
// Mirrors vite.config.js, which the router's `process.env.BASE_URL` needs.
|
||||
define: { "process.env": {} },
|
||||
test: {
|
||||
name: "storybook",
|
||||
browser: {
|
||||
enabled: true,
|
||||
headless: true,
|
||||
provider: playwright(),
|
||||
// Storybook's default `rommDesktopMd` viewport.
|
||||
viewport: { width: 1024, height: 768 },
|
||||
instances: [{ browser: "chromium" }],
|
||||
},
|
||||
},
|
||||
},
|
||||
// Lint rules parse source text, so they skip the app's DOM and Storybook setup.
|
||||
{
|
||||
test: {
|
||||
|
||||
Reference in New Issue
Block a user