From 242af430a3d331718cf7f5d1ca2c19f97019bc6f Mon Sep 17 00:00:00 2001 From: Robert DeLuca Date: Sat, 17 Jan 2026 01:23:54 -0600 Subject: [PATCH 1/6] =?UTF-8?q?=F0=9F=A7=AA=20Fix=20SDK=20E2E=20tests=20to?= =?UTF-8?q?=20work=20with=20vizzly=20tdd=20run?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Detect VIZZLY_SERVER_URL env var to skip starting own TDD server - Fix Vitest tests to use correct browser mode API (getByRole, getByText) - Fix Static-Site tests to use URL-style paths (/, /features) not filenames - Fix Storybook pattern matching to use story.id format (*button*) - Fix Ember assertions to check result.status instead of result.success - Add proper config structure for page discovery (pageDiscovery.useSitemap) All SDK E2E tests now pass when run via `vizzly tdd run`: - Storybook: 13/13 - Static-Site: 13/13 - Ember: 9/9 - Vitest: 24/24 - Ruby: 10/10 --- .github/workflows/ci.yml | 36 +- clients/ember/package.json | 5 + clients/ember/tests/integration/e2e.test.js | 425 +++++++++---- clients/ruby/Rakefile | 56 +- clients/ruby/test/e2e_test.rb | 291 +++++++++ clients/ruby/test/integration_test.rb | 298 ++++++++-- clients/static-site/package.json | 5 +- clients/static-site/tests/e2e.test.js | 478 +++++++++++++++ .../example-storybook/package-lock.json | 11 +- clients/storybook/package.json | 5 +- clients/storybook/tests/e2e.test.js | 476 +++++++++++++++ clients/vitest/package.json | 4 +- clients/vitest/tests/e2e/example.test.js | 557 +++++++++++++++--- clients/vitest/vitest.e2e.config.js | 2 + 14 files changed, 2389 insertions(+), 260 deletions(-) create mode 100644 clients/ruby/test/e2e_test.rb create mode 100644 clients/static-site/tests/e2e.test.js create mode 100644 clients/storybook/tests/e2e.test.js diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2f07e58d..ce19e39b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -403,7 +403,13 @@ jobs: working-directory: ./test-site run: npx playwright install firefox --with-deps - - name: Run E2E tests + - 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: @@ -454,7 +460,13 @@ jobs: working-directory: ./clients/vitest run: npx playwright install chromium --with-deps - - name: Run E2E tests + - 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: @@ -505,13 +517,21 @@ jobs: working-directory: ./clients/ember run: npx playwright install chromium --with-deps - - name: Run E2E tests - working-directory: ./clients/ember + - name: Build Ember test app + working-directory: ./clients/ember/test-app run: | - cd test-app npm install npm run build -- --mode development - ../../../bin/vizzly.js run "npx testem ci --file testem.cjs" + + - 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 }} @@ -550,9 +570,9 @@ jobs: gem install bundler bundle install - - name: Run integration tests + - name: Run integration tests (TDD mode) working-directory: ./clients/ruby - run: VIZZLY_INTEGRATION=1 ruby -I lib test/integration_test.rb + run: ../../bin/vizzly.js tdd run "VIZZLY_INTEGRATION=1 ruby -I lib test/integration_test.rb" env: CI: true 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..228b1fff --- /dev/null +++ b/clients/ruby/test/e2e_test.rb @@ -0,0 +1,291 @@ +# 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 + + def assert_screenshot_result(result) + assert result, 'Expected screenshot result to be non-nil' + assert %w[new match].include?(result['status']), + "Expected status 'new' or 'match', got: #{result['status']}" + 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..22681fb8 100644 --- a/clients/ruby/test/integration_test.rb +++ b/clients/ruby/test/integration_test.rb @@ -7,114 +7,304 @@ 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 + + # 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 %w[new match].include?(result['status']), "Expected status 'new' or 'match', got: #{result['status']}" + end + + 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 %w[new match].include?(result['status']), "Expected status 'new' or 'match', got: #{result['status']}" + end + + def test_screenshot_with_threshold + start_server + image_data = create_test_png + + result = Vizzly.screenshot('screenshot-threshold', image_data, threshold: 5) + + assert result, 'Expected result to be non-nil' + assert %w[new match].include?(result['status']), "Expected status 'new' or 'match', got: #{result['status']}" + end + + def test_screenshot_with_full_page + start_server + image_data = create_test_png - # Create a simple PNG (1x1 red pixel) + result = Vizzly.screenshot('screenshot-fullpage', image_data, full_page: true) + + assert result, 'Expected result to be non-nil' + assert %w[new match].include?(result['status']), "Expected status 'new' or 'match', got: #{result['status']}" + end + + def test_screenshot_with_all_options + 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-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' - # 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']}" end - def test_screenshot_with_auto_discovery + # =========================================================================== + # 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']}" 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.json b/clients/vitest/package.json index fc44760f..6274674e 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", diff --git a/clients/vitest/tests/e2e/example.test.js b/clients/vitest/tests/e2e/example.test.js index 410bb432..8a503d18 100644 --- a/clients/vitest/tests/e2e/example.test.js +++ b/clients/vitest/tests/e2e/example.test.js @@ -2,97 +2,496 @@ * E2E Integration Tests * * These tests verify the Vitest plugin integration with Vizzly. - * They also serve as examples for documentation. + * They cover the full API surface: screenshot capture, properties, + * threshold, fullPage, element screenshots, etc. + * + * IMPORTANT: Vitest browser mode renders content INSIDE the browser sandbox. + * We use document.body.innerHTML to inject HTML, not page.goto() for external URLs. + * + * The page object uses Testing Library style methods: + * - page.getByRole(), page.getByText(), page.getByTestId(), etc. + * - Full page screenshots: expect(page).toMatchScreenshot() * * Local TDD mode: * vizzly tdd start * npm run test:e2e * + * One-shot TDD mode: + * npm run test:e2e:tdd + * * Cloud mode (used in CI): - * vizzly run "npm run test:e2e" + * npm run test:e2e:cloud */ -import { expect, test } from 'vitest'; +import { 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

