From 3bfc95184b6597ebb3a729ee9f5225d5279522cb Mon Sep 17 00:00:00 2001 From: Tomas Grasl Date: Wed, 26 Aug 2026 16:11:27 +0200 Subject: [PATCH] test(windows): run the integration suite on Windows The vitest fork hang (#33) no longer reproduces on Vitest 4, so drop the Windows exclusion and the standalone runner, fix fixture URLs and .xpi packing for Windows, and run CI on windows-latest too. --- .github/workflows/ci.yml | 7 +- docs/ci-and-release.md | 7 - package.json | 3 +- scripts/run-integration-tests-windows.mjs | 188 ------------------ tests/helpers/firefox.ts | 19 ++ tests/helpers/fs.ts | 9 + tests/helpers/zip.ts | 123 ++++++++++++ tests/integration/console.integration.test.ts | 18 +- .../e2e-scenario.integration.test.ts | 17 +- tests/integration/form.integration.test.ts | 8 +- .../moz-extension.integration.test.ts | 11 +- tests/integration/network.integration.test.ts | 16 +- .../integration/snapshot.integration.test.ts | 28 ++- tests/integration/tabs.integration.test.ts | 18 +- tests/tools/console.test.ts | 5 +- tests/tools/network.test.ts | 7 +- tests/tools/pages.test.ts | 5 +- tests/tools/screenshot.test.ts | 3 +- tests/tools/script.test.ts | 5 +- tests/tools/snapshot.test.ts | 5 +- tests/utils/save-output.test.ts | 5 +- vitest.config.ts | 13 +- 22 files changed, 223 insertions(+), 297 deletions(-) delete mode 100644 scripts/run-integration-tests-windows.mjs create mode 100644 tests/helpers/fs.ts create mode 100644 tests/helpers/zip.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c801846c..ac7270ae 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,10 +10,11 @@ concurrency: jobs: test: - runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: + os: [ubuntu-latest, windows-latest] node-version: [20, 22] steps: @@ -53,14 +54,14 @@ jobs: run: npm run test:coverage - name: Upload coverage reports to Codecov - if: matrix.node-version == 20 + if: matrix.os == 'ubuntu-latest' && matrix.node-version == 20 uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} fail_ci_if_error: false - name: Upload artifacts - if: matrix.node-version == 20 + if: matrix.os == 'ubuntu-latest' && matrix.node-version == 20 uses: actions/upload-artifact@v7 with: name: dist diff --git a/docs/ci-and-release.md b/docs/ci-and-release.md index f81dd566..168f6488 100644 --- a/docs/ci-and-release.md +++ b/docs/ci-and-release.md @@ -37,13 +37,6 @@ Release flow 3) The `version-check` job validates the tag vs. package.json. 4) `release` creates a GitHub Release; `publish` publishes to npm. -Windows Integration Tests -- On Windows, vitest has known issues with process forking when running integration tests that spawn Firefox. -- See: https://github.com/mozilla/firefox-devtools-mcp/issues/33 -- To work around this, we use a separate test runner (`scripts/run-integration-tests-windows.mjs`) that runs integration tests directly via Node.js without vitest's process isolation. -- The CI workflow detects Windows and automatically uses this runner instead of vitest for integration tests. -- Unit tests still run via vitest on all platforms. - Notes - If you want Codecov upload to run, switch CI test step to `npm run test:coverage` or generate `coverage/lcov.info`. - Provenance is enabled for npm publish (Node 20+). diff --git a/package.json b/package.json index 796ebfe4..5a1172c6 100644 --- a/package.json +++ b/package.json @@ -46,8 +46,7 @@ "test:mozlog": "node scripts/test-mozlog.js", "test:navigation": "node scripts/test-navigation.js", "test:privileged": "node scripts/test-privileged-context.js", - "test:screenshot": "node scripts/test-screenshot.js", - "test:integration:win": "node scripts/run-integration-tests-windows.mjs" + "test:screenshot": "node scripts/test-screenshot.js" }, "keywords": [ "mcp", diff --git a/scripts/run-integration-tests-windows.mjs b/scripts/run-integration-tests-windows.mjs deleted file mode 100644 index 41583e54..00000000 --- a/scripts/run-integration-tests-windows.mjs +++ /dev/null @@ -1,188 +0,0 @@ -#!/usr/bin/env node -/** - * Windows Integration Tests Runner - * - * Runs integration tests directly via node to avoid vitest fork issues on Windows. - * See: https://github.com/mozilla/firefox-devtools-mcp/issues/33 - */ - -import { FirefoxDevTools } from '../dist/index.js'; -import { fileURLToPath } from 'node:url'; -import { dirname, resolve } from 'node:path'; -import assert from 'node:assert'; - -const __dirname = dirname(fileURLToPath(import.meta.url)); -const fixturesPath = resolve(__dirname, '../tests/fixtures'); - -let firefox = null; -let passed = 0; -let failed = 0; - -async function test(name, fn) { - process.stdout.write(` ${name}... `); - try { - await fn(); - console.log('\x1b[32m✓\x1b[0m'); - passed++; - } catch (error) { - console.log('\x1b[31m✗\x1b[0m'); - console.error(` Error: ${error.message}`); - failed++; - } -} - -async function setup() { - console.log('\n🚀 Starting Firefox...'); - firefox = new FirefoxDevTools({ - headless: true, - enableBidiLogging: false, - width: 1280, - height: 720, - }); - await firefox.connect(); - console.log('✅ Firefox connected\n'); -} - -async function teardown() { - if (firefox) { - await firefox.close(); - console.log('\n✅ Firefox closed'); - } -} - -// ============================================================================ -// Tab Management Tests -// ============================================================================ -async function tabTests() { - console.log('📋 Tab Management Tests:'); - - await test('should list tabs', async () => { - const fixturePath = `file://${fixturesPath}/simple.html`; - await firefox.navigate(fixturePath); - await firefox.refreshTabs(); - const tabs = firefox.getTabs(); - assert(Array.isArray(tabs), 'tabs should be an array'); - assert(tabs.length > 0, 'should have at least one tab'); - }); - - await test('should create new tab', async () => { - await firefox.refreshTabs(); - const initialTabs = firefox.getTabs(); - const initialCount = initialTabs.length; - - const fixturePath = `file://${fixturesPath}/simple.html`; - await firefox.createNewPage(fixturePath); - - await firefox.refreshTabs(); - const updatedTabs = firefox.getTabs(); - assert(updatedTabs.length === initialCount + 1, 'should have one more tab'); - }); - - await test('should get selected tab index', async () => { - const idx = firefox.getSelectedTabIdx(); - assert(typeof idx === 'number', 'index should be a number'); - assert(idx >= 0, 'index should be non-negative'); - }); -} - -// ============================================================================ -// Snapshot Tests -// ============================================================================ -async function snapshotTests() { - console.log('\n📸 Snapshot Tests:'); - - await test('should take snapshot', async () => { - const fixturePath = `file://${fixturesPath}/simple.html`; - await firefox.navigate(fixturePath); - await new Promise((r) => setTimeout(r, 500)); - - const snapshot = await firefox.takeSnapshot(); - assert(snapshot, 'snapshot should exist'); - assert(snapshot.text || snapshot.markdown, 'snapshot should have text or markdown'); - assert(snapshot.json, 'snapshot should have json'); - }); - - await test('should have uidMap in snapshot', async () => { - const snapshot = await firefox.takeSnapshot(); - assert(Array.isArray(snapshot.json.uidMap), 'uidMap should be an array'); - }); -} - -// ============================================================================ -// Console Tests -// ============================================================================ -async function consoleTests() { - console.log('\n💬 Console Tests:'); - - await test('should get console messages', async () => { - const messages = await firefox.getConsoleMessages(); - assert(Array.isArray(messages), 'messages should be an array'); - }); - - await test('should clear console messages', async () => { - firefox.clearConsoleMessages(); - const messages = await firefox.getConsoleMessages(); - assert(messages.length === 0, 'messages should be empty after clear'); - }); -} - -// ============================================================================ -// Network Tests -// ============================================================================ -async function networkTests() { - console.log('\n🌐 Network Tests:'); - - await test('should start network monitoring', async () => { - await firefox.startNetworkMonitoring(); - // No error means success - assert(true); - }); - - await test('should get network requests', async () => { - const requests = await firefox.getNetworkRequests(); - assert(Array.isArray(requests), 'requests should be an array'); - }); - - await test('should clear network requests', async () => { - firefox.clearNetworkRequests(); - const requests = await firefox.getNetworkRequests(); - assert(requests.length === 0, 'requests should be empty after clear'); - }); - - await test('should stop network monitoring', async () => { - await firefox.stopNetworkMonitoring(); - assert(true); - }); -} - -// ============================================================================ -// Main -// ============================================================================ -async function main() { - console.log('═══════════════════════════════════════════════════════════'); - console.log(' Windows Integration Tests (direct node runner)'); - console.log('═══════════════════════════════════════════════════════════'); - - try { - await setup(); - - await tabTests(); - await snapshotTests(); - await consoleTests(); - await networkTests(); - - await teardown(); - - console.log('\n═══════════════════════════════════════════════════════════'); - console.log(` Results: \x1b[32m${passed} passed\x1b[0m, \x1b[31m${failed} failed\x1b[0m`); - console.log('═══════════════════════════════════════════════════════════\n'); - - process.exit(failed > 0 ? 1 : 0); - } catch (error) { - console.error('\n❌ Fatal error:', error.message); - await teardown(); - process.exit(1); - } -} - -main(); diff --git a/tests/helpers/firefox.ts b/tests/helpers/firefox.ts index a8182d33..324a982d 100644 --- a/tests/helpers/firefox.ts +++ b/tests/helpers/firefox.ts @@ -6,6 +6,9 @@ import { FirefoxClient } from '@/firefox/index.js'; import type { FirefoxLaunchOptions } from '@/firefox/types.js'; import type { SnapshotNode } from '@/firefox/snapshot/types.js'; +// Aliased: several helpers below name a promise callback parameter `resolve`. +import { resolve as resolvePath } from 'node:path'; +import { fileURLToPath, pathToFileURL } from 'node:url'; /** * Creates a headless Firefox client for testing @@ -144,3 +147,19 @@ export async function waitForElementInSnapshot( export async function waitForPageLoad(delayMs = 300): Promise { await new Promise((resolve) => setTimeout(resolve, delayMs)); } + +/** + * Absolute path to the shared fixtures directory. + */ +export const fixturesPath = resolvePath( + fileURLToPath(new URL('.', import.meta.url)), + '../fixtures' +); + +/** + * Builds a file:// URL for a fixture. Interpolating `file://${path}` breaks on + * Windows (the drive letter parses as the host); pathToFileURL does not. + */ +export function fixtureUrl(fileName: string): string { + return pathToFileURL(resolvePath(fixturesPath, fileName)).href; +} diff --git a/tests/helpers/fs.ts b/tests/helpers/fs.ts new file mode 100644 index 00000000..3b6c77b1 --- /dev/null +++ b/tests/helpers/fs.ts @@ -0,0 +1,9 @@ +import { rmSync } from 'node:fs'; + +/** + * Recursive delete that tolerates Windows still holding the handles: `force` + * suppresses ENOENT but not the ENOTEMPTY/EPERM a just-closed file causes. + */ +export function removeDir(dir: string): void { + rmSync(dir, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 }); +} diff --git a/tests/helpers/zip.ts b/tests/helpers/zip.ts new file mode 100644 index 00000000..e49a5ba7 --- /dev/null +++ b/tests/helpers/zip.ts @@ -0,0 +1,123 @@ +/** + * Minimal ZIP writer for test .xpi archives. Replaces a shell-out to `zip`, + * which does not exist on Windows. + */ + +import { readFileSync, writeFileSync } from 'node:fs'; +import { basename } from 'node:path'; +import { deflateRawSync } from 'node:zlib'; + +const LOCAL_HEADER_SIGNATURE = 0x04034b50; +const CENTRAL_HEADER_SIGNATURE = 0x02014b50; +const END_OF_CENTRAL_DIR_SIGNATURE = 0x06054b50; +const METHOD_DEFLATE = 8; +const VERSION_NEEDED = 20; + +const CRC_TABLE = (() => { + const table = new Uint32Array(256); + for (let i = 0; i < 256; i++) { + let c = i; + for (let bit = 0; bit < 8; bit++) { + c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1; + } + table[i] = c >>> 0; + } + return table; +})(); + +function crc32(buffer: Buffer): number { + let crc = 0xffffffff; + for (const byte of buffer) { + crc = CRC_TABLE[(crc ^ byte) & 0xff] ^ (crc >>> 8); + } + return (crc ^ 0xffffffff) >>> 0; +} + +interface Entry { + name: string; + crc: number; + compressed: Buffer; + uncompressedSize: number; + offset: number; +} + +function localHeader(entry: Entry, nameBytes: Buffer): Buffer { + const header = Buffer.alloc(30); + header.writeUInt32LE(LOCAL_HEADER_SIGNATURE, 0); + header.writeUInt16LE(VERSION_NEEDED, 4); + header.writeUInt16LE(0, 6); // flags + header.writeUInt16LE(METHOD_DEFLATE, 8); + header.writeUInt16LE(0, 10); // mod time — fixed, for reproducible archives + header.writeUInt16LE(0x0021, 12); // mod date — 1980-01-01, the ZIP epoch + header.writeUInt32LE(entry.crc, 14); + header.writeUInt32LE(entry.compressed.length, 18); + header.writeUInt32LE(entry.uncompressedSize, 22); + header.writeUInt16LE(nameBytes.length, 26); + header.writeUInt16LE(0, 28); // extra field length + return header; +} + +function centralHeader(entry: Entry, nameBytes: Buffer): Buffer { + const header = Buffer.alloc(46); + header.writeUInt32LE(CENTRAL_HEADER_SIGNATURE, 0); + header.writeUInt16LE(VERSION_NEEDED, 4); // version made by + header.writeUInt16LE(VERSION_NEEDED, 6); // version needed + header.writeUInt16LE(0, 8); // flags + header.writeUInt16LE(METHOD_DEFLATE, 10); + header.writeUInt16LE(0, 12); + header.writeUInt16LE(0x0021, 14); + header.writeUInt32LE(entry.crc, 16); + header.writeUInt32LE(entry.compressed.length, 20); + header.writeUInt32LE(entry.uncompressedSize, 24); + header.writeUInt16LE(nameBytes.length, 28); + header.writeUInt16LE(0, 30); // extra field length + header.writeUInt16LE(0, 32); // comment length + header.writeUInt16LE(0, 34); // disk number start + header.writeUInt16LE(0, 36); // internal attributes + header.writeUInt32LE(0, 38); // external attributes + header.writeUInt32LE(entry.offset, 42); + return header; +} + +/** Writes `files` to `zipPath`, flattened to base names (`zip -j` behaviour). */ +export function writeZip(zipPath: string, files: string[]): void { + const chunks: Buffer[] = []; + const entries: Array<{ entry: Entry; nameBytes: Buffer }> = []; + let offset = 0; + + for (const file of files) { + const contents = readFileSync(file); + const nameBytes = Buffer.from(basename(file), 'utf8'); + const entry: Entry = { + name: basename(file), + crc: crc32(contents), + compressed: deflateRawSync(contents), + uncompressedSize: contents.length, + offset, + }; + const header = localHeader(entry, nameBytes); + chunks.push(header, nameBytes, entry.compressed); + offset += header.length + nameBytes.length + entry.compressed.length; + entries.push({ entry, nameBytes }); + } + + const centralStart = offset; + for (const { entry, nameBytes } of entries) { + const header = centralHeader(entry, nameBytes); + chunks.push(header, nameBytes); + offset += header.length + nameBytes.length; + } + + const end = Buffer.alloc(22); + end.writeUInt32LE(END_OF_CENTRAL_DIR_SIGNATURE, 0); + end.writeUInt16LE(0, 4); // disk number + end.writeUInt16LE(0, 6); // central directory start disk + end.writeUInt16LE(entries.length, 8); + end.writeUInt16LE(entries.length, 10); + end.writeUInt32LE(offset - centralStart, 12); + end.writeUInt32LE(centralStart, 16); + end.writeUInt16LE(0, 20); // comment length + chunks.push(end); + + writeFileSync(zipPath, Buffer.concat(chunks)); +} diff --git a/tests/integration/console.integration.test.ts b/tests/integration/console.integration.test.ts index 5beca7c0..1aa05f4b 100644 --- a/tests/integration/console.integration.test.ts +++ b/tests/integration/console.integration.test.ts @@ -10,13 +10,9 @@ import { waitFor, waitForElementInSnapshot, waitForPageLoad, + fixtureUrl, } from '../helpers/firefox.js'; import type { FirefoxClient } from '@/firefox/index.js'; -import { resolve } from 'node:path'; -import { fileURLToPath } from 'node:url'; - -const __dirname = fileURLToPath(new URL('.', import.meta.url)); -const fixturesPath = resolve(__dirname, '../fixtures'); describe('Console Capture Integration Tests', () => { let firefox: FirefoxClient; @@ -32,7 +28,7 @@ describe('Console Capture Integration Tests', () => { it('should capture console messages on page load', async () => { firefox.clearConsoleMessages(); - const fixturePath = `file://${fixturesPath}/console.html`; + const fixturePath = fixtureUrl('console.html'); await firefox.navigate(fixturePath); await waitForPageLoad(); @@ -58,7 +54,7 @@ describe('Console Capture Integration Tests', () => { }, 15000); it('should capture console.log from button click', async () => { - const fixturePath = `file://${fixturesPath}/console.html`; + const fixturePath = fixtureUrl('console.html'); await firefox.navigate(fixturePath); await waitForPageLoad(); @@ -89,7 +85,7 @@ describe('Console Capture Integration Tests', () => { }, 15000); it('should capture console.warn from button click', async () => { - const fixturePath = `file://${fixturesPath}/console.html`; + const fixturePath = fixtureUrl('console.html'); await firefox.navigate(fixturePath); await waitForPageLoad(); @@ -120,7 +116,7 @@ describe('Console Capture Integration Tests', () => { }, 15000); it('should capture console.error from button click', async () => { - const fixturePath = `file://${fixturesPath}/console.html`; + const fixturePath = fixtureUrl('console.html'); await firefox.navigate(fixturePath); await waitForPageLoad(); @@ -151,7 +147,7 @@ describe('Console Capture Integration Tests', () => { }, 15000); it('should clear console messages', async () => { - const fixturePath = `file://${fixturesPath}/console.html`; + const fixturePath = fixtureUrl('console.html'); await firefox.navigate(fixturePath); await waitForPageLoad(); @@ -174,7 +170,7 @@ describe('Console Capture Integration Tests', () => { it('should have timestamp in console messages', async () => { firefox.clearConsoleMessages(); - const fixturePath = `file://${fixturesPath}/console.html`; + const fixturePath = fixtureUrl('console.html'); await firefox.navigate(fixturePath); await waitForPageLoad(); diff --git a/tests/integration/e2e-scenario.integration.test.ts b/tests/integration/e2e-scenario.integration.test.ts index 5d4a3cc4..454d83f3 100644 --- a/tests/integration/e2e-scenario.integration.test.ts +++ b/tests/integration/e2e-scenario.integration.test.ts @@ -29,14 +29,11 @@ import { findNodeInSnapshot, waitForPageLoad, waitFor, + fixtureUrl, } from '../helpers/firefox.js'; import type { FirefoxClient } from '@/firefox/index.js'; -import { resolve } from 'node:path'; -import { fileURLToPath } from 'node:url'; -const __dirname = fileURLToPath(new URL('.', import.meta.url)); -const fixturesPath = resolve(__dirname, '../fixtures'); -const appUrl = `file://${fixturesPath}/e2e-app.html`; +const appUrl = fixtureUrl('e2e-app.html'); // --------------------------------------------------------------------------- // Todo App Workflow @@ -220,7 +217,7 @@ describe('E2E Scenario: Browser History', () => { expect(snapshot.text).toContain('E2E Test Application'); // Navigate to page B - const simpleUrl = `file://${fixturesPath}/simple.html`; + const simpleUrl = fixtureUrl('simple.html'); await firefox.navigate(simpleUrl); await waitForPageLoad(); @@ -478,7 +475,7 @@ describe('E2E Scenario: Network Monitoring', () => { firefox.clearNetworkRequests(); // Navigate to network fixture which has fetch buttons - const networkUrl = `file://${fixturesPath}/network.html`; + const networkUrl = fixtureUrl('network.html'); await firefox.navigate(networkUrl); await waitForPageLoad(); @@ -507,7 +504,7 @@ describe('E2E Scenario: Network Monitoring', () => { // Self-contained: generate a request first firefox.clearNetworkRequests(); - const networkUrl = `file://${fixturesPath}/network.html`; + const networkUrl = fixtureUrl('network.html'); await firefox.navigate(networkUrl); await waitForPageLoad(); @@ -578,7 +575,7 @@ describe('E2E Scenario: Tab Management', () => { }); it('should open a new tab and switch between tabs', async () => { - const simpleUrl = `file://${fixturesPath}/simple.html`; + const simpleUrl = fixtureUrl('simple.html'); const newTabIndex = await firefox.createNewPage(simpleUrl); await waitForPageLoad(); @@ -635,7 +632,7 @@ describe('E2E Scenario: Stale UID Detection', () => { expect(firstUid).toBeDefined(); // Navigate away — UIDs become stale - await firefox.navigate(`file://${fixturesPath}/simple.html`); + await firefox.navigate(fixtureUrl('simple.html')); await waitForPageLoad(); // Old UID should throw diff --git a/tests/integration/form.integration.test.ts b/tests/integration/form.integration.test.ts index 7e322a39..ed5e4415 100644 --- a/tests/integration/form.integration.test.ts +++ b/tests/integration/form.integration.test.ts @@ -9,13 +9,9 @@ import { closeFirefox, waitForElementInSnapshot, waitForPageLoad, + fixtureUrl, } from '../helpers/firefox.js'; import type { FirefoxClient } from '@/firefox/index.js'; -import { resolve } from 'node:path'; -import { fileURLToPath } from 'node:url'; - -const __dirname = fileURLToPath(new URL('.', import.meta.url)); -const fixturesPath = resolve(__dirname, '../fixtures'); describe('Form Interaction Integration Tests', () => { let firefox: FirefoxClient; @@ -29,7 +25,7 @@ describe('Form Interaction Integration Tests', () => { }); it('should hover over element by UID', async () => { - const fixturePath = `file://${fixturesPath}/form.html`; + const fixturePath = fixtureUrl('form.html'); await firefox.navigate(fixturePath); // Wait for page to be fully loaded diff --git a/tests/integration/moz-extension.integration.test.ts b/tests/integration/moz-extension.integration.test.ts index 66546bf0..1d613494 100644 --- a/tests/integration/moz-extension.integration.test.ts +++ b/tests/integration/moz-extension.integration.test.ts @@ -14,8 +14,8 @@ import { createTestFirefox, closeFirefox, waitFor } from '../helpers/firefox.js' import type { FirefoxClient } from '@/firefox/index.js'; import { resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; -import { execFileSync } from 'node:child_process'; import { unlinkSync, existsSync } from 'node:fs'; +import { writeZip } from '../helpers/zip.js'; const __dirname = fileURLToPath(new URL('.', import.meta.url)); const fixturesPath = resolve(__dirname, '../fixtures'); @@ -23,15 +23,12 @@ const extensionDir = resolve(fixturesPath, 'test-extension'); const xpiPath = resolve(fixturesPath, 'test-extension.xpi'); function packExtension(): void { - // Remove stale .xpi so zip creates a fresh archive + // Remove stale .xpi so the archive is always rebuilt from the fixture if (existsSync(xpiPath)) { unlinkSync(xpiPath); } - // Use execFileSync to avoid shell interpolation of paths - execFileSync('zip', ['-j', xpiPath, 'manifest.json', 'popup.html'], { - cwd: extensionDir, - stdio: 'pipe', - }); + // Packed in-process: the `zip` binary this used to call does not exist on Windows. + writeZip(xpiPath, [resolve(extensionDir, 'manifest.json'), resolve(extensionDir, 'popup.html')]); } /** diff --git a/tests/integration/network.integration.test.ts b/tests/integration/network.integration.test.ts index cee7b9b3..47f7290b 100644 --- a/tests/integration/network.integration.test.ts +++ b/tests/integration/network.integration.test.ts @@ -10,13 +10,9 @@ import { waitFor, waitForElementInSnapshot, waitForPageLoad, + fixtureUrl, } from '../helpers/firefox.js'; import type { FirefoxClient } from '@/firefox/index.js'; -import { resolve } from 'node:path'; -import { fileURLToPath } from 'node:url'; - -const __dirname = fileURLToPath(new URL('.', import.meta.url)); -const fixturesPath = resolve(__dirname, '../fixtures'); describe('Network Monitoring Integration Tests', () => { let firefox: FirefoxClient; @@ -33,7 +29,7 @@ describe('Network Monitoring Integration Tests', () => { it('should capture network requests on page load', async () => { firefox.clearNetworkRequests(); - const fixturePath = `file://${fixturesPath}/network.html`; + const fixturePath = fixtureUrl('network.html'); await firefox.navigate(fixturePath); await waitForPageLoad(); @@ -58,7 +54,7 @@ describe('Network Monitoring Integration Tests', () => { }, 15000); it('should capture fetch GET request', async () => { - const fixturePath = `file://${fixturesPath}/network.html`; + const fixturePath = fixtureUrl('network.html'); await firefox.navigate(fixturePath); await waitForPageLoad(); @@ -89,7 +85,7 @@ describe('Network Monitoring Integration Tests', () => { }, 20000); it('should capture fetch POST request', async () => { - const fixturePath = `file://${fixturesPath}/network.html`; + const fixturePath = fixtureUrl('network.html'); await firefox.navigate(fixturePath); await waitForPageLoad(); @@ -121,7 +117,7 @@ describe('Network Monitoring Integration Tests', () => { }, 20000); it('should capture XHR request', async () => { - const fixturePath = `file://${fixturesPath}/network.html`; + const fixturePath = fixtureUrl('network.html'); await firefox.navigate(fixturePath); await waitForPageLoad(); @@ -148,7 +144,7 @@ describe('Network Monitoring Integration Tests', () => { }, 20000); it('should clear network requests', async () => { - const fixturePath = `file://${fixturesPath}/network.html`; + const fixturePath = fixtureUrl('network.html'); await firefox.navigate(fixturePath); await waitForPageLoad(); diff --git a/tests/integration/snapshot.integration.test.ts b/tests/integration/snapshot.integration.test.ts index 35d6dd49..bdcbbe86 100644 --- a/tests/integration/snapshot.integration.test.ts +++ b/tests/integration/snapshot.integration.test.ts @@ -11,14 +11,10 @@ import { findNodeInSnapshot, findNodesInSnapshot, waitForPageLoad, + fixtureUrl, } from '../helpers/firefox.js'; import type { FirefoxClient } from '@/firefox/index.js'; import type { SnapshotNode } from '@/firefox/snapshot/types.js'; -import { resolve } from 'node:path'; -import { fileURLToPath } from 'node:url'; - -const __dirname = fileURLToPath(new URL('.', import.meta.url)); -const fixturesPath = resolve(__dirname, '../fixtures'); describe('Snapshot Integration Tests', () => { let firefox: FirefoxClient; @@ -32,7 +28,7 @@ describe('Snapshot Integration Tests', () => { }); it('should take snapshot and generate UIDs', async () => { - const fixturePath = `file://${fixturesPath}/simple.html`; + const fixturePath = fixtureUrl('simple.html'); await firefox.navigate(fixturePath); await waitForPageLoad(); @@ -46,7 +42,7 @@ describe('Snapshot Integration Tests', () => { }, 10000); it('should resolve UID to selector', async () => { - const fixturePath = `file://${fixturesPath}/simple.html`; + const fixturePath = fixtureUrl('simple.html'); await firefox.navigate(fixturePath); await waitForPageLoad(); @@ -65,7 +61,7 @@ describe('Snapshot Integration Tests', () => { }, 10000); it('should click element by UID', async () => { - const fixturePath = `file://${fixturesPath}/simple.html`; + const fixturePath = fixtureUrl('simple.html'); await firefox.navigate(fixturePath); await waitForPageLoad(); @@ -83,7 +79,7 @@ describe('Snapshot Integration Tests', () => { }, 10000); it('should detect stale UIDs after navigation', async () => { - const fixturePath = `file://${fixturesPath}/simple.html`; + const fixturePath = fixtureUrl('simple.html'); await firefox.navigate(fixturePath); await waitForPageLoad(); @@ -103,7 +99,7 @@ describe('Snapshot Integration Tests', () => { }, 10000); it('should assign fresh UIDs after navigation', async () => { - const fixturePath = `file://${fixturesPath}/simple.html`; + const fixturePath = fixtureUrl('simple.html'); await firefox.navigate(fixturePath); await waitForPageLoad(); @@ -122,7 +118,7 @@ describe('Snapshot Integration Tests', () => { }, 10000); it('should handle double-click by UID', async () => { - const fixturePath = `file://${fixturesPath}/simple.html`; + const fixturePath = fixtureUrl('simple.html'); await firefox.navigate(fixturePath); await waitForPageLoad(); @@ -140,7 +136,7 @@ describe('Snapshot Integration Tests', () => { }, 10000); it('should clear snapshot manually', async () => { - const fixturePath = `file://${fixturesPath}/simple.html`; + const fixturePath = fixtureUrl('simple.html'); await firefox.navigate(fixturePath); await waitForPageLoad(); @@ -159,7 +155,7 @@ describe('Snapshot Integration Tests', () => { }, 10000); it('should return clear error for invalid CSS selector', async () => { - const fixturePath = `file://${fixturesPath}/selector.html`; + const fixturePath = fixtureUrl('selector.html'); await firefox.navigate(fixturePath); await waitForPageLoad(); @@ -173,7 +169,7 @@ describe('Snapshot Integration Tests', () => { }, 10000); it('should exclude children of hidden parents even in includeAll mode', async () => { - const fixturePath = `file://${fixturesPath}/visibility.html`; + const fixturePath = fixtureUrl('visibility.html'); await firefox.navigate(fixturePath); await waitForPageLoad(); @@ -198,7 +194,7 @@ describe('Snapshot Integration Tests', () => { }, 10000); it('should treat opacity 0.0 and variations as invisible', async () => { - const fixturePath = `file://${fixturesPath}/visibility.html`; + const fixturePath = fixtureUrl('visibility.html'); await firefox.navigate(fixturePath); await waitForPageLoad(); @@ -224,7 +220,7 @@ describe('Snapshot Integration Tests', () => { }, 10000); it('should exclude children of visibility hidden parents', async () => { - const fixturePath = `file://${fixturesPath}/visibility.html`; + const fixturePath = fixtureUrl('visibility.html'); await firefox.navigate(fixturePath); await waitForPageLoad(); diff --git a/tests/integration/tabs.integration.test.ts b/tests/integration/tabs.integration.test.ts index 59250ac5..1d74a72e 100644 --- a/tests/integration/tabs.integration.test.ts +++ b/tests/integration/tabs.integration.test.ts @@ -10,14 +10,10 @@ import { waitForElementInSnapshot, findNodesInSnapshot, waitForPageLoad, + fixtureUrl, } from '../helpers/firefox.js'; import type { FirefoxClient } from '@/firefox/index.js'; import type { SnapshotNode } from '@/firefox/snapshot/types.js'; -import { resolve } from 'node:path'; -import { fileURLToPath } from 'node:url'; - -const __dirname = fileURLToPath(new URL('.', import.meta.url)); -const fixturesPath = resolve(__dirname, '../fixtures'); describe('Tab Management Integration Tests', () => { let firefox: FirefoxClient; @@ -31,7 +27,7 @@ describe('Tab Management Integration Tests', () => { }); it('should list tabs', async () => { - const fixturePath = `file://${fixturesPath}/simple.html`; + const fixturePath = fixtureUrl('simple.html'); await firefox.navigate(fixturePath); await firefox.refreshTabs(); @@ -47,7 +43,7 @@ describe('Tab Management Integration Tests', () => { const initialTabs = firefox.getTabs(); const initialTabCount = initialTabs.length; - const fixturePath = `file://${fixturesPath}/simple.html`; + const fixturePath = fixtureUrl('simple.html'); const newTabIndex = await firefox.createNewPage(fixturePath); await firefox.refreshTabs(); @@ -62,7 +58,7 @@ describe('Tab Management Integration Tests', () => { await firefox.refreshTabs(); // Create second tab - const fixturePath = `file://${fixturesPath}/form.html`; + const fixturePath = fixtureUrl('form.html'); const newTabIndex = await firefox.createNewPage(fixturePath); await firefox.refreshTabs(); @@ -86,7 +82,7 @@ describe('Tab Management Integration Tests', () => { if (initialTabs.length < 2) { // Create additional tab if needed - const fixturePath = `file://${fixturesPath}/simple.html`; + const fixturePath = fixtureUrl('simple.html'); await firefox.createNewPage(fixturePath); await firefox.refreshTabs(); } @@ -106,8 +102,8 @@ describe('Tab Management Integration Tests', () => { it('should have snapshot isolation between tabs', async () => { // Create two tabs with different pages - const simplePath = `file://${fixturesPath}/simple.html`; - const formPath = `file://${fixturesPath}/form.html`; + const simplePath = fixtureUrl('simple.html'); + const formPath = fixtureUrl('form.html'); await firefox.navigate(simplePath); await waitForPageLoad(); diff --git a/tests/tools/console.test.ts b/tests/tools/console.test.ts index 62e1db99..6d0a8e30 100644 --- a/tests/tools/console.test.ts +++ b/tests/tools/console.test.ts @@ -3,7 +3,8 @@ */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import { existsSync, readFileSync, rmSync } from 'node:fs'; +import { existsSync, readFileSync } from 'node:fs'; +import { removeDir } from '../helpers/fs.js'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; import { listConsoleMessagesTool, clearConsoleMessagesTool } from '../../src/tools/console.js'; @@ -100,7 +101,7 @@ describe('Console Tools', () => { afterEach(() => { vi.restoreAllMocks(); if (existsSync(tempDir)) { - rmSync(tempDir, { recursive: true, force: true }); + removeDir(tempDir); } }); diff --git a/tests/tools/network.test.ts b/tests/tools/network.test.ts index 38040bab..258e43fc 100644 --- a/tests/tools/network.test.ts +++ b/tests/tools/network.test.ts @@ -3,7 +3,8 @@ */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import { existsSync, readFileSync, rmSync } from 'node:fs'; +import { existsSync, readFileSync } from 'node:fs'; +import { removeDir } from '../helpers/fs.js'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; import { listNetworkRequestsTool, getNetworkRequestTool } from '../../src/tools/network.js'; @@ -129,7 +130,7 @@ describe('Network Tools', () => { afterEach(() => { vi.restoreAllMocks(); if (existsSync(tempDir)) { - rmSync(tempDir, { recursive: true, force: true }); + removeDir(tempDir); } }); @@ -311,7 +312,7 @@ describe('Network Tools', () => { expect(parsed.responseBodyEncoding).toBe('utf-8'); } finally { if (existsSync(tempDir)) { - rmSync(tempDir, { recursive: true, force: true }); + removeDir(tempDir); } } }); diff --git a/tests/tools/pages.test.ts b/tests/tools/pages.test.ts index f0130618..fa4bd774 100644 --- a/tests/tools/pages.test.ts +++ b/tests/tools/pages.test.ts @@ -3,7 +3,8 @@ */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import { existsSync, readFileSync, rmSync } from 'node:fs'; +import { existsSync, readFileSync } from 'node:fs'; +import { removeDir } from '../helpers/fs.js'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; import { @@ -121,7 +122,7 @@ describe('Pages Tools', () => { afterEach(() => { vi.restoreAllMocks(); if (existsSync(tempDir)) { - rmSync(tempDir, { recursive: true, force: true }); + removeDir(tempDir); } }); diff --git a/tests/tools/screenshot.test.ts b/tests/tools/screenshot.test.ts index d94adda8..2c9d8f03 100644 --- a/tests/tools/screenshot.test.ts +++ b/tests/tools/screenshot.test.ts @@ -5,6 +5,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { screenshotPageTool, screenshotByUidTool } from '../../src/tools/screenshot.js'; import { existsSync, readFileSync, rmSync } from 'node:fs'; +import { removeDir } from '../helpers/fs.js'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; @@ -71,7 +72,7 @@ describe('Screenshot Tools', () => { vi.restoreAllMocks(); for (const dir of [tempDir, MOCK_HOME]) { if (existsSync(dir)) { - rmSync(dir, { recursive: true, force: true }); + removeDir(dir); } } }); diff --git a/tests/tools/script.test.ts b/tests/tools/script.test.ts index a9b7bba9..5addc34f 100644 --- a/tests/tools/script.test.ts +++ b/tests/tools/script.test.ts @@ -3,7 +3,8 @@ */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import { existsSync, readFileSync, rmSync } from 'node:fs'; +import { existsSync, readFileSync } from 'node:fs'; +import { removeDir } from '../helpers/fs.js'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; import { evaluateScriptTool } from '../../src/tools/script.js'; @@ -96,7 +97,7 @@ describe('Script Tools', () => { vi.restoreAllMocks(); for (const dir of [tempDir, MOCK_HOME]) { if (existsSync(dir)) { - rmSync(dir, { recursive: true, force: true }); + removeDir(dir); } } }); diff --git a/tests/tools/snapshot.test.ts b/tests/tools/snapshot.test.ts index d26fa2bc..e69a8b50 100644 --- a/tests/tools/snapshot.test.ts +++ b/tests/tools/snapshot.test.ts @@ -3,7 +3,8 @@ */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import { existsSync, readFileSync, rmSync } from 'node:fs'; +import { existsSync, readFileSync } from 'node:fs'; +import { removeDir } from '../helpers/fs.js'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; import { @@ -106,7 +107,7 @@ describe('Snapshot Tools', () => { afterEach(() => { vi.restoreAllMocks(); if (existsSync(tempDir)) { - rmSync(tempDir, { recursive: true, force: true }); + removeDir(tempDir); } }); diff --git a/tests/utils/save-output.test.ts b/tests/utils/save-output.test.ts index 0c3b88b0..f4df9a07 100644 --- a/tests/utils/save-output.test.ts +++ b/tests/utils/save-output.test.ts @@ -3,7 +3,8 @@ */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync } from 'node:fs'; +import { existsSync, mkdirSync, readFileSync, readdirSync } from 'node:fs'; +import { removeDir } from '../helpers/fs.js'; import { basename, join, relative, sep } from 'node:path'; import { tmpdir } from 'node:os'; @@ -31,7 +32,7 @@ describe('saveOutput', () => { afterEach(() => { for (const dir of [tempDir, MOCK_HOME, cwdDir]) { if (existsSync(dir)) { - rmSync(dir, { recursive: true, force: true }); + removeDir(dir); } } }); diff --git a/vitest.config.ts b/vitest.config.ts index d804adce..3e069746 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,8 +1,6 @@ import { defineConfig } from 'vitest/config'; import path from 'path'; -const isWindows = process.platform === 'win32'; - export default defineConfig({ test: { globals: true, @@ -11,11 +9,6 @@ export default defineConfig({ // Run tests sequentially to avoid Firefox port conflicts fileParallelism: false, pool: 'forks', - poolOptions: { - forks: { - singleFork: true, - }, - }, coverage: { provider: 'v8', reporter: ['text', 'json', 'html', 'lcov'], @@ -37,11 +30,7 @@ export default defineConfig({ }, }, include: ['tests/**/*.test.ts'], - // Skip integration tests on Windows due to selenium-webdriver hanging issue - // See: https://github.com/elastic/kibana/issues/52053 - exclude: isWindows - ? ['node_modules', 'dist', 'tests/integration/**'] - : ['node_modules', 'dist'], + exclude: ['node_modules', 'dist'], }, resolve: { alias: {