diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 34bae337..22687e2e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -503,17 +503,13 @@ jobs: needs: [lint, changes-ember] if: needs.changes-ember.outputs.ember == 'true' - strategy: - matrix: - node-version: [22, 24] - steps: - uses: actions/checkout@v4 - - name: Use Node.js ${{ matrix.node-version }} + - name: Use Node.js 22 uses: actions/setup-node@v4 with: - node-version: ${{ matrix.node-version }} + node-version: 22 - name: Install CLI dependencies run: npm ci @@ -559,7 +555,6 @@ jobs: CI: true - name: Run Ember E2E visual tests - if: matrix.node-version == 22 working-directory: ./clients/ember run: | cd test-app diff --git a/clients/ember/README.md b/clients/ember/README.md index ae9a5eee..235c66c9 100644 --- a/clients/ember/README.md +++ b/clients/ember/README.md @@ -27,11 +27,11 @@ module.exports = configure({ test_page: 'tests/index.html?hidepassed', disable_watching: true, launch_in_ci: ['Chrome'], - launch_in_dev: ['Chrome'] + launch_in_dev: ['Chrome'], }); ``` -The `configure()` function replaces standard browser launchers (Chrome, Firefox, Safari) with Playwright-powered launchers that can capture screenshots. +The `configure()` function replaces standard browser launchers (Chrome, Firefox, Safari) with Playwright-powered launchers that can capture screenshots. Browsers run in **headless mode by default**. > **Note for Ember + Vite projects**: The `cwd: 'dist'` option is required because Vite builds test files into the `dist/` directory. Without this, Testem won't find your test assets. @@ -92,20 +92,51 @@ Screenshots are captured and compared locally. View results in the Vizzly dashbo ## API -### `configure(testemConfig)` +### `configure(testemConfig, playwrightOptions?)` Wraps your Testem configuration to use Vizzly-powered browser launchers. ```javascript const { configure } = require('@vizzly-testing/ember'); +// Basic - runs headless by default +module.exports = configure({ + launch_in_ci: ['Chrome'], + launch_in_dev: ['Chrome'], +}); + +// Headed mode for local debugging +const isCI = process.env.CI; + module.exports = configure({ - // Your existing testem.js options launch_in_ci: ['Chrome'], launch_in_dev: ['Chrome'], +}, { + headless: isCI, // Headed locally, headless in CI +}); + +// With debugging options +module.exports = configure({ + launch_in_ci: ['Chrome'], +}, { + headless: false, + slowMo: 100, // Slow down for debugging + timeout: 60000, // Longer launch timeout }); ``` +**Playwright Options:** + +The second argument accepts [Playwright browserType.launch() options](https://playwright.dev/docs/api/class-browsertype#browser-type-launch) directly: + +| Option | Type | Default | Description | +|--------|------|---------|-------------| +| `headless` | boolean | `true` | Run browser in headless mode | +| `slowMo` | number | - | Slow down operations by specified milliseconds | +| `timeout` | number | - | Maximum time to wait for browser to start | +| `proxy` | object | - | Proxy settings for the browser | +| `args` | string[] | - | Additional browser arguments | + **Browser Mapping:** - `Chrome` → Uses Playwright Chromium - `Firefox` → Uses Playwright Firefox @@ -151,7 +182,10 @@ await vizzlySnapshot('screenshot-name', { properties: { theme: 'dark', user: 'admin' - } + }, + + // Fail test if visual diff detected (overrides --fail-on-diff flag) + failOnDiff: true }); ``` @@ -165,6 +199,7 @@ await vizzlySnapshot('screenshot-name', { | `scope` | string | 'app' | What to capture: `'app'` (just #ember-testing), `'container'`, or `'page'` (full page including QUnit) | | `fullPage` | boolean | false | Capture full scrollable content | | `properties` | object | {} | Custom metadata attached to the snapshot | +| `failOnDiff` | boolean | null | Fail the test when visual diff is detected. `null` uses the `--fail-on-diff` CLI flag. | The function automatically: - Waits for Ember's `settled()` before capturing @@ -202,7 +237,7 @@ npx playwright install webkit ## How It Works 1. **Testem Configuration**: The `configure()` wrapper replaces standard browser launchers with custom Vizzly launchers -2. **Custom Launcher**: When Testem starts, it spawns `vizzly-browser` instead of the regular browser +2. **Custom Launcher**: When Testem starts, it spawns `vizzly-testem-launcher` which uses Playwright 3. **Playwright Integration**: The launcher uses Playwright to control the browser and capture screenshots 4. **Snapshot Server**: A local HTTP server receives screenshot requests from test code 5. **Vizzly Integration**: Screenshots are forwarded to the Vizzly TDD server for comparison @@ -225,6 +260,20 @@ For CI environments, ensure: run: ember test ``` +### Failing on Visual Diffs + +By default, visual differences don't fail tests (similar to Percy). To fail tests when diffs are detected: + +```bash +# Via CLI flag +vizzly tdd start --fail-on-diff + +# Or per-snapshot in your test +await vizzlySnapshot('critical-ui', { failOnDiff: true }); +``` + +The priority order is: per-snapshot option > `--fail-on-diff` CLI flag > default (no failure). + ## Troubleshooting ### "No snapshot server available" diff --git a/clients/ember/bin/vizzly-browser.js b/clients/ember/bin/vizzly-testem-launcher.js similarity index 63% rename from clients/ember/bin/vizzly-browser.js rename to clients/ember/bin/vizzly-testem-launcher.js index 07bce812..0d2d7a2b 100755 --- a/clients/ember/bin/vizzly-browser.js +++ b/clients/ember/bin/vizzly-testem-launcher.js @@ -1,19 +1,24 @@ #!/usr/bin/env node /** - * Vizzly Browser Launcher + * Vizzly Testem Launcher * - * Custom browser launcher spawned by Testem. Uses Playwright to launch - * a browser with screenshot capture capabilities. + * Custom Testem launcher that uses Playwright to control browsers, + * enabling screenshot capture for visual testing. * - * Usage: vizzly-browser + * Usage: vizzly-testem-launcher * browser: chromium | firefox | webkit * url: The test page URL (provided by Testem) * + * Playwright launch options are read from .vizzly/playwright.json + * (written by configure() in testem-config.js) + * * @example * # Testem spawns this command: - * npx vizzly-browser chromium http://localhost:7357/tests/index.html + * node vizzly-testem-launcher.js chromium http://localhost:7357/tests */ +import { existsSync, readFileSync } from 'node:fs'; +import { join } from 'node:path'; import { closeBrowser, launchBrowser } from '../src/launcher/browser.js'; import { getServerInfo, @@ -22,16 +27,34 @@ import { stopSnapshotServer, } from '../src/launcher/snapshot-server.js'; -let [, , browserType, testUrl] = process.argv; +// Parse arguments - format: vizzly-testem-launcher +// Testem appends the URL as the last argument +let args = process.argv.slice(2); +let browserType = args[0]; +let testUrl = args[args.length - 1]; // Validate arguments -if (!browserType || !testUrl) { - console.error('Usage: vizzly-browser '); +if (!browserType || !testUrl || !testUrl.startsWith('http')) { + console.error('Usage: vizzly-testem-launcher '); console.error(' browser: chromium | firefox | webkit'); console.error(' url: Test page URL (provided by Testem)'); process.exit(1); } +// Read Playwright launch options from config file (written by configure()) +let playwrightOptions = { headless: true }; // Default to headless +let configPath = join(process.cwd(), '.vizzly', 'playwright.json'); +if (existsSync(configPath)) { + try { + playwrightOptions = { + headless: true, // Default + ...JSON.parse(readFileSync(configPath, 'utf8')), + }; + } catch (err) { + console.warn('[vizzly] Failed to read playwright.json:', err.message); + } +} + let browserInstance = null; let snapshotServer = null; let isShuttingDown = false; @@ -72,7 +95,6 @@ async function main() { let snapshotUrl = `http://127.0.0.1:${snapshotServer.port}`; // 2. Determine failOnDiff: env var > server.json > default (false) - // getServerInfo() returns cached info from the TDD server discovery that happened above let failOnDiff = false; if (process.env.VIZZLY_FAIL_ON_DIFF === 'true' || process.env.VIZZLY_FAIL_ON_DIFF === '1') { failOnDiff = true; @@ -84,23 +106,17 @@ async function main() { } // 3. Launch browser with Playwright - // Note: We set the page reference in launchBrowser before navigation - // to avoid a race condition where tests run before page is set browserInstance = await launchBrowser(browserType, testUrl, { snapshotUrl, failOnDiff, + playwrightOptions, onPageCreated: page => { - // Set page reference immediately when page is created - // This happens BEFORE navigation so tests can capture screenshots setPage(page); - - // Listen for page close - this means browser was closed page.on('close', cleanup); }, }); - // 3. Monitor for test completion - // Hook into the test framework (QUnit or Mocha) to detect when tests finish + // 4. Monitor for test completion let { page } = browserInstance; // Wait for a test framework to be available, then hook into its completion @@ -110,7 +126,7 @@ async function main() { // Check for QUnit if (typeof QUnit !== 'undefined') { QUnit.done(() => { - console.log('[testem-vizzly] all-tests-complete'); + console.log('[vizzly-testem] tests-complete'); }); resolve(); return; @@ -120,11 +136,11 @@ async function main() { if (typeof Mocha !== 'undefined' || typeof mocha !== 'undefined') { let Runner = (typeof Mocha !== 'undefined' ? Mocha : mocha).Runner; let originalEmit = Runner.prototype.emit; - Runner.prototype.emit = function (evt) { - if (evt === 'end') { - console.log('[testem-vizzly] all-tests-complete'); + Runner.prototype.emit = function (...args) { + if (args[0] === 'end') { + console.log('[vizzly-testem] tests-complete'); } - return originalEmit.apply(this, arguments); + return originalEmit.apply(this, args); }; resolve(); return; @@ -139,17 +155,16 @@ async function main() { // Listen for the completion signal page.on('console', msg => { - if (msg.text() === '[testem-vizzly] all-tests-complete') { + if (msg.text() === '[vizzly-testem] tests-complete') { cleanup(); } }); - // 4. Keep process alive until cleanup is called + // 5. Keep process alive until cleanup is called await new Promise(() => {}); } catch (error) { - console.error('[vizzly-browser] Failed to start:', error.message); + console.error('[vizzly-testem-launcher] Failed to start:', error.message); - // Attempt cleanup before exiting if (snapshotServer) { await stopSnapshotServer(snapshotServer).catch(() => {}); } @@ -165,12 +180,12 @@ process.on('SIGHUP', cleanup); // Handle unexpected errors process.on('uncaughtException', error => { - console.error('[vizzly-browser] Uncaught exception:', error.message); + console.error('[vizzly-testem-launcher] Uncaught exception:', error.message); cleanup(); }); process.on('unhandledRejection', reason => { - console.error('[vizzly-browser] Unhandled rejection:', reason); + console.error('[vizzly-testem-launcher] Unhandled rejection:', reason); cleanup(); }); diff --git a/clients/ember/package-lock.json b/clients/ember/package-lock.json index 5ec1281e..640b4525 100644 --- a/clients/ember/package-lock.json +++ b/clients/ember/package-lock.json @@ -1,12 +1,12 @@ { "name": "@vizzly-testing/ember", - "version": "0.0.1-beta.1", + "version": "0.0.1-beta.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@vizzly-testing/ember", - "version": "0.0.1-beta.1", + "version": "0.0.1-beta.2", "license": "MIT", "dependencies": { "playwright-core": "^1.49.0" diff --git a/clients/ember/package.json b/clients/ember/package.json index 8d1a3002..e74e022b 100644 --- a/clients/ember/package.json +++ b/clients/ember/package.json @@ -37,7 +37,7 @@ }, "main": "./src/index.js", "bin": { - "vizzly-browser": "bin/vizzly-browser.js" + "vizzly-testem-launcher": "bin/vizzly-testem-launcher.js" }, "files": [ "bin", diff --git a/clients/ember/src/launcher/browser.js b/clients/ember/src/launcher/browser.js index 3518be79..7e0a34ab 100644 --- a/clients/ember/src/launcher/browser.js +++ b/clients/ember/src/launcher/browser.js @@ -35,10 +35,10 @@ function isCI() { } /** - * Get browser launch arguments for Chromium + * Get default Chromium args for stability * @returns {string[]} */ -function getChromiumArgs() { +function getDefaultChromiumArgs() { let args = ['--no-sandbox', '--disable-setuid-sandbox']; if (isCI()) { @@ -60,18 +60,18 @@ function getChromiumArgs() { * @param {string} testUrl - URL to navigate to (provided by Testem) * @param {Object} options - Launch options * @param {string} options.snapshotUrl - URL of the snapshot HTTP server - * @param {boolean} [options.headless] - Run in headless mode (default: true in CI) * @param {boolean} [options.failOnDiff] - Whether tests should fail on visual diffs + * @param {Object} [options.playwrightOptions] - Playwright launch options (headless, slowMo, timeout, etc.) * @param {Function} [options.onPageCreated] - Callback when page is created (before navigation) * @returns {Promise} Browser instance with page reference */ export async function launchBrowser(browserType, testUrl, options = {}) { - let { snapshotUrl, headless, failOnDiff, onPageCreated } = options; - - // Default headless based on CI environment - if (headless === undefined) { - headless = isCI(); - } + let { + snapshotUrl, + failOnDiff, + playwrightOptions = {}, + onPageCreated, + } = options; let factory = browserFactories[browserType]; if (!factory) { @@ -81,21 +81,30 @@ export async function launchBrowser(browserType, testUrl, options = {}) { ); } - // Launch browser with appropriate args - let launchOptions = { - headless, - }; - + // Build args: our defaults + user's args (user can override) + let args = []; if (browserType === 'chromium') { - launchOptions.args = getChromiumArgs(); + args = [...getDefaultChromiumArgs()]; } + // Merge user's args if provided + if (playwrightOptions.args) { + args.push(...playwrightOptions.args); + } + + // Build Playwright launch options + // User's playwrightOptions take precedence, but we ensure args are merged + let launchOptions = { + headless: true, // Default to headless + ...playwrightOptions, + args, + }; + let browser = await factory.launch(launchOptions); let context = await browser.newContext(); let page = await context.newPage(); // Inject Vizzly config into page context BEFORE navigation - // This ensures window.__VIZZLY_* is available when tests run await page.addInitScript( ({ snapshotUrl, failOnDiff }) => { window.__VIZZLY_SNAPSHOT_URL__ = snapshotUrl; @@ -105,12 +114,11 @@ export async function launchBrowser(browserType, testUrl, options = {}) { ); // Call onPageCreated callback BEFORE navigation - // This allows setting up the page reference before tests can run if (onPageCreated) { onPageCreated(page); } - // Navigate to test URL and wait for load (not networkidle - Socket.IO keeps network active) + // Navigate to test URL and wait for load await page.goto(testUrl, { waitUntil: 'load', timeout: 60000, diff --git a/clients/ember/src/testem-config.js b/clients/ember/src/testem-config.js index 315bdc9c..1ea9fc95 100644 --- a/clients/ember/src/testem-config.js +++ b/clients/ember/src/testem-config.js @@ -7,6 +7,10 @@ * @module @vizzly-testing/ember/testem */ +import { existsSync, mkdirSync, writeFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + /** * Browser name mappings from user-friendly names to Vizzly launchers */ @@ -21,15 +25,6 @@ let browserMappings = { webkit: 'VizzlyWebKit', }; -/** - * Playwright browser type for each Vizzly launcher - */ -let launcherBrowserTypes = { - VizzlyChrome: 'chromium', - VizzlyFirefox: 'firefox', - VizzlyWebKit: 'webkit', -}; - /** * Remap browser names to Vizzly launcher names * @param {string[]} browsers - Array of browser names @@ -40,31 +35,58 @@ function remapBrowsers(browsers) { return browsers.map(browser => browserMappings[browser] || browser); } +/** + * Get the path to the launcher script + * @returns {string} Absolute path to vizzly-testem-launcher.js + */ +function getLauncherPath() { + // This file is at src/testem-config.js, launcher is at bin/vizzly-testem-launcher.js + let currentDir = dirname(fileURLToPath(import.meta.url)); + return join(currentDir, '..', 'bin', 'vizzly-testem-launcher.js'); +} + /** * Create launcher definitions for Vizzly browsers - * Testem automatically appends the test URL to the args array * @returns {Object} Launcher configuration object */ function createLaunchers() { + let launcherPath = getLauncherPath(); + return { VizzlyChrome: { - exe: 'npx', - args: ['vizzly-browser', 'chromium'], + exe: 'node', + args: [launcherPath, 'chromium'], protocol: 'browser', }, VizzlyFirefox: { - exe: 'npx', - args: ['vizzly-browser', 'firefox'], + exe: 'node', + args: [launcherPath, 'firefox'], protocol: 'browser', }, VizzlyWebKit: { - exe: 'npx', - args: ['vizzly-browser', 'webkit'], + exe: 'node', + args: [launcherPath, 'webkit'], protocol: 'browser', }, }; } +/** + * Write Playwright options to config file for the launcher to read + * @param {Object} options - Playwright launch options + */ +function writePlaywrightConfig(options) { + if (!options || Object.keys(options).length === 0) return; + + let vizzlyDir = join(process.cwd(), '.vizzly'); + if (!existsSync(vizzlyDir)) { + mkdirSync(vizzlyDir, { recursive: true }); + } + + let configPath = join(vizzlyDir, 'playwright.json'); + writeFileSync(configPath, JSON.stringify(options, null, 2)); +} + /** * Wrap Testem configuration to enable Vizzly visual testing * @@ -72,27 +94,56 @@ function createLaunchers() { * Vizzly-powered browser launchers. It: * - Remaps Chrome/Firefox/Safari to VizzlyChrome/VizzlyFirefox/VizzlyWebKit * - Adds custom launcher definitions that use Playwright - * - Preserves all other configuration options + * - Preserves all other Testem configuration options + * + * The second argument accepts Playwright browserType.launch() options directly. + * See: https://playwright.dev/docs/api/class-browsertype#browser-type-launch * * @param {Object} userConfig - User's testem.js configuration + * @param {Object} [playwrightOptions] - Playwright launch options passed directly to browserType.launch() + * @param {boolean} [playwrightOptions.headless=true] - Run browser in headless mode + * @param {number} [playwrightOptions.slowMo] - Slow down operations by this many milliseconds + * @param {number} [playwrightOptions.timeout] - Browser launch timeout in milliseconds + * @param {Object} [playwrightOptions.proxy] - Proxy settings * @returns {Object} Modified configuration with Vizzly launchers * * @example - * // testem.js + * // testem.js - Basic usage (headless by default) * const { configure } = require('@vizzly-testing/ember'); * * module.exports = configure({ * test_page: 'tests/index.html?hidepassed', * launch_in_ci: ['Chrome'], * launch_in_dev: ['Chrome'], - * browser_args: { - * Chrome: { ci: ['--headless', '--no-sandbox'] } - * } + * }); + * + * @example + * // Headed mode for local debugging + * const isCI = process.env.CI; + * + * module.exports = configure({ + * launch_in_ci: ['Chrome'], + * launch_in_dev: ['Chrome'], + * }, { + * headless: isCI, // Headed locally, headless in CI + * }); + * + * @example + * // With debugging options + * module.exports = configure({ + * launch_in_ci: ['Chrome'], + * }, { + * headless: false, + * slowMo: 100, // Slow down for debugging + * timeout: 60000, // Longer timeout * }); */ -export function configure(userConfig = {}) { +export function configure(userConfig = {}, playwrightOptions = {}) { let config = { ...userConfig }; + // Write Playwright options to file for launcher to read + writePlaywrightConfig(playwrightOptions); + // Remap browser lists to use Vizzly launchers if (config.launch_in_ci) { config.launch_in_ci = remapBrowsers(config.launch_in_ci); @@ -111,4 +162,4 @@ export function configure(userConfig = {}) { return config; } -export { browserMappings, launcherBrowserTypes }; +export { browserMappings }; diff --git a/clients/ember/test-app/testem.cjs b/clients/ember/test-app/testem.cjs index 98e3b9b3..39649756 100644 --- a/clients/ember/test-app/testem.cjs +++ b/clients/ember/test-app/testem.cjs @@ -12,5 +12,11 @@ if (typeof module !== 'undefined') { launch_in_ci: ['Chrome'], launch_in_dev: ['Chrome'], browser_start_timeout: 120, + browser_args: { + Chrome: { + ci: ['--headless', '--disable-gpu'], + dev: [], // headed for local debugging + }, + }, }); } diff --git a/clients/ember/tests/integration/e2e.test.js b/clients/ember/tests/integration/e2e.test.js index 3cd91208..b2ce59f1 100644 --- a/clients/ember/tests/integration/e2e.test.js +++ b/clients/ember/tests/integration/e2e.test.js @@ -115,7 +115,7 @@ describe('e2e with TDD server', { skip: !process.env.RUN_E2E }, () => { let testUrl = `http://127.0.0.1:${testServerPort}/`; browserInstance = await launchBrowser('chromium', testUrl, { snapshotUrl, - headless: true, + playwrightOptions: { headless: true }, }); setPage(browserInstance.page); @@ -181,7 +181,7 @@ describe('e2e without TDD server', () => { let testUrl = `http://127.0.0.1:${testServerPort}/`; browserInstance = await launchBrowser('chromium', testUrl, { snapshotUrl, - headless: true, + playwrightOptions: { headless: true }, }); setPage(browserInstance.page); diff --git a/clients/ember/tests/integration/launcher.test.js b/clients/ember/tests/integration/launcher.test.js index 1da411e8..b6056f95 100644 --- a/clients/ember/tests/integration/launcher.test.js +++ b/clients/ember/tests/integration/launcher.test.js @@ -116,7 +116,7 @@ describe('launcher integration', () => { let testUrl = `http://127.0.0.1:${testServerPort}/`; browserInstance = await launchBrowser('chromium', testUrl, { snapshotUrl, - headless: true, + playwrightOptions: { headless: true }, }); // Set page reference for screenshot capture @@ -171,7 +171,7 @@ describe('launcher integration', () => { let testUrl = `http://127.0.0.1:${testServerPort}/`; browserInstance = await launchBrowser('chromium', testUrl, { snapshotUrl, - headless: true, + playwrightOptions: { headless: true }, }); // Verify the URL was injected @@ -190,7 +190,7 @@ describe('launcher integration', () => { let testUrl = `http://127.0.0.1:${testServerPort}/`; browserInstance = await launchBrowser('chromium', testUrl, { snapshotUrl, - headless: true, + playwrightOptions: { headless: true }, }); } diff --git a/clients/ember/tests/unit/testem-config.test.js b/clients/ember/tests/unit/testem-config.test.js index 0793b702..93ac4beb 100644 --- a/clients/ember/tests/unit/testem-config.test.js +++ b/clients/ember/tests/unit/testem-config.test.js @@ -1,10 +1,24 @@ import assert from 'node:assert'; -import { describe, it } from 'node:test'; +import { existsSync, readFileSync, rmSync } from 'node:fs'; +import { join } from 'node:path'; +import { afterEach, beforeEach, describe, it } from 'node:test'; import { browserMappings, configure } from '../../src/testem-config.js'; describe('testem-config', () => { + beforeEach(() => { + // Clean up before each test + }); + + afterEach(() => { + // Clean up any playwright.json that was written + let playwrightConfig = join(process.cwd(), '.vizzly', 'playwright.json'); + if (existsSync(playwrightConfig)) { + rmSync(playwrightConfig); + } + }); + describe('configure()', () => { - it('returns empty config when given empty object', () => { + it('returns config with Vizzly launchers when given empty object', () => { let result = configure({}); assert.ok(result.launchers, 'should have launchers'); assert.ok(result.launchers.VizzlyChrome, 'should have VizzlyChrome'); @@ -65,15 +79,11 @@ describe('testem-config', () => { let result = configure({ test_page: 'tests/index.html?hidepassed', launch_in_ci: ['Chrome'], - browser_args: { - Chrome: { ci: ['--headless'] }, - }, + disable_watching: true, }); assert.strictEqual(result.test_page, 'tests/index.html?hidepassed'); - assert.deepStrictEqual(result.browser_args, { - Chrome: { ci: ['--headless'] }, - }); + assert.strictEqual(result.disable_watching, true); }); it('merges with existing launchers', () => { @@ -91,30 +101,61 @@ describe('testem-config', () => { let result = configure({}); let launcher = result.launchers.VizzlyChrome; - assert.strictEqual(launcher.exe, 'npx'); - // Note: Testem automatically appends the test URL to args - assert.deepStrictEqual(launcher.args, ['vizzly-browser', 'chromium']); + assert.strictEqual(launcher.exe, 'node'); + assert.ok(Array.isArray(launcher.args)); assert.strictEqual(launcher.protocol, 'browser'); + + // Check args format: [launcherPath, browserType] + assert.ok(launcher.args[0].endsWith('vizzly-testem-launcher.js')); + assert.strictEqual(launcher.args[1], 'chromium'); }); it('creates correct VizzlyFirefox launcher config', () => { let result = configure({}); let launcher = result.launchers.VizzlyFirefox; - assert.strictEqual(launcher.exe, 'npx'); - // Note: Testem automatically appends the test URL to args - assert.deepStrictEqual(launcher.args, ['vizzly-browser', 'firefox']); + assert.strictEqual(launcher.exe, 'node'); + assert.ok(Array.isArray(launcher.args)); assert.strictEqual(launcher.protocol, 'browser'); + assert.ok(launcher.args[0].endsWith('vizzly-testem-launcher.js')); + assert.strictEqual(launcher.args[1], 'firefox'); }); it('creates correct VizzlyWebKit launcher config', () => { let result = configure({}); let launcher = result.launchers.VizzlyWebKit; - assert.strictEqual(launcher.exe, 'npx'); - // Note: Testem automatically appends the test URL to args - assert.deepStrictEqual(launcher.args, ['vizzly-browser', 'webkit']); + assert.strictEqual(launcher.exe, 'node'); + assert.ok(Array.isArray(launcher.args)); assert.strictEqual(launcher.protocol, 'browser'); + assert.ok(launcher.args[0].endsWith('vizzly-testem-launcher.js')); + assert.strictEqual(launcher.args[1], 'webkit'); + }); + + it('writes playwright.json when playwrightOptions provided', () => { + configure({}, { headless: false, slowMo: 100, timeout: 30000 }); + + let configPath = join(process.cwd(), '.vizzly', 'playwright.json'); + assert.ok(existsSync(configPath), 'should write playwright.json'); + + let content = JSON.parse(readFileSync(configPath, 'utf8')); + assert.deepStrictEqual(content, { + headless: false, + slowMo: 100, + timeout: 30000, + }); + }); + + it('does not write playwright.json when playwrightOptions is empty', () => { + // Clean up any existing file first + let configPath = join(process.cwd(), '.vizzly', 'playwright.json'); + if (existsSync(configPath)) { + rmSync(configPath); + } + + configure({}, {}); + + assert.ok(!existsSync(configPath), 'should not write empty playwright.json'); }); it('handles lowercase browser names', () => {