-
- `; - - await expect(page.getByRole('heading')).toMatchScreenshot('homepage.png'); +// Shared styles used across tests +let baseStyles = ` + body { + margin: 0; + font-family: system-ui, -apple-system, sans-serif; + background: #f8fafc; + } + .hero { + text-align: center; + padding: 4rem 2rem; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + } + .hero h1 { + font-size: 3rem; + margin: 0 0 1rem 0; + } + .hero p { + font-size: 1.25rem; + opacity: 0.9; + margin: 0; + } + .btn { + display: inline-block; + padding: 0.75rem 1.5rem; + background: white; + color: #667eea; + border-radius: 0.5rem; + text-decoration: none; + font-weight: 600; + margin-top: 1.5rem; + border: none; + cursor: pointer; + } + .nav { + display: flex; + justify-content: space-between; + align-items: center; + padding: 1rem 2rem; + background: white; + border-bottom: 1px solid #e2e8f0; + } + .card { + background: white; + padding: 2rem; + border-radius: 1rem; + box-shadow: 0 1px 3px rgba(0,0,0,0.1); + border: 1px solid #e2e8f0; + } + .footer { + background: #1e293b; + color: white; + padding: 2rem; + text-align: center; + } +`; + +// ============================================================================= +// Basic Screenshot Tests +// ============================================================================= + +describe('Basic Screenshots', () => { + test('element screenshot - heading', async () => { + document.body.innerHTML = ` + +
+

FluffyCloud

+

Cloud infrastructure made simple

+
+ `; + + await expect(page.getByRole('heading', { level: 1 })).toMatchScreenshot('basic-heading.png'); + }); + + test('element screenshot - button', async () => { + document.body.innerHTML = ` + +
+

FluffyCloud

+ +
+ `; + + await expect(page.getByRole('button', { name: 'Get Started' })).toMatchScreenshot('basic-button.png'); + }); + + test('element screenshot - paragraph', async () => { + document.body.innerHTML = ` + +
+

FluffyCloud

+

Cloud infrastructure made simple

+
+ `; + + await expect(page.getByText('Cloud infrastructure made simple')).toMatchScreenshot('basic-paragraph.png'); + }); +}); + +// ============================================================================= +// Properties Tests +// ============================================================================= + +describe('Properties', () => { + test('simple properties', async () => { + document.body.innerHTML = ` + + + `; + + await expect(page.getByRole('button', { name: 'Sign In' })).toMatchScreenshot('props-simple.png', { + properties: { + theme: 'light', + component: 'navigation', + }, + }); + }); + + test('nested properties', async () => { + document.body.innerHTML = ` + +
+

Properties Test

+
+ `; + + await expect(page.getByRole('heading')).toMatchScreenshot('props-nested.png', { + properties: { + browser: 'chromium', + viewport: { width: 1920, height: 1080 }, + flags: ['responsive', 'desktop'], + }, + }); + }); + + test('empty properties object', async () => { + document.body.innerHTML = ` + +
+

Empty Props

+
+ `; + + await expect(page.getByRole('heading')).toMatchScreenshot('props-empty.png', { properties: {} }); + }); }); -test('homepage with properties', async () => { - // Render the hero HTML directly - document.body.innerHTML = ` - -
-

Vizzly + Vitest

-

Visual regression testing made simple

-
- `; - - // New first-class API - properties at top level! - await expect(page.getByRole('heading')).toMatchScreenshot( - 'homepage-with-props.png', - { +// ============================================================================= +// Threshold Tests +// ============================================================================= + +describe('Threshold', () => { + test('default threshold (0)', async () => { + document.body.innerHTML = ` + +
+

Pro Plan

+

$29/month - Perfect for growing teams

+
+ `; + + await expect(page.getByRole('heading', { level: 3 })).toMatchScreenshot('threshold-default.png'); + }); + + test('custom threshold (5%)', async () => { + document.body.innerHTML = ` + +
+

Custom Threshold

+
+ `; + + await expect(page.getByRole('heading', { level: 3 })).toMatchScreenshot('threshold-5.png', { threshold: 5 }); + }); + + test('high threshold (10%)', async () => { + document.body.innerHTML = ` + +
+

High Threshold

+
+ `; + + await expect(page.getByRole('heading', { level: 3 })).toMatchScreenshot('threshold-10.png', { + threshold: 10, + properties: { note: 'high-threshold-for-animations' }, + }); + }); + + test('explicit zero threshold', async () => { + document.body.innerHTML = ` + +
+

Zero Threshold

+
+ `; + + await expect(page.getByRole('heading', { level: 3 })).toMatchScreenshot('threshold-zero.png', { threshold: 0 }); + }); +}); + +// ============================================================================= +// Full Page Screenshots +// ============================================================================= + +describe('Full Page', () => { + test('full page screenshot', async () => { + document.body.innerHTML = ` + + +
+

Cloud Infrastructure

+

Deploy in seconds, scale infinitely

+
+ + `; + + await expect(page).toMatchScreenshot('fullpage.png', { fullPage: true }); + }); + + test('full page with properties', async () => { + document.body.innerHTML = ` + +
+

Full Page Props

+

Testing full page with properties

+
+ `; + + await expect(page).toMatchScreenshot('fullpage-props.png', { + fullPage: true, properties: { - theme: 'dark', - viewport: '1920x1080', + page: 'homepage', + theme: 'light', }, - threshold: 5, - } - ); + }); + }); + + test('full page with threshold', async () => { + document.body.innerHTML = ` + +
+

Full Page Threshold

+

Testing full page with threshold

+
+ `; + + await expect(page).toMatchScreenshot('fullpage-threshold.png', { + fullPage: true, + threshold: 2, + }); + }); +}); + +// ============================================================================= +// Combined Options +// ============================================================================= + +describe('Combined Options', () => { + test('element with all options', async () => { + document.body.innerHTML = ` + +
+

Combined Test

+

Testing all options together

+
+ `; + + await expect(page.getByRole('heading')).toMatchScreenshot('combined-element.png', { + threshold: 3, + properties: { + testType: 'comprehensive', + browser: 'chromium', + section: 'hero', + }, + }); + }); + + test('full page with all options', async () => { + document.body.innerHTML = ` + +
+

Full Page Combined

+

Testing full page with all options

+
+ `; + + await expect(page).toMatchScreenshot('combined-fullpage-all.png', { + fullPage: true, + threshold: 2, + properties: { + viewport: 'desktop', + theme: 'default', + page: 'test', + }, + }); + }); +}); + +// ============================================================================= +// Multiple Screenshots Per Test +// ============================================================================= + +describe('Multiple Screenshots', () => { + test('captures multiple elements in sequence', async () => { + document.body.innerHTML = ` + +
+

First Heading

+

Second Heading

+

Third Heading

+
+ `; + + await expect(page.getByRole('heading', { level: 1 })).toMatchScreenshot('multi-h1.png'); + await expect(page.getByRole('heading', { level: 2 })).toMatchScreenshot('multi-h2.png'); + await expect(page.getByRole('heading', { level: 3 })).toMatchScreenshot('multi-h3.png'); + }); + + test('captures different page states', async () => { + // State 1: Loading + document.body.innerHTML = ` + +
+

Loading...

+
+ `; + await expect(page.getByText('Loading...')).toMatchScreenshot('state-loading.png'); + + // State 2: Loaded + document.body.innerHTML = ` + +
+

Content Loaded

+

Your data is ready

+
+ `; + await expect(page.getByRole('heading', { level: 3 })).toMatchScreenshot('state-loaded.png'); + + // State 3: Empty + document.body.innerHTML = ` + +
+

No Results

+

Try a different search

+
+ `; + await expect(page.getByRole('heading', { level: 3 })).toMatchScreenshot('state-empty.png'); + }); +}); + +// ============================================================================= +// Form Elements +// ============================================================================= + +describe('Form Elements', () => { + test('form inputs', async () => { + document.body.innerHTML = ` + +
+ + +
+ `; + + await expect(page.getByRole('textbox', { name: 'Name' })).toMatchScreenshot('form-name-input.png'); + await expect(page.getByRole('textbox', { name: 'Email' })).toMatchScreenshot('form-email-input.png'); + }); + + test('form buttons', async () => { + document.body.innerHTML = ` + +
+ + +
+ `; + + await expect(page.getByRole('button', { name: 'Submit' })).toMatchScreenshot('form-submit.png'); + await expect(page.getByRole('button', { name: 'Cancel' })).toMatchScreenshot('form-cancel.png'); + }); +}); + +// ============================================================================= +// Edge Cases +// ============================================================================= + +describe('Edge Cases', () => { + test('special characters in screenshot name', async () => { + document.body.innerHTML = ` + +
+

Special Chars

+
+ `; + + await expect(page.getByRole('heading', { level: 3 })).toMatchScreenshot('screenshot_with-special.chars.png'); + }); + + test('very long screenshot name', async () => { + document.body.innerHTML = ` + +
+

Long Name

+
+ `; + + await expect(page.getByRole('heading', { level: 3 })).toMatchScreenshot('this-is-a-very-long-screenshot-name-for-testing-purposes.png'); + }); + + test('small text element', async () => { + document.body.innerHTML = ` + + Tiny text + `; + + await expect(page.getByText('Tiny text')).toMatchScreenshot('edge-small-text.png'); + }); + + test('element with emoji', async () => { + document.body.innerHTML = ` + +
+

🚀 Launch Ready

+
+ `; + + await expect(page.getByRole('heading')).toMatchScreenshot('edge-emoji.png'); + }); +}); + +// ============================================================================= +// Test By Data Attribute (using getByTestId) +// ============================================================================= + +describe('Data Attributes', () => { + test('getByTestId screenshot', async () => { + document.body.innerHTML = ` + +
+

Enterprise Plan

+

$99/month

+
+ `; + + await expect(page.getByTestId('pricing-title')).toMatchScreenshot('testid-title.png'); + await expect(page.getByTestId('pricing-amount')).toMatchScreenshot('testid-amount.png'); + }); }); diff --git a/clients/vitest/vitest.e2e.config.js b/clients/vitest/vitest.e2e.config.js index 2bd7dde9..93a09c63 100644 --- a/clients/vitest/vitest.e2e.config.js +++ b/clients/vitest/vitest.e2e.config.js @@ -4,6 +4,8 @@ import { vizzlyPlugin } from './src/index.js'; // E2E tests config - runs in browser mode // These tests verify actual browser integration with Vizzly +// Tests render HTML inline (using document.body.innerHTML) since +// Vitest browser mode runs inside a browser sandbox export default defineConfig({ plugins: [vizzlyPlugin()], test: { From f84a657c42e1f2e6276c30f77f4519c75c2fca32 Mon Sep 17 00:00:00 2001 From: Robert DeLuca Date: Sat, 17 Jan 2026 16:17:37 -0600 Subject: [PATCH 2/6] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Refactor=20Vitest=20E2?= =?UTF-8?q?E=20tests=20to=20use=20shared=20test-site?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add custom `loadPage` command to fetch and inject HTML content - Start static server for test-site with dynamic port allocation - Use real FluffyCloud test-site instead of inline HTML - Screenshots now match other SDKs (Storybook, Static-Site, Ember, Ruby) The `loadPage` command works around Vitest browser mode's limitation of not supporting `page.goto()` by fetching HTML and using `frame.setContent()` to inject it into the test iframe. --- clients/vitest/package-lock.json | 1262 +++++----------------- clients/vitest/package.json | 2 +- clients/vitest/src/commands.js | 45 + clients/vitest/tests/e2e/example.test.js | 524 ++------- clients/vitest/vitest.e2e.config.js | 46 +- 5 files changed, 433 insertions(+), 1446 deletions(-) create mode 100644 clients/vitest/src/commands.js 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 6274674e..e57f0b7e 100644 --- a/clients/vitest/package.json +++ b/clients/vitest/package.json @@ -59,7 +59,6 @@ "access": "public", "registry": "https://registry.npmjs.org/" }, - "dependencies": {}, "peerDependencies": { "@vizzly-testing/cli": ">=0.12.0", "vitest": ">=4.0.0" @@ -69,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/src/commands.js b/clients/vitest/src/commands.js new file mode 100644 index 00000000..8a58c644 --- /dev/null +++ b/clients/vitest/src/commands.js @@ -0,0 +1,45 @@ +/** + * Custom Vitest browser commands for E2E testing + * + * These commands provide access to external content for testing + * while working within Vitest's iframe-based test harness. + */ + +/** + * Load external page content into the test frame + * Fetches HTML from URL and injects it into the current document + * + * @type {import('vitest/node').BrowserCommand<[url: string]>} + */ +export async function loadPage(ctx, url) { + if (ctx.provider.name !== 'playwright') { + throw new Error('loadPage command only supports Playwright provider'); + } + + let frame = await ctx.frame(); + + // Fetch the HTML content + let response = await fetch(url); + let html = await response.text(); + + // Inject the HTML content into the frame + await frame.setContent(html, { + waitUntil: 'networkidle', + }); +} + +/** + * Wait for network to be idle + * + * @type {import('vitest/node').BrowserCommand<[options?: { timeout?: number }]>} + */ +export async function waitForNetworkIdle(ctx, options = {}) { + if (ctx.provider.name !== 'playwright') { + throw new Error('waitForNetworkIdle command only supports Playwright provider'); + } + + let frame = await ctx.frame(); + await frame.waitForLoadState('networkidle', { + timeout: options.timeout || 30000, + }); +} diff --git a/clients/vitest/tests/e2e/example.test.js b/clients/vitest/tests/e2e/example.test.js index 8a503d18..5b942cdb 100644 --- a/clients/vitest/tests/e2e/example.test.js +++ b/clients/vitest/tests/e2e/example.test.js @@ -1,16 +1,10 @@ /** - * E2E Integration Tests + * E2E Integration Tests using shared test-site (FluffyCloud) * - * These tests verify the Vitest plugin integration with Vizzly. - * They cover the full API surface: screenshot capture, properties, - * threshold, fullPage, element screenshots, etc. + * These tests verify the Vitest plugin integration with Vizzly using + * the same test-site as other SDKs for visual consistency. * - * IMPORTANT: Vitest browser mode renders content INSIDE the browser sandbox. - * We use document.body.innerHTML to inject HTML, not page.goto() for external URLs. - * - * The page object uses Testing Library style methods: - * - page.getByRole(), page.getByText(), page.getByTestId(), etc. - * - Full page screenshots: expect(page).toMatchScreenshot() + * Uses custom commands to navigate Playwright to the test-site URLs. * * Local TDD mode: * vizzly tdd start @@ -18,480 +12,134 @@ * * One-shot TDD mode: * npm run test:e2e:tdd - * - * Cloud mode (used in CI): - * npm run test:e2e:cloud */ import { describe, expect, test } from 'vitest'; -import { page } from 'vitest/browser'; - -// Shared styles used across tests -let baseStyles = ` - body { - margin: 0; - font-family: system-ui, -apple-system, sans-serif; - background: #f8fafc; - } - .hero { - text-align: center; - padding: 4rem 2rem; - background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); - color: white; - } - .hero h1 { - font-size: 3rem; - margin: 0 0 1rem 0; - } - .hero p { - font-size: 1.25rem; - opacity: 0.9; - margin: 0; - } - .btn { - display: inline-block; - padding: 0.75rem 1.5rem; - background: white; - color: #667eea; - border-radius: 0.5rem; - text-decoration: none; - font-weight: 600; - margin-top: 1.5rem; - border: none; - cursor: pointer; - } - .nav { - display: flex; - justify-content: space-between; - align-items: center; - padding: 1rem 2rem; - background: white; - border-bottom: 1px solid #e2e8f0; - } - .card { - background: white; - padding: 2rem; - border-radius: 1rem; - box-shadow: 0 1px 3px rgba(0,0,0,0.1); - border: 1px solid #e2e8f0; - } - .footer { - background: #1e293b; - color: white; - padding: 2rem; - text-align: center; - } -`; - -// ============================================================================= -// Basic Screenshot Tests -// ============================================================================= - -describe('Basic Screenshots', () => { - test('element screenshot - heading', async () => { - document.body.innerHTML = ` - -
-

FluffyCloud

-

Cloud infrastructure made simple

-
- `; - - await expect(page.getByRole('heading', { level: 1 })).toMatchScreenshot('basic-heading.png'); - }); - - test('element screenshot - button', async () => { - document.body.innerHTML = ` - -
-

FluffyCloud

- -
- `; - - await expect(page.getByRole('button', { name: 'Get Started' })).toMatchScreenshot('basic-button.png'); - }); +import { commands, page } from 'vitest/browser'; - test('element screenshot - paragraph', async () => { - document.body.innerHTML = ` - -
-

FluffyCloud

-

Cloud infrastructure made simple

-
- `; +// Base URL for test-site (defined in vitest.e2e.config.js) +// eslint-disable-next-line no-undef +let baseUrl = __TEST_SITE_URL__; - await expect(page.getByText('Cloud infrastructure made simple')).toMatchScreenshot('basic-paragraph.png'); +describe('Homepage', () => { + test('full page screenshot', async () => { + await commands.loadPage(`${baseUrl}/index.html`); + await expect(page).toMatchScreenshot('homepage-full.png'); }); -}); - -// ============================================================================= -// Properties Tests -// ============================================================================= -describe('Properties', () => { - test('simple properties', async () => { - document.body.innerHTML = ` - - - `; - - await expect(page.getByRole('button', { name: 'Sign In' })).toMatchScreenshot('props-simple.png', { - properties: { - theme: 'light', - component: 'navigation', - }, - }); + test('navigation bar', async () => { + await commands.loadPage(`${baseUrl}/index.html`); + let nav = page.getByRole('navigation'); + await expect(nav).toMatchScreenshot('homepage-nav.png'); }); - test('nested properties', async () => { - document.body.innerHTML = ` - -
-

Properties Test

-
- `; - - await expect(page.getByRole('heading')).toMatchScreenshot('props-nested.png', { - properties: { - browser: 'chromium', - viewport: { width: 1920, height: 1080 }, - flags: ['responsive', 'desktop'], - }, - }); + test('hero section', async () => { + await commands.loadPage(`${baseUrl}/index.html`); + // Get hero by heading text + let hero = page.getByRole('heading', { name: 'Every Pet Deserves' }); + await expect(hero).toMatchScreenshot('homepage-hero.png'); }); - test('empty properties object', async () => { - document.body.innerHTML = ` - -
-

Empty Props

-
- `; - - await expect(page.getByRole('heading')).toMatchScreenshot('props-empty.png', { properties: {} }); + test('features heading', async () => { + await commands.loadPage(`${baseUrl}/index.html`); + // Get features section heading + let featuresHeading = page.getByRole('heading', { name: 'Why Choose FluffyCloud?' }); + await expect(featuresHeading).toMatchScreenshot('homepage-features-heading.png'); }); }); -// ============================================================================= -// Threshold Tests -// ============================================================================= - -describe('Threshold', () => { - test('default threshold (0)', async () => { - document.body.innerHTML = ` - -
-

Pro Plan

-

$29/month - Perfect for growing teams

-
- `; - - await expect(page.getByRole('heading', { level: 3 })).toMatchScreenshot('threshold-default.png'); +describe('Features Page', () => { + test('full page screenshot', async () => { + await commands.loadPage(`${baseUrl}/features.html`); + await expect(page).toMatchScreenshot('features-full.png'); }); - test('custom threshold (5%)', async () => { - document.body.innerHTML = ` - -
-

Custom Threshold

-
- `; - - await expect(page.getByRole('heading', { level: 3 })).toMatchScreenshot('threshold-5.png', { threshold: 5 }); + test('navigation bar', async () => { + await commands.loadPage(`${baseUrl}/features.html`); + let nav = page.getByRole('navigation'); + await expect(nav).toMatchScreenshot('features-nav.png'); }); +}); - test('high threshold (10%)', async () => { - document.body.innerHTML = ` - -
-

High Threshold

-
- `; - - await expect(page.getByRole('heading', { level: 3 })).toMatchScreenshot('threshold-10.png', { - threshold: 10, - properties: { note: 'high-threshold-for-animations' }, - }); +describe('Pricing Page', () => { + test('full page screenshot', async () => { + await commands.loadPage(`${baseUrl}/pricing.html`); + await expect(page).toMatchScreenshot('pricing-full.png'); }); - test('explicit zero threshold', async () => { - document.body.innerHTML = ` - -
-

Zero Threshold

-
- `; - - await expect(page.getByRole('heading', { level: 3 })).toMatchScreenshot('threshold-zero.png', { threshold: 0 }); + test('pricing heading', async () => { + await commands.loadPage(`${baseUrl}/pricing.html`); + // Get the pricing section heading + let pricingHeading = page.getByRole('heading', { level: 1 }); + await expect(pricingHeading).toMatchScreenshot('pricing-heading.png'); }); }); -// ============================================================================= -// Full Page Screenshots -// ============================================================================= - -describe('Full Page', () => { +describe('Contact Page', () => { test('full page screenshot', async () => { - document.body.innerHTML = ` - - -
-

Cloud Infrastructure

-

Deploy in seconds, scale infinitely

-
-
-

© 2024 FluffyCloud

-
- `; - - await expect(page).toMatchScreenshot('fullpage.png', { fullPage: true }); + await commands.loadPage(`${baseUrl}/contact.html`); + await expect(page).toMatchScreenshot('contact-full.png'); }); - test('full page with properties', async () => { - document.body.innerHTML = ` - -
-

Full Page Props

-

Testing full page with properties

-
- `; - - await expect(page).toMatchScreenshot('fullpage-props.png', { - fullPage: true, - properties: { - page: 'homepage', - theme: 'light', - }, - }); + test('contact heading', async () => { + await commands.loadPage(`${baseUrl}/contact.html`); + let heading = page.getByRole('heading', { level: 1 }); + await expect(heading).toMatchScreenshot('contact-heading.png'); }); +}); - test('full page with threshold', async () => { - document.body.innerHTML = ` - -
-

Full Page Threshold

-

Testing full page with threshold

-
- `; - - await expect(page).toMatchScreenshot('fullpage-threshold.png', { - fullPage: true, - threshold: 2, +describe('Screenshot Options', () => { + test('with custom threshold', async () => { + await commands.loadPage(`${baseUrl}/index.html`); + let nav = page.getByRole('navigation'); + await expect(nav).toMatchScreenshot('threshold-test.png', { + threshold: 5, }); }); -}); - -// ============================================================================= -// Combined Options -// ============================================================================= - -describe('Combined Options', () => { - test('element with all options', async () => { - document.body.innerHTML = ` - -
-

Combined Test

-

Testing all options together

-
- `; - await expect(page.getByRole('heading')).toMatchScreenshot('combined-element.png', { - threshold: 3, + test('with properties', async () => { + await commands.loadPage(`${baseUrl}/index.html`); + await expect(page).toMatchScreenshot('props-test.png', { properties: { - testType: 'comprehensive', browser: 'chromium', - section: 'hero', + viewport: '1280x720', + page: 'homepage', }, }); }); - test('full page with all options', async () => { - document.body.innerHTML = ` - -
-

Full Page Combined

-

Testing full page with all options

-
- `; - - await expect(page).toMatchScreenshot('combined-fullpage-all.png', { - fullPage: true, - threshold: 2, + test('element with threshold and properties', async () => { + await commands.loadPage(`${baseUrl}/index.html`); + let nav = page.getByRole('navigation'); + await expect(nav).toMatchScreenshot('combined-options.png', { + threshold: 3, properties: { - viewport: 'desktop', - theme: 'default', - page: 'test', + component: 'navigation', + variant: 'default', }, }); }); }); -// ============================================================================= -// Multiple Screenshots Per Test -// ============================================================================= +describe('Cross-Page Navigation', () => { + test('captures multiple pages in sequence', async () => { + let pages = ['index.html', 'features.html', 'pricing.html', 'contact.html']; -describe('Multiple Screenshots', () => { - test('captures multiple elements in sequence', async () => { - document.body.innerHTML = ` - -
-

First Heading

-

Second Heading

-

Third Heading

-
- `; - - await expect(page.getByRole('heading', { level: 1 })).toMatchScreenshot('multi-h1.png'); - await expect(page.getByRole('heading', { level: 2 })).toMatchScreenshot('multi-h2.png'); - await expect(page.getByRole('heading', { level: 3 })).toMatchScreenshot('multi-h3.png'); - }); - - test('captures different page states', async () => { - // State 1: Loading - document.body.innerHTML = ` - -
-

Loading...

-
- `; - await expect(page.getByText('Loading...')).toMatchScreenshot('state-loading.png'); - - // State 2: Loaded - document.body.innerHTML = ` - -
-

Content Loaded

-

Your data is ready

-
- `; - await expect(page.getByRole('heading', { level: 3 })).toMatchScreenshot('state-loaded.png'); - - // State 3: Empty - document.body.innerHTML = ` - -
-

No Results

-

Try a different search

-
- `; - await expect(page.getByRole('heading', { level: 3 })).toMatchScreenshot('state-empty.png'); + for (let pageName of pages) { + await commands.loadPage(`${baseUrl}/${pageName}`); + let nav = page.getByRole('navigation'); + await expect(nav).toMatchScreenshot(`nav-${pageName.replace('.html', '')}.png`, { + properties: { page: pageName }, + }); + } }); }); -// ============================================================================= -// Form Elements -// ============================================================================= - -describe('Form Elements', () => { - test('form inputs', async () => { - document.body.innerHTML = ` - -
- - -
- `; - - await expect(page.getByRole('textbox', { name: 'Name' })).toMatchScreenshot('form-name-input.png'); - await expect(page.getByRole('textbox', { name: 'Email' })).toMatchScreenshot('form-email-input.png'); - }); - - test('form buttons', async () => { - document.body.innerHTML = ` - -
- - -
- `; - - await expect(page.getByRole('button', { name: 'Submit' })).toMatchScreenshot('form-submit.png'); - await expect(page.getByRole('button', { name: 'Cancel' })).toMatchScreenshot('form-cancel.png'); - }); -}); - -// ============================================================================= -// Edge Cases -// ============================================================================= - -describe('Edge Cases', () => { - test('special characters in screenshot name', async () => { - document.body.innerHTML = ` - -
-

Special Chars

-
- `; - - await expect(page.getByRole('heading', { level: 3 })).toMatchScreenshot('screenshot_with-special.chars.png'); - }); - - test('very long screenshot name', async () => { - document.body.innerHTML = ` - -
-

Long Name

-
- `; - - await expect(page.getByRole('heading', { level: 3 })).toMatchScreenshot('this-is-a-very-long-screenshot-name-for-testing-purposes.png'); - }); - - test('small text element', async () => { - document.body.innerHTML = ` - - Tiny text - `; - - await expect(page.getByText('Tiny text')).toMatchScreenshot('edge-small-text.png'); - }); - - test('element with emoji', async () => { - document.body.innerHTML = ` - -
-

🚀 Launch Ready

-
- `; - - await expect(page.getByRole('heading')).toMatchScreenshot('edge-emoji.png'); - }); -}); - -// ============================================================================= -// Test By Data Attribute (using getByTestId) -// ============================================================================= - -describe('Data Attributes', () => { - test('getByTestId screenshot', async () => { - document.body.innerHTML = ` - -
-

Enterprise Plan

-

$99/month

-
- `; - - await expect(page.getByTestId('pricing-title')).toMatchScreenshot('testid-title.png'); - await expect(page.getByTestId('pricing-amount')).toMatchScreenshot('testid-amount.png'); +describe('Footer', () => { + test('footer brand on homepage', async () => { + await commands.loadPage(`${baseUrl}/index.html`); + // Get footer by text + let footerBrand = page.getByText('FluffyCloud', { exact: false }).last(); + await expect(footerBrand).toMatchScreenshot('footer-brand.png'); }); }); diff --git a/clients/vitest/vitest.e2e.config.js b/clients/vitest/vitest.e2e.config.js index 93a09c63..a0737409 100644 --- a/clients/vitest/vitest.e2e.config.js +++ b/clients/vitest/vitest.e2e.config.js @@ -1,11 +1,39 @@ +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'; +import * as commands from './src/commands.js'; -// E2E tests config - runs in browser mode -// These tests verify actual browser integration with Vizzly -// Tests render HTML inline (using document.body.innerHTML) since -// Vitest browser mode runs inside a browser sandbox +// Path to shared test-site +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 on a random available port +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 with real test-site export default defineConfig({ plugins: [vizzlyPlugin()], test: { @@ -14,11 +42,19 @@ export default defineConfig({ instances: [ { browser: 'chromium', - provider: playwright(), + provider: playwright({ + launch: { + viewport: { width: 1280, height: 720 }, + }, + }), }, ], headless: true, + commands, }, include: ['tests/e2e/**/*.test.js'], }, + define: { + __TEST_SITE_URL__: JSON.stringify(`http://localhost:${testSitePort}`), + }, }); From d55cca14a8b538b65dedbbdbd25b037fcb212e49 Mon Sep 17 00:00:00 2001 From: Robert DeLuca Date: Sat, 17 Jan 2026 16:29:36 -0600 Subject: [PATCH 3/6] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Reorganize=20CI=20work?= =?UTF-8?q?flows=20for=20better=20maintainability?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Split monolithic ci.yml (900+ lines) into focused workflows: - ci.yml: Core CLI lint, tests, build (78 lines) - reporter.yml: Reporter visual tests (Vizzly cloud) - tui.yml: TUI visual tests (Vizzly cloud) - sdk-e2e.yml: All SDK E2E tests (TDD + Cloud mode) - sdk-unit.yml: SDK unit tests (conditional on file changes) - Add cloud mode for Ruby, Storybook, and Static-Site SDKs - Each workflow has its own status check job for branch protection - SDK E2E always runs to catch CLI breaking changes - SDK unit tests only run when that SDK's files change New secrets needed: - VIZZLY_RUBY_CLIENT_TOKEN - VIZZLY_STORYBOOK_CLIENT_TOKEN - VIZZLY_STATIC_SITE_CLIENT_TOKEN --- .github/workflows/ci.yml | 775 ++------------------------------- .github/workflows/reporter.yml | 51 +++ .github/workflows/sdk-e2e.yml | 380 ++++++++++++++++ .github/workflows/sdk-unit.yml | 361 +++++++++++++++ .github/workflows/tui.yml | 39 ++ 5 files changed, 868 insertions(+), 738 deletions(-) create mode 100644 .github/workflows/reporter.yml create mode 100644 .github/workflows/sdk-e2e.yml create mode 100644 .github/workflows/sdk-unit.yml create mode 100644 .github/workflows/tui.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ce19e39b..1a7b979d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,33 +2,35 @@ 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 + - uses: actions/checkout@v4 - - name: Use Node.js 22 - uses: actions/setup-node@v4 - with: - node-version: 22 - cache: 'npm' + - name: Use Node.js 22 + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: 'npm' - - name: Install dependencies - run: npm ci + - name: Install dependencies + run: npm ci - - name: Run linter - run: npm run lint + - name: Run linter + run: npm run lint - - name: Check formatting - run: npm run format:check + - name: Check formatting + run: npm run format:check test: + name: Test (Node ${{ matrix.node-version }}) runs-on: blacksmith-4vcpu-ubuntu-2404 timeout-minutes: 8 needs: lint @@ -37,742 +39,39 @@ jobs: 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 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 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 - 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 - - - name: Build CLI - run: npm run build + node-version: ${{ matrix.node-version }} + cache: 'npm' - - name: Install Storybook client dependencies - working-directory: ./clients/storybook - run: npm install + - name: Install dependencies + run: npm ci - - name: Run linter - working-directory: ./clients/storybook - run: npm run lint + - name: Run tests + run: npm test + env: + CI: true - - name: Run tests - working-directory: ./clients/storybook - run: npm test - env: - CI: true + - name: Build + run: npm run build - - name: Build package - working-directory: ./clients/storybook - run: npm run build + - name: Run type tests + run: npm run test:types - test-static-site-client: + check: + name: CI Status 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 (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 }} - - 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 (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 }} - - 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: 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 }} - - 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 (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 - - 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 - - - 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 - - - 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 }} - - ci-check: - 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" - 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" + if [[ "${{ needs.lint.result }}" == "failure" || "${{ needs.test.result }}" == "failure" ]]; then + echo "CI checks 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..68785f04 --- /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: blacksmith-4vcpu-ubuntu-2404 + 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..74caded3 --- /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: blacksmith-4vcpu-ubuntu-2404 + 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: blacksmith-4vcpu-ubuntu-2404 + 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: blacksmith-4vcpu-ubuntu-2404 + 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: blacksmith-4vcpu-ubuntu-2404 + 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: blacksmith-4vcpu-ubuntu-2404 + 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: blacksmith-4vcpu-ubuntu-2404 + 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..a33bfb16 --- /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: blacksmith-4vcpu-ubuntu-2404 + 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: blacksmith-4vcpu-ubuntu-2404 + 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 }} From e30998dac7570d29fee64a29ed8404f77c2129d8 Mon Sep 17 00:00:00 2001 From: Robert DeLuca Date: Sun, 18 Jan 2026 21:19:46 -0600 Subject: [PATCH 4/6] =?UTF-8?q?=F0=9F=90=9B=20Fix=20Vitest=20E2E=20tests?= =?UTF-8?q?=20to=20properly=20test=20the=20SDK?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Test the actual SDK functionality (toMatchScreenshot matcher) - Load test-site CSS for consistent styling with other SDKs - Remove custom commands (loadPage was for full-page navigation, not SDK testing) - Test page screenshots, element screenshots, properties, and thresholds --- clients/vitest/src/commands.js | 45 ------ clients/vitest/tests/e2e/example.test.js | 191 +++++++++-------------- clients/vitest/vitest.e2e.config.js | 14 +- 3 files changed, 79 insertions(+), 171 deletions(-) delete mode 100644 clients/vitest/src/commands.js diff --git a/clients/vitest/src/commands.js b/clients/vitest/src/commands.js deleted file mode 100644 index 8a58c644..00000000 --- a/clients/vitest/src/commands.js +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Custom Vitest browser commands for E2E testing - * - * These commands provide access to external content for testing - * while working within Vitest's iframe-based test harness. - */ - -/** - * Load external page content into the test frame - * Fetches HTML from URL and injects it into the current document - * - * @type {import('vitest/node').BrowserCommand<[url: string]>} - */ -export async function loadPage(ctx, url) { - if (ctx.provider.name !== 'playwright') { - throw new Error('loadPage command only supports Playwright provider'); - } - - let frame = await ctx.frame(); - - // Fetch the HTML content - let response = await fetch(url); - let html = await response.text(); - - // Inject the HTML content into the frame - await frame.setContent(html, { - waitUntil: 'networkidle', - }); -} - -/** - * Wait for network to be idle - * - * @type {import('vitest/node').BrowserCommand<[options?: { timeout?: number }]>} - */ -export async function waitForNetworkIdle(ctx, options = {}) { - if (ctx.provider.name !== 'playwright') { - throw new Error('waitForNetworkIdle command only supports Playwright provider'); - } - - let frame = await ctx.frame(); - await frame.waitForLoadState('networkidle', { - timeout: options.timeout || 30000, - }); -} diff --git a/clients/vitest/tests/e2e/example.test.js b/clients/vitest/tests/e2e/example.test.js index 5b942cdb..e9a35561 100644 --- a/clients/vitest/tests/e2e/example.test.js +++ b/clients/vitest/tests/e2e/example.test.js @@ -1,145 +1,104 @@ /** - * E2E Integration Tests using shared test-site (FluffyCloud) + * E2E tests for the Vizzly Vitest plugin * - * These tests verify the Vitest plugin integration with Vizzly using - * the same test-site as other SDKs for visual consistency. + * Tests the toMatchScreenshot matcher works correctly with: + * - Page screenshots + * - Element screenshots + * - Properties/metadata + * - Threshold options * - * Uses custom commands to navigate Playwright to the test-site URLs. - * - * Local TDD mode: - * vizzly tdd start - * npm run test:e2e - * - * One-shot TDD mode: - * npm run test:e2e:tdd + * Uses the shared test-site CSS for consistent styling. */ -import { describe, expect, test } from 'vitest'; -import { commands, page } from 'vitest/browser'; +import { beforeAll, describe, expect, test } from 'vitest'; +import { page } from 'vitest/browser'; -// Base URL for test-site (defined in vitest.e2e.config.js) +// Base URL for test-site assets (defined in vitest.e2e.config.js) // eslint-disable-next-line no-undef let baseUrl = __TEST_SITE_URL__; -describe('Homepage', () => { - test('full page screenshot', async () => { - await commands.loadPage(`${baseUrl}/index.html`); - await expect(page).toMatchScreenshot('homepage-full.png'); - }); - - test('navigation bar', async () => { - await commands.loadPage(`${baseUrl}/index.html`); - let nav = page.getByRole('navigation'); - await expect(nav).toMatchScreenshot('homepage-nav.png'); - }); - - test('hero section', async () => { - await commands.loadPage(`${baseUrl}/index.html`); - // Get hero by heading text - let hero = page.getByRole('heading', { name: 'Every Pet Deserves' }); - await expect(hero).toMatchScreenshot('homepage-hero.png'); - }); - - test('features heading', async () => { - await commands.loadPage(`${baseUrl}/index.html`); - // Get features section heading - let featuresHeading = page.getByRole('heading', { name: 'Why Choose FluffyCloud?' }); - await expect(featuresHeading).toMatchScreenshot('homepage-features-heading.png'); - }); -}); - -describe('Features Page', () => { - test('full page screenshot', async () => { - await commands.loadPage(`${baseUrl}/features.html`); - await expect(page).toMatchScreenshot('features-full.png'); - }); - - test('navigation bar', async () => { - await commands.loadPage(`${baseUrl}/features.html`); - let nav = page.getByRole('navigation'); - await expect(nav).toMatchScreenshot('features-nav.png'); - }); -}); - -describe('Pricing Page', () => { - test('full page screenshot', async () => { - await commands.loadPage(`${baseUrl}/pricing.html`); - await expect(page).toMatchScreenshot('pricing-full.png'); - }); - - test('pricing heading', async () => { - await commands.loadPage(`${baseUrl}/pricing.html`); - // Get the pricing section heading - let pricingHeading = page.getByRole('heading', { level: 1 }); - await expect(pricingHeading).toMatchScreenshot('pricing-heading.png'); +// 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); + + // Wait for CSS to load + await new Promise((resolve) => { + link.onload = resolve; + link.onerror = resolve; }); }); -describe('Contact Page', () => { - test('full page screenshot', async () => { - await commands.loadPage(`${baseUrl}/contact.html`); - await expect(page).toMatchScreenshot('contact-full.png'); - }); +describe('Vizzly Vitest Plugin', () => { + test('page screenshot', async () => { + document.body.innerHTML = ` +
+

Hello Vizzly

+

Testing the Vitest plugin

+
+ `; - test('contact heading', async () => { - await commands.loadPage(`${baseUrl}/contact.html`); - let heading = page.getByRole('heading', { level: 1 }); - await expect(heading).toMatchScreenshot('contact-heading.png'); + await expect(page).toMatchScreenshot('page.png'); }); -}); -describe('Screenshot Options', () => { - test('with custom threshold', async () => { - await commands.loadPage(`${baseUrl}/index.html`); - let nav = page.getByRole('navigation'); - await expect(nav).toMatchScreenshot('threshold-test.png', { - threshold: 5, - }); + test('element screenshot', async () => { + document.body.innerHTML = ` +
+ +
+ `; + + let button = page.getByRole('button'); + await expect(button).toMatchScreenshot('button.png'); }); - test('with properties', async () => { - await commands.loadPage(`${baseUrl}/index.html`); - await expect(page).toMatchScreenshot('props-test.png', { + test('screenshot with properties', async () => { + document.body.innerHTML = ` +
+
+

Dark Theme Card

+

Component with metadata

+
+
+ `; + + await expect(page).toMatchScreenshot('card.png', { properties: { - browser: 'chromium', - viewport: '1280x720', - page: 'homepage', + theme: 'dark', + component: 'card', }, }); }); - test('element with threshold and properties', async () => { - await commands.loadPage(`${baseUrl}/index.html`); - let nav = page.getByRole('navigation'); - await expect(nav).toMatchScreenshot('combined-options.png', { - threshold: 3, - properties: { - component: 'navigation', - variant: 'default', - }, + test('screenshot with threshold', async () => { + document.body.innerHTML = ` +
+ Warning! +
+ `; + + await expect(page).toMatchScreenshot('warning.png', { + threshold: 5, }); }); -}); -describe('Cross-Page Navigation', () => { - test('captures multiple pages in sequence', async () => { - let pages = ['index.html', 'features.html', 'pricing.html', 'contact.html']; + test('multiple elements in sequence', async () => { + document.body.innerHTML = ` + + `; - for (let pageName of pages) { - await commands.loadPage(`${baseUrl}/${pageName}`); - let nav = page.getByRole('navigation'); - await expect(nav).toMatchScreenshot(`nav-${pageName.replace('.html', '')}.png`, { - properties: { page: pageName }, - }); - } - }); -}); + let nav = page.getByRole('navigation'); + await expect(nav).toMatchScreenshot('nav.png'); -describe('Footer', () => { - test('footer brand on homepage', async () => { - await commands.loadPage(`${baseUrl}/index.html`); - // Get footer by text - let footerBrand = page.getByText('FluffyCloud', { exact: false }).last(); - await expect(footerBrand).toMatchScreenshot('footer-brand.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 a0737409..fabc7240 100644 --- a/clients/vitest/vitest.e2e.config.js +++ b/clients/vitest/vitest.e2e.config.js @@ -5,9 +5,8 @@ import handler from 'serve-handler'; import { playwright } from '@vitest/browser-playwright'; import { defineConfig } from 'vitest/config'; import { vizzlyPlugin } from './src/index.js'; -import * as commands from './src/commands.js'; -// Path to shared test-site +// Path to shared test-site (for CSS assets) let testSitePath = resolve(import.meta.dirname, '../../test-site'); // Verify test-site exists @@ -15,7 +14,7 @@ if (!existsSync(resolve(testSitePath, 'index.html'))) { throw new Error(`test-site not found at ${testSitePath}`); } -// Start static server for test-site on a random available port +// Start static server for test-site assets let server = createServer((req, res) => { return handler(req, res, { public: testSitePath, @@ -33,7 +32,7 @@ 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 with real test-site +// E2E tests config - runs in browser mode export default defineConfig({ plugins: [vizzlyPlugin()], test: { @@ -42,15 +41,10 @@ export default defineConfig({ instances: [ { browser: 'chromium', - provider: playwright({ - launch: { - viewport: { width: 1280, height: 720 }, - }, - }), + provider: playwright(), }, ], headless: true, - commands, }, include: ['tests/e2e/**/*.test.js'], }, From 4bde262c13720cc9051a59164562558c8427aaf0 Mon Sep 17 00:00:00 2001 From: Robert DeLuca Date: Sun, 18 Jan 2026 22:05:05 -0600 Subject: [PATCH 5/6] =?UTF-8?q?=F0=9F=90=9B=20Fix=20Ruby=20SDK=20E2E=20tes?= =?UTF-8?q?ts=20for=20both=20TDD=20and=20cloud=20modes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add cloud_mode? helper to detect when running with VIZZLY_TOKEN - Add assert_screenshot_success helper for mode-aware assertions - TDD mode: asserts status is 'new' or 'match' - Cloud mode: asserts success is true (no local comparison) --- clients/ruby/test/e2e_test.rb | 16 ++++++++++++-- clients/ruby/test/integration_test.rb | 32 ++++++++++++++++++++++----- 2 files changed, 40 insertions(+), 8 deletions(-) diff --git a/clients/ruby/test/e2e_test.rb b/clients/ruby/test/e2e_test.rb index 228b1fff..2ec7f1b2 100644 --- a/clients/ruby/test/e2e_test.rb +++ b/clients/ruby/test/e2e_test.rb @@ -100,10 +100,22 @@ def capture_element_screenshot(name, element, options = {}) 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' - assert %w[new match].include?(result['status']), - "Expected status 'new' or 'match', got: #{result['status']}" + 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 diff --git a/clients/ruby/test/integration_test.rb b/clients/ruby/test/integration_test.rb index 22681fb8..3ef6ac54 100644 --- a/clients/ruby/test/integration_test.rb +++ b/clients/ruby/test/integration_test.rb @@ -51,6 +51,26 @@ def stop_server @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 [ @@ -118,7 +138,7 @@ def test_basic_screenshot result = Vizzly.screenshot('basic-screenshot', image_data) assert result, 'Expected result to be non-nil' - assert %w[new match].include?(result['status']), "Expected status 'new' or 'match', got: #{result['status']}" + assert_screenshot_success(result) end def test_screenshot_with_properties @@ -133,7 +153,7 @@ def test_screenshot_with_properties }) assert result, 'Expected result to be non-nil' - assert %w[new match].include?(result['status']), "Expected status 'new' or 'match', got: #{result['status']}" + assert_screenshot_success(result) end def test_screenshot_with_threshold @@ -143,7 +163,7 @@ def test_screenshot_with_threshold result = Vizzly.screenshot('screenshot-threshold', image_data, threshold: 5) assert result, 'Expected result to be non-nil' - assert %w[new match].include?(result['status']), "Expected status 'new' or 'match', got: #{result['status']}" + assert_screenshot_success(result) end def test_screenshot_with_full_page @@ -153,7 +173,7 @@ def test_screenshot_with_full_page result = Vizzly.screenshot('screenshot-fullpage', image_data, full_page: true) assert result, 'Expected result to be non-nil' - assert %w[new match].include?(result['status']), "Expected status 'new' or 'match', got: #{result['status']}" + assert_screenshot_success(result) end def test_screenshot_with_all_options @@ -170,7 +190,7 @@ def test_screenshot_with_all_options full_page: false) assert result, 'Expected result to be non-nil' - assert %w[new match].include?(result['status']), "Expected status 'new' or 'match', got: #{result['status']}" + assert_screenshot_success(result) end # =========================================================================== @@ -195,7 +215,7 @@ def test_auto_discovery_via_server_json result = client.screenshot('auto-discovered', image_data) assert result, 'Expected result to be non-nil' - assert %w[new match].include?(result['status']), "Expected status 'new' or 'match', got: #{result['status']}" + assert_screenshot_success(result) end # =========================================================================== From 29e55456e1cedebc4b60d8968d087b14eb1f7081 Mon Sep 17 00:00:00 2001 From: Robert DeLuca Date: Sun, 18 Jan 2026 22:14:33 -0600 Subject: [PATCH 6/6] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Use=20GitHub-hosted=20?= =?UTF-8?q?runners=20for=20CI=20workflows?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Switch from blacksmith to ubuntu-latest for all CI workflows since this is an open source project with free GitHub Actions minutes. Keeps blacksmith only for release workflows (internal). --- .github/workflows/ci.yml | 2 +- .github/workflows/reporter.yml | 2 +- .github/workflows/sdk-e2e.yml | 12 ++++++------ .github/workflows/sdk-unit.yml | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1a7b979d..923a21f1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,7 +31,7 @@ jobs: test: name: Test (Node ${{ matrix.node-version }}) - runs-on: blacksmith-4vcpu-ubuntu-2404 + runs-on: ubuntu-latest timeout-minutes: 8 needs: lint diff --git a/.github/workflows/reporter.yml b/.github/workflows/reporter.yml index 68785f04..4625c88a 100644 --- a/.github/workflows/reporter.yml +++ b/.github/workflows/reporter.yml @@ -9,7 +9,7 @@ on: jobs: visual: name: Visual Tests - runs-on: blacksmith-4vcpu-ubuntu-2404 + runs-on: ubuntu-latest timeout-minutes: 8 steps: diff --git a/.github/workflows/sdk-e2e.yml b/.github/workflows/sdk-e2e.yml index 74caded3..3e30d969 100644 --- a/.github/workflows/sdk-e2e.yml +++ b/.github/workflows/sdk-e2e.yml @@ -10,7 +10,7 @@ jobs: # Core JS Client (test-site with Playwright) core-js: name: Core JS Client - runs-on: blacksmith-4vcpu-ubuntu-2404 + runs-on: ubuntu-latest timeout-minutes: 8 steps: @@ -66,7 +66,7 @@ jobs: # Vitest SDK vitest: name: Vitest SDK - runs-on: blacksmith-4vcpu-ubuntu-2404 + runs-on: ubuntu-latest timeout-minutes: 8 steps: @@ -123,7 +123,7 @@ jobs: # Storybook SDK storybook: name: Storybook SDK - runs-on: blacksmith-4vcpu-ubuntu-2404 + runs-on: ubuntu-latest timeout-minutes: 8 steps: @@ -180,7 +180,7 @@ jobs: # Static-Site SDK static-site: name: Static-Site SDK - runs-on: blacksmith-4vcpu-ubuntu-2404 + runs-on: ubuntu-latest timeout-minutes: 8 steps: @@ -237,7 +237,7 @@ jobs: # Ember SDK ember: name: Ember SDK - runs-on: blacksmith-4vcpu-ubuntu-2404 + runs-on: ubuntu-latest timeout-minutes: 8 steps: @@ -300,7 +300,7 @@ jobs: # Ruby SDK ruby: name: Ruby SDK - runs-on: blacksmith-4vcpu-ubuntu-2404 + runs-on: ubuntu-latest timeout-minutes: 8 steps: diff --git a/.github/workflows/sdk-unit.yml b/.github/workflows/sdk-unit.yml index a33bfb16..79e5b95a 100644 --- a/.github/workflows/sdk-unit.yml +++ b/.github/workflows/sdk-unit.yml @@ -185,7 +185,7 @@ jobs: # Vitest SDK - runs on multiple Node versions vitest: name: Vitest SDK - runs-on: blacksmith-4vcpu-ubuntu-2404 + runs-on: ubuntu-latest timeout-minutes: 8 needs: changes if: needs.changes.outputs.vitest == 'true' @@ -268,7 +268,7 @@ jobs: # Ember SDK - runs unit and integration tests ember: name: Ember SDK - runs-on: blacksmith-4vcpu-ubuntu-2404 + runs-on: ubuntu-latest timeout-minutes: 8 needs: changes if: needs.changes.outputs.ember == 'true'