diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 640d5ee..1f738ce 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -3,6 +3,7 @@ name: CI
env:
# Test database.
TEST_DATABASE_URL: postgres://postgres:postgres@127.0.0.1:5432/river_test?sslmode=disable
+ NODE_VERSION: &node_version "24.14.1"
on:
push:
@@ -40,7 +41,7 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@v4
+ uses: actions/checkout@v6
- name: Setup Go ${{ matrix.go-version }}
uses: actions/setup-go@v6
@@ -99,7 +100,7 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@v4
+ uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
@@ -133,7 +134,7 @@ jobs:
strategy:
matrix:
node-version:
- - "22.18.0"
+ - *node_version
fail-fast: false
timeout-minutes: 5
@@ -145,10 +146,10 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@v4
+ uses: actions/checkout@v6
- name: Install Node.js
- uses: actions/setup-node@v4
+ uses: actions/setup-node@v6
with:
cache: "npm"
cache-dependency-path: package-lock.json
@@ -170,7 +171,7 @@ jobs:
strategy:
matrix:
node-version:
- - "22.18.0"
+ - *node_version
fail-fast: false
timeout-minutes: 5
@@ -179,10 +180,10 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@v4
+ uses: actions/checkout@v6
- name: Install Node.js
- uses: actions/setup-node@v4
+ uses: actions/setup-node@v6
with:
cache: "npm"
cache-dependency-path: package-lock.json
@@ -194,7 +195,7 @@ jobs:
- name: Cache ESLint
id: cache-eslint
- uses: actions/cache@v4
+ uses: actions/cache@v5
with:
path: .eslintcache
key: eslint-v1-${{ runner.os }}-${{ matrix.node-version }}-${{ hashFiles('**/package-lock.json') }}
@@ -209,6 +210,73 @@ jobs:
# Check tsc compilation even if there were linting issues:
if: always()
+ js_storybook_and_visual:
+ name: Storybook and visual regression
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ node-version:
+ - *node_version
+ fail-fast: false
+ timeout-minutes: 20
+
+ env:
+ NODE_ENV: test
+
+ permissions:
+ contents: read
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v6
+
+ - name: Install Node.js
+ uses: actions/setup-node@v6
+ with:
+ cache: "npm"
+ cache-dependency-path: package-lock.json
+ node-version: ${{ matrix.node-version }}
+
+ - name: Install dependencies
+ run: npm ci
+ shell: sh
+
+ - name: Install Playwright browser dependencies
+ run: npx playwright install --with-deps chromium
+
+ - name: Storybook story tests
+ run: npm run test-storybook
+
+ - name: Visual regression tests
+ run: npm run test-visual
+
+ - name: Upload Playwright HTML report
+ if: ${{ !cancelled() }}
+ uses: actions/upload-artifact@v7
+ with:
+ if-no-files-found: ignore
+ name: playwright-report
+ path: playwright-report/
+
+ - name: Upload Playwright test results
+ if: ${{ !cancelled() }}
+ uses: actions/upload-artifact@v7
+ with:
+ if-no-files-found: ignore
+ name: playwright-test-results
+ path: test-results/
+
+ - name: Visual test summary
+ if: ${{ !cancelled() }}
+ run: |
+ {
+ echo "### Storybook visual regression";
+ echo "";
+ echo "- Job result: \`${{ job.status }}\`";
+ echo "- Artifacts: \`playwright-report\`, \`playwright-test-results\`";
+ echo "- Download from this run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}";
+ } >> "$GITHUB_STEP_SUMMARY"
+
release:
name: Release
permissions:
@@ -218,7 +286,7 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@v4
+ uses: actions/checkout@v6
- name: Setup Go
uses: actions/setup-go@v6
@@ -227,11 +295,11 @@ jobs:
go-version-file: "go.mod"
- name: Install Node.js
- uses: actions/setup-node@v4
+ uses: actions/setup-node@v6
with:
cache: "npm"
cache-dependency-path: package-lock.json
- node-version: "22.18.0"
+ node-version: ${{ env.NODE_VERSION }}
- name: Install dependencies
run: npm install
diff --git a/.github/workflows/docker-riverproui.yaml b/.github/workflows/docker-riverproui.yaml
index 294db87..a70c09d 100644
--- a/.github/workflows/docker-riverproui.yaml
+++ b/.github/workflows/docker-riverproui.yaml
@@ -54,12 +54,12 @@ jobs:
steps:
- name: Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@v6
with:
ref: ${{ inputs.ref || github.ref }}
- name: Configure AWS Credentials
- uses: aws-actions/configure-aws-credentials@v4
+ uses: aws-actions/configure-aws-credentials@v6
with:
role-to-assume: ${{ env.ECR_ROLE_ARN }}
aws-region: ${{ env.ECR_REGION }}
@@ -74,11 +74,11 @@ jobs:
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
- name: Set up Docker Buildx
- uses: docker/setup-buildx-action@v3
+ uses: docker/setup-buildx-action@v4
- name: Docker meta for Pro
id: meta-pro
- uses: docker/metadata-action@v5
+ uses: docker/metadata-action@v6
with:
images: ${{ format('{0}.dkr.ecr.{1}.amazonaws.com/riverqueue/riverproui', env.ECR_ACCOUNT_ID, env.ECR_REGION) }}
labels: |
@@ -88,7 +88,7 @@ jobs:
- name: Build & push (by digest)
id: build-pro
- uses: docker/build-push-action@v6
+ uses: docker/build-push-action@v7
with:
context: .
file: Dockerfile.pro
@@ -109,7 +109,7 @@ jobs:
touch "/tmp/digests/${digest#sha256:}"
- name: Upload digest
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v7
with:
name: pro-digests-${{ env.PLATFORM_PAIR }}
path: /tmp/digests/*
@@ -141,7 +141,7 @@ jobs:
steps:
- name: Checkout full history (no tags yet)
- uses: actions/checkout@v4
+ uses: actions/checkout@v6
with:
ref: ${{ inputs.ref || github.ref }}
fetch-depth: 0 # full history
@@ -151,16 +151,16 @@ jobs:
run: git fetch --tags --force
- name: Set up Docker Buildx
- uses: docker/setup-buildx-action@v3
+ uses: docker/setup-buildx-action@v4
- name: Set up ORAS CLI
- uses: oras-project/setup-oras@v1
+ uses: oras-project/setup-oras@v2
- name: Install Cosign
- uses: sigstore/cosign-installer@v3
+ uses: sigstore/cosign-installer@v4.1.1
- name: Configure AWS Credentials
- uses: aws-actions/configure-aws-credentials@v4
+ uses: aws-actions/configure-aws-credentials@v6
with:
role-to-assume: ${{ env.ECR_ROLE_ARN }}
aws-region: ${{ env.ECR_REGION }}
@@ -170,7 +170,7 @@ jobs:
uses: aws-actions/amazon-ecr-login@v2
- name: Download digests
- uses: actions/download-artifact@v4
+ uses: actions/download-artifact@v8
with:
path: /tmp/digests
pattern: pro-digests-*
@@ -207,7 +207,7 @@ jobs:
- name: Docker meta
id: meta
- uses: docker/metadata-action@v5
+ uses: docker/metadata-action@v6
with:
images: ${{ env.ECR_IMAGE }}
tags: |
@@ -292,7 +292,7 @@ jobs:
fi
- name: Install crane
- uses: imjasonh/setup-crane@v0.4
+ uses: imjasonh/setup-crane@v0.5
- name: Compute manifest digest for attestation
id: manifest_digest
@@ -302,7 +302,7 @@ jobs:
- name: Generate build provenance attestation (not pushed)
id: attest
- uses: actions/attest-build-provenance@v2
+ uses: actions/attest-build-provenance@v4
with:
push-to-registry: false
subject-digest: ${{ steps.manifest_digest.outputs.digest }}
@@ -367,7 +367,7 @@ jobs:
contents: read
steps:
- name: Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@v6
with:
ref: ${{ inputs.ref || github.ref }}
fetch-depth: 0 # full history
@@ -432,20 +432,20 @@ jobs:
done
- name: Login to live registry
- uses: docker/login-action@v3
+ uses: docker/login-action@v4
with:
registry: riverqueue.com
username: river
password: ${{ secrets.RIVERPRO_GO_MOD_CREDENTIAL }}
- name: Install crane
- uses: imjasonh/setup-crane@v0.4
+ uses: imjasonh/setup-crane@v0.5
- name: Set up ORAS CLI
- uses: oras-project/setup-oras@v1
+ uses: oras-project/setup-oras@v2
- name: Install Cosign
- uses: sigstore/cosign-installer@v3
+ uses: sigstore/cosign-installer@v4.1.1
- name: Resolve manifest digest
id: resolve-digest
@@ -584,7 +584,7 @@ jobs:
- name: Upload debug artifacts
if: failure() || cancelled()
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v7
with:
name: prefetch-debug
path: |
diff --git a/.github/workflows/docker-riverui.yaml b/.github/workflows/docker-riverui.yaml
index abe9744..b1fe2c0 100644
--- a/.github/workflows/docker-riverui.yaml
+++ b/.github/workflows/docker-riverui.yaml
@@ -40,7 +40,7 @@ jobs:
steps:
- name: Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@v6
with:
ref: ${{ inputs.ref || github.ref }}
@@ -50,10 +50,10 @@ jobs:
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
- name: Set up Docker Buildx
- uses: docker/setup-buildx-action@v3
+ uses: docker/setup-buildx-action@v4
- name: Login to GitHub Container Registry
- uses: docker/login-action@v3
+ uses: docker/login-action@v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
@@ -61,7 +61,7 @@ jobs:
- name: Docker meta
id: meta
- uses: docker/metadata-action@v5
+ uses: docker/metadata-action@v6
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
labels: |
@@ -77,7 +77,7 @@ jobs:
- name: Build and push to GitHub Container Registry
id: build
- uses: docker/build-push-action@v6
+ uses: docker/build-push-action@v7
with:
context: .
pull: true
@@ -88,7 +88,7 @@ jobs:
outputs: type=image,name=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }},push-by-digest=true,name-canonical=true,push=true,annotation-index.org.opencontainers.image.description=River UI
- name: Generate artifact attestation
- uses: actions/attest-build-provenance@v2
+ uses: actions/attest-build-provenance@v4
with:
push-to-registry: true
subject-digest: ${{ steps.build.outputs.digest }}
@@ -101,7 +101,7 @@ jobs:
touch "/tmp/digests/${digest#sha256:}"
- name: Upload digest
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v7
with:
name: digests-${{ env.PLATFORM_PAIR }}
path: /tmp/digests/*
@@ -122,17 +122,17 @@ jobs:
steps:
- name: Download digests
- uses: actions/download-artifact@v4
+ uses: actions/download-artifact@v8
with:
path: /tmp/digests
pattern: digests-*
merge-multiple: true
- name: Set up Docker Buildx
- uses: docker/setup-buildx-action@v3
+ uses: docker/setup-buildx-action@v4
- name: Login to GitHub Container Registry
- uses: docker/login-action@v3
+ uses: docker/login-action@v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
@@ -140,7 +140,7 @@ jobs:
- name: Docker meta
id: meta
- uses: docker/metadata-action@v5
+ uses: docker/metadata-action@v6
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
labels: |
diff --git a/.github/workflows/package-and-release.yaml b/.github/workflows/package-and-release.yaml
index e310ccf..860138c 100644
--- a/.github/workflows/package-and-release.yaml
+++ b/.github/workflows/package-and-release.yaml
@@ -1,5 +1,8 @@
name: Package and Release
+env:
+ NODE_VERSION: "24.14.1"
+
on:
workflow_call:
inputs:
@@ -42,7 +45,7 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@v4
+ uses: actions/checkout@v6
with:
fetch-depth: 1
@@ -55,11 +58,11 @@ jobs:
arch: amd64
- name: Install Node.js
- uses: actions/setup-node@v4
+ uses: actions/setup-node@v6
with:
cache: "npm"
cache-dependency-path: package-lock.json
- node-version: "22.18.0"
+ node-version: ${{ env.NODE_VERSION }}
- name: Install dependencies
run: npm install
diff --git a/.storybook/main.ts b/.storybook/main.ts
index e78e982..be9a0d9 100644
--- a/.storybook/main.ts
+++ b/.storybook/main.ts
@@ -5,7 +5,7 @@ const config: StorybookConfig = {
"@storybook/addon-links",
"@storybook/addon-onboarding",
"@storybook/addon-themes",
- "@chromatic-com/storybook",
+ "@storybook/addon-vitest",
"@storybook/addon-docs",
],
diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx
index 09ea840..c854e41 100644
--- a/.storybook/preview.tsx
+++ b/.storybook/preview.tsx
@@ -17,6 +17,7 @@ import type { Features } from "../src/services/features";
import { FeaturesContext } from "../src/contexts/Features";
import "../src/global-type-overrides";
import "../src/index.css";
+import "../tests/visual/visual.css";
import {
$userSettings,
clearAllSettings,
@@ -98,6 +99,44 @@ export const withSettings: Decorator = (StoryFn, context) => {
return ;
};
+const isVisualTestRun = () => {
+ if (typeof window === "undefined") {
+ return false;
+ }
+
+ return (
+ new URLSearchParams(window.location.search).get("visual-test") === "true"
+ );
+};
+
+const visualNow = Date.parse("2025-03-01T12:00:00.000Z");
+
+export const withVisualTestingMode: Decorator = (StoryFn) => {
+ if (typeof document !== "undefined" && typeof window !== "undefined") {
+ const windowWithDateNowStore = window as {
+ __originalDateNow?: () => number;
+ } & Window;
+
+ if (isVisualTestRun()) {
+ document.documentElement.setAttribute("data-visual-test", "true");
+
+ if (!windowWithDateNowStore.__originalDateNow) {
+ windowWithDateNowStore.__originalDateNow = Date.now;
+ }
+
+ Date.now = () => visualNow;
+ } else {
+ document.documentElement.removeAttribute("data-visual-test");
+
+ if (windowWithDateNowStore.__originalDateNow) {
+ Date.now = windowWithDateNowStore.__originalDateNow;
+ }
+ }
+ }
+
+ return ;
+};
+
// Define parameter types
declare module "@storybook/react-vite" {
interface Parameters {
@@ -108,11 +147,16 @@ declare module "@storybook/react-vite" {
routes?: string[];
};
settings?: UserSettings;
+ visual?: {
+ viewport?: "desktop" | "mobile";
+ waitFor?: string;
+ };
}
}
const preview: Preview = {
decorators: [
+ withVisualTestingMode,
withFeatures,
withSettings,
withRouter,
diff --git a/Dockerfile b/Dockerfile
index 2c151cf..41d5ba5 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,6 +1,6 @@
# syntax=docker/dockerfile:1
-FROM node:22-alpine AS build-ui
+FROM node:24-alpine AS build-ui
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm install
diff --git a/Dockerfile.pro b/Dockerfile.pro
index dd02c1e..a38a104 100644
--- a/Dockerfile.pro
+++ b/Dockerfile.pro
@@ -1,6 +1,6 @@
# syntax=docker/dockerfile:1
-FROM node:22-alpine AS build-ui
+FROM node:24-alpine AS build-ui
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm install
diff --git a/docs/README.md b/docs/README.md
index 1e83904..c20d98c 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -82,3 +82,4 @@ By default logs are written with the [`slog.TextHandler`](https://pkg.go.dev/log
## Development
See [developing River UI](./development.md).
+See [storybook visual regression](./visual_regression.md).
diff --git a/docs/visual_regression.md b/docs/visual_regression.md
new file mode 100644
index 0000000..aa19d9a
--- /dev/null
+++ b/docs/visual_regression.md
@@ -0,0 +1,70 @@
+# Storybook Visual Regression
+
+River UI uses a self-hosted visual regression workflow based on Storybook and
+Playwright.
+
+## What runs in CI
+
+- `npm run test-storybook` runs Storybook stories with the Vitest addon so
+ rendering and interaction failures are caught.
+- `npm run test-visual` runs Playwright screenshot assertions against a built
+ static Storybook.
+- Visual baseline rendering is standardized on Linux Chromium.
+
+## Marking stories for visual snapshots
+
+Only stories tagged with `visual` are screenshot-tested.
+
+```ts
+export const Example: Story = {
+ tags: ["visual"],
+};
+```
+
+Stories without this tag still run in `test-storybook`, but they are skipped by
+`test-visual`.
+
+## Optional visual parameters
+
+Stories can define an optional `parameters.visual` contract:
+
+```ts
+parameters: {
+ visual: {
+ viewport: "mobile",
+ waitFor: "[data-testid='ready']",
+ },
+}
+```
+
+- `viewport`: `desktop` (default) or `mobile`.
+- `waitFor`: CSS selector to wait for before screenshotting.
+
+## Writing screenshot-safe stories
+
+To keep baselines stable:
+
+- Avoid `Date.now()`, `new Date()`, random values, and faker defaults unless
+ you pass fixed values.
+- Avoid async timing behavior unless there is a deterministic `waitFor`
+ selector.
+- Prefer static fixtures with explicit timestamps and IDs.
+
+## Local workflow
+
+- Run story execution tests: `npm run test-storybook`
+- Run visual tests: `npm run test-visual`
+- Update visual baselines intentionally: `npm run test-visual:update`
+
+Updated snapshots are written next to the visual test spec under Playwright's
+snapshot directory.
+
+## Debugging CI failures
+
+When visual regression fails, CI uploads Playwright artifacts:
+
+- `playwright-report/` (HTML report)
+- `test-results/` (diff images and failure diagnostics)
+
+Use those artifacts to inspect unexpected changes, then either fix regressions
+or intentionally update snapshots.
diff --git a/package-lock.json b/package-lock.json
index 34071c8..d49266a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -29,14 +29,15 @@
"zod": "^4.3.6"
},
"devDependencies": {
- "@chromatic-com/storybook": "^5.1.0",
"@eslint/css": "^0.14.1",
"@eslint/js": "^9.39.2",
"@faker-js/faker": "^10.4.0",
+ "@playwright/test": "^1.59.1",
"@storybook/addon-docs": "^10.3.3",
"@storybook/addon-links": "^10.3.3",
"@storybook/addon-onboarding": "^10.3.3",
"@storybook/addon-themes": "^10.3.3",
+ "@storybook/addon-vitest": "^10.3.5",
"@storybook/react-vite": "^10.3.3",
"@tailwindcss/forms": "^0.5.11",
"@tailwindcss/vite": "^4.2.2",
@@ -51,6 +52,7 @@
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react-swc": "^4.3.0",
+ "@vitest/browser-playwright": "^4.1.4",
"concurrently": "^9.2.1",
"eslint": "^9.39.2",
"eslint-config-prettier": "^10.1.8",
@@ -60,6 +62,7 @@
"eslint-plugin-storybook": "^10.3.3",
"fishery": "^2.4.0",
"globals": "^17.4.0",
+ "http-server": "^14.1.1",
"jsdom": "^29.0.1",
"prettier": "^3.8.1",
"prettier-plugin-tailwindcss": "^0.7.2",
@@ -427,6 +430,13 @@
"node": ">=6.9.0"
}
},
+ "node_modules/@blazediff/core": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/@blazediff/core/-/core-1.9.1.tgz",
+ "integrity": "sha512-ehg3jIkYKulZh+8om/O25vkvSsXXwC+skXmyA87FFx6A/45eqOkZsBltMw/TVteb0mloiGT8oGRTcjRAz66zaA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@bramus/specificity": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz",
@@ -440,27 +450,6 @@
"specificity": "bin/cli.js"
}
},
- "node_modules/@chromatic-com/storybook": {
- "version": "5.1.0",
- "resolved": "https://registry.npmjs.org/@chromatic-com/storybook/-/storybook-5.1.0.tgz",
- "integrity": "sha512-bXR1RxDO06QltZXYx4z7vxMgNtwKE7+fvRS2mNmPiheaKfld9fGH967ktAFrWCRn53MKIDVi4jipGJ5fEjePqA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@neoconfetti/react": "^1.0.0",
- "chromatic": "^13.3.4",
- "filesize": "^10.0.12",
- "jsonfile": "^6.1.0",
- "strip-ansi": "^7.1.0"
- },
- "engines": {
- "node": ">=20.0.0",
- "yarn": ">=1.22.18"
- },
- "peerDependencies": {
- "storybook": "^0.0.0-0 || ^10.1.0 || ^10.1.0-0 || ^10.2.0-0 || ^10.3.0-0 || ^10.4.0-0"
- }
- },
"node_modules/@csstools/color-helpers": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz",
@@ -1576,10 +1565,26 @@
"react": ">=18.0.0"
}
},
- "node_modules/@neoconfetti/react": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/@neoconfetti/react/-/react-1.0.0.tgz",
- "integrity": "sha512-klcSooChXXOzIm+SE5IISIAn3bYzYfPjbX7D7HoqZL84oAfgREeSg5vSIaSFH+DaGzzvImTyWe1OyrJ67vik4A==",
+ "node_modules/@playwright/test": {
+ "version": "1.59.1",
+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.59.1.tgz",
+ "integrity": "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright": "1.59.1"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@polka/url": {
+ "version": "1.0.0-next.29",
+ "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz",
+ "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==",
"dev": true,
"license": "MIT"
},
@@ -2157,6 +2162,42 @@
"storybook": "^10.3.3"
}
},
+ "node_modules/@storybook/addon-vitest": {
+ "version": "10.3.5",
+ "resolved": "https://registry.npmjs.org/@storybook/addon-vitest/-/addon-vitest-10.3.5.tgz",
+ "integrity": "sha512-PQDeeMwoF55kvzlhFqVKOryBJskkVk71AbDh7F0y8PdRRxlGbTvIUkKXktHZWBdESo0dV6BkeVxGQ4ZpiFxirg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@storybook/global": "^5.0.0",
+ "@storybook/icons": "^2.0.1"
+ },
+ "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.3.5",
+ "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.3.3",
"resolved": "https://registry.npmjs.org/@storybook/builder-vite/-/builder-vite-10.3.3.tgz",
@@ -3853,6 +3894,123 @@
"vite": "^4 || ^5 || ^6 || ^7 || ^8"
}
},
+ "node_modules/@vitest/browser": {
+ "version": "4.1.4",
+ "resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-4.1.4.tgz",
+ "integrity": "sha512-TrNaY/yVOwxtrxNsDUC/wQ56xSwplpytTeRAqF/197xV/ZddxxulBsxR6TrhVMyniJmp9in8d5u0AcDaNRY30w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@blazediff/core": "1.9.1",
+ "@vitest/mocker": "4.1.4",
+ "@vitest/utils": "4.1.4",
+ "magic-string": "^0.30.21",
+ "pngjs": "^7.0.0",
+ "sirv": "^3.0.2",
+ "tinyrainbow": "^3.1.0",
+ "ws": "^8.19.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "vitest": "4.1.4"
+ }
+ },
+ "node_modules/@vitest/browser-playwright": {
+ "version": "4.1.4",
+ "resolved": "https://registry.npmjs.org/@vitest/browser-playwright/-/browser-playwright-4.1.4.tgz",
+ "integrity": "sha512-q3PchVhZINX23Pv+RERgAtDlp6wzVkID/smOPnZ5YGWpeWUe3jMNYppeVh15j4il3G7JIJty1d1Kicpm0HSMig==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/browser": "4.1.4",
+ "@vitest/mocker": "4.1.4",
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "playwright": "*",
+ "vitest": "4.1.4"
+ },
+ "peerDependenciesMeta": {
+ "playwright": {
+ "optional": false
+ }
+ }
+ },
+ "node_modules/@vitest/browser-playwright/node_modules/tinyrainbow": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz",
+ "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@vitest/browser/node_modules/@vitest/pretty-format": {
+ "version": "4.1.4",
+ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.4.tgz",
+ "integrity": "sha512-ddmDHU0gjEUyEVLxtZa7xamrpIefdEETu3nZjWtHeZX4QxqJ7tRxSteHVXJOcr8jhiLoGAhkK4WJ3WqBpjx42A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/browser/node_modules/@vitest/utils": {
+ "version": "4.1.4",
+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.4.tgz",
+ "integrity": "sha512-13QMT+eysM5uVGa1rG4kegGYNp6cnQcsTc67ELFbhNLQO+vgsygtYJx2khvdt4gVQqSSpC/KT5FZZxUpP3Oatw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "4.1.4",
+ "convert-source-map": "^2.0.0",
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/browser/node_modules/tinyrainbow": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz",
+ "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@vitest/browser/node_modules/ws": {
+ "version": "8.20.0",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz",
+ "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@vitest/expect": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz",
@@ -3870,13 +4028,13 @@
}
},
"node_modules/@vitest/mocker": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.1.tgz",
- "integrity": "sha512-h3BOylsfsCLPeceuCPAAJ+BvNwSENgJa4hXoXu4im0bs9Lyp4URc4JYK4pWLZ4pG/UQn7AT92K6IByi6rE6g3A==",
+ "version": "4.1.4",
+ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.4.tgz",
+ "integrity": "sha512-R9HTZBhW6yCSGbGQnDnH3QHfJxokKN4KB+Yvk9Q1le7eQNYwiCyKxmLmurSpFy6BzJanSLuEUDrD+j97Q+ZLPg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/spy": "4.1.1",
+ "@vitest/spy": "4.1.4",
"estree-walker": "^3.0.3",
"magic-string": "^0.30.21"
},
@@ -3897,9 +4055,9 @@
}
},
"node_modules/@vitest/mocker/node_modules/@vitest/spy": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.1.tgz",
- "integrity": "sha512-6Ti/KT5OVaiupdIZEuZN7l3CZcR0cxnxt70Z0//3CtwgObwA6jZhmVBA3yrXSVN3gmwjgd7oDNLlsXz526gpRA==",
+ "version": "4.1.4",
+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.4.tgz",
+ "integrity": "sha512-XxNdAsKW7C+FLydqFJLb5KhJtl3PGCMmYwFRfhvIgxJvLSXhhVI1zM8f1qD3Zg7RCjTSzDVyct6sghs9UEgBEQ==",
"dev": true,
"license": "MIT",
"funding": {
@@ -3929,13 +4087,13 @@
}
},
"node_modules/@vitest/runner": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.1.tgz",
- "integrity": "sha512-f7+FPy75vN91QGWsITueq0gedwUZy1fLtHOCMeQpjs8jTekAHeKP80zfDEnhrleviLHzVSDXIWuCIOFn3D3f8A==",
+ "version": "4.1.4",
+ "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.4.tgz",
+ "integrity": "sha512-xTp7VZ5aXP5ZJrn15UtJUWlx6qXLnGtF6jNxHepdPHpMfz/aVPx+htHtgcAL2mDXJgKhpoo2e9/hVJsIeFbytQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/utils": "4.1.1",
+ "@vitest/utils": "4.1.4",
"pathe": "^2.0.3"
},
"funding": {
@@ -3943,28 +4101,28 @@
}
},
"node_modules/@vitest/runner/node_modules/@vitest/pretty-format": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.1.tgz",
- "integrity": "sha512-GM+TEQN5WhOygr1lp7skeVjdLPqqWMHsfzXrcHAqZJi/lIVh63H0kaRCY8MDhNWikx19zBUK8ceaLB7X5AH9NQ==",
+ "version": "4.1.4",
+ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.4.tgz",
+ "integrity": "sha512-ddmDHU0gjEUyEVLxtZa7xamrpIefdEETu3nZjWtHeZX4QxqJ7tRxSteHVXJOcr8jhiLoGAhkK4WJ3WqBpjx42A==",
"dev": true,
"license": "MIT",
"dependencies": {
- "tinyrainbow": "^3.0.3"
+ "tinyrainbow": "^3.1.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/runner/node_modules/@vitest/utils": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.1.tgz",
- "integrity": "sha512-cNxAlaB3sHoCdL6pj6yyUXv9Gry1NHNg0kFTXdvSIZXLHsqKH7chiWOkwJ5s5+d/oMwcoG9T0bKU38JZWKusrQ==",
+ "version": "4.1.4",
+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.4.tgz",
+ "integrity": "sha512-13QMT+eysM5uVGa1rG4kegGYNp6cnQcsTc67ELFbhNLQO+vgsygtYJx2khvdt4gVQqSSpC/KT5FZZxUpP3Oatw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/pretty-format": "4.1.1",
+ "@vitest/pretty-format": "4.1.4",
"convert-source-map": "^2.0.0",
- "tinyrainbow": "^3.0.3"
+ "tinyrainbow": "^3.1.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
@@ -3981,14 +4139,14 @@
}
},
"node_modules/@vitest/snapshot": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.1.tgz",
- "integrity": "sha512-kMVSgcegWV2FibXEx9p9WIKgje58lcTbXgnJixfcg15iK8nzCXhmalL0ZLtTWLW9PH1+1NEDShiFFedB3tEgWg==",
+ "version": "4.1.4",
+ "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.4.tgz",
+ "integrity": "sha512-MCjCFgaS8aZz+m5nTcEcgk/xhWv0rEH4Yl53PPlMXOZ1/Ka2VcZU6CJ+MgYCZbcJvzGhQRjVrGQNZqkGPttIKw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/pretty-format": "4.1.1",
- "@vitest/utils": "4.1.1",
+ "@vitest/pretty-format": "4.1.4",
+ "@vitest/utils": "4.1.4",
"magic-string": "^0.30.21",
"pathe": "^2.0.3"
},
@@ -3997,28 +4155,28 @@
}
},
"node_modules/@vitest/snapshot/node_modules/@vitest/pretty-format": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.1.tgz",
- "integrity": "sha512-GM+TEQN5WhOygr1lp7skeVjdLPqqWMHsfzXrcHAqZJi/lIVh63H0kaRCY8MDhNWikx19zBUK8ceaLB7X5AH9NQ==",
+ "version": "4.1.4",
+ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.4.tgz",
+ "integrity": "sha512-ddmDHU0gjEUyEVLxtZa7xamrpIefdEETu3nZjWtHeZX4QxqJ7tRxSteHVXJOcr8jhiLoGAhkK4WJ3WqBpjx42A==",
"dev": true,
"license": "MIT",
"dependencies": {
- "tinyrainbow": "^3.0.3"
+ "tinyrainbow": "^3.1.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/@vitest/snapshot/node_modules/@vitest/utils": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.1.tgz",
- "integrity": "sha512-cNxAlaB3sHoCdL6pj6yyUXv9Gry1NHNg0kFTXdvSIZXLHsqKH7chiWOkwJ5s5+d/oMwcoG9T0bKU38JZWKusrQ==",
+ "version": "4.1.4",
+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.4.tgz",
+ "integrity": "sha512-13QMT+eysM5uVGa1rG4kegGYNp6cnQcsTc67ELFbhNLQO+vgsygtYJx2khvdt4gVQqSSpC/KT5FZZxUpP3Oatw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/pretty-format": "4.1.1",
+ "@vitest/pretty-format": "4.1.4",
"convert-source-map": "^2.0.0",
- "tinyrainbow": "^3.0.3"
+ "tinyrainbow": "^3.1.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
@@ -4203,6 +4361,13 @@
"node": ">=4"
}
},
+ "node_modules/async": {
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz",
+ "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/babel-dead-code-elimination": {
"version": "1.0.12",
"resolved": "https://registry.npmjs.org/babel-dead-code-elimination/-/babel-dead-code-elimination-1.0.12.tgz",
@@ -4226,6 +4391,19 @@
"node": "18 || 20 || >=22"
}
},
+ "node_modules/basic-auth": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz",
+ "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "5.1.2"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/bidi-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz",
@@ -4323,6 +4501,37 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@@ -4450,30 +4659,6 @@
"node": ">= 6"
}
},
- "node_modules/chromatic": {
- "version": "13.3.4",
- "resolved": "https://registry.npmjs.org/chromatic/-/chromatic-13.3.4.tgz",
- "integrity": "sha512-TR5rvyH0ESXobBB3bV8jc87AEAFQC7/n+Eb4XWhJz6hW3YNxIQPVjcbgLv+a4oKHEl1dUBueWSoIQsOVGTd+RQ==",
- "dev": true,
- "license": "MIT",
- "bin": {
- "chroma": "dist/bin.js",
- "chromatic": "dist/bin.js",
- "chromatic-cli": "dist/bin.js"
- },
- "peerDependencies": {
- "@chromatic-com/cypress": "^0.*.* || ^1.0.0",
- "@chromatic-com/playwright": "^0.*.* || ^1.0.0"
- },
- "peerDependenciesMeta": {
- "@chromatic-com/cypress": {
- "optional": true
- },
- "@chromatic-com/playwright": {
- "optional": true
- }
- }
- },
"node_modules/classcat": {
"version": "5.0.5",
"resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz",
@@ -4642,6 +4827,16 @@
"integrity": "sha512-RAj4E421UYRgqokKUmotqAwuplYw15qtdXfY+hGzgCJ/MBjCVZcSoHK/kH9kocfjRjcDME7IiDWR/1WX1TM2Pg==",
"license": "MIT"
},
+ "node_modules/corser": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/corser/-/corser-2.0.1.tgz",
+ "integrity": "sha512-utCYNzRSQIZNPIcGZdQc92UVJYAhtGAteCFg0yRaFm8f0P+CPtyGyHXJcGXnffjCybUCEx3FQ2G7U3/o9eIkVQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -4970,6 +5165,21 @@
"dev": true,
"peer": true
},
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/electron-to-chromium": {
"version": "1.5.178",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.178.tgz",
@@ -5013,6 +5223,26 @@
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/es-module-lexer": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz",
@@ -5020,6 +5250,19 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/esbuild": {
"version": "0.27.4",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz",
@@ -5390,6 +5633,13 @@
"node": ">=0.10.0"
}
},
+ "node_modules/eventemitter3": {
+ "version": "4.0.7",
+ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
+ "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/expect-type": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz",
@@ -5433,16 +5683,6 @@
"node": ">=16.0.0"
}
},
- "node_modules/filesize": {
- "version": "10.1.6",
- "resolved": "https://registry.npmjs.org/filesize/-/filesize-10.1.6.tgz",
- "integrity": "sha512-sJslQKU2uM33qH5nqewAwVB2QgR6w1aMNsYUp3aN5rMRyXEwJGmZvaWzeJFNTOXWlHQyBFCWrdj3fV/fsTOX8w==",
- "dev": true,
- "license": "BSD-3-Clause",
- "engines": {
- "node": ">= 10.4.0"
- }
- },
"node_modules/fill-range": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
@@ -5515,6 +5755,27 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/follow-redirects": {
+ "version": "1.15.11",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
+ "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -5557,24 +5818,63 @@
"node": "6.* || 8.* || >= 10.*"
}
},
- "node_modules/get-tsconfig": {
- "version": "4.8.1",
- "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.8.1.tgz",
- "integrity": "sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==",
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"dev": true,
+ "license": "MIT",
"dependencies": {
- "resolve-pkg-maps": "^1.0.0"
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
},
"funding": {
- "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
+ "url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/glob": {
- "version": "13.0.6",
- "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz",
- "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==",
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/get-tsconfig": {
+ "version": "4.8.1",
+ "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.8.1.tgz",
+ "integrity": "sha512-k9PN+cFBmaLWtVz29SkUoqU5O0slLuHJXt/2P+tMVFT+phsSGXGkp9t3rQIqdz0e+06EHNGs3oM6ZX1s2zHxRg==",
"dev": true,
- "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "resolve-pkg-maps": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
+ }
+ },
+ "node_modules/glob": {
+ "version": "13.0.6",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz",
+ "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
"dependencies": {
"minimatch": "^10.2.2",
"minipass": "^7.1.3",
@@ -5626,6 +5926,19 @@
"csstype": "^3.0.10"
}
},
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
@@ -5642,6 +5955,19 @@
"node": ">=8"
}
},
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
@@ -5655,6 +5981,16 @@
"node": ">= 0.4"
}
},
+ "node_modules/he": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
+ "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "he": "bin/he"
+ }
+ },
"node_modules/hermes-estree": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz",
@@ -5685,6 +6021,75 @@
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
}
},
+ "node_modules/http-proxy": {
+ "version": "1.18.1",
+ "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz",
+ "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eventemitter3": "^4.0.0",
+ "follow-redirects": "^1.0.0",
+ "requires-port": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/http-server": {
+ "version": "14.1.1",
+ "resolved": "https://registry.npmjs.org/http-server/-/http-server-14.1.1.tgz",
+ "integrity": "sha512-+cbxadF40UXd9T01zUHgA+rlo2Bg1Srer4+B4NwIHdaGxAGGv59nYRnGGDJ9LBk7alpS0US+J+bLLdQOOkJq4A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "basic-auth": "^2.0.1",
+ "chalk": "^4.1.2",
+ "corser": "^2.0.1",
+ "he": "^1.2.0",
+ "html-encoding-sniffer": "^3.0.0",
+ "http-proxy": "^1.18.1",
+ "mime": "^1.6.0",
+ "minimist": "^1.2.6",
+ "opener": "^1.5.1",
+ "portfinder": "^1.0.28",
+ "secure-compare": "3.0.1",
+ "union": "~0.5.0",
+ "url-join": "^4.0.1"
+ },
+ "bin": {
+ "http-server": "bin/http-server"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/http-server/node_modules/html-encoding-sniffer": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz",
+ "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-encoding": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -5995,19 +6400,6 @@
"node": ">=6"
}
},
- "node_modules/jsonfile": {
- "version": "6.1.0",
- "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
- "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "universalify": "^2.0.0"
- },
- "optionalDependencies": {
- "graceful-fs": "^4.1.6"
- }
- },
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -6366,6 +6758,16 @@
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/mdn-data": {
"version": "2.23.0",
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.23.0.tgz",
@@ -6373,6 +6775,19 @@
"dev": true,
"license": "CC0-1.0"
},
+ "node_modules/mime": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "mime": "cli.js"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
"node_modules/min-indent": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
@@ -6427,6 +6842,16 @@
"node": ">=16 || 14 >=14.17"
}
},
+ "node_modules/mrmime": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz",
+ "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -6517,6 +6942,19 @@
"node": ">=0.10.0"
}
},
+ "node_modules/object-inspect": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/obug": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz",
@@ -6547,6 +6985,16 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/opener": {
+ "version": "1.5.2",
+ "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz",
+ "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==",
+ "dev": true,
+ "license": "(WTFPL OR MIT)",
+ "bin": {
+ "opener": "bin/opener-bin.js"
+ }
+ },
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@@ -6706,6 +7154,77 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
+ "node_modules/playwright": {
+ "version": "1.59.1",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz",
+ "integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright-core": "1.59.1"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "fsevents": "2.3.2"
+ }
+ },
+ "node_modules/playwright-core": {
+ "version": "1.59.1",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz",
+ "integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "playwright-core": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/playwright/node_modules/fsevents": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "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/portfinder": {
+ "version": "1.0.38",
+ "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.38.tgz",
+ "integrity": "sha512-rEwq/ZHlJIKw++XtLAO8PPuOQA/zaPJOZJ37BVuN97nLpMJeuDVLVGRwbFoBgLudgdTMP2hdRJP++H+8QOA3vg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "async": "^3.2.6",
+ "debug": "^4.3.6"
+ },
+ "engines": {
+ "node": ">= 10.12"
+ }
+ },
"node_modules/postcss": {
"version": "8.5.8",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
@@ -6904,6 +7423,22 @@
"node": ">=6"
}
},
+ "node_modules/qs": {
+ "version": "6.15.1",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz",
+ "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/query-string": {
"version": "9.3.1",
"resolved": "https://registry.npmjs.org/query-string/-/query-string-9.3.1.tgz",
@@ -7091,6 +7626,13 @@
"node": ">=0.10.0"
}
},
+ "node_modules/requires-port": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
+ "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/resolve": {
"version": "1.22.11",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
@@ -7199,6 +7741,20 @@
"tslib": "^2.1.0"
}
},
+ "node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/saxes": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
@@ -7218,6 +7774,13 @@
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
"license": "MIT"
},
+ "node_modules/secure-compare": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/secure-compare/-/secure-compare-3.0.1.tgz",
+ "integrity": "sha512-AckIIV90rPDcBcglUwXPF3kg0P0qmPsPXAj6BBEENQE1p5yA1xfmDJzfi1Tappj37Pv2mVbKpL3Z1T+Nn7k1Qw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
@@ -7282,12 +7845,103 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/side-channel": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3",
+ "side-channel-list": "^1.0.0",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
+ "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/siginfo": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
"integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
"dev": true
},
+ "node_modules/sirv": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz",
+ "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@polka/url": "^1.0.0-next.24",
+ "mrmime": "^2.0.0",
+ "totalist": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
@@ -7396,35 +8050,6 @@
"node": ">=10"
}
},
- "node_modules/strip-ansi": {
- "version": "7.1.0",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
- "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ansi-regex": "^6.0.1"
- },
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/strip-ansi?sponsor=1"
- }
- },
- "node_modules/strip-ansi/node_modules/ansi-regex": {
- "version": "6.1.0",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
- "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-regex?sponsor=1"
- }
- },
"node_modules/strip-bom": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
@@ -7661,6 +8286,16 @@
"node": ">=8.0"
}
},
+ "node_modules/totalist": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
+ "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/tough-cookie": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz",
@@ -8566,14 +9201,16 @@
"dev": true,
"license": "MIT"
},
- "node_modules/universalify": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
- "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+ "node_modules/union": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/union/-/union-0.5.0.tgz",
+ "integrity": "sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA==",
"dev": true,
- "license": "MIT",
+ "dependencies": {
+ "qs": "^6.4.0"
+ },
"engines": {
- "node": ">= 10.0.0"
+ "node": ">= 0.8.0"
}
},
"node_modules/unplugin": {
@@ -8645,6 +9282,13 @@
"punycode": "^2.1.0"
}
},
+ "node_modules/url-join": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz",
+ "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/use-state-with-deps": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/use-state-with-deps/-/use-state-with-deps-1.1.2.tgz",
@@ -8810,19 +9454,19 @@
}
},
"node_modules/vitest": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.1.tgz",
- "integrity": "sha512-yF+o4POL41rpAzj5KVILUxm1GCjKnELvaqmU9TLLUbMfDzuN0UpUR9uaDs+mCtjPe+uYPksXDRLQGGPvj1cTmA==",
+ "version": "4.1.4",
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.4.tgz",
+ "integrity": "sha512-tFuJqTxKb8AvfyqMfnavXdzfy3h3sWZRWwfluGbkeR7n0HUev+FmNgZ8SDrRBTVrVCjgH5cA21qGbCffMNtWvg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/expect": "4.1.1",
- "@vitest/mocker": "4.1.1",
- "@vitest/pretty-format": "4.1.1",
- "@vitest/runner": "4.1.1",
- "@vitest/snapshot": "4.1.1",
- "@vitest/spy": "4.1.1",
- "@vitest/utils": "4.1.1",
+ "@vitest/expect": "4.1.4",
+ "@vitest/mocker": "4.1.4",
+ "@vitest/pretty-format": "4.1.4",
+ "@vitest/runner": "4.1.4",
+ "@vitest/snapshot": "4.1.4",
+ "@vitest/spy": "4.1.4",
+ "@vitest/utils": "4.1.4",
"es-module-lexer": "^2.0.0",
"expect-type": "^1.3.0",
"magic-string": "^0.30.21",
@@ -8833,7 +9477,7 @@
"tinybench": "^2.9.0",
"tinyexec": "^1.0.2",
"tinyglobby": "^0.2.15",
- "tinyrainbow": "^3.0.3",
+ "tinyrainbow": "^3.1.0",
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0",
"why-is-node-running": "^2.3.0"
},
@@ -8850,10 +9494,12 @@
"@edge-runtime/vm": "*",
"@opentelemetry/api": "^1.9.0",
"@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
- "@vitest/browser-playwright": "4.1.1",
- "@vitest/browser-preview": "4.1.1",
- "@vitest/browser-webdriverio": "4.1.1",
- "@vitest/ui": "4.1.1",
+ "@vitest/browser-playwright": "4.1.4",
+ "@vitest/browser-preview": "4.1.4",
+ "@vitest/browser-webdriverio": "4.1.4",
+ "@vitest/coverage-istanbul": "4.1.4",
+ "@vitest/coverage-v8": "4.1.4",
+ "@vitest/ui": "4.1.4",
"happy-dom": "*",
"jsdom": "*",
"vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
@@ -8877,6 +9523,12 @@
"@vitest/browser-webdriverio": {
"optional": true
},
+ "@vitest/coverage-istanbul": {
+ "optional": true
+ },
+ "@vitest/coverage-v8": {
+ "optional": true
+ },
"@vitest/ui": {
"optional": true
},
@@ -8892,40 +9544,40 @@
}
},
"node_modules/vitest/node_modules/@vitest/expect": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.1.tgz",
- "integrity": "sha512-xAV0fqBTk44Rn6SjJReEQkHP3RrqbJo6JQ4zZ7/uVOiJZRarBtblzrOfFIZeYUrukp2YD6snZG6IBqhOoHTm+A==",
+ "version": "4.1.4",
+ "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.4.tgz",
+ "integrity": "sha512-iPBpra+VDuXmBFI3FMKHSFXp3Gx5HfmSCE8X67Dn+bwephCnQCaB7qWK2ldHa+8ncN8hJU8VTMcxjPpyMkUjww==",
"dev": true,
"license": "MIT",
"dependencies": {
"@standard-schema/spec": "^1.1.0",
"@types/chai": "^5.2.2",
- "@vitest/spy": "4.1.1",
- "@vitest/utils": "4.1.1",
+ "@vitest/spy": "4.1.4",
+ "@vitest/utils": "4.1.4",
"chai": "^6.2.2",
- "tinyrainbow": "^3.0.3"
+ "tinyrainbow": "^3.1.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/vitest/node_modules/@vitest/pretty-format": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.1.tgz",
- "integrity": "sha512-GM+TEQN5WhOygr1lp7skeVjdLPqqWMHsfzXrcHAqZJi/lIVh63H0kaRCY8MDhNWikx19zBUK8ceaLB7X5AH9NQ==",
+ "version": "4.1.4",
+ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.4.tgz",
+ "integrity": "sha512-ddmDHU0gjEUyEVLxtZa7xamrpIefdEETu3nZjWtHeZX4QxqJ7tRxSteHVXJOcr8jhiLoGAhkK4WJ3WqBpjx42A==",
"dev": true,
"license": "MIT",
"dependencies": {
- "tinyrainbow": "^3.0.3"
+ "tinyrainbow": "^3.1.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
}
},
"node_modules/vitest/node_modules/@vitest/spy": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.1.tgz",
- "integrity": "sha512-6Ti/KT5OVaiupdIZEuZN7l3CZcR0cxnxt70Z0//3CtwgObwA6jZhmVBA3yrXSVN3gmwjgd7oDNLlsXz526gpRA==",
+ "version": "4.1.4",
+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.4.tgz",
+ "integrity": "sha512-XxNdAsKW7C+FLydqFJLb5KhJtl3PGCMmYwFRfhvIgxJvLSXhhVI1zM8f1qD3Zg7RCjTSzDVyct6sghs9UEgBEQ==",
"dev": true,
"license": "MIT",
"funding": {
@@ -8933,15 +9585,15 @@
}
},
"node_modules/vitest/node_modules/@vitest/utils": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.1.tgz",
- "integrity": "sha512-cNxAlaB3sHoCdL6pj6yyUXv9Gry1NHNg0kFTXdvSIZXLHsqKH7chiWOkwJ5s5+d/oMwcoG9T0bKU38JZWKusrQ==",
+ "version": "4.1.4",
+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.4.tgz",
+ "integrity": "sha512-13QMT+eysM5uVGa1rG4kegGYNp6cnQcsTc67ELFbhNLQO+vgsygtYJx2khvdt4gVQqSSpC/KT5FZZxUpP3Oatw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@vitest/pretty-format": "4.1.1",
+ "@vitest/pretty-format": "4.1.4",
"convert-source-map": "^2.0.0",
- "tinyrainbow": "^3.0.3"
+ "tinyrainbow": "^3.1.0"
},
"funding": {
"url": "https://opencollective.com/vitest"
@@ -9009,6 +9661,20 @@
"integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==",
"dev": true
},
+ "node_modules/whatwg-encoding": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz",
+ "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==",
+ "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "iconv-lite": "0.6.3"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/whatwg-mimetype": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz",
diff --git a/package.json b/package.json
index a10c23e..e55332e 100644
--- a/package.json
+++ b/package.json
@@ -25,14 +25,15 @@
"zod": "^4.3.6"
},
"devDependencies": {
- "@chromatic-com/storybook": "^5.1.0",
"@eslint/css": "^0.14.1",
"@eslint/js": "^9.39.2",
"@faker-js/faker": "^10.4.0",
+ "@playwright/test": "^1.59.1",
"@storybook/addon-docs": "^10.3.3",
"@storybook/addon-links": "^10.3.3",
"@storybook/addon-onboarding": "^10.3.3",
"@storybook/addon-themes": "^10.3.3",
+ "@storybook/addon-vitest": "^10.3.5",
"@storybook/react-vite": "^10.3.3",
"@tailwindcss/forms": "^0.5.11",
"@tailwindcss/vite": "^4.2.2",
@@ -47,6 +48,7 @@
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react-swc": "^4.3.0",
+ "@vitest/browser-playwright": "^4.1.4",
"concurrently": "^9.2.1",
"eslint": "^9.39.2",
"eslint-config-prettier": "^10.1.8",
@@ -56,6 +58,7 @@
"eslint-plugin-storybook": "^10.3.3",
"fishery": "^2.4.0",
"globals": "^17.4.0",
+ "http-server": "^14.1.1",
"jsdom": "^29.0.1",
"prettier": "^3.8.1",
"prettier-plugin-tailwindcss": "^0.7.2",
@@ -80,8 +83,11 @@
"lint": "eslint --cache --report-unused-disable-directives --max-warnings 0 . && prettier --cache --log-level error --check .",
"preview": "LIVE_FS=true reflex -c .reflex.server",
"storybook": "storybook dev -p 6006",
- "test": "vitest",
- "test:once": "vitest run",
+ "test": "vitest --project=unit",
+ "test:once": "vitest run --project=unit",
+ "test-storybook": "vitest --project=storybook --run",
+ "test-visual": "playwright test tests/visual/storybook.spec.ts",
+ "test-visual:update": "playwright test tests/visual/storybook.spec.ts --update-snapshots",
"watch:frontend": "vite --open http://localhost:8080/",
"watch:backend": "LIVE_FS=true DEV=true reflex -c .reflex.server",
"watch:backend:pro": "LIVE_FS=true DEV=true reflex -c .reflex.pro.server"
diff --git a/playwright.config.ts b/playwright.config.ts
new file mode 100644
index 0000000..bdab65d
--- /dev/null
+++ b/playwright.config.ts
@@ -0,0 +1,41 @@
+import { defineConfig } from "@playwright/test";
+
+const isCI = process.env.CI === "true";
+const storybookPort = Number(process.env.STORYBOOK_PORT ?? "6006");
+
+export default defineConfig({
+ expect: {
+ toHaveScreenshot: {
+ animations: "disabled",
+ },
+ },
+ outputDir: "test-results",
+ reporter: isCI
+ ? [
+ ["line"],
+ [
+ "html",
+ {
+ open: "never",
+ outputFolder: "playwright-report",
+ },
+ ],
+ ]
+ : [["line"]],
+ snapshotPathTemplate: "{testDir}/{testFilePath}-snapshots/{arg}{ext}",
+ testDir: ".",
+ testMatch: ["tests/visual/**/*.spec.ts"],
+ timeout: 60_000,
+ use: {
+ browserName: "chromium",
+ headless: true,
+ locale: "en-US",
+ timezoneId: "UTC",
+ },
+ webServer: {
+ command: `npm run build-storybook && npx http-server storybook-static --port ${storybookPort} --silent`,
+ port: storybookPort,
+ reuseExistingServer: false,
+ timeout: 240_000,
+ },
+});
diff --git a/src/components/Badge.stories.tsx b/src/components/Badge.stories.tsx
index 100f966..89166d1 100644
--- a/src/components/Badge.stories.tsx
+++ b/src/components/Badge.stories.tsx
@@ -43,4 +43,5 @@ export const AllColors: Story = {
),
+ tags: ["visual"],
};
diff --git a/src/components/JSONView.stories.tsx b/src/components/JSONView.stories.tsx
index 38b530f..791fb0b 100644
--- a/src/components/JSONView.stories.tsx
+++ b/src/components/JSONView.stories.tsx
@@ -206,6 +206,7 @@ export const Simple: Story = {
data: simpleObject,
defaultExpandDepth: 5,
},
+ tags: ["visual"],
};
export const Nested: Story = {
@@ -222,6 +223,7 @@ export const NestedCollapsed: Story = {
data: nestedObject,
defaultExpandDepth: 0,
},
+ tags: ["visual"],
};
export const NestedCollapsedHiddenKeys: Story = {
@@ -254,6 +256,7 @@ export const LongStrings: Story = {
data: longStringExample,
defaultExpandDepth: 3,
},
+ tags: ["visual"],
};
export const LargeJSON: Story = {
diff --git a/src/components/JobDetail.stories.ts b/src/components/JobDetail.stories.ts
index c14e567..935d693 100644
--- a/src/components/JobDetail.stories.ts
+++ b/src/components/JobDetail.stories.ts
@@ -1,5 +1,7 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
+import { Job } from "@services/jobs";
+import { JobState } from "@services/types";
import { jobFactory } from "@test/factories/job";
import JobDetail from "./JobDetail";
@@ -13,6 +15,31 @@ export default meta;
type Story = StoryObj;
+const visualJob: Job = {
+ args: {
+ amountCents: 4200,
+ customerID: "cus_123",
+ },
+ attempt: 1,
+ attemptedAt: new Date("2025-02-28T12:04:00.000Z"),
+ attemptedBy: ["worker-billing-1"],
+ createdAt: new Date("2025-02-28T12:00:00.000Z"),
+ errors: [],
+ finalizedAt: undefined,
+ id: BigInt(4242),
+ kind: "ChargeCustomer",
+ logs: {
+ 1: "starting execution\ncalling payment provider",
+ },
+ maxAttempts: 5,
+ metadata: {},
+ priority: 2,
+ queue: "billing",
+ scheduledAt: new Date("2025-02-28T12:03:00.000Z"),
+ state: JobState.Running,
+ tags: ["billing", "critical"],
+};
+
export const Scheduled: Story = {
args: {
job: jobFactory.scheduled().build(),
@@ -54,3 +81,13 @@ export const Cancelled: Story = {
job: jobFactory.cancelled().build(),
},
};
+
+export const VisualRegression: Story = {
+ args: {
+ cancel: () => {},
+ deleteFn: () => {},
+ job: visualJob,
+ retry: () => {},
+ },
+ tags: ["visual"],
+};
diff --git a/src/components/JobList.stories.ts b/src/components/JobList.stories.ts
index 54bdd71..ee16bb8 100644
--- a/src/components/JobList.stories.ts
+++ b/src/components/JobList.stories.ts
@@ -1,5 +1,7 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
+import { JobMinimal } from "@services/jobs";
+import { StatesAndCounts } from "@services/states";
import { JobState } from "@services/types";
import { jobMinimalFactory } from "@test/factories/job";
import { createFeatures } from "@test/utils/features";
@@ -15,10 +17,74 @@ export default meta;
type Story = StoryObj;
+const buildRunningJobs = (count: number) =>
+ Array.from({ length: count }, () => jobMinimalFactory.running().build());
+
+const visualJobs: JobMinimal[] = [
+ {
+ args: { customerID: "cus_123", invoiceID: "inv_001" },
+ attempt: 1,
+ attemptedAt: new Date("2025-02-28T12:01:00.000Z"),
+ attemptedBy: ["worker-1"],
+ createdAt: new Date("2025-02-28T12:00:00.000Z"),
+ finalizedAt: undefined,
+ id: BigInt(101),
+ kind: "ChargeCustomer",
+ maxAttempts: 5,
+ priority: 1,
+ queue: "billing",
+ scheduledAt: new Date("2025-02-28T12:00:30.000Z"),
+ state: JobState.Running,
+ tags: ["billing"],
+ },
+ {
+ args: { customerID: "cus_456", invoiceID: "inv_002" },
+ attempt: 2,
+ attemptedAt: new Date("2025-02-28T11:56:00.000Z"),
+ attemptedBy: ["worker-2"],
+ createdAt: new Date("2025-02-28T11:55:00.000Z"),
+ finalizedAt: undefined,
+ id: BigInt(102),
+ kind: "ChargeCustomer",
+ maxAttempts: 5,
+ priority: 2,
+ queue: "billing",
+ scheduledAt: new Date("2025-02-28T11:55:30.000Z"),
+ state: JobState.Running,
+ tags: ["billing", "retrying"],
+ },
+ {
+ args: { customerID: "cus_789", invoiceID: "inv_003" },
+ attempt: 1,
+ attemptedAt: new Date("2025-02-28T11:52:00.000Z"),
+ attemptedBy: ["worker-3"],
+ createdAt: new Date("2025-02-28T11:50:00.000Z"),
+ finalizedAt: undefined,
+ id: BigInt(103),
+ kind: "ChargeCustomer",
+ maxAttempts: 3,
+ priority: 1,
+ queue: "default",
+ scheduledAt: new Date("2025-02-28T11:50:30.000Z"),
+ state: JobState.Running,
+ tags: ["default"],
+ },
+];
+
+const visualStatesAndCounts: StatesAndCounts = {
+ available: BigInt(8),
+ cancelled: BigInt(0),
+ completed: BigInt(31),
+ discarded: BigInt(1),
+ pending: BigInt(4),
+ retryable: BigInt(2),
+ running: BigInt(3),
+ scheduled: BigInt(6),
+};
// Default running jobs story
export const Running: Story = {
args: {
- jobs: jobMinimalFactory.running().buildList(10),
+ jobs: buildRunningJobs(10),
setJobRefetchesPaused: () => {},
state: JobState.Running,
},
@@ -34,7 +100,7 @@ export const Running: Story = {
export const ArgsHiddenByDefault: Story = {
args: {
...Running.args,
- jobs: jobMinimalFactory.running().buildList(10),
+ jobs: buildRunningJobs(10),
},
parameters: {
features: createFeatures({
@@ -48,7 +114,7 @@ export const ArgsHiddenByDefault: Story = {
export const ArgsVisibleUserOverride: Story = {
args: {
...Running.args,
- jobs: jobMinimalFactory.running().buildList(10),
+ jobs: buildRunningJobs(10),
},
parameters: {
features: createFeatures({
@@ -62,7 +128,7 @@ export const ArgsVisibleUserOverride: Story = {
export const ArgsHiddenUserOverride: Story = {
args: {
...Running.args,
- jobs: jobMinimalFactory.running().buildList(10),
+ jobs: buildRunningJobs(10),
},
parameters: {
features: createFeatures({
@@ -71,3 +137,30 @@ export const ArgsHiddenUserOverride: Story = {
settings: { showJobArgs: false },
},
};
+
+export const VisualRegression: Story = {
+ args: {
+ cancelJobs: () => {},
+ canShowFewer: true,
+ canShowMore: true,
+ deleteJobs: () => {},
+ jobs: visualJobs,
+ retryJobs: () => {},
+ setJobRefetchesPaused: () => {},
+ showFewer: () => {},
+ showMore: () => {},
+ state: JobState.Running,
+ statesAndCounts: visualStatesAndCounts,
+ },
+ parameters: {
+ features: createFeatures({
+ jobListHideArgsByDefault: false,
+ }),
+ router: {
+ initialEntries: ["/jobs?state=running"],
+ routes: ["/jobs", "/jobs/$jobId"],
+ },
+ settings: {},
+ },
+ tags: ["visual"],
+};
diff --git a/src/components/JobTimeline.stories.tsx b/src/components/JobTimeline.stories.tsx
index b381c02..5f24cd4 100644
--- a/src/components/JobTimeline.stories.tsx
+++ b/src/components/JobTimeline.stories.tsx
@@ -13,6 +13,7 @@ const meta: Meta = {
export default meta;
type Story = StoryObj;
+const fixedCurrentTime = new Date("2025-02-01T08:00:00.000Z");
export const Pending: Story = {
args: {
@@ -58,9 +59,9 @@ export const Retryable: Story = {
export const RetryableOverdue: Story = {
args: {
- job: jobFactory
- .retryable()
- .build({ scheduledAt: sub(Date.now(), { minutes: 2, seconds: 30 }) }),
+ job: jobFactory.retryable().build({
+ scheduledAt: sub(fixedCurrentTime, { minutes: 2, seconds: 30 }),
+ }),
},
};
diff --git a/src/components/PeriodicJobList.stories.tsx b/src/components/PeriodicJobList.stories.tsx
index 294bc92..a4dd77c 100644
--- a/src/components/PeriodicJobList.stories.tsx
+++ b/src/components/PeriodicJobList.stories.tsx
@@ -15,17 +15,19 @@ const meta: Meta = {
export default meta;
type Story = StoryObj;
+const baseTime = new Date("2025-01-15T12:00:00.000Z");
+const oneDayInMs = 24 * 60 * 60 * 1000;
+
// Helper function to create mock periodic jobs
const createMockJob = (
id: string,
daysAgo: number,
nextRunDays: number,
): PeriodicJob => {
- const now = new Date();
- const createdAt = new Date(now.getTime() - daysAgo * 24 * 60 * 60 * 1000);
- const nextRunAt = new Date(now.getTime() + nextRunDays * 24 * 60 * 60 * 1000);
+ const createdAt = new Date(baseTime.getTime() - daysAgo * oneDayInMs);
+ const nextRunAt = new Date(baseTime.getTime() + nextRunDays * oneDayInMs);
const updatedAt = new Date(
- now.getTime() - Math.floor(daysAgo / 2) * 24 * 60 * 60 * 1000,
+ baseTime.getTime() - Math.floor(daysAgo / 2) * oneDayInMs,
);
return {
@@ -36,6 +38,13 @@ const createMockJob = (
};
};
+const manyJobDaysAgo = [
+ 5, 11, 23, 2, 18, 7, 26, 13, 9, 30, 16, 4, 21, 14, 1, 28, 8, 19, 6, 24,
+];
+const manyJobNextRunDays = [
+ 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 3, 5, 7, 9, 11,
+];
+
export const Loading: Story = {
args: {
jobs: [],
@@ -104,12 +113,8 @@ export const LongJobIds: Story = {
export const ManyJobs: Story = {
args: {
- jobs: Array.from({ length: 20 }, (_, i) =>
- createMockJob(
- `periodic-job-${i + 1}`,
- Math.floor(Math.random() * 30),
- Math.floor(Math.random() * 30) + 1,
- ),
+ jobs: manyJobDaysAgo.map((daysAgo, i) =>
+ createMockJob(`periodic-job-${i + 1}`, daysAgo, manyJobNextRunDays[i]),
),
loading: false,
},
@@ -136,3 +141,10 @@ export const FutureJobs: Story = {
loading: false,
},
};
+
+export const VisualRegression: Story = {
+ args: {
+ ...MultipleJobs.args,
+ },
+ tags: ["visual"],
+};
diff --git a/src/components/QueueDetail.stories.tsx b/src/components/QueueDetail.stories.tsx
index 1adcff6..16d0df1 100644
--- a/src/components/QueueDetail.stories.tsx
+++ b/src/components/QueueDetail.stories.tsx
@@ -1,6 +1,6 @@
import { useFeatures } from "@contexts/Features.hook";
import { type Producer } from "@services/producers";
-import { type ConcurrencyConfig } from "@services/queues";
+import { type ConcurrencyConfig, type Queue } from "@services/queues";
import { Meta, StoryObj } from "@storybook/react-vite";
import { producerFactory } from "@test/factories/producer";
import { queueFactory } from "@test/factories/queue";
@@ -57,6 +57,59 @@ const createProducers = (
});
};
+const visualQueue: Queue = {
+ concurrency: {
+ global_limit: 10,
+ local_limit: 5,
+ partition: {
+ by_args: ["customer_id", "region"],
+ by_kind: null,
+ },
+ },
+ countAvailable: 27,
+ countRunning: 9,
+ createdAt: new Date("2025-02-20T10:00:00.000Z"),
+ name: "payments",
+ pausedAt: undefined,
+ updatedAt: new Date("2025-02-28T10:30:00.000Z"),
+};
+
+const visualProducers: Producer[] = [
+ {
+ clientId: "worker-a",
+ concurrency: visualQueue.concurrency,
+ createdAt: new Date("2025-02-20T10:00:00.000Z"),
+ id: 1,
+ maxWorkers: 20,
+ pausedAt: undefined,
+ queueName: "payments",
+ running: 4,
+ updatedAt: new Date("2025-02-28T10:31:00.000Z"),
+ },
+ {
+ clientId: "worker-b",
+ concurrency: visualQueue.concurrency,
+ createdAt: new Date("2025-02-20T10:05:00.000Z"),
+ id: 2,
+ maxWorkers: 20,
+ pausedAt: undefined,
+ queueName: "payments",
+ running: 3,
+ updatedAt: new Date("2025-02-28T10:31:00.000Z"),
+ },
+ {
+ clientId: "worker-c",
+ concurrency: visualQueue.concurrency,
+ createdAt: new Date("2025-02-20T10:10:00.000Z"),
+ id: 3,
+ maxWorkers: 20,
+ pausedAt: undefined,
+ queueName: "payments",
+ running: 2,
+ updatedAt: new Date("2025-02-28T10:31:00.000Z"),
+ },
+];
+
const meta: Meta = {
args: {
loading: false,
@@ -266,3 +319,18 @@ export const WithoutPro: Story = {
}),
},
};
+
+export const VisualRegression: Story = {
+ args: {
+ name: "payments",
+ producers: visualProducers,
+ queue: visualQueue,
+ },
+ parameters: {
+ features: createFeatures({
+ hasProducerTable: true,
+ producerQueries: true,
+ }),
+ },
+ tags: ["visual"],
+};
diff --git a/src/components/TagInput.stories.ts b/src/components/TagInput.stories.ts
index 761d04b..5f62702 100644
--- a/src/components/TagInput.stories.ts
+++ b/src/components/TagInput.stories.ts
@@ -32,6 +32,7 @@ export const Empty: Story = {
showHelpText: false,
tags: [],
},
+ tags: ["visual"],
};
export const EmptyWithHelp: Story = {
@@ -41,6 +42,7 @@ export const EmptyWithHelp: Story = {
showHelpText: true,
tags: [],
},
+ tags: ["visual"],
};
export const WithTags: Story = {
@@ -50,6 +52,7 @@ export const WithTags: Story = {
showHelpText: false,
tags: ["customer_id", "region", "user_id"],
},
+ tags: ["visual"],
};
export const WithTagsAndHelp: Story = {
@@ -74,6 +77,7 @@ export const WithManyTags: Story = {
"long_key_name_with_many_characters",
],
},
+ tags: ["visual"],
};
export const BlueBadges: Story = {
@@ -97,6 +101,7 @@ export const Disabled: Story = {
disabled: true,
tags: ["customer_id", "region"],
},
+ tags: ["visual"],
};
export const DisabledEmpty: Story = {
diff --git a/src/components/WorkflowDetail.stories.tsx b/src/components/WorkflowDetail.stories.tsx
new file mode 100644
index 0000000..b8b9f9f
--- /dev/null
+++ b/src/components/WorkflowDetail.stories.tsx
@@ -0,0 +1,83 @@
+import type { Meta, StoryObj } from "@storybook/react-vite";
+
+import { JobWithKnownMetadata } from "@services/jobs";
+import { JobState } from "@services/types";
+import { Workflow } from "@services/workflows";
+import { workflowJobFactory } from "@test/factories/workflowJob";
+import { createFeatures } from "@test/utils/features";
+
+import WorkflowDetail from "./WorkflowDetail";
+
+const workflowID = "wf_visual_001";
+const workflowName = "Nightly Billing Workflow";
+const workflowStagedAt = new Date("2025-02-28T12:00:00.000Z");
+
+const buildTask = (
+ id: bigint,
+ task: string,
+ state: JobState,
+ deps: string[] = [],
+): JobWithKnownMetadata => {
+ const workflowTask = workflowJobFactory
+ .params({
+ deps,
+ id,
+ state,
+ task,
+ workflowID,
+ workflowStagedAt,
+ })
+ .build();
+
+ return {
+ ...workflowTask,
+ metadata: {
+ ...workflowTask.metadata,
+ workflow_name: workflowName,
+ },
+ };
+};
+
+const visualWorkflow: Workflow = {
+ tasks: [
+ buildTask(BigInt(101), "ingest", JobState.Completed),
+ buildTask(BigInt(102), "validate", JobState.Running, ["ingest"]),
+ buildTask(BigInt(103), "notify", JobState.Pending, ["validate"]),
+ buildTask(BigInt(104), "archive", JobState.Pending, ["validate"]),
+ ],
+};
+
+const meta: Meta = {
+ component: WorkflowDetail,
+ title: "Pages/WorkflowDetail",
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const VisualRegression: Story = {
+ args: {
+ cancelPending: false,
+ loading: false,
+ onCancel: () => {},
+ onRetry: () => {},
+ retryPending: false,
+ selectedJobId: BigInt(102),
+ setSelectedJobId: () => {},
+ workflow: visualWorkflow,
+ },
+ parameters: {
+ features: createFeatures({
+ hasWorkflows: true,
+ workflowQueries: true,
+ }),
+ router: {
+ initialEntries: [`/workflows/${workflowID}`],
+ routes: ["/jobs/$jobId", "/workflows/$workflowId"],
+ },
+ visual: {
+ waitFor: ".react-flow__node",
+ },
+ },
+ tags: ["visual"],
+};
diff --git a/tests/visual/storybook.spec.ts b/tests/visual/storybook.spec.ts
new file mode 100644
index 0000000..9b4d081
--- /dev/null
+++ b/tests/visual/storybook.spec.ts
@@ -0,0 +1,79 @@
+import { expect, test } from "@playwright/test";
+import fs from "node:fs";
+import path from "node:path";
+
+type StoryIndex = {
+ entries?: Record;
+ stories?: Record;
+};
+
+type StoryIndexEntry = {
+ id: string;
+ name: string;
+ parameters?: {
+ visual?: VisualParameters;
+ };
+ tags?: string[];
+ type?: string;
+};
+
+type VisualParameters = {
+ viewport?: "desktop" | "mobile";
+ waitFor?: string;
+};
+
+const desktopViewport = { height: 900, width: 1280 };
+const mobileViewport = { height: 844, width: 390 };
+const storybookPort = process.env.STORYBOOK_PORT ?? "6006";
+const storybookBaseURL = `http://127.0.0.1:${storybookPort}`;
+const storybookIndexPath = path.resolve("storybook-static/index.json");
+
+const readVisualStories = (): StoryIndexEntry[] => {
+ const fileContents = fs.readFileSync(storybookIndexPath, "utf-8");
+ const index = JSON.parse(fileContents) as StoryIndex;
+
+ const entries = Object.values(index.entries ?? index.stories ?? {});
+
+ return entries
+ .filter((entry) => entry.type === "story" && entry.tags?.includes("visual"))
+ .sort((a, b) => a.id.localeCompare(b.id));
+};
+
+const visualStories = readVisualStories();
+
+test.describe("storybook visual snapshots", () => {
+ test("has at least one visual story", () => {
+ expect(visualStories.length).toBeGreaterThan(0);
+ });
+
+ for (const story of visualStories) {
+ test(`captures ${story.id}`, async ({ page }) => {
+ const viewport =
+ story.parameters?.visual?.viewport === "mobile"
+ ? mobileViewport
+ : desktopViewport;
+
+ await page.setViewportSize(viewport);
+ await page.emulateMedia({ reducedMotion: "reduce" });
+
+ const storyURL = new URL("/iframe.html", storybookBaseURL);
+ storyURL.searchParams.set("id", story.id);
+ storyURL.searchParams.set("viewMode", "story");
+ storyURL.searchParams.set("visual-test", "true");
+
+ await page.goto(storyURL.toString(), { waitUntil: "networkidle" });
+ await page.locator("#storybook-root").waitFor();
+
+ await page.evaluate(async () => {
+ await document.fonts.ready;
+ });
+
+ const waitForSelector = story.parameters?.visual?.waitFor;
+ if (waitForSelector) {
+ await page.locator(waitForSelector).first().waitFor();
+ }
+
+ await expect(page).toHaveScreenshot(`${story.id}.png`);
+ });
+ }
+});
diff --git a/tests/visual/storybook.spec.ts-snapshots/components-badge--all-colors.png b/tests/visual/storybook.spec.ts-snapshots/components-badge--all-colors.png
new file mode 100644
index 0000000..3182a74
Binary files /dev/null and b/tests/visual/storybook.spec.ts-snapshots/components-badge--all-colors.png differ
diff --git a/tests/visual/storybook.spec.ts-snapshots/components-jsonview--long-strings.png b/tests/visual/storybook.spec.ts-snapshots/components-jsonview--long-strings.png
new file mode 100644
index 0000000..51c9ba6
Binary files /dev/null and b/tests/visual/storybook.spec.ts-snapshots/components-jsonview--long-strings.png differ
diff --git a/tests/visual/storybook.spec.ts-snapshots/components-jsonview--nested-collapsed.png b/tests/visual/storybook.spec.ts-snapshots/components-jsonview--nested-collapsed.png
new file mode 100644
index 0000000..dd9145a
Binary files /dev/null and b/tests/visual/storybook.spec.ts-snapshots/components-jsonview--nested-collapsed.png differ
diff --git a/tests/visual/storybook.spec.ts-snapshots/components-jsonview--simple.png b/tests/visual/storybook.spec.ts-snapshots/components-jsonview--simple.png
new file mode 100644
index 0000000..6ed3258
Binary files /dev/null and b/tests/visual/storybook.spec.ts-snapshots/components-jsonview--simple.png differ
diff --git a/tests/visual/storybook.spec.ts-snapshots/components-taginput--disabled.png b/tests/visual/storybook.spec.ts-snapshots/components-taginput--disabled.png
new file mode 100644
index 0000000..0c54374
Binary files /dev/null and b/tests/visual/storybook.spec.ts-snapshots/components-taginput--disabled.png differ
diff --git a/tests/visual/storybook.spec.ts-snapshots/components-taginput--empty-with-help.png b/tests/visual/storybook.spec.ts-snapshots/components-taginput--empty-with-help.png
new file mode 100644
index 0000000..e8d92c6
Binary files /dev/null and b/tests/visual/storybook.spec.ts-snapshots/components-taginput--empty-with-help.png differ
diff --git a/tests/visual/storybook.spec.ts-snapshots/components-taginput--empty.png b/tests/visual/storybook.spec.ts-snapshots/components-taginput--empty.png
new file mode 100644
index 0000000..c39d107
Binary files /dev/null and b/tests/visual/storybook.spec.ts-snapshots/components-taginput--empty.png differ
diff --git a/tests/visual/storybook.spec.ts-snapshots/components-taginput--with-many-tags.png b/tests/visual/storybook.spec.ts-snapshots/components-taginput--with-many-tags.png
new file mode 100644
index 0000000..044eb4d
Binary files /dev/null and b/tests/visual/storybook.spec.ts-snapshots/components-taginput--with-many-tags.png differ
diff --git a/tests/visual/storybook.spec.ts-snapshots/components-taginput--with-tags.png b/tests/visual/storybook.spec.ts-snapshots/components-taginput--with-tags.png
new file mode 100644
index 0000000..7128dba
Binary files /dev/null and b/tests/visual/storybook.spec.ts-snapshots/components-taginput--with-tags.png differ
diff --git a/tests/visual/storybook.spec.ts-snapshots/pages-jobdetail--visual-regression.png b/tests/visual/storybook.spec.ts-snapshots/pages-jobdetail--visual-regression.png
new file mode 100644
index 0000000..9df0d20
Binary files /dev/null and b/tests/visual/storybook.spec.ts-snapshots/pages-jobdetail--visual-regression.png differ
diff --git a/tests/visual/storybook.spec.ts-snapshots/pages-joblist--visual-regression.png b/tests/visual/storybook.spec.ts-snapshots/pages-joblist--visual-regression.png
new file mode 100644
index 0000000..d084695
Binary files /dev/null and b/tests/visual/storybook.spec.ts-snapshots/pages-joblist--visual-regression.png differ
diff --git a/tests/visual/storybook.spec.ts-snapshots/pages-periodicjoblist--visual-regression.png b/tests/visual/storybook.spec.ts-snapshots/pages-periodicjoblist--visual-regression.png
new file mode 100644
index 0000000..dedb57f
Binary files /dev/null and b/tests/visual/storybook.spec.ts-snapshots/pages-periodicjoblist--visual-regression.png differ
diff --git a/tests/visual/storybook.spec.ts-snapshots/pages-queuedetail--visual-regression.png b/tests/visual/storybook.spec.ts-snapshots/pages-queuedetail--visual-regression.png
new file mode 100644
index 0000000..5ece80d
Binary files /dev/null and b/tests/visual/storybook.spec.ts-snapshots/pages-queuedetail--visual-regression.png differ
diff --git a/tests/visual/storybook.spec.ts-snapshots/pages-workflowdetail--visual-regression.png b/tests/visual/storybook.spec.ts-snapshots/pages-workflowdetail--visual-regression.png
new file mode 100644
index 0000000..072dfc2
Binary files /dev/null and b/tests/visual/storybook.spec.ts-snapshots/pages-workflowdetail--visual-regression.png differ
diff --git a/tests/visual/visual.css b/tests/visual/visual.css
new file mode 100644
index 0000000..ede26c6
--- /dev/null
+++ b/tests/visual/visual.css
@@ -0,0 +1,11 @@
+html[data-visual-test="true"] *,
+html[data-visual-test="true"] *::before,
+html[data-visual-test="true"] *::after {
+ animation: none !important;
+ caret-color: transparent !important;
+ transition: none !important;
+}
+
+html[data-visual-test="true"] {
+ scroll-behavior: auto !important;
+}
diff --git a/vitest.config.ts b/vitest.config.ts
index 28d763a..ccc8b27 100644
--- a/vitest.config.ts
+++ b/vitest.config.ts
@@ -1,11 +1,45 @@
+import { storybookTest } from "@storybook/addon-vitest/vitest-plugin";
+import { playwright } from "@vitest/browser-playwright";
+import path from "node:path";
+import { fileURLToPath } from "node:url";
import tsconfigPaths from "vite-tsconfig-paths";
import { defineConfig } from "vitest/config";
+const dirname =
+ typeof __dirname !== "undefined"
+ ? __dirname
+ : path.dirname(fileURLToPath(import.meta.url));
+
export default defineConfig({
plugins: [tsconfigPaths()],
test: {
- environment: "jsdom",
- globals: true,
- setupFiles: ["./src/test/setup.ts"],
+ projects: [
+ {
+ extends: true,
+ test: {
+ environment: "jsdom",
+ exclude: ["node_modules/**", "tests/visual/**"],
+ globals: true,
+ include: ["src/**/*.test.{ts,tsx}"],
+ name: "unit",
+ setupFiles: ["./src/test/setup.ts"],
+ },
+ },
+ {
+ extends: true,
+ plugins: [
+ storybookTest({ configDir: path.join(dirname, ".storybook") }),
+ ],
+ test: {
+ browser: {
+ enabled: true,
+ headless: true,
+ instances: [{ browser: "chromium" }],
+ provider: playwright({}),
+ },
+ name: "storybook",
+ },
+ },
+ ],
},
});