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