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", + }, + }, + ], }, });