diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 2f07e58d..923a21f1 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -2,757 +2,76 @@ name: CI
on:
push:
- branches: [ main ]
+ branches: [main]
pull_request:
- branches: [ main ]
+ branches: [main]
jobs:
lint:
+ name: Lint
runs-on: ubuntu-latest
timeout-minutes: 8
- steps:
- - uses: actions/checkout@v4
-
- - name: Use Node.js 22
- uses: actions/setup-node@v4
- with:
- node-version: 22
- cache: 'npm'
-
- - name: Install dependencies
- run: npm ci
-
- - name: Run linter
- run: npm run lint
-
- - name: Check formatting
- run: npm run format:check
-
- test:
- runs-on: blacksmith-4vcpu-ubuntu-2404
- timeout-minutes: 8
- needs: lint
-
- strategy:
- matrix:
- node-version: [22, 24]
-
- steps:
- - uses: actions/checkout@v4
-
- - name: Use Node.js ${{ matrix.node-version }}
- uses: actions/setup-node@v4
- with:
- node-version: ${{ matrix.node-version }}
- cache: 'npm'
-
- - name: Install dependencies
- run: npm ci
-
- - name: Run tests
- run: npm test
- env:
- CI: true
-
- - name: Build
- run: npm run build
-
- - name: Run type tests
- run: npm run test:types
-
- test-reporter:
- runs-on: blacksmith-4vcpu-ubuntu-2404
- timeout-minutes: 8
- needs: lint
-
- steps:
- - uses: actions/checkout@v4
-
- - name: Use Node.js 22
- uses: actions/setup-node@v4
- with:
- node-version: 22
- cache: 'npm'
-
- - name: Install dependencies
- run: npm ci
-
- - name: Build
- run: npm run build
-
- - name: Get installed Playwright version
- id: playwright-version
- run: echo "version=$(npx playwright --version | grep -oE '[0-9]+\.[0-9]+\.[0-9]+')" >> $GITHUB_OUTPUT
-
- - name: Cache Playwright browsers
- uses: actions/cache@v4
- id: playwright-cache
- with:
- path: ~/.cache/ms-playwright
- key: playwright-browsers-${{ steps.playwright-version.outputs.version }}-firefox
-
- - name: Install Playwright browsers
- if: steps.playwright-cache.outputs.cache-hit != 'true'
- run: npx playwright install firefox --with-deps
-
- - name: Run reporter visual tests
- run: npm run test:reporter:visual
- env:
- CI: true
- VIZZLY_TOKEN: ${{ secrets.VIZZLY_REPORTER_TOKEN }}
- VIZZLY_COMMIT_MESSAGE: ${{ github.event.pull_request.head.commit.message || github.event.head_commit.message }}
- VIZZLY_COMMIT_SHA: ${{ github.event.pull_request.head.sha || github.event.head_commit.id }}
-
- test-tui:
- runs-on: ubuntu-latest
- timeout-minutes: 8
- needs: lint
-
- steps:
- - uses: actions/checkout@v4
-
- - name: Use Node.js 22
- uses: actions/setup-node@v4
- with:
- node-version: 22
- cache: 'npm'
-
- - name: Setup tui-driver
- uses: vizzly-testing/tui-driver@main
-
- - name: Install dependencies
- run: npm ci
-
- - name: Build
- run: npm run build
-
- - name: Run TUI visual tests
- run: node bin/vizzly.js run "npm run test:tui"
- env:
- CI: true
- VIZZLY_TOKEN: ${{ secrets.VIZZLY_TUI_TOKEN }}
- VIZZLY_COMMIT_MESSAGE: ${{ github.event.pull_request.head.commit.message || github.event.head_commit.message }}
- VIZZLY_COMMIT_SHA: ${{ github.event.pull_request.head.sha || github.event.head_commit.id }}
-
- changes-ruby:
- runs-on: ubuntu-latest
- outputs:
- ruby: ${{ steps.filter.outputs.ruby }}
steps:
- uses: actions/checkout@v4
- - uses: dorny/paths-filter@v3
- id: filter
- with:
- filters: |
- ruby:
- - 'clients/ruby/**'
- - '.github/workflows/ci.yml'
- - '.github/workflows/release-ruby-client.yml'
-
- test-ruby-client:
- runs-on: ubuntu-latest
- timeout-minutes: 8
- needs: [lint, changes-ruby]
- if: needs.changes-ruby.outputs.ruby == 'true'
-
- strategy:
- matrix:
- ruby-version: ['3.0', '3.1', '3.2', '3.3']
-
- steps:
- - uses: actions/checkout@v4
-
- - name: Set up Ruby ${{ matrix.ruby-version }}
- uses: ruby/setup-ruby@v1
- with:
- ruby-version: ${{ matrix.ruby-version }}
- - name: Use Node.js 22
- uses: actions/setup-node@v4
- with:
- node-version: 22
- cache: 'npm'
-
- - name: Install CLI dependencies
- run: npm ci
-
- - name: Build CLI
- run: npm run build
-
- - name: Install Ruby dependencies
- working-directory: ./clients/ruby
- run: |
- gem install bundler
- bundle install
-
- - name: Run RuboCop
- working-directory: ./clients/ruby
- run: bundle exec rubocop
-
- - name: Run Ruby unit tests
- working-directory: ./clients/ruby
- run: ruby -I lib test/vizzly_test.rb
-
- - name: Run Ruby integration tests
- working-directory: ./clients/ruby
- run: VIZZLY_INTEGRATION=1 ruby -I lib test/integration_test.rb
- env:
- CI: true
-
- changes-storybook:
- runs-on: ubuntu-latest
- outputs:
- storybook: ${{ steps.filter.outputs.storybook }}
- steps:
- - uses: actions/checkout@v4
- - uses: dorny/paths-filter@v3
- id: filter
- with:
- filters: |
- storybook:
- - 'clients/storybook/**'
- - '.github/workflows/ci.yml'
- - '.github/workflows/release-storybook-client.yml'
-
- changes-static-site:
- runs-on: ubuntu-latest
- outputs:
- static-site: ${{ steps.filter.outputs.static-site }}
- steps:
- - uses: actions/checkout@v4
- - uses: dorny/paths-filter@v3
- id: filter
- with:
- filters: |
- static-site:
- - 'clients/static-site/**'
- - '.github/workflows/ci.yml'
- - '.github/workflows/release-static-site-client.yml'
-
- changes-vitest:
- runs-on: ubuntu-latest
- outputs:
- vitest: ${{ steps.filter.outputs.vitest }}
- steps:
- - uses: actions/checkout@v4
- - uses: dorny/paths-filter@v3
- id: filter
- with:
- filters: |
- vitest:
- - 'clients/vitest/**'
- - '.github/workflows/ci.yml'
- - '.github/workflows/release-vitest-client.yml'
-
- changes-swift:
- runs-on: ubuntu-latest
- outputs:
- swift: ${{ steps.filter.outputs.swift }}
- steps:
- - uses: actions/checkout@v4
- - uses: dorny/paths-filter@v3
- id: filter
- with:
- filters: |
- swift:
- - 'clients/swift/**'
- - '.github/workflows/ci.yml'
- - '.github/workflows/release-swift-client.yml'
-
- changes-ember:
- runs-on: ubuntu-latest
- outputs:
- ember: ${{ steps.filter.outputs.ember }}
- steps:
- - uses: actions/checkout@v4
- - uses: dorny/paths-filter@v3
- id: filter
+ - name: Use Node.js 22
+ uses: actions/setup-node@v4
with:
- filters: |
- ember:
- - 'clients/ember/**'
- - '.github/workflows/ci.yml'
- - '.github/workflows/release-ember-client.yml'
-
- test-storybook-client:
- runs-on: ubuntu-latest
- timeout-minutes: 8
- needs: [lint, changes-storybook]
- if: needs.changes-storybook.outputs.storybook == 'true'
-
- strategy:
- matrix:
- node-version: [22, 24]
-
- steps:
- - uses: actions/checkout@v4
-
- - name: Use Node.js ${{ matrix.node-version }}
- uses: actions/setup-node@v4
- with:
- node-version: ${{ matrix.node-version }}
-
- - name: Install CLI dependencies
- run: npm ci
+ node-version: 22
+ cache: 'npm'
- - name: Build CLI
- run: npm run build
+ - name: Install dependencies
+ run: npm ci
- - name: Install Storybook client dependencies
- working-directory: ./clients/storybook
- run: npm install
+ - name: Run linter
+ run: npm run lint
- - name: Run linter
- working-directory: ./clients/storybook
- run: npm run lint
+ - name: Check formatting
+ run: npm run format:check
- - name: Run tests
- working-directory: ./clients/storybook
- run: npm test
- env:
- CI: true
-
- - name: Build package
- working-directory: ./clients/storybook
- run: npm run build
-
- test-static-site-client:
+ test:
+ name: Test (Node ${{ matrix.node-version }})
runs-on: ubuntu-latest
timeout-minutes: 8
- needs: [lint, changes-static-site]
- if: needs.changes-static-site.outputs.static-site == 'true'
-
- strategy:
- matrix:
- node-version: [22, 24]
-
- steps:
- - uses: actions/checkout@v4
-
- - name: Use Node.js ${{ matrix.node-version }}
- uses: actions/setup-node@v4
- with:
- node-version: ${{ matrix.node-version }}
-
- - name: Install CLI dependencies
- run: npm ci
-
- - name: Build CLI
- run: npm run build
-
- - name: Install static-site client dependencies
- working-directory: ./clients/static-site
- run: npm install
-
- - name: Run linter
- working-directory: ./clients/static-site
- run: npm run lint
-
- - name: Run tests
- working-directory: ./clients/static-site
- run: npm test
- env:
- CI: true
-
- - name: Build package
- working-directory: ./clients/static-site
- run: npm run build
-
- # SDK E2E Integration Tests
- # Always run to catch CLI changes that break SDK integrations
- # Each SDK has its own job for isolation, parallelism, and clear failure diagnosis
-
- e2e-core-js-client:
- name: E2E - Core JS Client
- runs-on: blacksmith-4vcpu-ubuntu-2404
- timeout-minutes: 8
- needs: lint
-
- steps:
- - uses: actions/checkout@v4
-
- - name: Use Node.js 22
- uses: actions/setup-node@v4
- with:
- node-version: 22
- cache: 'npm'
-
- - name: Install CLI dependencies
- run: npm ci
-
- - name: Build CLI
- run: npm run build
-
- - name: Install test-site dependencies
- working-directory: ./test-site
- run: npm ci
-
- - name: Get Playwright version
- id: playwright-version
- run: echo "version=$(npx playwright --version | grep -oE '[0-9]+\.[0-9]+\.[0-9]+')" >> $GITHUB_OUTPUT
-
- - name: Cache Playwright browsers
- uses: actions/cache@v4
- id: playwright-cache
- with:
- path: ~/.cache/ms-playwright
- key: playwright-${{ steps.playwright-version.outputs.version }}-firefox
-
- - name: Install Playwright browsers
- if: steps.playwright-cache.outputs.cache-hit != 'true'
- working-directory: ./test-site
- run: npx playwright install firefox --with-deps
-
- - name: Run E2E tests
- working-directory: ./test-site
- run: node ../bin/vizzly.js run "npm test"
- env:
- CI: true
- VIZZLY_TOKEN: ${{ secrets.VIZZLY_TOKEN }}
- VIZZLY_COMMIT_MESSAGE: ${{ github.event.pull_request.head.commit.message || github.event.head_commit.message }}
- VIZZLY_COMMIT_SHA: ${{ github.event.pull_request.head.sha || github.event.head_commit.id }}
-
- e2e-vitest-sdk:
- name: E2E - Vitest SDK
- runs-on: blacksmith-4vcpu-ubuntu-2404
- timeout-minutes: 8
- needs: lint
-
- steps:
- - uses: actions/checkout@v4
-
- - name: Use Node.js 22
- uses: actions/setup-node@v4
- with:
- node-version: 22
- cache: 'npm'
-
- - name: Install CLI dependencies
- run: npm ci
-
- - name: Build CLI
- run: npm run build
-
- - name: Install Vitest client dependencies
- working-directory: ./clients/vitest
- run: npm install
-
- - name: Get Playwright version
- working-directory: ./clients/vitest
- id: playwright-version
- run: echo "version=$(npx playwright --version | grep -oE '[0-9]+\.[0-9]+\.[0-9]+')" >> $GITHUB_OUTPUT
-
- - name: Cache Playwright browsers
- uses: actions/cache@v4
- id: playwright-cache
- with:
- path: ~/.cache/ms-playwright
- key: playwright-${{ steps.playwright-version.outputs.version }}-chromium
-
- - name: Install Playwright browsers
- if: steps.playwright-cache.outputs.cache-hit != 'true'
- working-directory: ./clients/vitest
- run: npx playwright install chromium --with-deps
-
- - name: Run E2E tests
- working-directory: ./clients/vitest
- run: ../../bin/vizzly.js run "npm run test:e2e"
- env:
- CI: true
- VIZZLY_TOKEN: ${{ secrets.VIZZLY_VITEST_CLIENT_TOKEN }}
- VIZZLY_COMMIT_MESSAGE: ${{ github.event.pull_request.head.commit.message || github.event.head_commit.message }}
- VIZZLY_COMMIT_SHA: ${{ github.event.pull_request.head.sha || github.event.head_commit.id }}
-
- e2e-ember-sdk:
- name: E2E - Ember SDK
- runs-on: blacksmith-4vcpu-ubuntu-2404
- timeout-minutes: 8
needs: lint
- steps:
- - uses: actions/checkout@v4
-
- - name: Use Node.js 22
- uses: actions/setup-node@v4
- with:
- node-version: 22
- cache: 'npm'
-
- - name: Install CLI dependencies
- run: npm ci
-
- - name: Build CLI
- run: npm run build
-
- - name: Install Ember client dependencies
- working-directory: ./clients/ember
- run: npm install
-
- - name: Get Playwright version
- working-directory: ./clients/ember
- id: playwright-version
- run: echo "version=$(npx playwright --version | grep -oE '[0-9]+\.[0-9]+\.[0-9]+')" >> $GITHUB_OUTPUT
-
- - name: Cache Playwright browsers
- uses: actions/cache@v4
- id: playwright-cache
- with:
- path: ~/.cache/ms-playwright
- key: playwright-${{ steps.playwright-version.outputs.version }}-chromium
-
- - name: Install Playwright browsers
- if: steps.playwright-cache.outputs.cache-hit != 'true'
- working-directory: ./clients/ember
- run: npx playwright install chromium --with-deps
-
- - name: Run E2E tests
- working-directory: ./clients/ember
- run: |
- cd test-app
- npm install
- npm run build -- --mode development
- ../../../bin/vizzly.js run "npx testem ci --file testem.cjs"
- env:
- CI: true
- VIZZLY_TOKEN: ${{ secrets.VIZZLY_EMBER_CLIENT_TOKEN }}
- VIZZLY_COMMIT_MESSAGE: ${{ github.event.pull_request.head.commit.message || github.event.head_commit.message }}
- VIZZLY_COMMIT_SHA: ${{ github.event.pull_request.head.sha || github.event.head_commit.id }}
-
- e2e-ruby-sdk:
- name: E2E - Ruby SDK
- runs-on: blacksmith-4vcpu-ubuntu-2404
- timeout-minutes: 8
- needs: lint
-
- steps:
- - uses: actions/checkout@v4
-
- - name: Use Node.js 22
- uses: actions/setup-node@v4
- with:
- node-version: 22
- cache: 'npm'
-
- - name: Install CLI dependencies
- run: npm ci
-
- - name: Build CLI
- run: npm run build
-
- - name: Set up Ruby
- uses: ruby/setup-ruby@v1
- with:
- ruby-version: '3.3'
-
- - name: Install Ruby dependencies
- working-directory: ./clients/ruby
- run: |
- gem install bundler
- bundle install
-
- - name: Run integration tests
- working-directory: ./clients/ruby
- run: VIZZLY_INTEGRATION=1 ruby -I lib test/integration_test.rb
- env:
- CI: true
-
- test-vitest-client:
- runs-on: blacksmith-4vcpu-ubuntu-2404
- timeout-minutes: 8
- needs: [lint, changes-vitest]
- if: needs.changes-vitest.outputs.vitest == 'true'
-
strategy:
matrix:
node-version: [22, 24]
steps:
- - uses: actions/checkout@v4
-
- - name: Use Node.js ${{ matrix.node-version }}
- uses: actions/setup-node@v4
- with:
- node-version: ${{ matrix.node-version }}
-
- - name: Install CLI dependencies
- run: npm ci
-
- - name: Build CLI
- run: npm run build
-
- - name: Install Vitest client dependencies
- working-directory: ./clients/vitest
- run: npm install
-
- - name: Get installed Playwright version
- working-directory: ./clients/vitest
- id: playwright-version
- run: echo "version=$(npx playwright --version | grep -oE '[0-9]+\.[0-9]+\.[0-9]+')" >> $GITHUB_OUTPUT
-
- - name: Cache Playwright browsers
- uses: actions/cache@v4
- id: playwright-cache
- with:
- path: ~/.cache/ms-playwright
- key: playwright-browsers-${{ steps.playwright-version.outputs.version }}-chromium
-
- - name: Install Playwright browsers
- if: steps.playwright-cache.outputs.cache-hit != 'true'
- working-directory: ./clients/vitest
- run: npx playwright install chromium --with-deps
-
- - name: Run Vitest client linter
- working-directory: ./clients/vitest
- run: npm run lint
-
- - name: Run Vitest client unit tests
- working-directory: ./clients/vitest
- run: npm run test:unit
- env:
- CI: true
-
- test-swift-client:
- runs-on: macos-latest
- timeout-minutes: 8
- needs: [lint, changes-swift]
- if: needs.changes-swift.outputs.swift == 'true'
-
- strategy:
- matrix:
- xcode-version: ['16.2', '16.4']
-
- steps:
- - uses: actions/checkout@v4
-
- - name: Select Xcode version
- run: sudo xcode-select -s /Applications/Xcode_${{ matrix.xcode-version }}.app
-
- - name: Build Swift package
- working-directory: ./clients/swift
- run: swift build
-
- - name: Run Swift tests
- working-directory: ./clients/swift
- run: swift test
-
- test-ember-client:
- runs-on: blacksmith-4vcpu-ubuntu-2404
- timeout-minutes: 8
- needs: [lint, changes-ember]
- if: needs.changes-ember.outputs.ember == 'true'
-
- steps:
- - uses: actions/checkout@v4
-
- - name: Use Node.js 22
- uses: actions/setup-node@v4
- with:
- node-version: 22
-
- - name: Install CLI dependencies
- run: npm ci
-
- - name: Build CLI
- run: npm run build
-
- - name: Install Ember client dependencies
- working-directory: ./clients/ember
- run: npm install
-
- - name: Get installed Playwright version
- working-directory: ./clients/ember
- id: playwright-version
- run: echo "version=$(npx playwright --version | grep -oE '[0-9]+\.[0-9]+\.[0-9]+')" >> $GITHUB_OUTPUT
-
- - name: Cache Playwright browsers
- uses: actions/cache@v4
- id: playwright-cache
- with:
- path: ~/.cache/ms-playwright
- key: playwright-browsers-${{ steps.playwright-version.outputs.version }}-chromium
+ - uses: actions/checkout@v4
- - name: Install Playwright browsers
- if: steps.playwright-cache.outputs.cache-hit != 'true'
- working-directory: ./clients/ember
- run: npx playwright install chromium --with-deps
+ - name: Use Node.js ${{ matrix.node-version }}
+ uses: actions/setup-node@v4
+ with:
+ node-version: ${{ matrix.node-version }}
+ cache: 'npm'
- - name: Run linter
- working-directory: ./clients/ember
- run: npm run lint
+ - name: Install dependencies
+ run: npm ci
- - name: Run unit tests
- working-directory: ./clients/ember
- run: npm test
- env:
- CI: true
+ - name: Run tests
+ run: npm test
+ env:
+ CI: true
- - name: Run integration tests
- working-directory: ./clients/ember
- run: npm run test:integration
- env:
- CI: true
+ - name: Build
+ run: npm run build
- - name: Run Ember E2E visual tests
- working-directory: ./clients/ember
- run: |
- cd test-app
- npm install
- npm run build -- --mode development
- ../../../bin/vizzly.js run "npx testem ci --file testem.cjs"
- env:
- CI: true
- VIZZLY_TOKEN: ${{ secrets.VIZZLY_EMBER_CLIENT_TOKEN }}
- VIZZLY_COMMIT_MESSAGE: ${{ github.event.pull_request.head.commit.message || github.event.head_commit.message }}
- VIZZLY_COMMIT_SHA: ${{ github.event.pull_request.head.sha || github.event.head_commit.id }}
+ - name: Run type tests
+ run: npm run test:types
- ci-check:
+ check:
+ name: CI Status
runs-on: ubuntu-latest
- needs: [lint, test, test-reporter, test-tui, e2e-core-js-client, e2e-vitest-sdk, e2e-ember-sdk, e2e-ruby-sdk, changes-ruby, test-ruby-client, changes-storybook, test-storybook-client, changes-static-site, test-static-site-client, changes-vitest, test-vitest-client, changes-swift, test-swift-client, changes-ember, test-ember-client]
+ needs: [lint, test]
if: always()
steps:
- - name: Check if all jobs passed
+ - name: Check all jobs passed
run: |
- # Required jobs that always run
- if [[ "${{ needs.lint.result }}" == "failure" || "${{ needs.test.result }}" == "failure" || "${{ needs.test-reporter.result }}" == "failure" || "${{ needs.test-tui.result }}" == "failure" ]]; then
- echo "One or more required jobs failed"
- exit 1
- fi
-
- # SDK E2E jobs (always run)
- if [[ "${{ needs.e2e-core-js-client.result }}" == "failure" || "${{ needs.e2e-vitest-sdk.result }}" == "failure" || "${{ needs.e2e-ember-sdk.result }}" == "failure" || "${{ needs.e2e-ruby-sdk.result }}" == "failure" ]]; then
- echo "One or more SDK E2E jobs failed"
- exit 1
- fi
-
- # Conditional jobs - only check if they ran
- if [[ "${{ needs.changes-ruby.outputs.ruby }}" == "true" && "${{ needs.test-ruby-client.result }}" == "failure" ]]; then
- echo "Ruby client tests failed"
- exit 1
- fi
-
- if [[ "${{ needs.changes-storybook.outputs.storybook }}" == "true" && "${{ needs.test-storybook-client.result }}" == "failure" ]]; then
- echo "Storybook client tests failed"
+ if [[ "${{ needs.lint.result }}" == "failure" || "${{ needs.test.result }}" == "failure" ]]; then
+ echo "CI checks failed"
exit 1
fi
-
- if [[ "${{ needs.changes-static-site.outputs.static-site }}" == "true" && "${{ needs.test-static-site-client.result }}" == "failure" ]]; then
- echo "Static site client tests failed"
- exit 1
- fi
-
- if [[ "${{ needs.changes-vitest.outputs.vitest }}" == "true" && "${{ needs.test-vitest-client.result }}" == "failure" ]]; then
- echo "Vitest client tests failed"
- exit 1
- fi
-
- if [[ "${{ needs.changes-swift.outputs.swift }}" == "true" && "${{ needs.test-swift-client.result }}" == "failure" ]]; then
- echo "Swift client tests failed"
- exit 1
- fi
-
- if [[ "${{ needs.changes-ember.outputs.ember }}" == "true" && "${{ needs.test-ember-client.result }}" == "failure" ]]; then
- echo "Ember client tests failed"
- exit 1
- fi
-
- echo "All jobs passed"
+ echo "All CI checks passed"
diff --git a/.github/workflows/reporter.yml b/.github/workflows/reporter.yml
new file mode 100644
index 00000000..4625c88a
--- /dev/null
+++ b/.github/workflows/reporter.yml
@@ -0,0 +1,51 @@
+name: Reporter Visual Tests
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches: [main]
+
+jobs:
+ visual:
+ name: Visual Tests
+ runs-on: ubuntu-latest
+ timeout-minutes: 8
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Use Node.js 22
+ uses: actions/setup-node@v4
+ with:
+ node-version: 22
+ cache: 'npm'
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Build
+ run: npm run build
+
+ - name: Get installed Playwright version
+ id: playwright-version
+ run: echo "version=$(npx playwright --version | grep -oE '[0-9]+\.[0-9]+\.[0-9]+')" >> $GITHUB_OUTPUT
+
+ - name: Cache Playwright browsers
+ uses: actions/cache@v4
+ id: playwright-cache
+ with:
+ path: ~/.cache/ms-playwright
+ key: playwright-browsers-${{ steps.playwright-version.outputs.version }}-firefox
+
+ - name: Install Playwright browsers
+ if: steps.playwright-cache.outputs.cache-hit != 'true'
+ run: npx playwright install firefox --with-deps
+
+ - name: Run reporter visual tests
+ run: npm run test:reporter:visual
+ env:
+ CI: true
+ VIZZLY_TOKEN: ${{ secrets.VIZZLY_REPORTER_TOKEN }}
+ VIZZLY_COMMIT_MESSAGE: ${{ github.event.pull_request.head.commit.message || github.event.head_commit.message }}
+ VIZZLY_COMMIT_SHA: ${{ github.event.pull_request.head.sha || github.event.head_commit.id }}
diff --git a/.github/workflows/sdk-e2e.yml b/.github/workflows/sdk-e2e.yml
new file mode 100644
index 00000000..3e30d969
--- /dev/null
+++ b/.github/workflows/sdk-e2e.yml
@@ -0,0 +1,380 @@
+name: SDK E2E Tests
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches: [main]
+
+jobs:
+ # Core JS Client (test-site with Playwright)
+ core-js:
+ name: Core JS Client
+ runs-on: ubuntu-latest
+ timeout-minutes: 8
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Use Node.js 22
+ uses: actions/setup-node@v4
+ with:
+ node-version: 22
+ cache: 'npm'
+
+ - name: Install CLI dependencies
+ run: npm ci
+
+ - name: Build CLI
+ run: npm run build
+
+ - name: Install test-site dependencies
+ working-directory: ./test-site
+ run: npm ci
+
+ - name: Get Playwright version
+ id: playwright-version
+ run: echo "version=$(npx playwright --version | grep -oE '[0-9]+\.[0-9]+\.[0-9]+')" >> $GITHUB_OUTPUT
+
+ - name: Cache Playwright browsers
+ uses: actions/cache@v4
+ id: playwright-cache
+ with:
+ path: ~/.cache/ms-playwright
+ key: playwright-${{ steps.playwright-version.outputs.version }}-firefox
+
+ - name: Install Playwright browsers
+ if: steps.playwright-cache.outputs.cache-hit != 'true'
+ working-directory: ./test-site
+ run: npx playwright install firefox --with-deps
+
+ - name: Run E2E tests (TDD mode)
+ working-directory: ./test-site
+ run: node ../bin/vizzly.js tdd run "npm test"
+ env:
+ CI: true
+
+ - name: Run E2E tests (Cloud mode)
+ working-directory: ./test-site
+ run: node ../bin/vizzly.js run "npm test"
+ env:
+ CI: true
+ VIZZLY_TOKEN: ${{ secrets.VIZZLY_TOKEN }}
+ VIZZLY_COMMIT_MESSAGE: ${{ github.event.pull_request.head.commit.message || github.event.head_commit.message }}
+ VIZZLY_COMMIT_SHA: ${{ github.event.pull_request.head.sha || github.event.head_commit.id }}
+
+ # Vitest SDK
+ vitest:
+ name: Vitest SDK
+ runs-on: ubuntu-latest
+ timeout-minutes: 8
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Use Node.js 22
+ uses: actions/setup-node@v4
+ with:
+ node-version: 22
+ cache: 'npm'
+
+ - name: Install CLI dependencies
+ run: npm ci
+
+ - name: Build CLI
+ run: npm run build
+
+ - name: Install Vitest client dependencies
+ working-directory: ./clients/vitest
+ run: npm install
+
+ - name: Get Playwright version
+ working-directory: ./clients/vitest
+ id: playwright-version
+ run: echo "version=$(npx playwright --version | grep -oE '[0-9]+\.[0-9]+\.[0-9]+')" >> $GITHUB_OUTPUT
+
+ - name: Cache Playwright browsers
+ uses: actions/cache@v4
+ id: playwright-cache
+ with:
+ path: ~/.cache/ms-playwright
+ key: playwright-${{ steps.playwright-version.outputs.version }}-chromium
+
+ - name: Install Playwright browsers
+ if: steps.playwright-cache.outputs.cache-hit != 'true'
+ working-directory: ./clients/vitest
+ run: npx playwright install chromium --with-deps
+
+ - name: Run E2E tests (TDD mode)
+ working-directory: ./clients/vitest
+ run: ../../bin/vizzly.js tdd run "npm run test:e2e"
+ env:
+ CI: true
+
+ - name: Run E2E tests (Cloud mode)
+ working-directory: ./clients/vitest
+ run: ../../bin/vizzly.js run "npm run test:e2e"
+ env:
+ CI: true
+ VIZZLY_TOKEN: ${{ secrets.VIZZLY_VITEST_CLIENT_TOKEN }}
+ VIZZLY_COMMIT_MESSAGE: ${{ github.event.pull_request.head.commit.message || github.event.head_commit.message }}
+ VIZZLY_COMMIT_SHA: ${{ github.event.pull_request.head.sha || github.event.head_commit.id }}
+
+ # Storybook SDK
+ storybook:
+ name: Storybook SDK
+ runs-on: ubuntu-latest
+ timeout-minutes: 8
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Use Node.js 22
+ uses: actions/setup-node@v4
+ with:
+ node-version: 22
+ cache: 'npm'
+
+ - name: Install CLI dependencies
+ run: npm ci
+
+ - name: Build CLI
+ run: npm run build
+
+ - name: Install Storybook client dependencies
+ working-directory: ./clients/storybook
+ run: npm install
+
+ - name: Get Playwright version
+ working-directory: ./clients/storybook
+ id: playwright-version
+ run: echo "version=$(npx playwright --version | grep -oE '[0-9]+\.[0-9]+\.[0-9]+')" >> $GITHUB_OUTPUT
+
+ - name: Cache Playwright browsers
+ uses: actions/cache@v4
+ id: playwright-cache
+ with:
+ path: ~/.cache/ms-playwright
+ key: playwright-${{ steps.playwright-version.outputs.version }}-chromium
+
+ - name: Install Playwright browsers
+ if: steps.playwright-cache.outputs.cache-hit != 'true'
+ working-directory: ./clients/storybook
+ run: npx playwright install chromium --with-deps
+
+ - name: Run E2E tests (TDD mode)
+ working-directory: ./clients/storybook
+ run: ../../bin/vizzly.js tdd run "npm run test:e2e"
+ env:
+ CI: true
+
+ - name: Run E2E tests (Cloud mode)
+ working-directory: ./clients/storybook
+ run: ../../bin/vizzly.js run "npm run test:e2e"
+ env:
+ CI: true
+ VIZZLY_TOKEN: ${{ secrets.VIZZLY_STORYBOOK_CLIENT_TOKEN }}
+ VIZZLY_COMMIT_MESSAGE: ${{ github.event.pull_request.head.commit.message || github.event.head_commit.message }}
+ VIZZLY_COMMIT_SHA: ${{ github.event.pull_request.head.sha || github.event.head_commit.id }}
+
+ # Static-Site SDK
+ static-site:
+ name: Static-Site SDK
+ runs-on: ubuntu-latest
+ timeout-minutes: 8
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Use Node.js 22
+ uses: actions/setup-node@v4
+ with:
+ node-version: 22
+ cache: 'npm'
+
+ - name: Install CLI dependencies
+ run: npm ci
+
+ - name: Build CLI
+ run: npm run build
+
+ - name: Install Static-Site client dependencies
+ working-directory: ./clients/static-site
+ run: npm install
+
+ - name: Get Playwright version
+ working-directory: ./clients/static-site
+ id: playwright-version
+ run: echo "version=$(npx playwright --version | grep -oE '[0-9]+\.[0-9]+\.[0-9]+')" >> $GITHUB_OUTPUT
+
+ - name: Cache Playwright browsers
+ uses: actions/cache@v4
+ id: playwright-cache
+ with:
+ path: ~/.cache/ms-playwright
+ key: playwright-${{ steps.playwright-version.outputs.version }}-chromium
+
+ - name: Install Playwright browsers
+ if: steps.playwright-cache.outputs.cache-hit != 'true'
+ working-directory: ./clients/static-site
+ run: npx playwright install chromium --with-deps
+
+ - name: Run E2E tests (TDD mode)
+ working-directory: ./clients/static-site
+ run: ../../bin/vizzly.js tdd run "npm run test:e2e"
+ env:
+ CI: true
+
+ - name: Run E2E tests (Cloud mode)
+ working-directory: ./clients/static-site
+ run: ../../bin/vizzly.js run "npm run test:e2e"
+ env:
+ CI: true
+ VIZZLY_TOKEN: ${{ secrets.VIZZLY_STATIC_SITE_CLIENT_TOKEN }}
+ VIZZLY_COMMIT_MESSAGE: ${{ github.event.pull_request.head.commit.message || github.event.head_commit.message }}
+ VIZZLY_COMMIT_SHA: ${{ github.event.pull_request.head.sha || github.event.head_commit.id }}
+
+ # Ember SDK
+ ember:
+ name: Ember SDK
+ runs-on: ubuntu-latest
+ timeout-minutes: 8
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Use Node.js 22
+ uses: actions/setup-node@v4
+ with:
+ node-version: 22
+ cache: 'npm'
+
+ - name: Install CLI dependencies
+ run: npm ci
+
+ - name: Build CLI
+ run: npm run build
+
+ - name: Install Ember client dependencies
+ working-directory: ./clients/ember
+ run: npm install
+
+ - name: Get Playwright version
+ working-directory: ./clients/ember
+ id: playwright-version
+ run: echo "version=$(npx playwright --version | grep -oE '[0-9]+\.[0-9]+\.[0-9]+')" >> $GITHUB_OUTPUT
+
+ - name: Cache Playwright browsers
+ uses: actions/cache@v4
+ id: playwright-cache
+ with:
+ path: ~/.cache/ms-playwright
+ key: playwright-${{ steps.playwright-version.outputs.version }}-chromium
+
+ - name: Install Playwright browsers
+ if: steps.playwright-cache.outputs.cache-hit != 'true'
+ working-directory: ./clients/ember
+ run: npx playwright install chromium --with-deps
+
+ - name: Build Ember test app
+ working-directory: ./clients/ember/test-app
+ run: |
+ npm install
+ npm run build -- --mode development
+
+ - name: Run E2E tests (TDD mode)
+ working-directory: ./clients/ember/test-app
+ run: ../../../bin/vizzly.js tdd run "npx testem ci --file testem.cjs"
+ env:
+ CI: true
+
+ - name: Run E2E tests (Cloud mode)
+ working-directory: ./clients/ember/test-app
+ run: ../../../bin/vizzly.js run "npx testem ci --file testem.cjs"
+ env:
+ CI: true
+ VIZZLY_TOKEN: ${{ secrets.VIZZLY_EMBER_CLIENT_TOKEN }}
+ VIZZLY_COMMIT_MESSAGE: ${{ github.event.pull_request.head.commit.message || github.event.head_commit.message }}
+ VIZZLY_COMMIT_SHA: ${{ github.event.pull_request.head.sha || github.event.head_commit.id }}
+
+ # Ruby SDK
+ ruby:
+ name: Ruby SDK
+ runs-on: ubuntu-latest
+ timeout-minutes: 8
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Use Node.js 22
+ uses: actions/setup-node@v4
+ with:
+ node-version: 22
+ cache: 'npm'
+
+ - name: Install CLI dependencies
+ run: npm ci
+
+ - name: Build CLI
+ run: npm run build
+
+ - name: Set up Ruby
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: '3.3'
+
+ - name: Install Ruby dependencies
+ working-directory: ./clients/ruby
+ run: |
+ gem install bundler
+ bundle install
+
+ - name: Run integration tests (TDD mode)
+ working-directory: ./clients/ruby
+ run: ../../bin/vizzly.js tdd run "VIZZLY_INTEGRATION=1 ruby -I lib test/integration_test.rb"
+ env:
+ CI: true
+
+ - name: Run integration tests (Cloud mode)
+ working-directory: ./clients/ruby
+ run: ../../bin/vizzly.js run "VIZZLY_INTEGRATION=1 ruby -I lib test/integration_test.rb"
+ env:
+ CI: true
+ VIZZLY_TOKEN: ${{ secrets.VIZZLY_RUBY_CLIENT_TOKEN }}
+ VIZZLY_COMMIT_MESSAGE: ${{ github.event.pull_request.head.commit.message || github.event.head_commit.message }}
+ VIZZLY_COMMIT_SHA: ${{ github.event.pull_request.head.sha || github.event.head_commit.id }}
+
+ # Status check for branch protection
+ check:
+ name: E2E Status
+ runs-on: ubuntu-latest
+ needs: [core-js, vitest, storybook, static-site, ember, ruby]
+ if: always()
+ steps:
+ - name: Check all SDK E2E tests passed
+ run: |
+ if [[ "${{ needs.core-js.result }}" == "failure" ]]; then
+ echo "Core JS Client E2E tests failed"
+ exit 1
+ fi
+ if [[ "${{ needs.vitest.result }}" == "failure" ]]; then
+ echo "Vitest SDK E2E tests failed"
+ exit 1
+ fi
+ if [[ "${{ needs.storybook.result }}" == "failure" ]]; then
+ echo "Storybook SDK E2E tests failed"
+ exit 1
+ fi
+ if [[ "${{ needs.static-site.result }}" == "failure" ]]; then
+ echo "Static-Site SDK E2E tests failed"
+ exit 1
+ fi
+ if [[ "${{ needs.ember.result }}" == "failure" ]]; then
+ echo "Ember SDK E2E tests failed"
+ exit 1
+ fi
+ if [[ "${{ needs.ruby.result }}" == "failure" ]]; then
+ echo "Ruby SDK E2E tests failed"
+ exit 1
+ fi
+ echo "All SDK E2E tests passed"
diff --git a/.github/workflows/sdk-unit.yml b/.github/workflows/sdk-unit.yml
new file mode 100644
index 00000000..79e5b95a
--- /dev/null
+++ b/.github/workflows/sdk-unit.yml
@@ -0,0 +1,361 @@
+name: SDK Unit Tests
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches: [main]
+
+jobs:
+ # Detect which SDKs have changes
+ changes:
+ runs-on: ubuntu-latest
+ outputs:
+ ruby: ${{ steps.filter.outputs.ruby }}
+ storybook: ${{ steps.filter.outputs.storybook }}
+ static-site: ${{ steps.filter.outputs.static-site }}
+ vitest: ${{ steps.filter.outputs.vitest }}
+ swift: ${{ steps.filter.outputs.swift }}
+ ember: ${{ steps.filter.outputs.ember }}
+ steps:
+ - uses: actions/checkout@v4
+ - uses: dorny/paths-filter@v3
+ id: filter
+ with:
+ filters: |
+ ruby:
+ - 'clients/ruby/**'
+ - '.github/workflows/sdk-unit.yml'
+ storybook:
+ - 'clients/storybook/**'
+ - '.github/workflows/sdk-unit.yml'
+ static-site:
+ - 'clients/static-site/**'
+ - '.github/workflows/sdk-unit.yml'
+ vitest:
+ - 'clients/vitest/**'
+ - '.github/workflows/sdk-unit.yml'
+ swift:
+ - 'clients/swift/**'
+ - '.github/workflows/sdk-unit.yml'
+ ember:
+ - 'clients/ember/**'
+ - '.github/workflows/sdk-unit.yml'
+
+ # Ruby SDK - runs on multiple Ruby versions
+ ruby:
+ name: Ruby SDK
+ runs-on: ubuntu-latest
+ timeout-minutes: 8
+ needs: changes
+ if: needs.changes.outputs.ruby == 'true'
+
+ strategy:
+ matrix:
+ ruby-version: ['3.0', '3.1', '3.2', '3.3']
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up Ruby ${{ matrix.ruby-version }}
+ uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: ${{ matrix.ruby-version }}
+
+ - name: Use Node.js 22
+ uses: actions/setup-node@v4
+ with:
+ node-version: 22
+ cache: 'npm'
+
+ - name: Install CLI dependencies
+ run: npm ci
+
+ - name: Build CLI
+ run: npm run build
+
+ - name: Install Ruby dependencies
+ working-directory: ./clients/ruby
+ run: |
+ gem install bundler
+ bundle install
+
+ - name: Run RuboCop
+ working-directory: ./clients/ruby
+ run: bundle exec rubocop
+
+ - name: Run Ruby unit tests
+ working-directory: ./clients/ruby
+ run: ruby -I lib test/vizzly_test.rb
+
+ - name: Run Ruby integration tests
+ working-directory: ./clients/ruby
+ run: VIZZLY_INTEGRATION=1 ruby -I lib test/integration_test.rb
+ env:
+ CI: true
+
+ # Storybook SDK - runs on multiple Node versions
+ storybook:
+ name: Storybook SDK
+ runs-on: ubuntu-latest
+ timeout-minutes: 8
+ needs: changes
+ if: needs.changes.outputs.storybook == 'true'
+
+ strategy:
+ matrix:
+ node-version: [22, 24]
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Use Node.js ${{ matrix.node-version }}
+ uses: actions/setup-node@v4
+ with:
+ node-version: ${{ matrix.node-version }}
+
+ - name: Install CLI dependencies
+ run: npm ci
+
+ - name: Build CLI
+ run: npm run build
+
+ - name: Install Storybook client dependencies
+ working-directory: ./clients/storybook
+ run: npm install
+
+ - name: Run linter
+ working-directory: ./clients/storybook
+ run: npm run lint
+
+ - name: Run tests
+ working-directory: ./clients/storybook
+ run: npm test
+ env:
+ CI: true
+
+ - name: Build package
+ working-directory: ./clients/storybook
+ run: npm run build
+
+ # Static-Site SDK - runs on multiple Node versions
+ static-site:
+ name: Static-Site SDK
+ runs-on: ubuntu-latest
+ timeout-minutes: 8
+ needs: changes
+ if: needs.changes.outputs.static-site == 'true'
+
+ strategy:
+ matrix:
+ node-version: [22, 24]
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Use Node.js ${{ matrix.node-version }}
+ uses: actions/setup-node@v4
+ with:
+ node-version: ${{ matrix.node-version }}
+
+ - name: Install CLI dependencies
+ run: npm ci
+
+ - name: Build CLI
+ run: npm run build
+
+ - name: Install Static-Site client dependencies
+ working-directory: ./clients/static-site
+ run: npm install
+
+ - name: Run linter
+ working-directory: ./clients/static-site
+ run: npm run lint
+
+ - name: Run tests
+ working-directory: ./clients/static-site
+ run: npm test
+ env:
+ CI: true
+
+ - name: Build package
+ working-directory: ./clients/static-site
+ run: npm run build
+
+ # Vitest SDK - runs on multiple Node versions
+ vitest:
+ name: Vitest SDK
+ runs-on: ubuntu-latest
+ timeout-minutes: 8
+ needs: changes
+ if: needs.changes.outputs.vitest == 'true'
+
+ strategy:
+ matrix:
+ node-version: [22, 24]
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Use Node.js ${{ matrix.node-version }}
+ uses: actions/setup-node@v4
+ with:
+ node-version: ${{ matrix.node-version }}
+
+ - name: Install CLI dependencies
+ run: npm ci
+
+ - name: Build CLI
+ run: npm run build
+
+ - name: Install Vitest client dependencies
+ working-directory: ./clients/vitest
+ run: npm install
+
+ - name: Get installed Playwright version
+ working-directory: ./clients/vitest
+ id: playwright-version
+ run: echo "version=$(npx playwright --version | grep -oE '[0-9]+\.[0-9]+\.[0-9]+')" >> $GITHUB_OUTPUT
+
+ - name: Cache Playwright browsers
+ uses: actions/cache@v4
+ id: playwright-cache
+ with:
+ path: ~/.cache/ms-playwright
+ key: playwright-browsers-${{ steps.playwright-version.outputs.version }}-chromium
+
+ - name: Install Playwright browsers
+ if: steps.playwright-cache.outputs.cache-hit != 'true'
+ working-directory: ./clients/vitest
+ run: npx playwright install chromium --with-deps
+
+ - name: Run Vitest client linter
+ working-directory: ./clients/vitest
+ run: npm run lint
+
+ - name: Run Vitest client unit tests
+ working-directory: ./clients/vitest
+ run: npm run test:unit
+ env:
+ CI: true
+
+ # Swift SDK - runs on multiple Xcode versions
+ swift:
+ name: Swift SDK
+ runs-on: macos-latest
+ timeout-minutes: 8
+ needs: changes
+ if: needs.changes.outputs.swift == 'true'
+
+ strategy:
+ matrix:
+ xcode-version: ['16.2', '16.4']
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Select Xcode version
+ run: sudo xcode-select -s /Applications/Xcode_${{ matrix.xcode-version }}.app
+
+ - name: Build Swift package
+ working-directory: ./clients/swift
+ run: swift build
+
+ - name: Run Swift tests
+ working-directory: ./clients/swift
+ run: swift test
+
+ # Ember SDK - runs unit and integration tests
+ ember:
+ name: Ember SDK
+ runs-on: ubuntu-latest
+ timeout-minutes: 8
+ needs: changes
+ if: needs.changes.outputs.ember == 'true'
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Use Node.js 22
+ uses: actions/setup-node@v4
+ with:
+ node-version: 22
+
+ - name: Install CLI dependencies
+ run: npm ci
+
+ - name: Build CLI
+ run: npm run build
+
+ - name: Install Ember client dependencies
+ working-directory: ./clients/ember
+ run: npm install
+
+ - name: Get installed Playwright version
+ working-directory: ./clients/ember
+ id: playwright-version
+ run: echo "version=$(npx playwright --version | grep -oE '[0-9]+\.[0-9]+\.[0-9]+')" >> $GITHUB_OUTPUT
+
+ - name: Cache Playwright browsers
+ uses: actions/cache@v4
+ id: playwright-cache
+ with:
+ path: ~/.cache/ms-playwright
+ key: playwright-browsers-${{ steps.playwright-version.outputs.version }}-chromium
+
+ - name: Install Playwright browsers
+ if: steps.playwright-cache.outputs.cache-hit != 'true'
+ working-directory: ./clients/ember
+ run: npx playwright install chromium --with-deps
+
+ - name: Run linter
+ working-directory: ./clients/ember
+ run: npm run lint
+
+ - name: Run unit tests
+ working-directory: ./clients/ember
+ run: npm test
+ env:
+ CI: true
+
+ - name: Run integration tests
+ working-directory: ./clients/ember
+ run: npm run test:integration
+ env:
+ CI: true
+
+ # Status check for branch protection
+ check:
+ name: SDK Unit Status
+ runs-on: ubuntu-latest
+ needs: [changes, ruby, storybook, static-site, vitest, swift, ember]
+ if: always()
+ steps:
+ - name: Check SDK unit tests
+ run: |
+ # Only check jobs that were supposed to run
+ if [[ "${{ needs.changes.outputs.ruby }}" == "true" && "${{ needs.ruby.result }}" == "failure" ]]; then
+ echo "Ruby SDK tests failed"
+ exit 1
+ fi
+ if [[ "${{ needs.changes.outputs.storybook }}" == "true" && "${{ needs.storybook.result }}" == "failure" ]]; then
+ echo "Storybook SDK tests failed"
+ exit 1
+ fi
+ if [[ "${{ needs.changes.outputs.static-site }}" == "true" && "${{ needs.static-site.result }}" == "failure" ]]; then
+ echo "Static-Site SDK tests failed"
+ exit 1
+ fi
+ if [[ "${{ needs.changes.outputs.vitest }}" == "true" && "${{ needs.vitest.result }}" == "failure" ]]; then
+ echo "Vitest SDK tests failed"
+ exit 1
+ fi
+ if [[ "${{ needs.changes.outputs.swift }}" == "true" && "${{ needs.swift.result }}" == "failure" ]]; then
+ echo "Swift SDK tests failed"
+ exit 1
+ fi
+ if [[ "${{ needs.changes.outputs.ember }}" == "true" && "${{ needs.ember.result }}" == "failure" ]]; then
+ echo "Ember SDK tests failed"
+ exit 1
+ fi
+ echo "All SDK unit tests passed (or skipped)"
diff --git a/.github/workflows/tui.yml b/.github/workflows/tui.yml
new file mode 100644
index 00000000..f7d1aec8
--- /dev/null
+++ b/.github/workflows/tui.yml
@@ -0,0 +1,39 @@
+name: TUI Visual Tests
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches: [main]
+
+jobs:
+ visual:
+ name: Visual Tests
+ runs-on: ubuntu-latest
+ timeout-minutes: 8
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Use Node.js 22
+ uses: actions/setup-node@v4
+ with:
+ node-version: 22
+ cache: 'npm'
+
+ - name: Setup tui-driver
+ uses: vizzly-testing/tui-driver@main
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Build
+ run: npm run build
+
+ - name: Run TUI visual tests
+ run: node bin/vizzly.js run "npm run test:tui"
+ env:
+ CI: true
+ VIZZLY_TOKEN: ${{ secrets.VIZZLY_TUI_TOKEN }}
+ VIZZLY_COMMIT_MESSAGE: ${{ github.event.pull_request.head.commit.message || github.event.head_commit.message }}
+ VIZZLY_COMMIT_SHA: ${{ github.event.pull_request.head.sha || github.event.head_commit.id }}
diff --git a/clients/ember/package.json b/clients/ember/package.json
index 7874e3ad..61a31b06 100644
--- a/clients/ember/package.json
+++ b/clients/ember/package.json
@@ -49,9 +49,14 @@
"scripts": {
"test": "node --test --test-reporter=spec 'tests/unit/**/*.test.js'",
"test:integration": "node --test --test-reporter=spec 'tests/integration/**/*.test.js'",
+ "test:integration:e2e": "RUN_E2E=1 node --test --test-reporter=spec 'tests/integration/e2e.test.js'",
"test:all": "node --test --test-reporter=spec 'tests/**/*.test.js'",
"test:watch": "node --test --test-reporter=spec --watch 'tests/**/*.test.js'",
"test:ember": "cd test-app && npm install && npm run build -- --mode development && npx testem ci --file testem.cjs",
+ "test:e2e:tdd": "../../bin/vizzly.js tdd run 'RUN_E2E=1 node --test --test-reporter=spec tests/integration/e2e.test.js'",
+ "test:e2e:cloud": "../../bin/vizzly.js run 'RUN_E2E=1 node --test --test-reporter=spec tests/integration/e2e.test.js'",
+ "test:ember:tdd": "../../bin/vizzly.js tdd run 'npm run test:ember'",
+ "test:ember:cloud": "../../bin/vizzly.js run 'npm run test:ember'",
"lint": "biome lint src tests bin",
"lint:fix": "biome lint --write src tests bin",
"format": "biome format --write src tests bin",
diff --git a/clients/ember/tests/integration/e2e.test.js b/clients/ember/tests/integration/e2e.test.js
index 209672ec..cf84a6ff 100644
--- a/clients/ember/tests/integration/e2e.test.js
+++ b/clients/ember/tests/integration/e2e.test.js
@@ -1,21 +1,21 @@
/**
- * End-to-end test with Vizzly TDD server
+ * End-to-end tests for Ember SDK
*
- * This test runs the full flow including the TDD server:
+ * Uses the shared test-site (FluffyCloud) for consistent testing across all SDKs.
+ * These tests verify the full flow:
* 1. Start TDD server
- * 2. Launch browser via our launcher
- * 3. Capture screenshot
- * 4. Verify it reaches the TDD server
+ * 2. Start test-site server
+ * 3. Launch browser via our Playwright-based launcher
+ * 4. Capture screenshots and verify they reach the TDD server
*
- * Skip this test if TDD server dependencies aren't available.
+ * Run with: RUN_E2E=1 npm run test:integration
*/
import assert from 'node:assert';
import { spawn } from 'node:child_process';
-import { mkdirSync, rmSync } from 'node:fs';
-import { createServer } from 'node:http';
+import { existsSync, mkdirSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
-import { join } from 'node:path';
+import { join, resolve } from 'node:path';
import { after, before, describe, it } from 'node:test';
import { closeBrowser, launchBrowser } from '../../src/launcher/browser.js';
import {
@@ -24,179 +24,380 @@ import {
stopScreenshotServer,
} from '../../src/launcher/screenshot-server.js';
-// Create a temporary directory for this test
+// Paths
let testDir = join(tmpdir(), `vizzly-ember-test-${Date.now()}`);
+let testSitePath = resolve(import.meta.dirname, '../../../../test-site');
-describe('e2e with TDD server', { skip: !process.env.RUN_E2E }, () => {
+// Helper to start a static file server for test-site
+function startTestSiteServer(port = 3030) {
+ return new Promise((resolve, reject) => {
+ let server = spawn('python3', ['-m', 'http.server', String(port)], {
+ cwd: testSitePath,
+ stdio: ['ignore', 'pipe', 'pipe'],
+ });
+
+ let resolved = false;
+
+ server.stderr.on('data', data => {
+ let msg = data.toString();
+ if (msg.includes('Serving HTTP') && !resolved) {
+ resolved = true;
+ resolve({ server, port, url: `http://127.0.0.1:${port}` });
+ }
+ });
+
+ server.on('error', err => {
+ if (!resolved) {
+ resolved = true;
+ reject(err);
+ }
+ });
+
+ // Fallback timeout
+ setTimeout(() => {
+ if (!resolved) {
+ resolved = true;
+ resolve({ server, port, url: `http://127.0.0.1:${port}` });
+ }
+ }, 2000);
+ });
+}
+
+// Check if running under `vizzly tdd run` or `vizzly run`
+let externalServer = !!process.env.VIZZLY_SERVER_URL;
+
+// =============================================================================
+// E2E Tests with TDD Server and Test Site
+// =============================================================================
+
+describe('e2e with TDD server using shared test-site', { skip: !process.env.RUN_E2E }, () => {
let tddServer = null;
+ let testSiteServer = null;
let screenshotServer = null;
- let testServer = null;
- let testServerPort = null;
let browserInstance = null;
+ let testSiteUrl = null;
before(async () => {
- // Create test directory
- mkdirSync(testDir, { recursive: true });
+ // Check test-site exists
+ assert.ok(
+ existsSync(join(testSitePath, 'index.html')),
+ 'test-site/index.html should exist'
+ );
- // Start TDD server
- tddServer = spawn('npx', ['vizzly', 'tdd', 'start'], {
- cwd: testDir,
- stdio: ['ignore', 'pipe', 'pipe'],
- env: { ...process.env, VIZZLY_HOME: testDir },
- });
+ // Start test-site server
+ let testSiteInfo = await startTestSiteServer(3030 + Math.floor(Math.random() * 1000));
+ testSiteServer = testSiteInfo.server;
+ testSiteUrl = testSiteInfo.url;
- // Wait for TDD server to start
- await new Promise((resolve, reject) => {
- let timeout = setTimeout(() => reject(new Error('TDD server timeout')), 10000);
+ // Start TDD server only if not running under vizzly wrapper
+ if (!externalServer) {
+ // Create test directory for TDD server
+ mkdirSync(testDir, { recursive: true });
- tddServer.stdout.on('data', data => {
- if (data.toString().includes('TDD server started') ||
- data.toString().includes('localhost:47392')) {
- clearTimeout(timeout);
- resolve();
- }
+ tddServer = spawn('npx', ['vizzly', 'tdd', 'start'], {
+ cwd: testDir,
+ stdio: ['ignore', 'pipe', 'pipe'],
+ env: { ...process.env, VIZZLY_HOME: testDir },
});
- tddServer.on('error', err => {
- clearTimeout(timeout);
- reject(err);
- });
- });
+ // Wait for TDD server to start
+ await new Promise((resolve, reject) => {
+ let timeout = setTimeout(() => reject(new Error('TDD server timeout')), 10000);
- // Start test page server
- testServer = createServer((_req, res) => {
- res.writeHead(200, { 'Content-Type': 'text/html' });
- res.end(`
-
-
-
E2E Test
-
- E2E Test Page
-
-
-
- `);
- });
+ tddServer.stdout.on('data', data => {
+ if (
+ data.toString().includes('TDD server started') ||
+ data.toString().includes('localhost:47392')
+ ) {
+ clearTimeout(timeout);
+ resolve();
+ }
+ });
- await new Promise(resolve => {
- testServer.listen(0, '127.0.0.1', () => {
- testServerPort = testServer.address().port;
- resolve();
+ tddServer.on('error', err => {
+ clearTimeout(timeout);
+ reject(err);
+ });
});
- });
+ }
+
+ // Start screenshot server
+ screenshotServer = await startScreenshotServer();
});
after(async () => {
if (browserInstance) await closeBrowser(browserInstance);
if (screenshotServer) await stopScreenshotServer(screenshotServer);
- if (testServer) testServer.close();
- if (tddServer) {
+ if (testSiteServer) testSiteServer.kill('SIGTERM');
+ if (tddServer && !externalServer) {
tddServer.kill('SIGTERM');
- // Wait for process to exit
await new Promise(resolve => {
tddServer.on('exit', resolve);
setTimeout(resolve, 2000);
});
}
- // Cleanup test directory
- try {
- rmSync(testDir, { recursive: true, force: true });
- } catch {
- // Ignore cleanup errors
+ if (!externalServer) {
+ try {
+ rmSync(testDir, { recursive: true, force: true });
+ } catch {
+ // Ignore cleanup errors
+ }
}
});
- it('captures screenshot and sends to TDD server', async () => {
- // Start screenshot server
- screenshotServer = await startScreenshotServer();
+ // ===========================================================================
+ // Homepage Tests
+ // ===========================================================================
+
+ it('captures homepage full page screenshot', async () => {
let screenshotUrl = `http://127.0.0.1:${screenshotServer.port}`;
- // Launch browser
- let testUrl = `http://127.0.0.1:${testServerPort}/`;
- browserInstance = await launchBrowser('chromium', testUrl, {
+ browserInstance = await launchBrowser('chromium', `${testSiteUrl}/index.html`, {
screenshotUrl,
playwrightOptions: { headless: true },
});
setPage(browserInstance.page);
- // Make screenshot request directly from test
let response = await fetch(`${screenshotUrl}/screenshot`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
- name: 'e2e-test-screenshot',
- properties: { test: 'e2e' },
+ name: 'homepage-full',
+ properties: { page: 'homepage', fullPage: true },
}),
});
let result = await response.json();
+ assert.strictEqual(response.status, 200, 'Should succeed');
+ assert.ok(['new', 'match'].includes(result.status), `Should have status 'new' or 'match', got: ${result.status}`);
+ });
+
+ it('captures navigation bar with selector', async () => {
+ let screenshotUrl = `http://127.0.0.1:${screenshotServer.port}`;
+
+ let response = await fetch(`${screenshotUrl}/screenshot`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ name: 'homepage-nav',
+ selector: 'nav',
+ properties: { component: 'navigation' },
+ }),
+ });
+ let result = await response.json();
assert.strictEqual(response.status, 200, 'Should succeed');
- assert.ok(result.success || result.comparison, 'Should have success or comparison');
+ assert.ok(['new', 'match'].includes(result.status), `Should have status 'new' or 'match', got: ${result.status}`);
});
-});
-// Run a simpler version without TDD server
-describe('e2e without TDD server', () => {
- let screenshotServer = null;
- let testServer = null;
- let testServerPort = null;
- let browserInstance = null;
+ it('captures hero section', async () => {
+ let screenshotUrl = `http://127.0.0.1:${screenshotServer.port}`;
- before(async () => {
- // Start test page server
- testServer = createServer((_req, res) => {
- res.writeHead(200, { 'Content-Type': 'text/html' });
- res.end(`
-
-
- Simple E2E
-
- Simple Test
-
-
-
- `);
+ let response = await fetch(`${screenshotUrl}/screenshot`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ name: 'homepage-hero',
+ selector: 'section',
+ properties: { section: 'hero' },
+ }),
});
- await new Promise(resolve => {
- testServer.listen(0, '127.0.0.1', () => {
- testServerPort = testServer.address().port;
- resolve();
- });
+ let result = await response.json();
+ assert.strictEqual(response.status, 200, 'Should succeed');
+ assert.ok(['new', 'match'].includes(result.status), `Should have status 'new' or 'match', got: ${result.status}`);
+ });
+
+ // ===========================================================================
+ // Multiple Pages
+ // ===========================================================================
+
+ it('captures features page', async () => {
+ let screenshotUrl = `http://127.0.0.1:${screenshotServer.port}`;
+
+ // Navigate to features page
+ await browserInstance.page.goto(`${testSiteUrl}/features.html`);
+ await browserInstance.page.waitForLoadState('networkidle');
+
+ let response = await fetch(`${screenshotUrl}/screenshot`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ name: 'features-full',
+ properties: { page: 'features' },
+ }),
});
+
+ let result = await response.json();
+ assert.strictEqual(response.status, 200, 'Should succeed');
+ assert.ok(['new', 'match'].includes(result.status), `Should have status 'new' or 'match', got: ${result.status}`);
});
- after(async () => {
- if (browserInstance) await closeBrowser(browserInstance);
- if (screenshotServer) await stopScreenshotServer(screenshotServer);
- if (testServer) testServer.close();
+ it('captures pricing page', async () => {
+ let screenshotUrl = `http://127.0.0.1:${screenshotServer.port}`;
+
+ await browserInstance.page.goto(`${testSiteUrl}/pricing.html`);
+ await browserInstance.page.waitForLoadState('networkidle');
+
+ let response = await fetch(`${screenshotUrl}/screenshot`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ name: 'pricing-full',
+ properties: { page: 'pricing' },
+ }),
+ });
+
+ let result = await response.json();
+ assert.strictEqual(response.status, 200, 'Should succeed');
+ assert.ok(['new', 'match'].includes(result.status), `Should have status 'new' or 'match', got: ${result.status}`);
+ });
+
+ it('captures contact page', async () => {
+ let screenshotUrl = `http://127.0.0.1:${screenshotServer.port}`;
+
+ await browserInstance.page.goto(`${testSiteUrl}/contact.html`);
+ await browserInstance.page.waitForLoadState('networkidle');
+
+ let response = await fetch(`${screenshotUrl}/screenshot`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ name: 'contact-full',
+ properties: { page: 'contact' },
+ }),
+ });
+
+ let result = await response.json();
+ assert.strictEqual(response.status, 200, 'Should succeed');
+ assert.ok(['new', 'match'].includes(result.status), `Should have status 'new' or 'match', got: ${result.status}`);
+ });
+
+ // ===========================================================================
+ // Screenshot Options
+ // ===========================================================================
+
+ it('captures screenshot with threshold option', async () => {
+ let screenshotUrl = `http://127.0.0.1:${screenshotServer.port}`;
+
+ await browserInstance.page.goto(`${testSiteUrl}/index.html`);
+
+ let response = await fetch(`${screenshotUrl}/screenshot`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ name: 'threshold-test',
+ selector: 'nav',
+ threshold: 5,
+ properties: { test: 'threshold' },
+ }),
+ });
+
+ let result = await response.json();
+ assert.strictEqual(response.status, 200, 'Should succeed');
+ assert.ok(['new', 'match'].includes(result.status), `Should have status 'new' or 'match', got: ${result.status}`);
});
- it('screenshot server receives request and captures screenshot', async () => {
+ it('captures screenshot with all options', async () => {
+ let screenshotUrl = `http://127.0.0.1:${screenshotServer.port}`;
+
+ let response = await fetch(`${screenshotUrl}/screenshot`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ name: 'all-options-test',
+ selector: 'footer',
+ threshold: 3,
+ properties: {
+ browser: 'chromium',
+ viewport: { width: 1920, height: 1080 },
+ testType: 'comprehensive',
+ },
+ }),
+ });
+
+ let result = await response.json();
+ assert.strictEqual(response.status, 200, 'Should succeed');
+ assert.ok(['new', 'match'].includes(result.status), `Should have status 'new' or 'match', got: ${result.status}`);
+ });
+
+ // ===========================================================================
+ // Multiple Screenshots Per Test
+ // ===========================================================================
+
+ it('captures multiple screenshots in sequence', async () => {
+ let screenshotUrl = `http://127.0.0.1:${screenshotServer.port}`;
+ let pages = ['index.html', 'features.html', 'pricing.html'];
+
+ for (let i = 0; i < pages.length; i++) {
+ await browserInstance.page.goto(`${testSiteUrl}/${pages[i]}`);
+ await browserInstance.page.waitForLoadState('networkidle');
+
+ let response = await fetch(`${screenshotUrl}/screenshot`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ name: `sequence-${i}`,
+ selector: 'nav',
+ properties: { page: pages[i], index: i },
+ }),
+ });
+
+ let result = await response.json();
+ assert.strictEqual(response.status, 200, `Should succeed for ${pages[i]}`);
+ assert.ok(['new', 'match'].includes(result.status), `Should have status 'new' or 'match', got: ${result.status}`);
+ }
+ });
+});
+
+// =============================================================================
+// Tests without TDD Server (verify screenshot server mechanics)
+// Skip this suite when running under vizzly wrapper since a TDD server IS available
+// =============================================================================
+
+describe('screenshot server mechanics (without TDD server)', { skip: externalServer }, () => {
+ let screenshotServer = null;
+ let testSiteServer = null;
+ let browserInstance = null;
+ let testSiteUrl = null;
+
+ before(async () => {
+ // Start test-site server
+ let testSiteInfo = await startTestSiteServer(4030 + Math.floor(Math.random() * 1000));
+ testSiteServer = testSiteInfo.server;
+ testSiteUrl = testSiteInfo.url;
+
+ // Start screenshot server
screenshotServer = await startScreenshotServer();
let screenshotUrl = `http://127.0.0.1:${screenshotServer.port}`;
- let testUrl = `http://127.0.0.1:${testServerPort}/`;
- browserInstance = await launchBrowser('chromium', testUrl, {
+ // Launch browser pointing to test-site
+ browserInstance = await launchBrowser('chromium', `${testSiteUrl}/index.html`, {
screenshotUrl,
playwrightOptions: { headless: true },
});
setPage(browserInstance.page);
+ });
- // Request screenshot - will fail at forward step since no TDD server
- let response = await fetch(`${screenshotUrl}/screenshot`, {
+ after(async () => {
+ if (browserInstance) await closeBrowser(browserInstance);
+ if (screenshotServer) await stopScreenshotServer(screenshotServer);
+ if (testSiteServer) testSiteServer.kill('SIGTERM');
+ });
+
+ it('screenshot server receives request and captures screenshot (fails without TDD)', async () => {
+ let response = await fetch(`http://127.0.0.1:${screenshotServer.port}/screenshot`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
- name: 'simple-test',
- selector: '#target',
+ name: 'no-tdd-test',
+ selector: 'nav',
}),
});
- // We expect 500 because no TDD server, but the screenshot was captured
+ // Without TDD server, should fail at forwarding step
assert.strictEqual(response.status, 500, 'Should fail without TDD server');
let result = await response.json();
@@ -207,9 +408,7 @@ describe('e2e without TDD server', () => {
});
it('health endpoint works while page is set', async () => {
- let response = await fetch(
- `http://127.0.0.1:${screenshotServer.port}/health`
- );
+ let response = await fetch(`http://127.0.0.1:${screenshotServer.port}/health`);
let result = await response.json();
assert.strictEqual(response.status, 200);
diff --git a/clients/ruby/Rakefile b/clients/ruby/Rakefile
index 89cd8566..c5ae5cb1 100644
--- a/clients/ruby/Rakefile
+++ b/clients/ruby/Rakefile
@@ -3,12 +3,66 @@
require 'rake/testtask'
require 'rubocop/rake_task'
+# Path to vizzly CLI
+VIZZLY_CLI = File.expand_path('../../bin/vizzly.js', __dir__)
+
+# Unit tests
Rake::TestTask.new(:test) do |t|
t.libs << 'test'
t.libs << 'lib'
- t.test_files = FileList['test/**/*_test.rb']
+ t.test_files = FileList['test/vizzly_test.rb']
end
RuboCop::RakeTask.new
task default: %i[rubocop test]
+
+# =============================================================================
+# Integration Tests (starts TDD server internally)
+# =============================================================================
+
+desc 'Run integration tests (starts TDD server internally)'
+task 'test:integration' do
+ ENV['VIZZLY_INTEGRATION'] = '1'
+ sh 'ruby -I lib test/integration_test.rb'
+end
+
+# =============================================================================
+# E2E Tests with Browser (requires selenium-webdriver)
+# =============================================================================
+
+desc 'Run E2E tests with browser (requires selenium-webdriver and TDD server)'
+task 'test:e2e' do
+ ENV['VIZZLY_E2E'] = '1'
+ sh 'ruby -I lib test/e2e_test.rb'
+end
+
+# =============================================================================
+# TDD Run Mode - One-shot with static report
+# =============================================================================
+
+desc 'Run integration tests in TDD mode (one-shot, generates static report)'
+task 'test:tdd:run' do
+ sh "node #{VIZZLY_CLI} tdd run 'VIZZLY_INTEGRATION=1 ruby -I lib test/integration_test.rb'"
+end
+
+desc 'Run E2E tests in TDD mode (one-shot, generates static report)'
+task 'test:e2e:tdd:run' do
+ sh "node #{VIZZLY_CLI} tdd run 'VIZZLY_E2E=1 ruby -I lib test/e2e_test.rb'"
+end
+
+# =============================================================================
+# Cloud Run Mode - Uploads to Vizzly (requires VIZZLY_TOKEN)
+# =============================================================================
+
+desc 'Run integration tests in cloud mode (uploads to Vizzly)'
+task 'test:cloud' do
+ abort 'VIZZLY_TOKEN required for cloud mode' unless ENV['VIZZLY_TOKEN']
+ sh "node #{VIZZLY_CLI} run 'VIZZLY_INTEGRATION=1 ruby -I lib test/integration_test.rb'"
+end
+
+desc 'Run E2E tests in cloud mode (uploads to Vizzly)'
+task 'test:e2e:cloud' do
+ abort 'VIZZLY_TOKEN required for cloud mode' unless ENV['VIZZLY_TOKEN']
+ sh "node #{VIZZLY_CLI} run 'VIZZLY_E2E=1 ruby -I lib test/e2e_test.rb'"
+end
diff --git a/clients/ruby/test/e2e_test.rb b/clients/ruby/test/e2e_test.rb
new file mode 100644
index 00000000..2ec7f1b2
--- /dev/null
+++ b/clients/ruby/test/e2e_test.rb
@@ -0,0 +1,303 @@
+# frozen_string_literal: true
+
+require 'minitest/autorun'
+require 'json'
+require 'fileutils'
+require 'tmpdir'
+require 'webrick'
+require_relative '../lib/vizzly'
+
+# Helper methods for E2E test setup and teardown
+module E2ETestHelpers
+ def find_vizzly_cli
+ path = File.expand_path('../../../dist/cli.js', __dir__)
+ return nil unless File.exist?(path)
+
+ path
+ end
+
+ def cli_path
+ @cli_path ||= find_vizzly_cli
+ end
+
+ def find_test_site
+ test_site_path = File.expand_path('../../../test-site', __dir__)
+ return nil unless File.exist?(File.join(test_site_path, 'index.html'))
+
+ test_site_path
+ end
+
+ def start_test_site_server
+ @test_site_port = rand(3030..4029)
+ @test_site_url = "http://localhost:#{@test_site_port}"
+
+ @test_site_server = WEBrick::HTTPServer.new(
+ Port: @test_site_port,
+ DocumentRoot: @test_site_path,
+ Logger: WEBrick::Log.new(File::NULL),
+ AccessLog: []
+ )
+
+ @test_site_thread = Thread.new { @test_site_server.start }
+ sleep 0.5
+ end
+
+ def stop_test_site_server
+ @test_site_server&.shutdown
+ @test_site_thread&.join(2)
+ end
+
+ def start_vizzly_server
+ return if @external_server
+
+ pid = spawn('node', cli_path, 'tdd', 'start', %i[out err] => File::NULL)
+ _pid, status = Process.wait2(pid)
+ raise 'Failed to start Vizzly TDD server' unless status.success?
+
+ 30.times do
+ break if File.exist?('.vizzly/server.json')
+
+ sleep 0.1
+ end
+
+ raise 'Vizzly server failed to start' unless File.exist?('.vizzly/server.json')
+ end
+
+ def stop_vizzly_server
+ return if @external_server
+
+ pid = spawn('node', cli_path, 'tdd', 'stop', %i[out err] => File::NULL)
+ Process.wait(pid)
+ end
+
+ def setup_selenium
+ options = Selenium::WebDriver::Chrome::Options.new
+ options.add_argument('--headless')
+ options.add_argument('--disable-gpu')
+ options.add_argument('--window-size=1920,1080')
+ options.add_argument('--no-sandbox')
+ options.add_argument('--disable-dev-shm-usage')
+
+ @driver = Selenium::WebDriver.for :chrome, options: options
+ end
+
+ def wait_for_page_load
+ wait = Selenium::WebDriver::Wait.new(timeout: 10)
+ wait.until { @driver.find_element(tag_name: 'body') }
+ sleep 0.3
+ end
+
+ def capture_screenshot(name, options = {})
+ image_data = @driver.screenshot_as(:png)
+ Vizzly.screenshot(name, image_data, options)
+ end
+
+ def capture_element_screenshot(name, element, options = {})
+ @driver.execute_script('arguments[0].scrollIntoView(true);', element)
+ sleep 0.1
+
+ image_data = element.screenshot_as(:png)
+ Vizzly.screenshot(name, image_data, options)
+ end
+
+ # Check if running in cloud mode (vizzly run) vs TDD mode (vizzly tdd run)
+ # Cloud mode returns { success: true } without a status field
+ # TDD mode returns comparison results with status field
+ def cloud_mode?
+ token = ENV.fetch('VIZZLY_TOKEN', nil)
+ token && !token.empty?
+ end
+
+ def assert_screenshot_result(result)
+ assert result, 'Expected screenshot result to be non-nil'
+ if cloud_mode?
+ assert result['success'], "Expected success to be true in cloud mode, got: #{result.inspect}"
+ else
+ assert %w[new match].include?(result['status']),
+ "Expected status 'new' or 'match', got: #{result['status']}"
+ end
+ end
+end
+
+# E2E test using the shared test-site (FluffyCloud)
+# Run with: VIZZLY_E2E=1 ruby test/e2e_test.rb
+#
+# When run via `vizzly tdd run`, VIZZLY_SERVER_URL is set and we use that server.
+# When run standalone, we start our own server.
+#
+# Requires:
+# - Selenium WebDriver gem: gem install selenium-webdriver
+# - Chrome/Chromium browser
+# - ChromeDriver in PATH
+#
+# This test captures real browser screenshots from the shared test-site
+# to ensure consistency with other SDK E2E tests.
+class E2ETest < Minitest::Test
+ include E2ETestHelpers
+
+ def setup
+ skip 'Set VIZZLY_E2E=1 to run E2E tests' unless ENV['VIZZLY_E2E']
+
+ begin
+ require 'selenium-webdriver'
+ rescue LoadError
+ skip 'selenium-webdriver gem not installed (gem install selenium-webdriver)'
+ end
+
+ @original_dir = Dir.pwd
+ Vizzly.reset!
+
+ # Check if we're running under `vizzly tdd run` or `vizzly run`
+ @external_server = !ENV['VIZZLY_SERVER_URL'].nil?
+
+ if @external_server
+ # Running under vizzly wrapper - server is already running
+ @temp_dir = nil
+ else
+ # Running standalone - create temp dir and start our own server
+ @temp_dir = Dir.mktmpdir
+ Dir.chdir(@temp_dir)
+ skip 'Vizzly CLI not found' unless cli_path
+ end
+
+ @test_site_path = find_test_site
+ skip 'test-site not found' unless @test_site_path
+
+ # Start test site server
+ start_test_site_server
+
+ # Start Vizzly TDD server (only if not using external)
+ start_vizzly_server
+
+ # Setup Selenium
+ setup_selenium
+ end
+
+ def teardown
+ @driver&.quit
+ stop_vizzly_server
+ stop_test_site_server
+ return unless @temp_dir
+
+ Dir.chdir(@original_dir) if @original_dir
+ FileUtils.rm_rf(@temp_dir)
+ end
+
+ # ===========================================================================
+ # Homepage Tests
+ # ===========================================================================
+
+ def test_homepage_full_page
+ @driver.navigate.to "#{@test_site_url}/index.html"
+ wait_for_page_load
+
+ result = capture_screenshot('homepage-full', full_page: true)
+ assert_screenshot_result(result)
+ end
+
+ def test_homepage_navigation
+ @driver.navigate.to "#{@test_site_url}/index.html"
+ wait_for_page_load
+
+ nav = @driver.find_element(tag_name: 'nav')
+ result = capture_element_screenshot('homepage-nav', nav)
+ assert_screenshot_result(result)
+ end
+
+ def test_homepage_hero_section
+ @driver.navigate.to "#{@test_site_url}/index.html"
+ wait_for_page_load
+
+ hero = @driver.find_element(tag_name: 'section')
+ result = capture_element_screenshot('homepage-hero', hero,
+ properties: { section: 'hero', page: 'homepage' })
+ assert_screenshot_result(result)
+ end
+
+ # ===========================================================================
+ # Multiple Pages
+ # ===========================================================================
+
+ def test_features_page
+ @driver.navigate.to "#{@test_site_url}/features.html"
+ wait_for_page_load
+
+ result = capture_screenshot('features-full', full_page: true)
+ assert_screenshot_result(result)
+ end
+
+ def test_pricing_page
+ @driver.navigate.to "#{@test_site_url}/pricing.html"
+ wait_for_page_load
+
+ result = capture_screenshot('pricing-full', full_page: true)
+ assert_screenshot_result(result)
+ end
+
+ def test_contact_page
+ @driver.navigate.to "#{@test_site_url}/contact.html"
+ wait_for_page_load
+
+ result = capture_screenshot('contact-full', full_page: true)
+ assert_screenshot_result(result)
+ end
+
+ # ===========================================================================
+ # Options Testing
+ # ===========================================================================
+
+ def test_screenshot_with_threshold
+ @driver.navigate.to "#{@test_site_url}/index.html"
+ wait_for_page_load
+
+ nav = @driver.find_element(tag_name: 'nav')
+ result = capture_element_screenshot('threshold-test', nav, threshold: 5)
+ assert_screenshot_result(result)
+ end
+
+ def test_screenshot_with_properties
+ @driver.navigate.to "#{@test_site_url}/index.html"
+ wait_for_page_load
+
+ result = capture_screenshot('props-test',
+ properties: {
+ browser: 'chrome',
+ viewport: { width: 1920, height: 1080 },
+ theme: 'light'
+ })
+ assert_screenshot_result(result)
+ end
+
+ def test_screenshot_with_all_options
+ @driver.navigate.to "#{@test_site_url}/index.html"
+ wait_for_page_load
+
+ result = capture_screenshot('all-options-test',
+ full_page: true,
+ threshold: 3,
+ properties: {
+ browser: 'chrome',
+ page: 'homepage',
+ test_type: 'comprehensive'
+ })
+ assert_screenshot_result(result)
+ end
+
+ # ===========================================================================
+ # Multiple Screenshots
+ # ===========================================================================
+
+ def test_navigation_across_pages
+ pages = %w[index.html features.html pricing.html contact.html]
+
+ pages.each_with_index do |page, index|
+ @driver.navigate.to "#{@test_site_url}/#{page}"
+ wait_for_page_load
+
+ nav = @driver.find_element(tag_name: 'nav')
+ result = capture_element_screenshot("nav-page-#{index}", nav,
+ properties: { page: page })
+ assert_screenshot_result(result)
+ end
+ end
+end
diff --git a/clients/ruby/test/integration_test.rb b/clients/ruby/test/integration_test.rb
index 6c2bf6e1..3ef6ac54 100644
--- a/clients/ruby/test/integration_test.rb
+++ b/clients/ruby/test/integration_test.rb
@@ -7,114 +7,324 @@
require 'open3'
require_relative '../lib/vizzly'
+# Helper methods for integration test setup and server management
+module IntegrationTestHelpers
+ def find_vizzly_cli
+ path = File.expand_path('../../../dist/cli.js', __dir__)
+ return nil unless File.exist?(path)
+
+ path
+ end
+
+ def cli_path
+ @cli_path ||= find_vizzly_cli
+ end
+
+ def start_server
+ return if @external_server
+
+ pid = spawn('node', cli_path, 'tdd', 'start', %i[out err] => File::NULL)
+ _pid, status = Process.wait2(pid)
+ raise 'Failed to execute vizzly tdd start' unless status.success?
+
+ 30.times do
+ break if File.exist?('.vizzly/server.json')
+
+ sleep 0.1
+ end
+
+ unless File.exist?('.vizzly/server.json')
+ error_log = File.join('.vizzly', 'daemon-error.log')
+ puts "Error log: #{File.read(error_log)}" if File.exist?(error_log)
+ raise 'Server failed to start'
+ end
+
+ @server_pid = true
+ end
+
+ def stop_server
+ return unless @server_pid
+ return if @external_server
+
+ pid = spawn('node', cli_path, 'tdd', 'stop', %i[out err] => File::NULL)
+ Process.wait(pid)
+ @server_pid = nil
+ end
+
+ # Check if running in cloud mode (vizzly run) vs TDD mode (vizzly tdd run)
+ # Cloud mode returns { success: true } without a status field
+ # TDD mode returns comparison results with status field
+ def cloud_mode?
+ # In cloud mode, VIZZLY_TOKEN is typically set
+ token = ENV.fetch('VIZZLY_TOKEN', nil)
+ token && !token.empty?
+ end
+
+ # Assert that a screenshot result indicates success
+ # In TDD mode: expects 'new' or 'match' status
+ # In cloud mode: expects 'success' to be true
+ def assert_screenshot_success(result)
+ if cloud_mode?
+ assert result['success'], "Expected success to be true in cloud mode, got: #{result.inspect}"
+ else
+ assert %w[new match].include?(result['status']), "Expected status 'new' or 'match', got: #{result['status']}"
+ end
+ end
+
+ # Create a minimal valid PNG (1x1 red pixel)
+ def create_test_png
+ [
+ 137, 80, 78, 71, 13, 10, 26, 10,
+ 0, 0, 0, 13, 73, 72, 68, 82,
+ 0, 0, 0, 1, 0, 0, 0, 1,
+ 8, 2, 0, 0, 0, 144, 119, 83, 222,
+ 0, 0, 0, 12, 73, 68, 65, 84,
+ 8, 215, 99, 248, 207, 192, 0, 0, 3, 1, 1, 0,
+ 24, 221, 141, 176,
+ 0, 0, 0, 0, 73, 69, 78, 68,
+ 174, 66, 96, 130
+ ].pack('C*')
+ end
+end
+
# Integration test that requires a running Vizzly server
# Run with: VIZZLY_INTEGRATION=1 ruby test/integration_test.rb
+#
+# When run via `vizzly tdd run`, VIZZLY_SERVER_URL is set and we use that server.
+# When run standalone, we start our own server.
+#
+# These tests use a minimal PNG for fast execution. For browser-based tests
+# with the shared test-site, see example/test_screenshot.rb
class IntegrationTest < Minitest::Test
+ include IntegrationTestHelpers
+
def setup
skip 'Set VIZZLY_INTEGRATION=1 to run integration tests' unless ENV['VIZZLY_INTEGRATION']
@original_dir = Dir.pwd
- @temp_dir = Dir.mktmpdir
- Dir.chdir(@temp_dir)
Vizzly.reset!
- # Ensure we have a Vizzly CLI available
- @vizzly_cli = find_vizzly_cli
- skip 'Vizzly CLI not found' unless @vizzly_cli
+ # Check if we're running under `vizzly tdd run` or `vizzly run`
+ @external_server = !ENV['VIZZLY_SERVER_URL'].nil?
+
+ if @external_server
+ # Running under vizzly wrapper - server is already running
+ # Stay in current directory (where server.json exists)
+ @temp_dir = nil
+ else
+ # Running standalone - create temp dir and start our own server
+ @temp_dir = Dir.mktmpdir
+ Dir.chdir(@temp_dir)
+ skip 'Vizzly CLI not found' unless cli_path
+ end
end
def teardown
stop_server if @server_pid
+ return unless @temp_dir
+
Dir.chdir(@original_dir)
FileUtils.rm_rf(@temp_dir)
end
- def test_screenshot_with_running_server
+ # ===========================================================================
+ # Basic Screenshot Capture
+ # ===========================================================================
+
+ def test_basic_screenshot
start_server
+ image_data = create_test_png
+
+ result = Vizzly.screenshot('basic-screenshot', image_data)
+
+ assert result, 'Expected result to be non-nil'
+ assert_screenshot_success(result)
+ end
- # Create a simple PNG (1x1 red pixel)
+ def test_screenshot_with_properties
+ start_server
+ image_data = create_test_png
+
+ result = Vizzly.screenshot('screenshot-with-props', image_data,
+ properties: {
+ browser: 'chrome',
+ viewport: { width: 1920, height: 1080 },
+ theme: 'light'
+ })
+
+ assert result, 'Expected result to be non-nil'
+ assert_screenshot_success(result)
+ end
+
+ def test_screenshot_with_threshold
+ start_server
image_data = create_test_png
- # Take a screenshot
- result = Vizzly.screenshot('test-screenshot', image_data,
- properties: { browser: 'chrome', viewport: { width: 1920, height: 1080 } })
+ result = Vizzly.screenshot('screenshot-threshold', image_data, threshold: 5)
assert result, 'Expected result to be non-nil'
- # TDD mode returns status: 'new' for first screenshot, 'match' for subsequent
- assert %w[new match].include?(result['status']), "Expected status 'new' or 'match', got: #{result['status']}"
+ assert_screenshot_success(result)
end
- def test_screenshot_with_auto_discovery
+ def test_screenshot_with_full_page
+ start_server
+ image_data = create_test_png
+
+ result = Vizzly.screenshot('screenshot-fullpage', image_data, full_page: true)
+
+ assert result, 'Expected result to be non-nil'
+ assert_screenshot_success(result)
+ end
+
+ def test_screenshot_with_all_options
+ start_server
+ image_data = create_test_png
+
+ result = Vizzly.screenshot('screenshot-all-options', image_data,
+ properties: {
+ browser: 'firefox',
+ viewport: { width: 1280, height: 720 },
+ component: 'hero'
+ },
+ threshold: 3,
+ full_page: false)
+
+ assert result, 'Expected result to be non-nil'
+ assert_screenshot_success(result)
+ end
+
+ # ===========================================================================
+ # Auto-Discovery
+ # ===========================================================================
+
+ def test_auto_discovery_via_server_json
+ # Skip when running under vizzly wrapper (server.json is in different directory)
+ skip 'Skipped under vizzly tdd run (uses external server)' if @external_server
+
start_server
# Verify server.json was created
- assert File.exist?('.vizzly/server.json')
+ assert File.exist?('.vizzly/server.json'), 'server.json should be created'
# Create new client (should auto-discover)
client = Vizzly::Client.new
- assert client.ready?
+ assert client.ready?, 'Client should be ready after auto-discovery'
assert_match(/localhost:\d+/, client.server_url)
image_data = create_test_png
result = client.screenshot('auto-discovered', image_data)
assert result, 'Expected result to be non-nil'
- # TDD mode returns status: 'new' for first screenshot, 'match' for subsequent
- assert %w[new match].include?(result['status']), "Expected status 'new' or 'match', got: #{result['status']}"
+ assert_screenshot_success(result)
end
- private
+ # ===========================================================================
+ # Client Configuration
+ # ===========================================================================
- def find_vizzly_cli
- # Try to find vizzly CLI in parent directories
- cli_path = File.expand_path('../../../dist/cli.js', __dir__)
- return nil unless File.exist?(cli_path)
+ def test_explicit_server_url
+ # Skip when running under vizzly wrapper (server.json is in different directory)
+ skip 'Skipped under vizzly tdd run (uses external server)' if @external_server
- "node #{cli_path}"
+ start_server
+
+ # Read port from server.json
+ server_info = JSON.parse(File.read('.vizzly/server.json'))
+ port = server_info['port']
+
+ client = Vizzly::Client.new(server_url: "http://localhost:#{port}")
+ assert client.ready?, 'Client with explicit URL should be ready'
+ assert_equal "http://localhost:#{port}", client.server_url
+
+ image_data = create_test_png
+ result = client.screenshot('explicit-url', image_data)
+
+ assert result, 'Expected result to be non-nil'
end
- def start_server
- # Start vizzly tdd in background (it daemonizes itself)
- success = system("#{@vizzly_cli} tdd start > /dev/null 2>&1")
+ def test_client_info
+ start_server
- raise 'Failed to execute vizzly tdd start' unless success
+ client = Vizzly::Client.new
+ info = client.info
- # Wait for server to be ready
- 30.times do
- break if File.exist?('.vizzly/server.json')
+ assert_equal true, info[:enabled]
+ assert_equal true, info[:ready]
+ assert_equal false, info[:disabled]
+ assert_match(/localhost:\d+/, info[:server_url])
+ end
- sleep 0.1
- end
+ def test_client_ready_state
+ start_server
- unless File.exist?('.vizzly/server.json')
- # Try to read error log if it exists
- error_log = File.join('.vizzly', 'daemon-error.log')
- puts "Error log: #{File.read(error_log)}" if File.exist?(error_log)
- raise 'Server failed to start'
- end
+ client = Vizzly::Client.new
+ assert client.ready?, 'Client should be ready with running server'
+ refute client.disabled?, 'Client should not be disabled'
+ end
+
+ # ===========================================================================
+ # Multiple Screenshots
+ # ===========================================================================
- @server_pid = true # Flag that server is running
+ def test_multiple_screenshots_sequence
+ start_server
+ image_data = create_test_png
+
+ # Capture multiple screenshots in sequence
+ result1 = Vizzly.screenshot('sequence-1', image_data, properties: { index: 1 })
+ result2 = Vizzly.screenshot('sequence-2', image_data, properties: { index: 2 })
+ result3 = Vizzly.screenshot('sequence-3', image_data, properties: { index: 3 })
+
+ assert result1, 'First screenshot should succeed'
+ assert result2, 'Second screenshot should succeed'
+ assert result3, 'Third screenshot should succeed'
end
- def stop_server
- return unless @server_pid
+ # ===========================================================================
+ # Singleton Client
+ # ===========================================================================
- system("#{@vizzly_cli} tdd stop")
- @server_pid = nil
+ def test_singleton_client
+ start_server
+ image_data = create_test_png
+
+ # Use module-level methods (singleton)
+ assert Vizzly.ready?, 'Singleton client should be ready'
+
+ result = Vizzly.screenshot('singleton-test', image_data)
+ assert result, 'Screenshot via singleton should succeed'
+
+ Vizzly.flush # Should complete without error
end
- # Create a minimal valid PNG (1x1 red pixel)
- def create_test_png
- [
- 137, 80, 78, 71, 13, 10, 26, 10, # PNG signature
- 0, 0, 0, 13, 73, 72, 68, 82, # IHDR chunk
- 0, 0, 0, 1, 0, 0, 0, 1, # 1x1 dimensions
- 8, 2, 0, 0, 0, 144, 119, 83, 222, # bit depth, color type, etc
- 0, 0, 0, 12, 73, 68, 65, 84, # IDAT chunk
- 8, 215, 99, 248, 207, 192, 0, 0, 3, 1, 1, 0, # compressed data
- 24, 221, 141, 176, # CRC
- 0, 0, 0, 0, 73, 69, 78, 68, # IEND chunk
- 174, 66, 96, 130 # CRC
- ].pack('C*')
+ # ===========================================================================
+ # Edge Cases
+ # ===========================================================================
+
+ def test_empty_properties
+ start_server
+ image_data = create_test_png
+
+ result = Vizzly.screenshot('empty-props', image_data, properties: {})
+
+ assert result, 'Screenshot with empty properties should succeed'
+ end
+
+ def test_zero_threshold
+ start_server
+ image_data = create_test_png
+
+ result = Vizzly.screenshot('zero-threshold', image_data, threshold: 0)
+
+ assert result, 'Screenshot with zero threshold should succeed'
+ end
+
+ def test_special_characters_in_name
+ start_server
+ image_data = create_test_png
+
+ result = Vizzly.screenshot('screenshot_with-special.chars', image_data)
+
+ assert result, 'Screenshot with special characters in name should succeed'
end
end
diff --git a/clients/static-site/package.json b/clients/static-site/package.json
index d28617ca..0c456cfe 100644
--- a/clients/static-site/package.json
+++ b/clients/static-site/package.json
@@ -45,7 +45,10 @@
"clean": "rimraf dist",
"compile": "babel src --out-dir dist --ignore '**/*.test.js'",
"prepublishOnly": "npm run lint && npm run build",
- "test": "node --test --test-reporter=spec $(find tests -name '*.test.js')",
+ "test": "node --test --test-reporter=spec $(find tests -name '*.test.js' ! -name 'e2e.test.js')",
+ "test:e2e": "VIZZLY_E2E=1 node --test --test-reporter=spec tests/e2e.test.js",
+ "test:e2e:tdd": "../../bin/vizzly.js tdd run 'VIZZLY_E2E=1 node --test --test-reporter=spec tests/e2e.test.js'",
+ "test:e2e:cloud": "../../bin/vizzly.js run 'VIZZLY_E2E=1 node --test --test-reporter=spec tests/e2e.test.js'",
"test:watch": "node --test --test-reporter=spec --watch $(find tests -name '*.test.js')",
"lint": "biome lint src",
"lint:fix": "biome lint --write src",
diff --git a/clients/static-site/tests/e2e.test.js b/clients/static-site/tests/e2e.test.js
new file mode 100644
index 00000000..7ee200a3
--- /dev/null
+++ b/clients/static-site/tests/e2e.test.js
@@ -0,0 +1,478 @@
+/**
+ * E2E Integration Tests for Static-Site SDK
+ *
+ * Uses the shared test-site (FluffyCloud) to verify the full screenshot
+ * capture flow: page discovery → browser launch → screenshot capture → TDD server.
+ *
+ * Run with: VIZZLY_E2E=1 npm test -- e2e.test.js
+ *
+ * Requires:
+ * - TDD server running: `vizzly tdd start`
+ * - test-site available at ../../../test-site
+ */
+
+import assert from 'node:assert';
+import { spawn } from 'node:child_process';
+import { existsSync, mkdirSync, rmSync } from 'node:fs';
+import { tmpdir } from 'node:os';
+import { join, resolve } from 'node:path';
+import { after, before, describe, it } from 'node:test';
+
+import { closeBrowser, launchBrowser } from '../src/browser.js';
+import { discoverPages } from '../src/crawler.js';
+import { createTabPool } from '../src/pool.js';
+import { captureScreenshot } from '../src/screenshot.js';
+import { startStaticServer, stopStaticServer } from '../src/server.js';
+import { generateTasks, processAllTasks } from '../src/tasks.js';
+
+// Paths
+let testDir = join(tmpdir(), `vizzly-static-site-e2e-${Date.now()}`);
+let testSitePath = resolve(import.meta.dirname, '../../../test-site');
+
+// Skip E2E tests unless explicitly enabled
+let runE2E = process.env.VIZZLY_E2E === '1';
+
+// Check if running under `vizzly tdd run` or `vizzly run`
+let externalServer = !!process.env.VIZZLY_SERVER_URL;
+
+describe('Static-Site E2E with shared test-site', { skip: !runE2E }, () => {
+ let tddServer = null;
+ let serverInfo = null;
+ let browser = null;
+ let pool = null;
+
+ // Mock logger for tests
+ let logger = {
+ info: () => {},
+ warn: () => {},
+ error: () => {},
+ debug: () => {},
+ };
+
+ before(async () => {
+ // Verify test-site exists
+ assert.ok(
+ existsSync(join(testSitePath, 'index.html')),
+ 'test-site/index.html should exist'
+ );
+
+ // Start TDD server only if not running under vizzly wrapper
+ if (!externalServer) {
+ // Create temp directory
+ mkdirSync(testDir, { recursive: true });
+
+ tddServer = spawn('npx', ['vizzly', 'tdd', 'start'], {
+ cwd: testDir,
+ stdio: ['ignore', 'pipe', 'pipe'],
+ env: { ...process.env, VIZZLY_HOME: testDir },
+ });
+
+ // Wait for TDD server to start
+ await new Promise((resolve, reject) => {
+ let timeout = setTimeout(
+ () => reject(new Error('TDD server timeout')),
+ 15000
+ );
+
+ tddServer.stdout.on('data', data => {
+ if (
+ data.toString().includes('TDD server started') ||
+ data.toString().includes('localhost:47392')
+ ) {
+ clearTimeout(timeout);
+ resolve();
+ }
+ });
+
+ tddServer.on('error', err => {
+ clearTimeout(timeout);
+ reject(err);
+ });
+ });
+ }
+
+ // Start static server for test-site
+ serverInfo = await startStaticServer(testSitePath);
+
+ // Launch browser
+ browser = await launchBrowser({ headless: true });
+ });
+
+ after(async () => {
+ if (pool) await pool.drain();
+ if (browser) await closeBrowser(browser);
+ if (serverInfo) await stopStaticServer(serverInfo);
+
+ if (tddServer && !externalServer) {
+ tddServer.kill('SIGTERM');
+ await new Promise(resolve => {
+ tddServer.on('exit', resolve);
+ setTimeout(resolve, 2000);
+ });
+ }
+
+ if (!externalServer) {
+ try {
+ rmSync(testDir, { recursive: true, force: true });
+ } catch {
+ // Ignore cleanup errors
+ }
+ }
+ });
+
+ // ===========================================================================
+ // Page Discovery Tests
+ // ===========================================================================
+
+ describe('Page Discovery', () => {
+ it('discovers all pages from test-site', async () => {
+ // Note: page.path is URL-like (/contact, /features) not file-like (contact.html)
+ let config = {
+ buildPath: testSitePath,
+ include: null, // No filtering - discover all
+ exclude: ['/playwright-report**'], // Exclude build artifacts
+ pageDiscovery: {
+ useSitemap: false,
+ scanHtml: true,
+ sitemapPath: 'sitemap.xml',
+ },
+ };
+
+ let pages = await discoverPages(testSitePath, config);
+
+ assert.ok(pages.length >= 4, `Should find at least 4 pages, found ${pages.length}`);
+
+ let pagePaths = pages.map(p => p.path);
+ assert.ok(
+ pagePaths.includes('/') || pagePaths.some(p => p.includes('index')),
+ 'Should find index page'
+ );
+ assert.ok(
+ pagePaths.some(p => p.includes('features')),
+ 'Should find features page'
+ );
+ assert.ok(
+ pagePaths.some(p => p.includes('pricing')),
+ 'Should find pricing page'
+ );
+ assert.ok(
+ pagePaths.some(p => p.includes('contact')),
+ 'Should find contact page'
+ );
+ });
+
+ it('filters pages with include patterns', async () => {
+ // page.path uses URL format: /, /features, /pricing, /contact
+ let config = {
+ buildPath: testSitePath,
+ include: ['/', '/features'],
+ exclude: null,
+ pageDiscovery: {
+ useSitemap: false,
+ scanHtml: true,
+ sitemapPath: 'sitemap.xml',
+ },
+ };
+
+ let pages = await discoverPages(testSitePath, config);
+ assert.strictEqual(pages.length, 2, `Should find exactly 2 pages, found ${pages.length}: ${pages.map(p => p.path).join(', ')}`);
+ });
+
+ it('excludes pages with exclude patterns', async () => {
+ // page.path uses URL format: /, /features, /pricing, /contact
+ let config = {
+ buildPath: testSitePath,
+ include: null,
+ exclude: ['/pricing', '/contact', '/playwright-report**'],
+ pageDiscovery: {
+ useSitemap: false,
+ scanHtml: true,
+ sitemapPath: 'sitemap.xml',
+ },
+ };
+
+ let pages = await discoverPages(testSitePath, config);
+ let pagePaths = pages.map(p => p.path);
+
+ assert.ok(
+ !pagePaths.some(p => p.includes('pricing')),
+ 'Should exclude pricing page'
+ );
+ assert.ok(
+ !pagePaths.some(p => p.includes('contact')),
+ 'Should exclude contact page'
+ );
+ assert.ok(pages.length >= 2, `Should have at least 2 pages (/ and /features), found ${pages.length}`);
+ });
+ });
+
+ // ===========================================================================
+ // Screenshot Capture Tests
+ // ===========================================================================
+
+ describe('Screenshot Capture', () => {
+ it('captures homepage screenshot', async () => {
+ let page = await browser.newPage();
+
+ try {
+ await page.goto(`${serverInfo.url}/index.html`, {
+ waitUntil: 'networkidle0',
+ });
+
+ let result = await captureScreenshot(
+ page,
+ 'e2e-homepage',
+ { width: 1920, height: 1080, name: 'desktop' },
+ {
+ threshold: 0,
+ fullPage: true,
+ properties: { page: 'homepage', test: 'e2e' },
+ }
+ );
+
+ assert.ok(result, 'Screenshot should succeed');
+ } finally {
+ await page.close();
+ }
+ });
+
+ it('captures element screenshot with selector', async () => {
+ let page = await browser.newPage();
+
+ try {
+ await page.goto(`${serverInfo.url}/index.html`, {
+ waitUntil: 'networkidle0',
+ });
+
+ let result = await captureScreenshot(
+ page,
+ 'e2e-nav',
+ { width: 1920, height: 1080, name: 'desktop' },
+ {
+ selector: 'nav',
+ properties: { component: 'navigation' },
+ }
+ );
+
+ assert.ok(result, 'Element screenshot should succeed');
+ } finally {
+ await page.close();
+ }
+ });
+
+ it('captures screenshots with custom threshold', async () => {
+ let page = await browser.newPage();
+
+ try {
+ await page.goto(`${serverInfo.url}/index.html`, {
+ waitUntil: 'networkidle0',
+ });
+
+ let result = await captureScreenshot(
+ page,
+ 'e2e-threshold',
+ { width: 1920, height: 1080, name: 'desktop' },
+ {
+ threshold: 5,
+ properties: { test: 'threshold' },
+ }
+ );
+
+ assert.ok(result, 'Screenshot with threshold should succeed');
+ } finally {
+ await page.close();
+ }
+ });
+ });
+
+ // ===========================================================================
+ // Task Generation Tests
+ // ===========================================================================
+
+ describe('Task Generation', () => {
+ it('generates tasks for pages and viewports', () => {
+ let pages = [
+ { path: '/index.html', source: 'html' },
+ { path: '/features.html', source: 'html' },
+ ];
+
+ let config = {
+ viewports: [
+ { name: 'desktop', width: 1920, height: 1080 },
+ { name: 'mobile', width: 375, height: 812 },
+ ],
+ threshold: 0,
+ };
+
+ let tasks = generateTasks(pages, serverInfo.url, config);
+
+ assert.strictEqual(tasks.length, 4, 'Should generate 4 tasks (2 pages × 2 viewports)');
+
+ // Verify task structure
+ let firstTask = tasks[0];
+ assert.ok(firstTask.url, 'Task should have URL');
+ assert.ok(firstTask.viewport, 'Task should have viewport');
+ assert.ok(firstTask.page, 'Task should have page');
+ assert.ok(firstTask.page.path, 'Task page should have path');
+ });
+ });
+
+ // ===========================================================================
+ // Tab Pool Tests
+ // ===========================================================================
+
+ describe('Tab Pool Processing', () => {
+ it('processes multiple pages through tab pool', async () => {
+ pool = createTabPool(browser, 2);
+
+ let pages = [
+ { path: '/index.html', source: 'html' },
+ { path: '/features.html', source: 'html' },
+ { path: '/pricing.html', source: 'html' },
+ ];
+
+ let config = {
+ viewports: [{ name: 'desktop', width: 1920, height: 1080 }],
+ threshold: 0,
+ fullPage: true,
+ hooks: [],
+ };
+
+ let tasks = generateTasks(pages, serverInfo.url, config);
+ let errors = await processAllTasks(tasks, pool, config, logger);
+
+ assert.strictEqual(errors.length, 0, 'All tasks should succeed');
+ });
+ });
+
+ // ===========================================================================
+ // Multi-Page Tests
+ // ===========================================================================
+
+ describe('Multi-Page Screenshots', () => {
+ it('captures all test-site pages', async () => {
+ let pages = ['index.html', 'features.html', 'pricing.html', 'contact.html'];
+ let results = [];
+
+ for (let pageName of pages) {
+ let page = await browser.newPage();
+
+ try {
+ await page.goto(`${serverInfo.url}/${pageName}`, {
+ waitUntil: 'networkidle0',
+ });
+
+ let result = await captureScreenshot(
+ page,
+ `e2e-${pageName.replace('.html', '')}`,
+ { width: 1920, height: 1080, name: 'desktop' },
+ {
+ fullPage: true,
+ properties: { page: pageName },
+ }
+ );
+
+ results.push({ page: pageName, success: !!result });
+ } finally {
+ await page.close();
+ }
+ }
+
+ let allSucceeded = results.every(r => r.success);
+ assert.ok(allSucceeded, 'All page screenshots should succeed');
+ });
+
+ it('captures pages with multiple viewports', async () => {
+ let viewports = [
+ { name: 'desktop', width: 1920, height: 1080 },
+ { name: 'tablet', width: 768, height: 1024 },
+ { name: 'mobile', width: 375, height: 812 },
+ ];
+
+ let results = [];
+
+ for (let viewport of viewports) {
+ let page = await browser.newPage();
+
+ try {
+ await page.setViewport(viewport);
+ await page.goto(`${serverInfo.url}/index.html`, {
+ waitUntil: 'networkidle0',
+ });
+
+ let result = await captureScreenshot(
+ page,
+ `e2e-homepage-${viewport.name}`,
+ viewport,
+ {
+ fullPage: true,
+ properties: { viewport: viewport.name },
+ }
+ );
+
+ results.push({ viewport: viewport.name, success: !!result });
+ } finally {
+ await page.close();
+ }
+ }
+
+ let allSucceeded = results.every(r => r.success);
+ assert.ok(allSucceeded, 'All viewport screenshots should succeed');
+ });
+ });
+});
+
+// ===========================================================================
+// Unit tests that don't require TDD server
+// ===========================================================================
+
+describe('Static-Site SDK (unit tests)', () => {
+ it('discovers pages from test-site directory', async () => {
+ let config = {
+ buildPath: testSitePath,
+ include: null, // No filtering
+ exclude: ['/playwright-report**'],
+ pageDiscovery: {
+ useSitemap: false,
+ scanHtml: true,
+ sitemapPath: 'sitemap.xml',
+ },
+ };
+
+ let pages = await discoverPages(testSitePath, config);
+ assert.ok(pages.length > 0, `Should discover pages, found ${pages.length}`);
+ });
+
+ it('generates correct task structure', () => {
+ let pages = [{ path: '/about', filePath: 'about.html', source: 'html' }];
+ let config = {
+ viewports: [{ name: 'desktop', width: 1920, height: 1080 }],
+ threshold: 0,
+ };
+
+ let tasks = generateTasks(pages, 'http://localhost:3000', config);
+
+ assert.strictEqual(tasks.length, 1);
+ assert.ok(tasks[0].page.path.includes('about'), 'Task page path should include page name');
+ assert.strictEqual(tasks[0].viewport.name, 'desktop', 'Task should have viewport');
+ assert.ok(tasks[0].url.includes('about'), 'Task URL should include page path');
+ });
+
+ it('handles include patterns correctly', async () => {
+ // page.path uses URL format: /
+ let config = {
+ buildPath: testSitePath,
+ include: ['/'],
+ exclude: null,
+ pageDiscovery: {
+ useSitemap: false,
+ scanHtml: true,
+ sitemapPath: 'sitemap.xml',
+ },
+ };
+
+ let pages = await discoverPages(testSitePath, config);
+ assert.strictEqual(pages.length, 1, `Should find only index page, found ${pages.length}: ${pages.map(p => p.path).join(', ')}`);
+ assert.strictEqual(pages[0].path, '/');
+ });
+});
diff --git a/clients/storybook/example-storybook/package-lock.json b/clients/storybook/example-storybook/package-lock.json
index e2878342..c3642648 100644
--- a/clients/storybook/example-storybook/package-lock.json
+++ b/clients/storybook/example-storybook/package-lock.json
@@ -48,6 +48,7 @@
"integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.3",
@@ -1875,6 +1876,7 @@
}
],
"license": "MIT",
+ "peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.8.9",
"caniuse-lite": "^1.0.30001746",
@@ -2007,8 +2009,7 @@
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"dev": true,
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/debug": {
"version": "4.4.3",
@@ -2155,6 +2156,7 @@
"dev": true,
"hasInstallScript": true,
"license": "MIT",
+ "peer": true,
"bin": {
"esbuild": "bin/esbuild"
},
@@ -3029,6 +3031,7 @@
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"loose-envify": "^1.1.0"
},
@@ -3074,6 +3077,7 @@
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.2"
@@ -3126,6 +3130,7 @@
"integrity": "sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@types/estree": "1.0.8"
},
@@ -3280,6 +3285,7 @@
"integrity": "sha512-sVKbCj/OTx67jhmauhxc2dcr1P+yOgz/x3h0krwjyMgdc5Oubvxyg4NYDZmzAw+ym36g/lzH8N0Ccp4dwtdfxw==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@storybook/core": "8.6.14"
},
@@ -3591,6 +3597,7 @@
"integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.4.4",
diff --git a/clients/storybook/package.json b/clients/storybook/package.json
index 5c454a6c..07d45a55 100644
--- a/clients/storybook/package.json
+++ b/clients/storybook/package.json
@@ -41,7 +41,10 @@
"clean": "rimraf dist",
"compile": "babel src --out-dir dist --ignore '**/*.test.js'",
"prepublishOnly": "npm run lint && npm run build",
- "test": "node --test --test-reporter=spec 'tests/**/*.test.js'",
+ "test": "node --test --test-reporter=spec $(find tests -name '*.test.js' ! -name 'e2e.test.js')",
+ "test:e2e": "VIZZLY_E2E=1 node --test --test-reporter=spec tests/e2e.test.js",
+ "test:e2e:tdd": "../../bin/vizzly.js tdd run 'VIZZLY_E2E=1 node --test --test-reporter=spec tests/e2e.test.js'",
+ "test:e2e:cloud": "../../bin/vizzly.js run 'VIZZLY_E2E=1 node --test --test-reporter=spec tests/e2e.test.js'",
"test:watch": "node --test --test-reporter=spec --watch 'tests/**/*.test.js'",
"lint": "biome lint src tests",
"lint:fix": "biome lint --write src tests",
diff --git a/clients/storybook/tests/e2e.test.js b/clients/storybook/tests/e2e.test.js
new file mode 100644
index 00000000..5a2db8cb
--- /dev/null
+++ b/clients/storybook/tests/e2e.test.js
@@ -0,0 +1,476 @@
+/**
+ * E2E Integration Tests for Storybook SDK
+ *
+ * Uses the example-storybook to verify the full screenshot capture flow:
+ * story discovery → browser launch → screenshot capture → TDD server.
+ *
+ * Run with: VIZZLY_E2E=1 npm test -- e2e.test.js
+ *
+ * Requires:
+ * - TDD server running: `vizzly tdd start`
+ * - example-storybook built: `cd example-storybook && npm run build`
+ */
+
+import assert from 'node:assert';
+import { spawn, execSync } from 'node:child_process';
+import { existsSync, mkdirSync, rmSync } from 'node:fs';
+import { tmpdir } from 'node:os';
+import { join, resolve } from 'node:path';
+import { after, before, describe, it } from 'node:test';
+
+import {
+ closeBrowser,
+ closePage,
+ launchBrowser,
+ prepareStoryPage,
+} from '../src/browser.js';
+import { discoverStories, generateStoryUrl } from '../src/crawler.js';
+import { getBeforeScreenshotHook, getStoryConfig } from '../src/hooks.js';
+import { captureAndSendScreenshot } from '../src/screenshot.js';
+import { startStaticServer, stopStaticServer } from '../src/server.js';
+
+// Paths
+let testDir = join(tmpdir(), `vizzly-storybook-e2e-${Date.now()}`);
+let exampleStorybookPath = resolve(import.meta.dirname, '../example-storybook');
+let storybookBuildPath = join(exampleStorybookPath, 'dist');
+
+// Skip E2E tests unless explicitly enabled
+let runE2E = process.env.VIZZLY_E2E === '1';
+
+// Check if running under `vizzly tdd run` or `vizzly run`
+let externalServer = !!process.env.VIZZLY_SERVER_URL;
+
+describe('Storybook E2E with example-storybook', { skip: !runE2E }, () => {
+ let tddServer = null;
+ let serverInfo = null;
+ let browser = null;
+
+ // Mock logger for tests (unused but kept for future use)
+ let _logger = {
+ info: () => {},
+ warn: () => {},
+ error: () => {},
+ debug: () => {},
+ };
+
+ before(async () => {
+ // Check if example-storybook is built, if not build it
+ if (!existsSync(storybookBuildPath)) {
+ console.log('Building example-storybook...');
+ try {
+ execSync('npm install && npm run build-storybook', {
+ cwd: exampleStorybookPath,
+ stdio: 'pipe',
+ });
+ } catch (error) {
+ console.error('Failed to build example-storybook:', error.message);
+ throw new Error(
+ 'example-storybook build required. Run: cd example-storybook && npm run build-storybook'
+ );
+ }
+ }
+
+ assert.ok(
+ existsSync(join(storybookBuildPath, 'index.html')),
+ 'dist/index.html should exist'
+ );
+
+ // Start TDD server only if not running under vizzly wrapper
+ if (!externalServer) {
+ // Create temp directory
+ mkdirSync(testDir, { recursive: true });
+
+ tddServer = spawn('npx', ['vizzly', 'tdd', 'start'], {
+ cwd: testDir,
+ stdio: ['ignore', 'pipe', 'pipe'],
+ env: { ...process.env, VIZZLY_HOME: testDir },
+ });
+
+ // Wait for TDD server to start
+ await new Promise((resolve, reject) => {
+ let timeout = setTimeout(
+ () => reject(new Error('TDD server timeout')),
+ 15000
+ );
+
+ tddServer.stdout.on('data', data => {
+ if (
+ data.toString().includes('TDD server started') ||
+ data.toString().includes('localhost:47392')
+ ) {
+ clearTimeout(timeout);
+ resolve();
+ }
+ });
+
+ tddServer.on('error', err => {
+ clearTimeout(timeout);
+ reject(err);
+ });
+ });
+ }
+
+ // Start static server for Storybook
+ serverInfo = await startStaticServer(storybookBuildPath);
+
+ // Launch browser
+ browser = await launchBrowser({ headless: true });
+ });
+
+ after(async () => {
+ if (browser) await closeBrowser(browser);
+ if (serverInfo) await stopStaticServer(serverInfo);
+
+ if (tddServer && !externalServer) {
+ tddServer.kill('SIGTERM');
+ await new Promise(resolve => {
+ tddServer.on('exit', resolve);
+ setTimeout(resolve, 2000);
+ });
+ }
+
+ if (!externalServer) {
+ try {
+ rmSync(testDir, { recursive: true, force: true });
+ } catch {
+ // Ignore cleanup errors
+ }
+ }
+ });
+
+ // ===========================================================================
+ // Story Discovery Tests
+ // ===========================================================================
+
+ describe('Story Discovery', () => {
+ it('discovers stories from example-storybook', async () => {
+ let config = {
+ storybookPath: storybookBuildPath,
+ include: null,
+ exclude: null,
+ };
+
+ let stories = await discoverStories(storybookBuildPath, config);
+
+ assert.ok(stories.length > 0, `Should find stories, found ${stories.length}`);
+
+ // Each story should have required fields
+ for (let story of stories) {
+ assert.ok(story.id, 'Story should have id');
+ assert.ok(story.title, 'Story should have title');
+ assert.ok(story.name, 'Story should have name');
+ }
+ });
+
+ it('filters stories with include patterns', async () => {
+ // Pattern matches against story.id (e.g., 'components-button--primary')
+ let config = {
+ storybookPath: storybookBuildPath,
+ include: '*button*',
+ exclude: null,
+ };
+
+ let stories = await discoverStories(storybookBuildPath, config);
+
+ assert.ok(stories.length > 0, `Should find Button stories, found ${stories.length}`);
+
+ // Should only find Button stories
+ for (let story of stories) {
+ assert.ok(
+ story.id.toLowerCase().includes('button'),
+ `Should only include Button stories, got: ${story.id}`
+ );
+ }
+ });
+
+ it('excludes stories with exclude patterns', async () => {
+ // Pattern matches against story.id (e.g., 'components-button--primary')
+ let config = {
+ storybookPath: storybookBuildPath,
+ include: null,
+ exclude: '*button*',
+ };
+
+ let stories = await discoverStories(storybookBuildPath, config);
+
+ // Should not find Button stories
+ for (let story of stories) {
+ assert.ok(
+ !story.id.toLowerCase().includes('button'),
+ `Should exclude Button stories, got: ${story.id}`
+ );
+ }
+ });
+ });
+
+ // ===========================================================================
+ // Screenshot Capture Tests
+ // ===========================================================================
+
+ describe('Screenshot Capture', () => {
+ it('captures story screenshot', async () => {
+ let config = {
+ storybookPath: storybookBuildPath,
+ include: null,
+ exclude: null,
+ };
+
+ let stories = await discoverStories(storybookBuildPath, config);
+ assert.ok(stories.length > 0, 'Need at least one story');
+
+ let story = stories[0];
+ let storyUrl = generateStoryUrl(serverInfo.url, story.id);
+ let viewport = { name: 'desktop', width: 1920, height: 1080 };
+
+ let page = await prepareStoryPage(browser, storyUrl, viewport);
+
+ try {
+ await captureAndSendScreenshot(page, story, viewport, {
+ threshold: 0,
+ properties: { test: 'e2e' },
+ });
+
+ // If we get here without error, screenshot succeeded
+ assert.ok(true, 'Screenshot should succeed');
+ } finally {
+ await closePage(page);
+ }
+ });
+
+ it('captures story with custom threshold', async () => {
+ let config = {
+ storybookPath: storybookBuildPath,
+ include: null,
+ exclude: null,
+ };
+
+ let stories = await discoverStories(storybookBuildPath, config);
+ let story = stories[0];
+ let storyUrl = generateStoryUrl(serverInfo.url, story.id);
+ let viewport = { name: 'desktop', width: 1920, height: 1080 };
+
+ let page = await prepareStoryPage(browser, storyUrl, viewport);
+
+ try {
+ await captureAndSendScreenshot(page, story, viewport, {
+ threshold: 5,
+ properties: { test: 'threshold' },
+ });
+
+ assert.ok(true, 'Screenshot with threshold should succeed');
+ } finally {
+ await closePage(page);
+ }
+ });
+ });
+
+ // ===========================================================================
+ // Multi-Story Tests
+ // ===========================================================================
+
+ describe('Multi-Story Screenshots', () => {
+ it('captures multiple stories', async () => {
+ let config = {
+ storybookPath: storybookBuildPath,
+ include: null,
+ exclude: null,
+ };
+
+ let stories = await discoverStories(storybookBuildPath, config);
+ let storiesToTest = stories.slice(0, 3); // Test first 3 stories
+ let viewport = { name: 'desktop', width: 1920, height: 1080 };
+
+ let results = [];
+
+ for (let story of storiesToTest) {
+ let storyUrl = generateStoryUrl(serverInfo.url, story.id);
+ let page = await prepareStoryPage(browser, storyUrl, viewport);
+
+ try {
+ await captureAndSendScreenshot(page, story, viewport, {
+ threshold: 0,
+ properties: { storyId: story.id },
+ });
+ results.push({ story: story.id, success: true });
+ } catch (error) {
+ results.push({ story: story.id, success: false, error: error.message });
+ } finally {
+ await closePage(page);
+ }
+ }
+
+ let allSucceeded = results.every(r => r.success);
+ assert.ok(allSucceeded, 'All story screenshots should succeed');
+ });
+
+ it('captures stories with multiple viewports', async () => {
+ let config = {
+ storybookPath: storybookBuildPath,
+ include: null,
+ exclude: null,
+ };
+
+ let stories = await discoverStories(storybookBuildPath, config);
+ let story = stories[0];
+
+ let viewports = [
+ { name: 'desktop', width: 1920, height: 1080 },
+ { name: 'tablet', width: 768, height: 1024 },
+ { name: 'mobile', width: 375, height: 812 },
+ ];
+
+ let results = [];
+
+ for (let viewport of viewports) {
+ let storyUrl = generateStoryUrl(serverInfo.url, story.id);
+ let page = await prepareStoryPage(browser, storyUrl, viewport);
+
+ try {
+ await captureAndSendScreenshot(page, story, viewport, {
+ threshold: 0,
+ properties: { viewport: viewport.name },
+ });
+ results.push({ viewport: viewport.name, success: true });
+ } catch (error) {
+ results.push({
+ viewport: viewport.name,
+ success: false,
+ error: error.message,
+ });
+ } finally {
+ await closePage(page);
+ }
+ }
+
+ let allSucceeded = results.every(r => r.success);
+ assert.ok(allSucceeded, 'All viewport screenshots should succeed');
+ });
+ });
+
+ // ===========================================================================
+ // Story Configuration Tests
+ // ===========================================================================
+
+ describe('Story Configuration', () => {
+ it('generates correct story URLs', () => {
+ let baseUrl = 'http://localhost:6006';
+ let storyId = 'example-button--primary';
+
+ let url = generateStoryUrl(baseUrl, storyId);
+
+ assert.ok(url.includes(baseUrl), 'URL should include base URL');
+ assert.ok(url.includes(storyId), 'URL should include story ID');
+ assert.ok(url.includes('viewMode=story'), 'URL should have viewMode=story');
+ });
+
+ it('gets story-specific config', () => {
+ let story = {
+ id: 'example-button--primary',
+ title: 'Example/Button',
+ name: 'Primary',
+ };
+
+ let config = {
+ viewports: [{ name: 'desktop', width: 1920, height: 1080 }],
+ threshold: 0,
+ stories: {
+ 'Example/Button': {
+ threshold: 5,
+ },
+ },
+ };
+
+ let storyConfig = getStoryConfig(story, config);
+
+ // Should have viewports from config
+ assert.ok(storyConfig.viewports, 'Should have viewports');
+ });
+
+ it('gets before-screenshot hook for story', () => {
+ let story = {
+ id: 'example-button--primary',
+ title: 'Example/Button',
+ name: 'Primary',
+ };
+
+ let config = {
+ interactions: {
+ // Pattern matches against story.id (example-button--primary)
+ '*button*': async page => {
+ await page.waitForSelector('button');
+ },
+ },
+ };
+
+ let hook = getBeforeScreenshotHook(story, config);
+
+ // Hook should be a function (pattern *button* matches story.id)
+ assert.ok(
+ typeof hook === 'function',
+ `Hook should be function, got ${typeof hook}`
+ );
+ });
+ });
+});
+
+// ===========================================================================
+// Unit tests that don't require TDD server
+// ===========================================================================
+
+describe('Storybook SDK (unit tests)', () => {
+ it('generates correct iframe URL for story', () => {
+ let baseUrl = 'http://localhost:6006';
+ let storyId = 'components-button--primary';
+
+ let url = generateStoryUrl(baseUrl, storyId);
+
+ assert.ok(url.startsWith(baseUrl), 'Should start with base URL');
+ assert.ok(url.includes('iframe.html'), 'Should use iframe.html');
+ assert.ok(url.includes(`id=${storyId}`), 'Should include story ID');
+ assert.ok(url.includes('viewMode=story'), 'Should set viewMode to story');
+ });
+
+ it('discovers stories from storybook build', async () => {
+ // Skip if example-storybook not built
+ if (!existsSync(storybookBuildPath)) {
+ return;
+ }
+
+ let config = {
+ storybookPath: storybookBuildPath,
+ include: null,
+ exclude: null,
+ };
+
+ let stories = await discoverStories(storybookBuildPath, config);
+ assert.ok(stories.length > 0, 'Should find stories');
+ });
+
+ it('matches stories against include patterns', async () => {
+ // Skip if example-storybook not built
+ if (!existsSync(storybookBuildPath)) {
+ return;
+ }
+
+ let config = {
+ storybookPath: storybookBuildPath,
+ include: null, // Include all
+ exclude: null,
+ };
+
+ let allStories = await discoverStories(storybookBuildPath, config);
+
+ let filteredConfig = {
+ storybookPath: storybookBuildPath,
+ include: '**/Button*',
+ exclude: null,
+ };
+
+ let filteredStories = await discoverStories(storybookBuildPath, filteredConfig);
+
+ assert.ok(
+ filteredStories.length <= allStories.length,
+ 'Filtered count should be less than or equal to all'
+ );
+ });
+});
diff --git a/clients/vitest/package-lock.json b/clients/vitest/package-lock.json
index 4d4d61af..86a593e5 100644
--- a/clients/vitest/package-lock.json
+++ b/clients/vitest/package-lock.json
@@ -1,22 +1,19 @@
{
"name": "@vizzly-testing/vitest",
- "version": "0.0.2",
+ "version": "0.1.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@vizzly-testing/vitest",
- "version": "0.0.2",
+ "version": "0.1.1",
"license": "MIT",
"devDependencies": {
- "@eslint/js": "^9.17.0",
+ "@biomejs/biome": "^2.3.8",
"@vitest/browser": "^4.0.0",
"@vitest/browser-playwright": "^4.0.2",
- "eslint": "^9.17.0",
- "eslint-config-prettier": "^10.0.1",
- "eslint-plugin-prettier": "^5.2.1",
"playwright": "^1.49.1",
- "prettier": "^3.4.2",
+ "serve-handler": "^6.1.6",
"vitest": "^4.0.0"
},
"engines": {
@@ -32,7 +29,6 @@
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
"integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@babel/helper-validator-identifier": "^7.27.1",
"js-tokens": "^4.0.0",
@@ -47,11 +43,173 @@
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=6.9.0"
}
},
+ "node_modules/@biomejs/biome": {
+ "version": "2.3.11",
+ "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.3.11.tgz",
+ "integrity": "sha512-/zt+6qazBWguPG6+eWmiELqO+9jRsMZ/DBU3lfuU2ngtIQYzymocHhKiZRyrbra4aCOoyTg/BmY+6WH5mv9xmQ==",
+ "dev": true,
+ "license": "MIT OR Apache-2.0",
+ "bin": {
+ "biome": "bin/biome"
+ },
+ "engines": {
+ "node": ">=14.21.3"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/biome"
+ },
+ "optionalDependencies": {
+ "@biomejs/cli-darwin-arm64": "2.3.11",
+ "@biomejs/cli-darwin-x64": "2.3.11",
+ "@biomejs/cli-linux-arm64": "2.3.11",
+ "@biomejs/cli-linux-arm64-musl": "2.3.11",
+ "@biomejs/cli-linux-x64": "2.3.11",
+ "@biomejs/cli-linux-x64-musl": "2.3.11",
+ "@biomejs/cli-win32-arm64": "2.3.11",
+ "@biomejs/cli-win32-x64": "2.3.11"
+ }
+ },
+ "node_modules/@biomejs/cli-darwin-arm64": {
+ "version": "2.3.11",
+ "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.3.11.tgz",
+ "integrity": "sha512-/uXXkBcPKVQY7rc9Ys2CrlirBJYbpESEDme7RKiBD6MmqR2w3j0+ZZXRIL2xiaNPsIMMNhP1YnA+jRRxoOAFrA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT OR Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=14.21.3"
+ }
+ },
+ "node_modules/@biomejs/cli-darwin-x64": {
+ "version": "2.3.11",
+ "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.3.11.tgz",
+ "integrity": "sha512-fh7nnvbweDPm2xEmFjfmq7zSUiox88plgdHF9OIW4i99WnXrAC3o2P3ag9judoUMv8FCSUnlwJCM1B64nO5Fbg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT OR Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=14.21.3"
+ }
+ },
+ "node_modules/@biomejs/cli-linux-arm64": {
+ "version": "2.3.11",
+ "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.3.11.tgz",
+ "integrity": "sha512-l4xkGa9E7Uc0/05qU2lMYfN1H+fzzkHgaJoy98wO+b/7Gl78srbCRRgwYSW+BTLixTBrM6Ede5NSBwt7rd/i6g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT OR Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=14.21.3"
+ }
+ },
+ "node_modules/@biomejs/cli-linux-arm64-musl": {
+ "version": "2.3.11",
+ "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.3.11.tgz",
+ "integrity": "sha512-XPSQ+XIPZMLaZ6zveQdwNjbX+QdROEd1zPgMwD47zvHV+tCGB88VH+aynyGxAHdzL+Tm/+DtKST5SECs4iwCLg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT OR Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=14.21.3"
+ }
+ },
+ "node_modules/@biomejs/cli-linux-x64": {
+ "version": "2.3.11",
+ "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.3.11.tgz",
+ "integrity": "sha512-/1s9V/H3cSe0r0Mv/Z8JryF5x9ywRxywomqZVLHAoa/uN0eY7F8gEngWKNS5vbbN/BsfpCG5yeBT5ENh50Frxg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT OR Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=14.21.3"
+ }
+ },
+ "node_modules/@biomejs/cli-linux-x64-musl": {
+ "version": "2.3.11",
+ "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.3.11.tgz",
+ "integrity": "sha512-vU7a8wLs5C9yJ4CB8a44r12aXYb8yYgBn+WeyzbMjaCMklzCv1oXr8x+VEyWodgJt9bDmhiaW/I0RHbn7rsNmw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT OR Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=14.21.3"
+ }
+ },
+ "node_modules/@biomejs/cli-win32-arm64": {
+ "version": "2.3.11",
+ "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.3.11.tgz",
+ "integrity": "sha512-PZQ6ElCOnkYapSsysiTy0+fYX+agXPlWugh6+eQ6uPKI3vKAqNp6TnMhoM3oY2NltSB89hz59o8xIfOdyhi9Iw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT OR Apache-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=14.21.3"
+ }
+ },
+ "node_modules/@biomejs/cli-win32-x64": {
+ "version": "2.3.11",
+ "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.3.11.tgz",
+ "integrity": "sha512-43VrG813EW+b5+YbDbz31uUsheX+qFKCpXeY9kfdAx+ww3naKxeVkTD9zLIWxUPfJquANMHrmW3wbe/037G0Qg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT OR Apache-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=14.21.3"
+ }
+ },
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.11",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.11.tgz",
@@ -494,208 +652,11 @@
"node": ">=18"
}
},
- "node_modules/@eslint-community/eslint-utils": {
- "version": "4.9.0",
- "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz",
- "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "eslint-visitor-keys": "^3.4.3"
- },
- "engines": {
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/eslint"
- },
- "peerDependencies": {
- "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
- }
- },
- "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
- "version": "3.4.3",
- "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
- "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
- "dev": true,
- "license": "Apache-2.0",
- "engines": {
- "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/eslint"
- }
- },
- "node_modules/@eslint-community/regexpp": {
- "version": "4.12.2",
- "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
- "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
- }
- },
- "node_modules/@eslint/config-array": {
- "version": "0.21.1",
- "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz",
- "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==",
- "dev": true,
- "license": "Apache-2.0",
- "dependencies": {
- "@eslint/object-schema": "^2.1.7",
- "debug": "^4.3.1",
- "minimatch": "^3.1.2"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- }
- },
- "node_modules/@eslint/config-helpers": {
- "version": "0.4.1",
- "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.1.tgz",
- "integrity": "sha512-csZAzkNhsgwb0I/UAV6/RGFTbiakPCf0ZrGmrIxQpYvGZ00PhTkSnyKNolphgIvmnJeGw6rcGVEXfTzUnFuEvw==",
- "dev": true,
- "license": "Apache-2.0",
- "dependencies": {
- "@eslint/core": "^0.16.0"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- }
- },
- "node_modules/@eslint/core": {
- "version": "0.16.0",
- "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.16.0.tgz",
- "integrity": "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==",
- "dev": true,
- "license": "Apache-2.0",
- "dependencies": {
- "@types/json-schema": "^7.0.15"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- }
- },
- "node_modules/@eslint/eslintrc": {
- "version": "3.3.1",
- "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz",
- "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ajv": "^6.12.4",
- "debug": "^4.3.2",
- "espree": "^10.0.1",
- "globals": "^14.0.0",
- "ignore": "^5.2.0",
- "import-fresh": "^3.2.1",
- "js-yaml": "^4.1.0",
- "minimatch": "^3.1.2",
- "strip-json-comments": "^3.1.1"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "url": "https://opencollective.com/eslint"
- }
- },
- "node_modules/@eslint/js": {
- "version": "9.38.0",
- "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.38.0.tgz",
- "integrity": "sha512-UZ1VpFvXf9J06YG9xQBdnzU+kthors6KjhMAl6f4gH4usHyh31rUf2DLGInT8RFYIReYXNSydgPY0V2LuWgl7A==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "url": "https://eslint.org/donate"
- }
- },
- "node_modules/@eslint/object-schema": {
- "version": "2.1.7",
- "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz",
- "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==",
- "dev": true,
- "license": "Apache-2.0",
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- }
- },
- "node_modules/@eslint/plugin-kit": {
- "version": "0.4.0",
- "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.0.tgz",
- "integrity": "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==",
- "dev": true,
- "license": "Apache-2.0",
- "dependencies": {
- "@eslint/core": "^0.16.0",
- "levn": "^0.4.1"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- }
- },
- "node_modules/@humanfs/core": {
- "version": "0.19.1",
- "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
- "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
- "dev": true,
- "license": "Apache-2.0",
- "engines": {
- "node": ">=18.18.0"
- }
- },
- "node_modules/@humanfs/node": {
- "version": "0.16.7",
- "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz",
- "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==",
- "dev": true,
- "license": "Apache-2.0",
- "dependencies": {
- "@humanfs/core": "^0.19.1",
- "@humanwhocodes/retry": "^0.4.0"
- },
- "engines": {
- "node": ">=18.18.0"
- }
- },
- "node_modules/@humanwhocodes/module-importer": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
- "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
- "dev": true,
- "license": "Apache-2.0",
- "engines": {
- "node": ">=12.22"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/nzakas"
- }
- },
- "node_modules/@humanwhocodes/retry": {
- "version": "0.4.3",
- "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
- "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
- "dev": true,
- "license": "Apache-2.0",
- "engines": {
- "node": ">=18.18"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/nzakas"
- }
- },
"node_modules/@isaacs/balanced-match": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz",
"integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==",
"license": "MIT",
- "peer": true,
"engines": {
"node": "20 || >=22"
}
@@ -705,7 +666,6 @@
"resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz",
"integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@isaacs/balanced-match": "^4.0.1"
},
@@ -718,7 +678,6 @@
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
"integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
"license": "ISC",
- "peer": true,
"dependencies": {
"string-width": "^5.1.2",
"string-width-cjs": "npm:string-width@^4.2.0",
@@ -738,19 +697,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/@pkgr/core": {
- "version": "0.2.9",
- "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz",
- "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": "^12.20.0 || ^14.18.0 || >=16.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/pkgr"
- }
- },
"node_modules/@polka/url": {
"version": "1.0.0-next.29",
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz",
@@ -1098,13 +1044,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/@types/json-schema": {
- "version": "7.0.15",
- "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
- "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/@vitest/browser": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-4.0.2.tgz",
@@ -1134,6 +1073,7 @@
"integrity": "sha512-sEtZ4o2lsWx8lmRHP8oe1wplhY5J2fKr2YTA83NAJlXVdFlul/utxyPeCUVpADaggNMz3GnAz2Y/BkG0f86KmQ==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@vitest/browser": "4.0.2",
"@vitest/mocker": "4.0.2",
@@ -1268,7 +1208,6 @@
"resolved": "https://registry.npmjs.org/@vizzly-testing/cli/-/cli-0.12.0.tgz",
"integrity": "sha512-UskkJVX4zcKUEh/C3LmBB98HAZpt5j2XF+eCey+4fgn+P2T19TKvbm/cYNxVukDE7GlHQ+TH+FE20t8v3R1yEA==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@vizzly-testing/honeydiff": "^0.1.0",
"commander": "^14.0.0",
@@ -1290,7 +1229,6 @@
"resolved": "https://registry.npmjs.org/commander/-/commander-14.0.1.tgz",
"integrity": "sha512-2JkV3gUZUVrbNA+1sjBOYLsMZ5cEEl8GTFP2a4AVz5hvasAMCQ1D2l2le/cX+pV4N6ZU17zjUahLpIXRrnWL8A==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=20"
}
@@ -1300,7 +1238,6 @@
"resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz",
"integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==",
"license": "ISC",
- "peer": true,
"dependencies": {
"foreground-child": "^3.3.1",
"jackspeak": "^4.1.1",
@@ -1324,7 +1261,6 @@
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz",
"integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==",
"license": "ISC",
- "peer": true,
"dependencies": {
"@isaacs/brace-expansion": "^5.0.0"
},
@@ -1340,57 +1276,15 @@
"resolved": "https://registry.npmjs.org/@vizzly-testing/honeydiff/-/honeydiff-0.1.0.tgz",
"integrity": "sha512-/Hvq7/tJ4gfS4Lp48RHBNetFRaGLkq37FNGsRHroz72xsShyWsUueZ0zM24hWNrd6g54Qx9LPk7AoswEgZoczw==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=22.0.0"
}
},
- "node_modules/acorn": {
- "version": "8.15.0",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
- "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
- "dev": true,
- "license": "MIT",
- "bin": {
- "acorn": "bin/acorn"
- },
- "engines": {
- "node": ">=0.4.0"
- }
- },
- "node_modules/acorn-jsx": {
- "version": "5.3.2",
- "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
- "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
- "dev": true,
- "license": "MIT",
- "peerDependencies": {
- "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
- }
- },
- "node_modules/ajv": {
- "version": "6.12.6",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
- "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "fast-deep-equal": "^3.1.1",
- "fast-json-stable-stringify": "^2.0.0",
- "json-schema-traverse": "^0.4.1",
- "uri-js": "^4.2.2"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/epoberezkin"
- }
- },
"node_modules/ansi-regex": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=12"
},
@@ -1433,8 +1327,7 @@
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/balanced-match": {
"version": "1.0.2",
@@ -1454,12 +1347,21 @@
"concat-map": "0.0.1"
}
},
+ "node_modules/bytes": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz",
+ "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"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==",
"license": "MIT",
- "peer": true,
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
@@ -1487,27 +1389,10 @@
"node": ">=18"
}
},
- "node_modules/chalk": {
- "version": "4.1.2",
- "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
- "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ansi-styles": "^4.1.0",
- "supports-color": "^7.1.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/chalk/chalk?sponsor=1"
- }
- },
- "node_modules/color-convert": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
- "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
@@ -1527,7 +1412,6 @@
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
- "peer": true,
"dependencies": {
"delayed-stream": "~1.0.0"
},
@@ -1542,12 +1426,21 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/content-disposition": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz",
+ "integrity": "sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/cosmiconfig": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz",
"integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==",
"license": "MIT",
- "peer": true,
"dependencies": {
"env-paths": "^2.2.1",
"import-fresh": "^3.3.0",
@@ -1601,19 +1494,11 @@
}
}
},
- "node_modules/deep-is": {
- "version": "0.1.4",
- "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
- "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=0.4.0"
}
@@ -1623,7 +1508,6 @@
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz",
"integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==",
"license": "BSD-2-Clause",
- "peer": true,
"engines": {
"node": ">=12"
},
@@ -1636,7 +1520,6 @@
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"license": "MIT",
- "peer": true,
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
@@ -1650,22 +1533,19 @@
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/emoji-regex": {
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/env-paths": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz",
"integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=6"
}
@@ -1675,7 +1555,6 @@
"resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz",
"integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==",
"license": "MIT",
- "peer": true,
"dependencies": {
"is-arrayish": "^0.2.1"
}
@@ -1685,7 +1564,6 @@
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">= 0.4"
}
@@ -1695,7 +1573,6 @@
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">= 0.4"
}
@@ -1712,7 +1589,6 @@
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"license": "MIT",
- "peer": true,
"dependencies": {
"es-errors": "^1.3.0"
},
@@ -1725,7 +1601,6 @@
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"license": "MIT",
- "peer": true,
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
@@ -1778,223 +1653,6 @@
"@esbuild/win32-x64": "0.25.11"
}
},
- "node_modules/escape-string-regexp": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
- "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/eslint": {
- "version": "9.38.0",
- "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.38.0.tgz",
- "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@eslint-community/eslint-utils": "^4.8.0",
- "@eslint-community/regexpp": "^4.12.1",
- "@eslint/config-array": "^0.21.1",
- "@eslint/config-helpers": "^0.4.1",
- "@eslint/core": "^0.16.0",
- "@eslint/eslintrc": "^3.3.1",
- "@eslint/js": "9.38.0",
- "@eslint/plugin-kit": "^0.4.0",
- "@humanfs/node": "^0.16.6",
- "@humanwhocodes/module-importer": "^1.0.1",
- "@humanwhocodes/retry": "^0.4.2",
- "@types/estree": "^1.0.6",
- "ajv": "^6.12.4",
- "chalk": "^4.0.0",
- "cross-spawn": "^7.0.6",
- "debug": "^4.3.2",
- "escape-string-regexp": "^4.0.0",
- "eslint-scope": "^8.4.0",
- "eslint-visitor-keys": "^4.2.1",
- "espree": "^10.4.0",
- "esquery": "^1.5.0",
- "esutils": "^2.0.2",
- "fast-deep-equal": "^3.1.3",
- "file-entry-cache": "^8.0.0",
- "find-up": "^5.0.0",
- "glob-parent": "^6.0.2",
- "ignore": "^5.2.0",
- "imurmurhash": "^0.1.4",
- "is-glob": "^4.0.0",
- "json-stable-stringify-without-jsonify": "^1.0.1",
- "lodash.merge": "^4.6.2",
- "minimatch": "^3.1.2",
- "natural-compare": "^1.4.0",
- "optionator": "^0.9.3"
- },
- "bin": {
- "eslint": "bin/eslint.js"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "url": "https://eslint.org/donate"
- },
- "peerDependencies": {
- "jiti": "*"
- },
- "peerDependenciesMeta": {
- "jiti": {
- "optional": true
- }
- }
- },
- "node_modules/eslint-config-prettier": {
- "version": "10.1.8",
- "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz",
- "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==",
- "dev": true,
- "license": "MIT",
- "bin": {
- "eslint-config-prettier": "bin/cli.js"
- },
- "funding": {
- "url": "https://opencollective.com/eslint-config-prettier"
- },
- "peerDependencies": {
- "eslint": ">=7.0.0"
- }
- },
- "node_modules/eslint-plugin-prettier": {
- "version": "5.5.4",
- "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.4.tgz",
- "integrity": "sha512-swNtI95SToIz05YINMA6Ox5R057IMAmWZ26GqPxusAp1TZzj+IdY9tXNWWD3vkF/wEqydCONcwjTFpxybBqZsg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "prettier-linter-helpers": "^1.0.0",
- "synckit": "^0.11.7"
- },
- "engines": {
- "node": "^14.18.0 || >=16.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/eslint-plugin-prettier"
- },
- "peerDependencies": {
- "@types/eslint": ">=8.0.0",
- "eslint": ">=8.0.0",
- "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0",
- "prettier": ">=3.0.0"
- },
- "peerDependenciesMeta": {
- "@types/eslint": {
- "optional": true
- },
- "eslint-config-prettier": {
- "optional": true
- }
- }
- },
- "node_modules/eslint-scope": {
- "version": "8.4.0",
- "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
- "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==",
- "dev": true,
- "license": "BSD-2-Clause",
- "dependencies": {
- "esrecurse": "^4.3.0",
- "estraverse": "^5.2.0"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "url": "https://opencollective.com/eslint"
- }
- },
- "node_modules/eslint-visitor-keys": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
- "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
- "dev": true,
- "license": "Apache-2.0",
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "url": "https://opencollective.com/eslint"
- }
- },
- "node_modules/eslint/node_modules/glob-parent": {
- "version": "6.0.2",
- "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
- "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
- "dev": true,
- "license": "ISC",
- "dependencies": {
- "is-glob": "^4.0.3"
- },
- "engines": {
- "node": ">=10.13.0"
- }
- },
- "node_modules/espree": {
- "version": "10.4.0",
- "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
- "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
- "dev": true,
- "license": "BSD-2-Clause",
- "dependencies": {
- "acorn": "^8.15.0",
- "acorn-jsx": "^5.3.2",
- "eslint-visitor-keys": "^4.2.1"
- },
- "engines": {
- "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
- },
- "funding": {
- "url": "https://opencollective.com/eslint"
- }
- },
- "node_modules/esquery": {
- "version": "1.6.0",
- "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
- "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
- "dev": true,
- "license": "BSD-3-Clause",
- "dependencies": {
- "estraverse": "^5.1.0"
- },
- "engines": {
- "node": ">=0.10"
- }
- },
- "node_modules/esrecurse": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
- "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
- "dev": true,
- "license": "BSD-2-Clause",
- "dependencies": {
- "estraverse": "^5.2.0"
- },
- "engines": {
- "node": ">=4.0"
- }
- },
- "node_modules/estraverse": {
- "version": "5.3.0",
- "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
- "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
- "dev": true,
- "license": "BSD-2-Clause",
- "engines": {
- "node": ">=4.0"
- }
- },
"node_modules/estree-walker": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
@@ -2005,16 +1663,6 @@
"@types/estree": "^1.0.0"
}
},
- "node_modules/esutils": {
- "version": "2.0.3",
- "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
- "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
- "dev": true,
- "license": "BSD-2-Clause",
- "engines": {
- "node": ">=0.10.0"
- }
- },
"node_modules/expect-type": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz",
@@ -2025,91 +1673,11 @@
"node": ">=12.0.0"
}
},
- "node_modules/fast-deep-equal": {
- "version": "3.1.3",
- "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
- "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/fast-diff": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz",
- "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==",
- "dev": true,
- "license": "Apache-2.0"
- },
- "node_modules/fast-json-stable-stringify": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
- "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/fast-levenshtein": {
- "version": "2.0.6",
- "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
- "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/file-entry-cache": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
- "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "flat-cache": "^4.0.0"
- },
- "engines": {
- "node": ">=16.0.0"
- }
- },
- "node_modules/find-up": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
- "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "locate-path": "^6.0.0",
- "path-exists": "^4.0.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/flat-cache": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
- "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "flatted": "^3.2.9",
- "keyv": "^4.5.4"
- },
- "engines": {
- "node": ">=16"
- }
- },
- "node_modules/flatted": {
- "version": "3.3.3",
- "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
- "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
- "dev": true,
- "license": "ISC"
- },
"node_modules/foreground-child": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
"integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
"license": "ISC",
- "peer": true,
"dependencies": {
"cross-spawn": "^7.0.6",
"signal-exit": "^4.0.1"
@@ -2126,7 +1694,6 @@
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"license": "MIT",
- "peer": true,
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
@@ -2158,7 +1725,6 @@
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"license": "MIT",
- "peer": true,
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
@@ -2168,7 +1734,6 @@
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"license": "MIT",
- "peer": true,
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
@@ -2193,7 +1758,6 @@
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"license": "MIT",
- "peer": true,
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
@@ -2202,25 +1766,11 @@
"node": ">= 0.4"
}
},
- "node_modules/globals": {
- "version": "14.0.0",
- "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
- "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=18"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">= 0.4"
},
@@ -2228,22 +1778,11 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/has-flag": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
- "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "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==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">= 0.4"
},
@@ -2256,7 +1795,6 @@
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"license": "MIT",
- "peer": true,
"dependencies": {
"has-symbols": "^1.0.3"
},
@@ -2272,7 +1810,6 @@
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"license": "MIT",
- "peer": true,
"dependencies": {
"function-bind": "^1.1.2"
},
@@ -2280,16 +1817,6 @@
"node": ">= 0.4"
}
},
- "node_modules/ignore": {
- "version": "5.3.2",
- "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
- "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 4"
- }
- },
"node_modules/import-fresh": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
@@ -2306,56 +1833,21 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/imurmurhash": {
- "version": "0.1.4",
- "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
- "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.8.19"
- }
- },
"node_modules/is-arrayish": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz",
"integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==",
- "license": "MIT",
- "peer": true
- },
- "node_modules/is-extglob": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
- "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
+ "license": "MIT"
},
"node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=8"
}
},
- "node_modules/is-glob": {
- "version": "4.0.3",
- "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
- "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "is-extglob": "^2.1.1"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@@ -2367,7 +1859,6 @@
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz",
"integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==",
"license": "BlueOak-1.0.0",
- "peer": true,
"dependencies": {
"@isaacs/cliui": "^8.0.2"
},
@@ -2382,8 +1873,7 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/js-yaml": {
"version": "4.1.0",
@@ -2397,86 +1887,16 @@
"js-yaml": "bin/js-yaml.js"
}
},
- "node_modules/json-buffer": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
- "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/json-parse-even-better-errors": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
"integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==",
- "license": "MIT",
- "peer": true
- },
- "node_modules/json-schema-traverse": {
- "version": "0.4.1",
- "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
- "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/json-stable-stringify-without-jsonify": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
- "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
- "dev": true,
"license": "MIT"
},
- "node_modules/keyv": {
- "version": "4.5.4",
- "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
- "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "json-buffer": "3.0.1"
- }
- },
- "node_modules/levn": {
- "version": "0.4.1",
- "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
- "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "prelude-ls": "^1.2.1",
- "type-check": "~0.4.0"
- },
- "engines": {
- "node": ">= 0.8.0"
- }
- },
"node_modules/lines-and-columns": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
- "license": "MIT",
- "peer": true
- },
- "node_modules/locate-path": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
- "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "p-locate": "^5.0.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/lodash.merge": {
- "version": "4.6.2",
- "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
- "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
- "dev": true,
"license": "MIT"
},
"node_modules/magic-string": {
@@ -2494,7 +1914,6 @@
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">= 0.4"
}
@@ -2504,7 +1923,6 @@
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">= 0.6"
}
@@ -2514,7 +1932,6 @@
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
- "peer": true,
"dependencies": {
"mime-db": "1.52.0"
},
@@ -2540,7 +1957,6 @@
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
"license": "ISC",
- "peer": true,
"engines": {
"node": ">=16 || 14 >=14.17"
}
@@ -2581,69 +1997,11 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
- "node_modules/natural-compare": {
- "version": "1.4.0",
- "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
- "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/optionator": {
- "version": "0.9.4",
- "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
- "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "deep-is": "^0.1.3",
- "fast-levenshtein": "^2.0.6",
- "levn": "^0.4.1",
- "prelude-ls": "^1.2.1",
- "type-check": "^0.4.0",
- "word-wrap": "^1.2.5"
- },
- "engines": {
- "node": ">= 0.8.0"
- }
- },
- "node_modules/p-limit": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
- "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "yocto-queue": "^0.1.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/p-locate": {
- "version": "5.0.0",
- "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
- "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "p-limit": "^3.0.2"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/package-json-from-dist": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
- "license": "BlueOak-1.0.0",
- "peer": true
+ "license": "BlueOak-1.0.0"
},
"node_modules/parent-module": {
"version": "1.0.1",
@@ -2662,7 +2020,6 @@
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz",
"integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==",
"license": "MIT",
- "peer": true,
"dependencies": {
"@babel/code-frame": "^7.0.0",
"error-ex": "^1.3.1",
@@ -2676,15 +2033,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/path-exists": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
- "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "node_modules/path-is-inside": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz",
+ "integrity": "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==",
"dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
+ "license": "(WTFPL OR MIT)"
},
"node_modules/path-key": {
"version": "3.1.1",
@@ -2700,7 +2054,6 @@
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz",
"integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==",
"license": "BlueOak-1.0.0",
- "peer": true,
"dependencies": {
"lru-cache": "^11.0.0",
"minipass": "^7.1.2"
@@ -2717,11 +2070,17 @@
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz",
"integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==",
"license": "ISC",
- "peer": true,
"engines": {
"node": "20 || >=22"
}
},
+ "node_modules/path-to-regexp": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.3.0.tgz",
+ "integrity": "sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/pathe": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
@@ -2834,53 +2193,14 @@
"node": "^10 || ^12 || >=14"
}
},
- "node_modules/prelude-ls": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
- "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">= 0.8.0"
- }
- },
- "node_modules/prettier": {
- "version": "3.6.2",
- "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
- "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
- "dev": true,
- "license": "MIT",
- "bin": {
- "prettier": "bin/prettier.cjs"
- },
- "engines": {
- "node": ">=14"
- },
- "funding": {
- "url": "https://github.com/prettier/prettier?sponsor=1"
- }
- },
- "node_modules/prettier-linter-helpers": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz",
- "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "fast-diff": "^1.1.2"
- },
- "engines": {
- "node": ">=6.0.0"
- }
- },
- "node_modules/punycode": {
- "version": "2.3.1",
- "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
- "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "node_modules/range-parser": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz",
+ "integrity": "sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A==",
"dev": true,
"license": "MIT",
"engines": {
- "node": ">=6"
+ "node": ">= 0.6"
}
},
"node_modules/resolve-from": {
@@ -2934,6 +2254,45 @@
"fsevents": "~2.3.2"
}
},
+ "node_modules/serve-handler": {
+ "version": "6.1.6",
+ "resolved": "https://registry.npmjs.org/serve-handler/-/serve-handler-6.1.6.tgz",
+ "integrity": "sha512-x5RL9Y2p5+Sh3D38Fh9i/iQ5ZK+e4xuXRd/pGbM4D13tgo/MGwbttUk8emytcr1YYzBYs+apnUngBDFYfpjPuQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "bytes": "3.0.0",
+ "content-disposition": "0.5.2",
+ "mime-types": "2.1.18",
+ "minimatch": "3.1.2",
+ "path-is-inside": "1.0.2",
+ "path-to-regexp": "3.3.0",
+ "range-parser": "1.2.0"
+ }
+ },
+ "node_modules/serve-handler/node_modules/mime-db": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz",
+ "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/serve-handler/node_modules/mime-types": {
+ "version": "2.1.18",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz",
+ "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "~1.33.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -2967,7 +2326,6 @@
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
"license": "ISC",
- "peer": true,
"engines": {
"node": ">=14"
},
@@ -3019,7 +2377,6 @@
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
"license": "MIT",
- "peer": true,
"dependencies": {
"eastasianwidth": "^0.2.0",
"emoji-regex": "^9.2.2",
@@ -3038,7 +2395,6 @@
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"license": "MIT",
- "peer": true,
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
@@ -3053,7 +2409,6 @@
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=8"
}
@@ -3062,15 +2417,13 @@
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/string-width-cjs/node_modules/strip-ansi": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"license": "MIT",
- "peer": true,
"dependencies": {
"ansi-regex": "^5.0.1"
},
@@ -3083,7 +2436,6 @@
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
"integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
"license": "MIT",
- "peer": true,
"dependencies": {
"ansi-regex": "^6.0.1"
},
@@ -3100,7 +2452,6 @@
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"license": "MIT",
- "peer": true,
"dependencies": {
"ansi-regex": "^5.0.1"
},
@@ -3113,53 +2464,10 @@
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"license": "MIT",
- "peer": true,
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/strip-json-comments": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
- "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/supports-color": {
- "version": "7.2.0",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
- "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "has-flag": "^4.0.0"
- },
"engines": {
"node": ">=8"
}
},
- "node_modules/synckit": {
- "version": "0.11.11",
- "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz",
- "integrity": "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@pkgr/core": "^0.2.9"
- },
- "engines": {
- "node": "^14.18.0 || >=16.0.0"
- },
- "funding": {
- "url": "https://opencollective.com/synckit"
- }
- },
"node_modules/tinybench": {
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
@@ -3215,6 +2523,7 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=12"
},
@@ -3242,35 +2551,13 @@
"node": ">=6"
}
},
- "node_modules/type-check": {
- "version": "0.4.0",
- "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
- "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "prelude-ls": "^1.2.1"
- },
- "engines": {
- "node": ">= 0.8.0"
- }
- },
- "node_modules/uri-js": {
- "version": "4.4.1",
- "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
- "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
- "dev": true,
- "license": "BSD-2-Clause",
- "dependencies": {
- "punycode": "^2.1.0"
- }
- },
"node_modules/vite": {
"version": "7.1.12",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.12.tgz",
"integrity": "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.5.0",
@@ -3364,6 +2651,7 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=12"
},
@@ -3377,6 +2665,7 @@
"integrity": "sha512-SXrA2ZzOPulX479d8W13RqKSmvHb9Bfg71eW7Fbs6ZjUFcCCXyt/OzFCkNyiUE8mFlPHa4ZVUGw0ky+5ndKnrg==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@vitest/expect": "4.0.2",
"@vitest/mocker": "4.0.2",
@@ -3494,22 +2783,11 @@
"node": ">=8"
}
},
- "node_modules/word-wrap": {
- "version": "1.2.5",
- "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
- "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
"node_modules/wrap-ansi": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
"license": "MIT",
- "peer": true,
"dependencies": {
"ansi-styles": "^6.1.0",
"string-width": "^5.0.1",
@@ -3528,7 +2806,6 @@
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
"license": "MIT",
- "peer": true,
"dependencies": {
"ansi-styles": "^4.0.0",
"string-width": "^4.1.0",
@@ -3546,7 +2823,6 @@
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=8"
}
@@ -3555,15 +2831,13 @@
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/wrap-ansi-cjs/node_modules/string-width": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
"license": "MIT",
- "peer": true,
"dependencies": {
"emoji-regex": "^8.0.0",
"is-fullwidth-code-point": "^3.0.0",
@@ -3578,7 +2852,6 @@
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
"license": "MIT",
- "peer": true,
"dependencies": {
"ansi-regex": "^5.0.1"
},
@@ -3591,7 +2864,6 @@
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
"integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=12"
},
@@ -3621,25 +2893,11 @@
}
}
},
- "node_modules/yocto-queue": {
- "version": "0.1.0",
- "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
- "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/zod": {
"version": "4.1.12",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz",
"integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==",
"license": "MIT",
- "peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
diff --git a/clients/vitest/package.json b/clients/vitest/package.json
index fc44760f..e57f0b7e 100644
--- a/clients/vitest/package.json
+++ b/clients/vitest/package.json
@@ -39,9 +39,11 @@
"CHANGELOG.md"
],
"scripts": {
- "test": "npm run test:unit && npm run test:e2e",
+ "test": "npm run test:unit",
"test:unit": "vitest run --config vitest.config.js",
"test:e2e": "vitest run --config vitest.e2e.config.js",
+ "test:e2e:tdd": "../../bin/vizzly.js tdd run 'npm run test:e2e'",
+ "test:e2e:cloud": "../../bin/vizzly.js run 'npm run test:e2e'",
"lint": "biome lint src tests",
"lint:fix": "biome lint --write src tests",
"format": "biome format --write src tests",
@@ -57,7 +59,6 @@
"access": "public",
"registry": "https://registry.npmjs.org/"
},
- "dependencies": {},
"peerDependencies": {
"@vizzly-testing/cli": ">=0.12.0",
"vitest": ">=4.0.0"
@@ -67,6 +68,7 @@
"@vitest/browser": "^4.0.0",
"@vitest/browser-playwright": "^4.0.2",
"playwright": "^1.49.1",
+ "serve-handler": "^6.1.6",
"vitest": "^4.0.0"
}
}
diff --git a/clients/vitest/tests/e2e/example.test.js b/clients/vitest/tests/e2e/example.test.js
index 410bb432..e9a35561 100644
--- a/clients/vitest/tests/e2e/example.test.js
+++ b/clients/vitest/tests/e2e/example.test.js
@@ -1,98 +1,104 @@
/**
- * E2E Integration Tests
+ * E2E tests for the Vizzly Vitest plugin
*
- * These tests verify the Vitest plugin integration with Vizzly.
- * They also serve as examples for documentation.
+ * Tests the toMatchScreenshot matcher works correctly with:
+ * - Page screenshots
+ * - Element screenshots
+ * - Properties/metadata
+ * - Threshold options
*
- * Local TDD mode:
- * vizzly tdd start
- * npm run test:e2e
- *
- * Cloud mode (used in CI):
- * vizzly run "npm run test:e2e"
+ * Uses the shared test-site CSS for consistent styling.
*/
-import { expect, test } from 'vitest';
+import { beforeAll, describe, expect, test } from 'vitest';
import { page } from 'vitest/browser';
-test('homepage matches screenshot', async () => {
- // Render the hero HTML directly
- document.body.innerHTML = `
-
-
-
Vizzly + Vitest
-
Visual regression testing made simple
-
- `;
+// Base URL for test-site assets (defined in vitest.e2e.config.js)
+// eslint-disable-next-line no-undef
+let baseUrl = __TEST_SITE_URL__;
+
+// Load test-site CSS before all tests
+beforeAll(async () => {
+ let link = document.createElement('link');
+ link.rel = 'stylesheet';
+ link.href = `${baseUrl}/dist/output.css`;
+ document.head.appendChild(link);
- await expect(page.getByRole('heading')).toMatchScreenshot('homepage.png');
+ // Wait for CSS to load
+ await new Promise((resolve) => {
+ link.onload = resolve;
+ link.onerror = resolve;
+ });
});
-test('homepage with properties', async () => {
- // Render the hero HTML directly
- document.body.innerHTML = `
-
-
-
Vizzly + Vitest
-
Visual regression testing made simple
-
- `;
+describe('Vizzly Vitest Plugin', () => {
+ test('page screenshot', async () => {
+ document.body.innerHTML = `
+
+
Hello Vizzly
+
Testing the Vitest plugin
+
+ `;
+
+ await expect(page).toMatchScreenshot('page.png');
+ });
+
+ test('element screenshot', async () => {
+ document.body.innerHTML = `
+
+
+
+ `;
+
+ let button = page.getByRole('button');
+ await expect(button).toMatchScreenshot('button.png');
+ });
+
+ test('screenshot with properties', async () => {
+ document.body.innerHTML = `
+
+
+
Dark Theme Card
+
Component with metadata
+
+
+ `;
- // New first-class API - properties at top level!
- await expect(page.getByRole('heading')).toMatchScreenshot(
- 'homepage-with-props.png',
- {
+ await expect(page).toMatchScreenshot('card.png', {
properties: {
theme: 'dark',
- viewport: '1920x1080',
+ component: 'card',
},
+ });
+ });
+
+ test('screenshot with threshold', async () => {
+ document.body.innerHTML = `
+
+ Warning!
+
+ `;
+
+ await expect(page).toMatchScreenshot('warning.png', {
threshold: 5,
- }
- );
+ });
+ });
+
+ test('multiple elements in sequence', async () => {
+ document.body.innerHTML = `
+
+ `;
+
+ let nav = page.getByRole('navigation');
+ await expect(nav).toMatchScreenshot('nav.png');
+
+ let homeLink = page.getByRole('link', { name: 'Home' });
+ await expect(homeLink).toMatchScreenshot('home-link.png');
+ });
});
diff --git a/clients/vitest/vitest.e2e.config.js b/clients/vitest/vitest.e2e.config.js
index 2bd7dde9..fabc7240 100644
--- a/clients/vitest/vitest.e2e.config.js
+++ b/clients/vitest/vitest.e2e.config.js
@@ -1,9 +1,38 @@
+import { existsSync } from 'node:fs';
+import { createServer } from 'node:http';
+import { resolve } from 'node:path';
+import handler from 'serve-handler';
import { playwright } from '@vitest/browser-playwright';
import { defineConfig } from 'vitest/config';
import { vizzlyPlugin } from './src/index.js';
+// Path to shared test-site (for CSS assets)
+let testSitePath = resolve(import.meta.dirname, '../../test-site');
+
+// Verify test-site exists
+if (!existsSync(resolve(testSitePath, 'index.html'))) {
+ throw new Error(`test-site not found at ${testSitePath}`);
+}
+
+// Start static server for test-site assets
+let server = createServer((req, res) => {
+ return handler(req, res, {
+ public: testSitePath,
+ cleanUrls: false,
+ });
+});
+
+// Listen on port 0 to get a random available port
+await new Promise((resolve) => {
+ server.listen(0, () => resolve());
+});
+
+let testSitePort = server.address().port;
+
+// Clean up server on exit - use unref() so it doesn't keep the process alive
+server.unref();
+
// E2E tests config - runs in browser mode
-// These tests verify actual browser integration with Vizzly
export default defineConfig({
plugins: [vizzlyPlugin()],
test: {
@@ -19,4 +48,7 @@ export default defineConfig({
},
include: ['tests/e2e/**/*.test.js'],
},
+ define: {
+ __TEST_SITE_URL__: JSON.stringify(`http://localhost:${testSitePort}`),
+ },
});