diff --git a/example/wdio.conf.ts b/example/wdio.conf.ts index 7401cae..1d5bc16 100644 --- a/example/wdio.conf.ts +++ b/example/wdio.conf.ts @@ -63,7 +63,7 @@ export const config: Options.Testrunner = { capabilities: [ { browserName: 'chrome', - browserVersion: '144.0.7559.60', // specify chromium browser version for testing + browserVersion: '145.0.7632.110', // specify chromium browser version for testing 'goog:chromeOptions': { args: [ '--headless', diff --git a/package.json b/package.json index 20ae232..1cf55c4 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "scripts": { "build": "pnpm --parallel build", "demo": "wdio run ./example/wdio.conf.ts", + "demo:nightwatch": "pnpm --filter @wdio/nightwatch-devtools example", "dev": "pnpm --parallel dev", "preview": "pnpm --parallel preview", "test": "vitest run", @@ -17,7 +18,10 @@ "pnpm": { "overrides": { "vite": "^7.3.0" - } + }, + "ignoredBuiltDependencies": [ + "chromedriver" + ] }, "devDependencies": { "@types/node": "^25.0.3", diff --git a/packages/app/src/components/sidebar/constants.ts b/packages/app/src/components/sidebar/constants.ts index 85ff538..d5f9290 100644 --- a/packages/app/src/components/sidebar/constants.ts +++ b/packages/app/src/components/sidebar/constants.ts @@ -1,3 +1,11 @@ +import { TestState } from './types.js' + +export const STATE_MAP: Record = { + running: TestState.RUNNING, + failed: TestState.FAILED, + passed: TestState.PASSED, + skipped: TestState.SKIPPED +} import type { RunCapabilities } from './types.js' export const DEFAULT_CAPABILITIES: RunCapabilities = { diff --git a/packages/app/src/components/sidebar/explorer.ts b/packages/app/src/components/sidebar/explorer.ts index 0d2835f..c7520fc 100644 --- a/packages/app/src/components/sidebar/explorer.ts +++ b/packages/app/src/components/sidebar/explorer.ts @@ -1,6 +1,6 @@ import { Element } from '@core/element' import { html, css, nothing, type TemplateResult } from 'lit' -import { customElement } from 'lit/decorators.js' +import { customElement, property } from 'lit/decorators.js' import { consume } from '@lit/context' import type { TestStats, SuiteStats } from '@wdio/reporter' import type { Metadata } from '@wdio/devtools-service/types' @@ -17,7 +17,7 @@ import type { TestRunDetail } from './types.js' import { TestState } from './types.js' -import { DEFAULT_CAPABILITIES, FRAMEWORK_CAPABILITIES } from './constants.js' +import { DEFAULT_CAPABILITIES, FRAMEWORK_CAPABILITIES, STATE_MAP } from './constants.js' import '~icons/mdi/play.js' import '~icons/mdi/stop.js' @@ -63,6 +63,7 @@ export class DevtoolsSidebarExplorer extends CollapseableEntry { ] @consume({ context: suiteContext, subscribe: true }) + @property({ type: Array }) suites: Record[] | undefined = undefined @consume({ context: metadataContext, subscribe: true }) @@ -71,6 +72,10 @@ export class DevtoolsSidebarExplorer extends CollapseableEntry { @consume({ context: isTestRunningContext, subscribe: true }) isTestRunning = false + updated(changedProperties: Map) { + super.updated(changedProperties) + } + connectedCallback(): void { super.connectedCallback() window.addEventListener('app-test-filter', this.#filterListener) @@ -285,6 +290,7 @@ export class DevtoolsSidebarExplorer extends CollapseableEntry { feature-file="${entry.featureFile || ''}" feature-line="${entry.featureLine ?? ''}" suite-type="${entry.suiteType || ''}" + ?has-children="${entry.children && entry.children.length > 0}" .runDisabled=${this.#isRunDisabled(entry)} .runDisabledReason=${this.#getRunDisabledReason(entry)} > @@ -326,6 +332,62 @@ export class DevtoolsSidebarExplorer extends CollapseableEntry { ) } + #isRunning(entry: TestStats | SuiteStats): boolean { + if ('tests' in entry) { + // Check if any immediate test is running + if (entry.tests.some((t) => !t.end)) { + return true + } + // Check if any nested suite is running + if (entry.suites.some((s) => this.#isRunning(s))) { + return true + } + return false + } + // For individual tests, check if end is not set + return !entry.end + } + + #hasFailed(entry: TestStats | SuiteStats): boolean { + if ('tests' in entry) { + // Check if any immediate test failed + if (entry.tests.find((t) => t.state === 'failed')) { + return true + } + // Check if any nested suite has failures + if (entry.suites.some((s) => this.#hasFailed(s))) { + return true + } + return false + } + // For individual tests + return entry.state === 'failed' + } + + #computeEntryState(entry: TestStats | SuiteStats): TestState { + const state = (entry as any).state + + // Check explicit state first + const mappedState = STATE_MAP[state] + if (mappedState) { + return mappedState + } + + // For suites, compute state from children + if ('tests' in entry) { + if (this.#isRunning(entry)) { + return TestState.RUNNING + } + if (this.#hasFailed(entry)) { + return TestState.FAILED + } + return TestState.PASSED + } + + // For individual tests, check if still running + return !entry.end ? TestState.RUNNING : TestState.PASSED + } + #getTestEntry(entry: TestStats | SuiteStats): TestEntry { if ('tests' in entry) { const entries = [...entry.tests, ...entry.suites] @@ -333,11 +395,7 @@ export class DevtoolsSidebarExplorer extends CollapseableEntry { uid: entry.uid, label: entry.title, type: 'suite', - state: entry.tests.some((t) => !t.end) - ? TestState.RUNNING - : entry.tests.find((t) => t.state === 'failed') - ? TestState.FAILED - : TestState.PASSED, + state: this.#computeEntryState(entry), callSource: (entry as any).callSource, specFile: (entry as any).file, fullTitle: entry.title, @@ -353,11 +411,7 @@ export class DevtoolsSidebarExplorer extends CollapseableEntry { uid: entry.uid, label: entry.title, type: 'test', - state: !entry.end - ? TestState.RUNNING - : entry.state === 'failed' - ? TestState.FAILED - : TestState.PASSED, + state: this.#computeEntryState(entry), callSource: (entry as any).callSource, specFile: (entry as any).file, fullTitle: (entry as any).fullTitle || entry.title, @@ -421,9 +475,13 @@ export class DevtoolsSidebarExplorer extends CollapseableEntry { (suite) => suite.uid, (suite) => this.#renderEntry(suite) ) - : html`

- No tests found -

`} + : html`
+

No tests to display

+

+ Debug: suites=${this.suites?.length || 0}, + rootSuites=${uniqueSuites.length}, filtered=${suites.length} +

+
`} ` } diff --git a/packages/app/src/components/sidebar/test-suite.ts b/packages/app/src/components/sidebar/test-suite.ts index 428a267..934bf21 100644 --- a/packages/app/src/components/sidebar/test-suite.ts +++ b/packages/app/src/components/sidebar/test-suite.ts @@ -80,6 +80,9 @@ export class ExplorerTestEntry extends CollapseableEntry { @property({ type: String, attribute: 'suite-type' }) suiteType?: string + @property({ type: Boolean, attribute: 'has-children' }) + hasChildren = false + static styles = [ ...Element.styles, css` @@ -206,8 +209,7 @@ export class ExplorerTestEntry extends CollapseableEntry { } render() { - const hasNoChildren = - this.querySelectorAll('[slot="children"]').length === 0 + const hasNoChildren = !this.hasChildren const isCollapsed = this.isCollapsed === 'true' const runTooltip = this.runDisabled ? this.runDisabledReason || diff --git a/packages/app/src/controller/DataManager.ts b/packages/app/src/controller/DataManager.ts index e4c546d..d36062d 100644 --- a/packages/app/src/controller/DataManager.ts +++ b/packages/app/src/controller/DataManager.ts @@ -49,10 +49,17 @@ export const isTestRunningContext = createContext( ) interface SocketMessage< - T extends keyof TraceLog | 'testStopped' = keyof TraceLog | 'testStopped' + T extends keyof TraceLog | 'testStopped' | 'clearExecutionData' = + | keyof TraceLog + | 'testStopped' + | 'clearExecutionData' > { scope: T - data: T extends keyof TraceLog ? TraceLog[T] : unknown + data: T extends keyof TraceLog + ? TraceLog[T] + : T extends 'clearExecutionData' + ? { uid?: string } + : unknown } export class DataManagerController implements ReactiveController { @@ -270,6 +277,14 @@ export class DataManagerController implements ReactiveController { return } + // Handle clear execution data event (when tests change) + if (scope === 'clearExecutionData') { + const clearData = data as { uid?: string } + this.clearExecutionData(clearData.uid) + this.#host.requestUpdate() + return + } + // Check for new run BEFORE processing suites data if (scope === 'suites') { const shouldReset = this.#shouldResetForNewRun(data) diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 89c2809..6e162b5 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -30,9 +30,19 @@ export function broadcastToClients(message: string) { }) } -export async function start(opts: DevtoolsBackendOptions = {}) { +export async function start( + opts: DevtoolsBackendOptions = {} +): Promise<{ server: FastifyInstance; port: number }> { const host = opts.hostname || 'localhost' - const port = opts.port || (await getPort({ port: DEFAULT_PORT })) + // Use getPort to find an available port, starting with the preferred port + const preferredPort = opts.port || DEFAULT_PORT + const port = await getPort({ port: preferredPort }) + + // Log if we had to use a different port + if (opts.port && port !== opts.port) { + log.warn(`Port ${opts.port} is already in use, using port ${port} instead`) + } + const appPath = await getDevtoolsApp() server = Fastify({ logger: true }) @@ -91,6 +101,34 @@ export async function start(opts: DevtoolsBackendOptions = {}) { log.info( `received ${message.length} byte message from worker to ${clients.size} client${clients.size > 1 ? 's' : ''}` ) + + // Parse message to check if it's a clearCommands message + try { + const parsed = JSON.parse(message.toString()) + + // If this is a clearCommands message, transform it to clear-execution-data format + if (parsed.scope === 'clearCommands') { + const testUid = parsed.data?.testUid + log.info(`Clearing commands for test: ${testUid || 'all'}`) + + // Create a synthetic message that DataManager will understand + const clearMessage = JSON.stringify({ + scope: 'clearExecutionData', + data: { uid: testUid } + }) + + clients.forEach((client) => { + if (client.readyState === WebSocket.OPEN) { + client.send(clearMessage) + } + }) + return + } + } catch { + // Not JSON or parsing failed, forward as-is + } + + // Forward all other messages as-is clients.forEach((client) => { if (client.readyState === WebSocket.OPEN) { client.send(message.toString()) @@ -102,7 +140,7 @@ export async function start(opts: DevtoolsBackendOptions = {}) { log.info(`Starting WebdriverIO Devtools application on port ${port}`) await server.listen({ port, host }) - return server + return { server, port } } export async function stop() { @@ -111,8 +149,19 @@ export async function stop() { } log.info('Shutting down WebdriverIO Devtools application') - await server.close() + + // Close all WebSocket connections first + clients.forEach((client) => { + if ( + client.readyState === WebSocket.OPEN || + client.readyState === WebSocket.CONNECTING + ) { + client.terminate() + } + }) clients.clear() + + await server.close() } /** diff --git a/packages/nightwatch-devtools/.gitignore b/packages/nightwatch-devtools/.gitignore new file mode 100644 index 0000000..f68e975 --- /dev/null +++ b/packages/nightwatch-devtools/.gitignore @@ -0,0 +1,36 @@ +# Build output +dist/ +*.tsbuildinfo + +# Dependencies +node_modules/ + +# Test outputs +tests_output/ +logs/ +example/logs/ + +# Trace files +*-trace-*.json +nightwatch-trace-*.json + +# Log files +*.log +npm-debug.log* +pnpm-debug.log* + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Temporary files +*.tmp +*.temp +*.bak diff --git a/packages/nightwatch-devtools/README.md b/packages/nightwatch-devtools/README.md new file mode 100644 index 0000000..79932c0 --- /dev/null +++ b/packages/nightwatch-devtools/README.md @@ -0,0 +1,122 @@ +# @wdio/nightwatch-devtools + +> Nightwatch adapter for WebdriverIO DevTools - Visual debugging UI for your Nightwatch tests + +## What is this? + +Brings the powerful WebdriverIO DevTools visual debugging interface to Nightwatch tests with **zero test code changes**. + +See everything in real-time: +- 📋 **Commands** - Every action executed +- 🖥️ **Console** - Browser console logs +- 🌐 **Network** - HTTP requests/responses +- ✅ **Tests** - Suite structure and results +- 📁 **Sources** - Test file contents +- 📝 **Logs** - Framework debugging + +## Installation + +```bash +npm install @wdio/nightwatch-devtools --save-dev +# or +pnpm add -D @wdio/nightwatch-devtools +``` + +## Usage + +Add to your Nightwatch config: + +```javascript +// nightwatch.conf.js +const nightwatchDevtools = require('@wdio/nightwatch-devtools').default; + +module.exports = { + src_folders: ['tests'], + + test_settings: { + default: { + desiredCapabilities: { + browserName: 'chrome' + }, + // Add DevTools globals with lifecycle hooks + globals: nightwatchDevtools() + } + } +} +``` + +Run your tests: + +```bash +nightwatch +``` + +The DevTools UI will automatically: +1. Start backend server on port 3000 +2. Open in a new browser window +3. Stream test data in real-time +4. Stay open after tests finish (close manually to exit) + +## Example + +See [`example/`](./example) directory for a working sample with: +- Sample test suite +- Nightwatch configuration +- Setup instructions + +Run it: +```bash +cd packages/nightwatch-devtools +pnpm build +pnpm example # Run tests with DevTools UI +``` + +## How It Works + +This is a **thin adapter** (~210 lines) that: + +1. ✅ Reuses `@wdio/devtools-backend` - Fastify server + WebSocket +2. ✅ Reuses `@wdio/devtools-app` - Lit-based UI components +3. ✅ Reuses `@wdio/devtools-script` - Browser capture +4. ✅ Adds only Nightwatch lifecycle hooks: `before`, `beforeSuite`, `beforeEach`, `afterEach`, `after` + +Same backend, same UI, same capture as WDIO - just different framework hooks! + +## Options + +```javascript +const nightwatchDevtools = require('@wdio/nightwatch-devtools').default; + +module.exports = { + test_settings: { + default: { + globals: nightwatchDevtools({ + port: 3000, // DevTools server port (default: 3000) + hostname: 'localhost' // DevTools server hostname (default: 'localhost') + }) + } + } +} +``` + +## What Gets Captured + +✅ Test suites and hierarchy +✅ Test pass/fail status +✅ Execution timing +✅ Error messages and stack traces +✅ Browser console logs (automatic) +✅ Network requests (automatic) +✅ DOM mutations (automatic) + +Browser-side capture works automatically via `@wdio/devtools-script`. + +## Requirements + +- **Nightwatch**: >= 3.0.0 +- **Node.js**: >= 18.0.0 +- **Chrome/Chromium**: For tests and UI + +## License + +MIT diff --git a/packages/nightwatch-devtools/example/README.md b/packages/nightwatch-devtools/example/README.md new file mode 100644 index 0000000..b6e7b09 --- /dev/null +++ b/packages/nightwatch-devtools/example/README.md @@ -0,0 +1,135 @@ +# Nightwatch DevTools Example + +This example demonstrates the `@wdio/nightwatch-devtools` plugin in action. + +## Prerequisites + +Make sure you have Chrome/Chromium installed on your system. The example uses Nightwatch's built-in chromedriver manager. + +## Setup + +1. Build the plugin: +```bash +cd packages/nightwatch-devtools +pnpm build +``` + +2. Install dependencies: +```bash +pnpm install +``` + +## Running the Example + +### Option 1: Automatic (Recommended) + +Run the example tests with DevTools UI: + +```bash +pnpm example +``` + +### Option 2: Manual Setup + +If you encounter chromedriver issues, you can: + +1. **Install chromedriver globally:** +```bash +npm install -g chromedriver +``` + +2. **Or download Chrome for Testing:** +Visit: https://googlechromelabs.github.io/chrome-for-testing/ + +3. **Update nightwatch.conf.cjs** with your chromedriver path: +```javascript +webdriver: { + start_process: true, + server_path: '/path/to/chromedriver', + port: 9515 +} +``` + +## What Happens + +When you run the example, the plugin will: + +1. ✅ Start the DevTools backend server on port 3000 +2. ✅ Open the DevTools UI in a new browser window +3. ✅ Run your Nightwatch tests +4. ✅ Stream all commands, logs, and results to the UI in real-time +5. ✅ Keep the UI open until you close the browser window + +## What You'll See in the DevTools UI + +- **Commands Tab**: Every Nightwatch command executed (url, click, assert, etc.) +- **Console Tab**: Browser console logs +- **Network Tab**: All HTTP requests made during tests +- **Tests Tab**: Test suite structure and results (pass/fail) +- **Metadata Tab**: Session information and test timing +- **Sources Tab**: Test file sources +- **Logs Tab**: Framework logs and debugging information + +## Zero Test Changes Required + +Notice the test files in `example/tests/` have **zero DevTools-specific code**. They're pure Nightwatch tests. The plugin automatically: +- Hooks into Nightwatch's lifecycle +- Captures all test data +- Sends it to the DevTools backend +- Updates the UI in real-time + +## Configuration + +### Minimal (Default) + +```javascript +// nightwatch.conf.cjs +module.exports = { + plugins: ['@wdio/nightwatch-devtools'] +} +``` + +### Custom Port + +```javascript +module.exports = { + plugins: [ + ['@wdio/nightwatch-devtools', { + port: 4000, + hostname: 'localhost' + }] + ] +} +``` + +## Troubleshooting + +### "Failed to connect to ChromeDriver" + +Make sure chromedriver is installed: +```bash +pnpm install chromedriver +# Then rebuild it +pnpm rebuild chromedriver +``` + +Or install globally: +```bash +npm install -g chromedriver +``` + +### "Module not found" + +Make sure you've built the plugin: +```bash +pnpm build +``` + +### Port Already in Use + +Change the port in your config: +```javascript +plugins: [ + ['@wdio/nightwatch-devtools', { port: 4000 }] +] +``` diff --git a/packages/nightwatch-devtools/example/nightwatch.conf.cjs b/packages/nightwatch-devtools/example/nightwatch.conf.cjs new file mode 100644 index 0000000..a03c688 --- /dev/null +++ b/packages/nightwatch-devtools/example/nightwatch.conf.cjs @@ -0,0 +1,32 @@ +// Simple import - just require the package +const nightwatchDevtools = require('@wdio/nightwatch-devtools').default; + +module.exports = { + src_folders: ['example/tests'], + + // Add custom reporter to capture commands + custom_commands_path: [], + custom_assertions_path: [], + + webdriver: { + start_process: true, + server_path: '/opt/homebrew/bin/chromedriver', + port: 9515 + }, + + test_settings: { + default: { + // Ensure all tests run even if one fails + skip_testcases_on_fail: false, + + desiredCapabilities: { + browserName: 'chrome', + 'goog:chromeOptions': { + args: ['--headless', '--no-sandbox', '--disable-dev-shm-usage'] + } + }, + // Simple configuration - just call the function to get globals + globals: nightwatchDevtools({ port: 3000 }) + } + } +}; diff --git a/packages/nightwatch-devtools/example/tests/login.test.js b/packages/nightwatch-devtools/example/tests/login.test.js new file mode 100644 index 0000000..ca2ae78 --- /dev/null +++ b/packages/nightwatch-devtools/example/tests/login.test.js @@ -0,0 +1,43 @@ +describe('The Internet Guinea Pig Website', function () { + it('should log into the secure area with valid credentials', async function (browser) { + console.log('[TEST] Navigating to login page') + browser + .url('https://the-internet.herokuapp.com/login') + .waitForElementVisible('body') + + console.log('[TEST] Attempting login with username: tomsmith') + await browser + .setValue('#username', 'tomsmith') + .setValue('#password', 'SuperSecretPassword!') + .click('button[type="submit"]') + + console.log( + '[TEST] Verifying flash message: You logged into a secure area!' + ) + await browser + .waitForElementVisible('#flash') + .assert.textContains('#flash', 'You logged into a secure area!') + + console.log('[TEST] Flash message verified successfully') + }) + + it('should show error with invalid credentials', async function (browser) { + console.log('[TEST] Navigating to login page') + await browser + .url('https://the-internet.herokuapp.com/login') + .waitForElementVisible('body') + + console.log('[TEST] Attempting login with username: foobar') + await browser + .setValue('#username', 'foobar') + .setValue('#password', 'barfoo') + .click('button[type="submit"]') + + console.log('[TEST] Verifying flash message: Your username is invalid!') + await browser + .waitForElementVisible('.flash', 5000) + .assert.textContains('.flash', 'Your username is invalid!') + + console.log('[TEST] Flash message verified successfully') + }) +}) diff --git a/packages/nightwatch-devtools/example/tests/sample.test.js b/packages/nightwatch-devtools/example/tests/sample.test.js new file mode 100644 index 0000000..ff2e4b2 --- /dev/null +++ b/packages/nightwatch-devtools/example/tests/sample.test.js @@ -0,0 +1,21 @@ +describe('Sample Nightwatch Test with DevTools', function () { + it('should navigate to example.com and check title', async function (browser) { + await browser + .url('https://example.com') + .waitForElementVisible('body', 5000) + .assert.titleContains('Example') + .assert.visible('h1') + + const result = await browser.getText('h1') + browser.assert.ok(result.includes('Example'), 'H1 contains "Example"') + }) + + it('should perform basic interactions', async function (browser) { + await browser + .url('https://www.google.com') + .waitForElementVisible('body', 5000) + .assert.visible('textarea[name="q"]') + .setValue('textarea[name="q"]', 'WebdriverIO DevTools') + .pause(1000) + }) +}) diff --git a/packages/nightwatch-devtools/nightwatch.conf.cjs b/packages/nightwatch-devtools/nightwatch.conf.cjs new file mode 100644 index 0000000..b261e70 --- /dev/null +++ b/packages/nightwatch-devtools/nightwatch.conf.cjs @@ -0,0 +1,361 @@ +// +// Refer to the online docs for more details: +// https://nightwatchjs.org/guide/configuration/nightwatch-configuration-file.html +// +// _ _ _ _ _ _ _ +// | \ | |(_) | | | | | | | | +// | \| | _ __ _ | |__ | |_ __ __ __ _ | |_ ___ | |__ +// | . ` || | / _` || '_ \ | __|\ \ /\ / / / _` || __| / __|| '_ \ +// | |\ || || (_| || | | || |_ \ V V / | (_| || |_ | (__ | | | | +// \_| \_/|_| \__, ||_| |_| \__| \_/\_/ \__,_| \__| \___||_| |_| +// __/ | +// |___/ +// + +module.exports = { + // An array of folders (excluding subfolders) where your tests are located; + // if this is not specified, the test source must be passed as the second argument to the test runner. + src_folders: [], + + // See https://nightwatchjs.org/guide/concepts/page-object-model.html + page_objects_path: ['node_modules/nightwatch/examples/pages/'], + + // See https://nightwatchjs.org/guide/extending-nightwatch/adding-custom-commands.html + custom_commands_path: ['node_modules/nightwatch/examples/custom-commands/'], + + // See https://nightwatchjs.org/guide/extending-nightwatch/adding-custom-assertions.html + custom_assertions_path: '', + + // See https://nightwatchjs.org/guide/extending-nightwatch/adding-plugins.html + plugins: [], + + // See https://nightwatchjs.org/guide/concepts/test-globals.html#external-test-globals + globals_path : '', + + // Set this to true to disable bounding boxes on terminal output. Useful when running in some CI environments. + disable_output_boxes: false, + + webdriver: {}, + + test_workers: { + enabled: true, + workers: 'auto' + }, + + test_settings: { + default: { + disable_error_log: false, + launch_url: 'https://nightwatchjs.org', + + screenshots: { + enabled: false, + path: 'screens', + on_failure: true + }, + + desiredCapabilities: { + browserName : 'firefox' + }, + + webdriver: { + start_process: true, + server_path: '' + } + }, + + safari: { + desiredCapabilities : { + browserName : 'safari', + alwaysMatch: { + acceptInsecureCerts: false + } + }, + webdriver: { + start_process: true, + server_path: '' + } + }, + + firefox: { + desiredCapabilities : { + browserName : 'firefox', + acceptInsecureCerts: true, + 'moz:firefoxOptions': { + args: [ + // '-headless', + // '-verbose' + ] + } + }, + webdriver: { + start_process: true, + server_path: '', + cli_args: [ + // very verbose geckodriver logs + // '-vv' + ] + } + }, + + chrome: { + desiredCapabilities : { + browserName : 'chrome', + 'goog:chromeOptions' : { + // More info on Chromedriver: https://sites.google.com/a/chromium.org/chromedriver/ + // + // w3c:false tells Chromedriver to run using the legacy JSONWire protocol (not required in Chrome 78) + w3c: true, + args: [ + //'--no-sandbox', + //'--ignore-certificate-errors', + //'--allow-insecure-localhost', + //'--headless' + ] + } + }, + + webdriver: { + start_process: true, + server_path: '', + cli_args: [ + // '--verbose' + ] + } + }, + + edge: { + desiredCapabilities : { + browserName : 'MicrosoftEdge', + 'ms:edgeOptions' : { + w3c: true, + // More info on EdgeDriver: https://docs.microsoft.com/en-us/microsoft-edge/webdriver-chromium/capabilities-edge-options + args: [ + //'--headless' + ] + } + }, + + webdriver: { + start_process: true, + // Download msedgedriver from https://docs.microsoft.com/en-us/microsoft-edge/webdriver-chromium/ + // and set the location below: + server_path: '', + cli_args: [ + // '--verbose' + ] + } + }, + + ////////////////////////////////////////////////////////////////////////////////// + // Configuration for when using cucumber-js (https://cucumber.io) | + // | + // It uses the bundled examples inside the nightwatch examples folder; feel free | + // to adapt this to your own project needs | + ////////////////////////////////////////////////////////////////////////////////// + 'cucumber-js': { + src_folders: ['examples/cucumber-js/features/step_definitions'], + + test_runner: { + // set cucumber as the runner + type: 'cucumber', + + // define cucumber specific options + options: { + //set the feature path + feature_path: 'node_modules/nightwatch/examples/cucumber-js/*/*.feature', + + // start the webdriver session automatically (enabled by default) + // auto_start_session: true + + // use parallel execution in Cucumber + // workers: 2 // set number of workers to use (can also be defined in the cli as --workers=2 + } + } + }, + + ////////////////////////////////////////////////////////////////////////////////// + // Configuration for when using the browserstack.com cloud service | + // | + // Please set the username and access key by setting the environment variables: | + // - BROWSERSTACK_USERNAME | + // - BROWSERSTACK_ACCESS_KEY | + // .env files are supported | + ////////////////////////////////////////////////////////////////////////////////// + browserstack: { + selenium: { + host: 'hub.browserstack.com', + port: 443 + }, + // More info on configuring capabilities can be found on: + // https://www.browserstack.com/automate/capabilities?tag=selenium-4 + desiredCapabilities: { + 'bstack:options' : { + userName: '${BROWSERSTACK_USERNAME}', + accessKey: '${BROWSERSTACK_ACCESS_KEY}', + } + }, + + disable_error_log: true, + webdriver: { + timeout_options: { + timeout: 60000, + retry_attempts: 3 + }, + keep_alive: true, + start_process: false + } + }, + + 'browserstack.local': { + extends: 'browserstack', + desiredCapabilities: { + 'browserstack.local': true + } + }, + + 'browserstack.chrome': { + extends: 'browserstack', + desiredCapabilities: { + browserName: 'chrome', + chromeOptions : { + w3c: true + } + } + }, + + 'browserstack.firefox': { + extends: 'browserstack', + desiredCapabilities: { + browserName: 'firefox' + } + }, + + 'browserstack.ie': { + extends: 'browserstack', + desiredCapabilities: { + browserName: 'internet explorer', + browserVersion: '11.0' + } + }, + + 'browserstack.safari': { + extends: 'browserstack', + desiredCapabilities: { + browserName: 'safari' + } + }, + + 'browserstack.local_chrome': { + extends: 'browserstack.local', + desiredCapabilities: { + browserName: 'chrome' + } + }, + + 'browserstack.local_firefox': { + extends: 'browserstack.local', + desiredCapabilities: { + browserName: 'firefox' + } + }, + ////////////////////////////////////////////////////////////////////////////////// + // Configuration for when using the SauceLabs cloud service | + // | + // Please set the username and access key by setting the environment variables: | + // - SAUCE_USERNAME | + // - SAUCE_ACCESS_KEY | + ////////////////////////////////////////////////////////////////////////////////// + saucelabs: { + selenium: { + host: 'ondemand.saucelabs.com', + port: 443 + }, + // More info on configuring capabilities can be found on: + // https://docs.saucelabs.com/dev/test-configuration-options/ + desiredCapabilities: { + 'sauce:options' : { + username: '${SAUCE_USERNAME}', + accessKey: '${SAUCE_ACCESS_KEY}', + screenResolution: '1280x1024' + // https://docs.saucelabs.com/dev/cli/sauce-connect-proxy/#--region + // region: 'us-west-1' + // https://docs.saucelabs.com/dev/test-configuration-options/#tunnelidentifier + // parentTunnel: '', + // tunnelIdentifier: '', + } + }, + disable_error_log: false, + webdriver: { + start_process: false + } + }, + 'saucelabs.chrome': { + extends: 'saucelabs', + desiredCapabilities: { + browserName: 'chrome', + browserVersion: 'latest', + javascriptEnabled: true, + acceptSslCerts: true, + timeZone: 'London', + chromeOptions : { + w3c: true + } + } + }, + 'saucelabs.firefox': { + extends: 'saucelabs', + desiredCapabilities: { + browserName: 'firefox', + browserVersion: 'latest', + javascriptEnabled: true, + acceptSslCerts: true, + timeZone: 'London' + } + }, + ////////////////////////////////////////////////////////////////////////////////// + // Configuration for when using the Selenium service, either locally or remote, | + // like Selenium Grid | + ////////////////////////////////////////////////////////////////////////////////// + selenium_server: { + // Selenium Server is running locally and is managed by Nightwatch + // Install the NPM package @nightwatch/selenium-server or download the selenium server jar file from https://github.com/SeleniumHQ/selenium/releases/, e.g.: selenium-server-4.1.1.jar + selenium: { + start_process: true, + port: 4444, + server_path: '', // Leave empty if @nightwatch/selenium-server is installed + command: 'standalone', // Selenium 4 only + cli_args: { + //'webdriver.gecko.driver': '', + //'webdriver.chrome.driver': '' + } + }, + webdriver: { + start_process: false, + default_path_prefix: '/wd/hub' + } + }, + + 'selenium.chrome': { + extends: 'selenium_server', + desiredCapabilities: { + browserName: 'chrome', + chromeOptions : { + w3c: true + } + } + }, + + 'selenium.firefox': { + extends: 'selenium_server', + desiredCapabilities: { + browserName: 'firefox', + 'moz:firefoxOptions': { + args: [ + // '-headless', + // '-verbose' + ] + } + } + } + } +}; diff --git a/packages/nightwatch-devtools/package.json b/packages/nightwatch-devtools/package.json new file mode 100644 index 0000000..75a2db1 --- /dev/null +++ b/packages/nightwatch-devtools/package.json @@ -0,0 +1,43 @@ +{ + "name": "@wdio/nightwatch-devtools", + "version": "0.1.0", + "description": "Nightwatch adapter for WebdriverIO DevTools - reuses existing backend, UI, and capture infrastructure", + "type": "module", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "nightwatch": { + "plugin": true + }, + "scripts": { + "build": "tsc", + "watch": "tsc --watch", + "clean": "rm -rf dist", + "example": "nightwatch -c example/nightwatch.conf.cjs" + }, + "keywords": [ + "nightwatch", + "devtools", + "debugging", + "testing" + ], + "author": "WebdriverIO Team", + "license": "MIT", + "dependencies": { + "@wdio/devtools-backend": "workspace:*", + "@wdio/logger": "^9.6.0", + "import-meta-resolve": "^4.2.0", + "stacktrace-parser": "^0.1.10", + "webdriverio": "^9.18.0", + "ws": "^8.18.3" + }, + "devDependencies": { + "@types/node": "^22.10.5", + "@types/ws": "^8.18.1", + "chromedriver": "^133.0.0", + "nightwatch": "^3.0.0", + "typescript": "^5.9.2" + }, + "peerDependencies": { + "nightwatch": ">=3.0.0" + } +} diff --git a/packages/nightwatch-devtools/src/constants.ts b/packages/nightwatch-devtools/src/constants.ts new file mode 100644 index 0000000..8458654 --- /dev/null +++ b/packages/nightwatch-devtools/src/constants.ts @@ -0,0 +1,117 @@ +export const PAGE_TRANSITION_COMMANDS = [ + 'url', + 'navigateTo', + 'click', + 'submitForm' +] as const + +/** + * Internal Nightwatch commands to exclude from capture + * These are helper/platform detection commands not relevant to users + */ +export const INTERNAL_COMMANDS_TO_IGNORE = [ + 'isAppiumClient', + 'isSafari', + 'isChrome', + 'isFirefox', + 'isEdge', + 'isMobile', + 'isAndroid', + 'isIOS', + 'session', + 'sessions', + 'timeouts', + 'timeoutsAsyncScript', + 'timeoutsImplicitWait', + 'getLog', + 'getLogTypes', + 'screenshot', + 'availableContexts', + 'currentContext', + 'setChromeOptions', + 'setDeviceName', + 'perform', + 'execute', + 'executeAsync', + 'executeScript' +] as const + +/** + * Console method types for log capturing + */ +export const CONSOLE_METHODS = ['log', 'info', 'warn', 'error'] as const + +/** + * Log level detection patterns + */ +export const LOG_LEVEL_PATTERNS: ReadonlyArray<{ + level: 'trace' | 'debug' | 'info' | 'warn' | 'error' + pattern: RegExp +}> = [ + { level: 'trace', pattern: /\btrace\b/i }, + { level: 'debug', pattern: /\bdebug\b/i }, + { level: 'info', pattern: /\binfo\b/i }, + { level: 'warn', pattern: /\bwarn(ing)?\b/i }, + { level: 'error', pattern: /\berror\b/i } +] as const + +/** + * Console log source types + */ +export const LOG_SOURCES = { + BROWSER: 'browser', + TEST: 'test', + TERMINAL: 'terminal' +} as const + +/** + * ANSI escape code regex - matches all ANSI escape sequences including: + * - Color codes: \x1b[36m, \x1b[39m + * - Cursor control: \x1b[2K, \x1b[1G, \x1b[1A + * - Cursor visibility: \x1b[?25l, \x1b[?25h + * - SGR parameters: \x1b[1m, \x1b[22m + * Pattern: ESC [ (optional ?)(digits/semicolons)(letter) + */ +export const ANSI_REGEX = /\x1b\[[?]?[0-9;]*[A-Za-z]/g + +/** + * Default values + */ +export const DEFAULTS = { + CID: '0-0', + TEST_NAME: 'unknown', + FILE_NAME: 'unknown', + RETRIES: 0, + DURATION: 0 +} as const + +/** + * Timing constants (in milliseconds) + */ +export const TIMING = { + UI_RENDER_DELAY: 150, + TEST_START_DELAY: 100, + SUITE_COMPLETE_DELAY: 200, + UI_CONNECTION_WAIT: 10000, + BROWSER_CLOSE_WAIT: 2000, + INITIAL_CONNECTION_WAIT: 500, + BROWSER_POLL_INTERVAL: 1000 +} as const + +/** + * Test states + */ +export const TEST_STATE = { + PENDING: 'pending', + RUNNING: 'running', + PASSED: 'passed', + FAILED: 'failed', + SKIPPED: 'skipped' +} as const + +export type TestState = (typeof TEST_STATE)[keyof typeof TEST_STATE] + +/** + * Temporary UID generation pattern + */ +export const TEMP_UID_PREFIX = 'temp' diff --git a/packages/nightwatch-devtools/src/helpers/browserProxy.ts b/packages/nightwatch-devtools/src/helpers/browserProxy.ts new file mode 100644 index 0000000..404f82e --- /dev/null +++ b/packages/nightwatch-devtools/src/helpers/browserProxy.ts @@ -0,0 +1,262 @@ +/** + * Browser Proxy + * Handles browser command interception and tracking + */ + +import logger from '@wdio/logger' +import { INTERNAL_COMMANDS_TO_IGNORE } from '../constants.js' +import { getCallSourceFromStack } from './utils.js' +import type { SessionCapturer } from '../session.js' +import type { TestManager } from './testManager.js' +import type { NightwatchBrowser } from '../types.js' + +const log = logger('@wdio/nightwatch-devtools:browserProxy') + +interface CommandStackFrame { + command: string + callSource?: string + signature: string +} + +export class BrowserProxy { + private browserProxied = false + private commandStack: CommandStackFrame[] = [] + private lastCommandSig: string | null = null + private currentTestFullPath: string | null = null + + constructor( + private sessionCapturer: SessionCapturer, + private testManager: TestManager, + private getCurrentTest: () => any + ) {} + + /** + * Reset command tracking for new test + */ + resetCommandTracking(): void { + this.commandStack = [] + this.lastCommandSig = null + } + + /** + * Get current test file path + */ + getCurrentTestFullPath(): string | null { + return this.currentTestFullPath + } + + /** + * Set current test file path + */ + setCurrentTestFullPath(path: string | null): void { + this.currentTestFullPath = path + } + + /** + * Wrap browser.url to inject script after navigation + */ + wrapUrlMethod(browser: NightwatchBrowser): void { + const originalUrl = browser.url.bind(browser) + const sessionCapturer = this.sessionCapturer + + browser.url = function(url: string) { + const result = originalUrl(url) as any + + if (result && typeof result.perform === 'function') { + result.perform(async function(this: any) { + try { + log.info(`Injecting script after navigation to: ${url}`) + await sessionCapturer.injectScript(this) + } catch (err) { + log.error(`Failed to inject script: ${(err as Error).message}`) + } + }) + } + + return result + } as any + + log.info('✓ Script injection wrapped') + } + + /** + * Wrap all browser commands to capture them + */ + wrapBrowserCommands(browser: NightwatchBrowser): void { + if (this.browserProxied) return + + const browserAny = browser as any + const allMethods = new Set([ + ...Object.keys(browser), + ...Object.getOwnPropertyNames(Object.getPrototypeOf(browser)) + ]) + const wrappedMethods: string[] = [] + + allMethods.forEach(methodName => { + if (methodName === 'constructor' || typeof browserAny[methodName] !== 'function') { + return + } + + if (INTERNAL_COMMANDS_TO_IGNORE.includes(methodName as any) || methodName.startsWith('__')) { + return + } + + const originalMethod = browserAny[methodName].bind(browser) + + browserAny[methodName] = (...args: any[]) => { + return this.handleCommandExecution(browser, browserAny, methodName, originalMethod, args) + } + + wrappedMethods.push(methodName) + }) + + this.browserProxied = true + log.info(`✓ Wrapped ${wrappedMethods.length} browser methods`) + } + + /** + * Handle command execution with tracking + */ + private handleCommandExecution( + browser: NightwatchBrowser, + browserAny: any, + methodName: string, + originalMethod: Function, + args: any[] + ): any { + // Detect test boundaries + const currentNightwatchTest = browserAny.currentTest + const currentTestName = this.testManager.detectTestBoundary(currentNightwatchTest) + + // Start test if this is its first command + this.testManager.startTestIfPending(currentTestName) + + // Get call source + const callInfo = getCallSourceFromStack() + if (callInfo.filePath && !this.currentTestFullPath) { + this.currentTestFullPath = callInfo.filePath + } + + // Check for duplicate commands + const cmdSig = JSON.stringify({ command: methodName, args, src: callInfo.callSource }) + const isDuplicate = this.lastCommandSig === cmdSig + + if (!isDuplicate) { + this.commandStack.push({ + command: methodName, + callSource: callInfo.callSource, + signature: cmdSig + }) + this.lastCommandSig = cmdSig + } + + try { + const result = originalMethod(...args) + + // Capture command after execution + const stackFrame = this.commandStack[this.commandStack.length - 1] + if (stackFrame?.command === methodName && stackFrame.signature === cmdSig) { + this.commandStack.pop() + this.captureCommandResult(methodName, args, result, callInfo.callSource, browser, browserAny) + } + + return result + } catch (error) { + // Capture command error + const stackFrame = this.commandStack[this.commandStack.length - 1] + if (stackFrame?.command === methodName && stackFrame.signature === cmdSig) { + this.commandStack.pop() + this.captureCommandError(methodName, args, error, callInfo.callSource) + } + + throw error + } + } + + /** + * Capture command result + */ + private captureCommandResult( + methodName: string, + args: any[], + result: any, + callSource: string | undefined, + browser: NightwatchBrowser, + browserAny: any + ): void { + const currentTest = this.getCurrentTest() + if (!currentTest) return + + // Serialize result + let serializedResult: any = undefined + const isBrowserObject = result === browser || result === browserAny + const isChainableAPI = result && typeof result === 'object' && + ('queue' in result || 'sessionId' in result || 'capabilities' in result) + + if (isBrowserObject || isChainableAPI) { + const isWaitCommand = methodName.startsWith('waitFor') + serializedResult = isWaitCommand ? true : undefined + } else if (result && typeof result === 'object') { + if ('value' in result) { + serializedResult = result.value + } else { + try { + serializedResult = JSON.parse(JSON.stringify(result)) + } catch { + serializedResult = String(result) + } + } + } else if (result !== undefined) { + serializedResult = result + } + + // Capture and send command immediately + this.sessionCapturer.captureCommand( + methodName, + args, + serializedResult, + undefined, + currentTest.uid, + callSource + ).catch((err: any) => log.error(`Failed to capture ${methodName}: ${err.message}`)) + + const lastCommand = this.sessionCapturer.commandsLog[this.sessionCapturer.commandsLog.length - 1] + if (lastCommand) { + this.sessionCapturer.sendCommand(lastCommand) + } + } + + /** + * Capture command error + */ + private captureCommandError( + methodName: string, + args: any[], + error: any, + callSource: string | undefined + ): void { + const currentTest = this.getCurrentTest() + if (!currentTest) return + + this.sessionCapturer.captureCommand( + methodName, + args, + undefined, + error instanceof Error ? error : new Error(String(error)), + currentTest.uid, + callSource + ).catch((err: any) => log.error(`Failed to capture ${methodName}: ${err.message}`)) + + const lastCommand = this.sessionCapturer.commandsLog[this.sessionCapturer.commandsLog.length - 1] + if (lastCommand) { + this.sessionCapturer.sendCommand(lastCommand) + } + } + + /** + * Check if browser is already proxied + */ + isProxied(): boolean { + return this.browserProxied + } +} diff --git a/packages/nightwatch-devtools/src/helpers/capturePerformance.ts b/packages/nightwatch-devtools/src/helpers/capturePerformance.ts new file mode 100644 index 0000000..6ec7934 --- /dev/null +++ b/packages/nightwatch-devtools/src/helpers/capturePerformance.ts @@ -0,0 +1,58 @@ +/** + * Script to capture performance data, cookies, and document info from the browser + * This gets injected and executed in the browser context + */ + +/** + * Returns the script as a string to be executed in the browser + */ +export const getCapturePerformanceScript = (): string => { + return ` + (function() { + const performance = window.performance; + const navigation = performance.getEntriesByType?.('navigation')?.[0]; + const resources = performance.getEntriesByType?.('resource') || []; + + return { + navigation: navigation ? { + url: window.location.href, + timing: { + loadTime: navigation.loadEventEnd - navigation.fetchStart, + domReady: navigation.domContentLoadedEventEnd - navigation.fetchStart, + responseTime: navigation.responseEnd - navigation.requestStart, + dnsLookup: navigation.domainLookupEnd - navigation.domainLookupStart, + tcpConnection: navigation.connectEnd - navigation.connectStart, + serverResponse: navigation.responseEnd - navigation.responseStart + } + } : undefined, + resources: resources.map(function(resource) { + return { + url: resource.name, + duration: resource.duration, + size: resource.transferSize || 0, + type: resource.initiatorType, + startTime: resource.startTime, + responseEnd: resource.responseEnd + }; + }), + cookies: (function() { + try { return document.cookie; } catch (e) { return ''; } + })(), + documentInfo: { + url: window.location.href, + title: document.title, + headers: { + userAgent: navigator.userAgent, + language: navigator.language, + platform: navigator.platform + }, + documentInfo: { + readyState: document.readyState, + referrer: document.referrer, + characterSet: document.characterSet + } + } + }; + })() + ` +} diff --git a/packages/nightwatch-devtools/src/helpers/suiteManager.ts b/packages/nightwatch-devtools/src/helpers/suiteManager.ts new file mode 100644 index 0000000..dfa01f4 --- /dev/null +++ b/packages/nightwatch-devtools/src/helpers/suiteManager.ts @@ -0,0 +1,126 @@ +/** + * Suite Manager + * Handles test suite creation and management + */ + +import logger from '@wdio/logger' +import { DEFAULTS, TIMING, TEST_STATE } from '../constants.js' +import { determineTestState, type SuiteStats, type TestStats, type NightwatchTestCase } from '../types.js' +import type { TestReporter } from '../reporter.js' + +const log = logger('@wdio/nightwatch-devtools:suiteManager') + +export class SuiteManager { + private currentSuiteByFile = new Map() + + constructor(private testReporter: TestReporter) {} + + /** + * Get or create suite for a test file + */ + getOrCreateSuite( + testFile: string, + suiteTitle: string, + fullPath: string | null, + testNames: string[] + ): SuiteStats { + if (!this.currentSuiteByFile.has(testFile)) { + const suiteStats: SuiteStats = { + uid: '', + cid: DEFAULTS.CID, + title: suiteTitle, + fullTitle: suiteTitle, + file: fullPath || testFile, + type: 'suite' as const, + start: new Date(), + state: TEST_STATE.PENDING, + end: null, + tests: [], + suites: [], + hooks: [], + _duration: DEFAULTS.DURATION + } + + suiteStats.uid = this.testReporter.generateStableUid(suiteStats.file, suiteStats.title) + + // Create test entries with pending state + if (testNames.length > 0) { + for (const testName of testNames) { + const fullTitle = `${suiteTitle} ${testName}` + // Generate stable UID using same method as onTestStart + const testUid = this.testReporter.generateStableUid(fullPath || testFile, fullTitle) + const testEntry: TestStats = { + uid: testUid, + cid: DEFAULTS.CID, + title: testName, + fullTitle: fullTitle, + parent: suiteStats.uid, + state: TEST_STATE.PENDING as TestStats['state'], + start: new Date(), + end: null, + type: 'test' as const, + file: fullPath || testFile, + retries: DEFAULTS.RETRIES, + _duration: DEFAULTS.DURATION, + hooks: [] + } + suiteStats.tests.push(testEntry) + } + // Don't send updates here - onSuiteStart will send it + } + + this.currentSuiteByFile.set(testFile, suiteStats) + this.testReporter.onSuiteStart(suiteStats) + } + + return this.currentSuiteByFile.get(testFile)! + } + + /** + * Get suite for a test file + */ + getSuite(testFile: string): SuiteStats | undefined { + return this.currentSuiteByFile.get(testFile) + } + + /** + * Mark suite as running + */ + markSuiteAsRunning(suite: SuiteStats): void { + suite.state = TEST_STATE.RUNNING + this.testReporter.updateSuites() + } + + /** + * Finalize suite with test results + */ + finalizeSuite(suite: SuiteStats): void { + if (suite.end) return // Already finalized + + suite.end = new Date() + suite._duration = suite.end.getTime() - (suite.start?.getTime() || 0) + + const hasFailures = suite.tests.some((t: any) => t.state === TEST_STATE.FAILED) + const allPassed = suite.tests.every((t: any) => t.state === TEST_STATE.PASSED) + const hasSkipped = suite.tests.some((t: any) => t.state === TEST_STATE.SKIPPED) + + if (hasFailures) { + suite.state = TEST_STATE.FAILED + } else if (allPassed) { + suite.state = TEST_STATE.PASSED + } else if (hasSkipped) { + suite.state = TEST_STATE.PASSED + } else { + suite.state = TEST_STATE.FAILED + } + + this.testReporter.onSuiteEnd(suite) + } + + /** + * Get all suites + */ + getAllSuites(): Map { + return this.currentSuiteByFile + } +} diff --git a/packages/nightwatch-devtools/src/helpers/testManager.ts b/packages/nightwatch-devtools/src/helpers/testManager.ts new file mode 100644 index 0000000..dd29ef2 --- /dev/null +++ b/packages/nightwatch-devtools/src/helpers/testManager.ts @@ -0,0 +1,165 @@ +/** + * Test Manager + * Handles test lifecycle, state management, and boundary detection + */ + +import logger from '@wdio/logger' +import { TEST_STATE, DEFAULTS } from '../constants.js' +import { determineTestState, type TestStats, type SuiteStats, type NightwatchTestCase } from '../types.js' +import type { TestReporter } from '../reporter.js' + +const log = logger('@wdio/nightwatch-devtools:testManager') + +export class TestManager { + private processedTests = new Map>() + private lastKnownTestName: string | null = null + + constructor(private testReporter: TestReporter) {} + + /** + * Update test state and report to UI + */ + updateTestState( + test: TestStats, + state: TestStats['state'], + endTime?: Date, + duration?: number + ): void { + test.state = state + if (endTime) test.end = endTime + if (duration !== undefined) test._duration = duration + + if (state === TEST_STATE.PASSED) { + this.testReporter.onTestPass(test) + } else if (state === TEST_STATE.FAILED) { + this.testReporter.onTestFail(test) + } else if (state === TEST_STATE.RUNNING) { + this.testReporter.onTestStart(test) + } + + if (state !== TEST_STATE.RUNNING) { + this.testReporter.onTestEnd(test) + } + } + + /** + * Find test in suite by title + */ + findTestInSuite(suite: SuiteStats, testTitle: string): TestStats | undefined { + return suite.tests.find( + (t): t is TestStats => typeof t !== 'string' && t.title === testTitle + ) + } + + /** + * Mark test as processed to avoid duplicate reporting + */ + markTestAsProcessed(testFile: string, testTitle: string): void { + if (!this.processedTests.has(testFile)) { + this.processedTests.set(testFile, new Set()) + } + this.processedTests.get(testFile)!.add(testTitle) + } + + /** + * Check if test has been processed + */ + isTestProcessed(testFile: string, testTitle: string): boolean { + return this.processedTests.get(testFile)?.has(testTitle) ?? false + } + + /** + * Get processed tests for a file + */ + getProcessedTests(testFile: string): Set { + return this.processedTests.get(testFile) || new Set() + } + + /** + * Detect test boundary and finalize previous test if needed + * Returns the current test name + */ + detectTestBoundary(currentNightwatchTest: any): string { + const currentTestName = currentNightwatchTest?.name || DEFAULTS.TEST_NAME + + // If test name changed, finalize previous test + if (this.lastKnownTestName && + currentTestName !== this.lastKnownTestName && + currentTestName !== DEFAULTS.TEST_NAME) { + + const testFile = currentNightwatchTest.module?.split('/').pop() || DEFAULTS.FILE_NAME + const currentSuite = this.testReporter.getCurrentSuite() + + if (currentSuite) { + const prevTest = this.findTestInSuite(currentSuite, this.lastKnownTestName) + if (prevTest && prevTest.state === TEST_STATE.RUNNING) { + const prevTestCase = currentNightwatchTest.results?.testcases?.[this.lastKnownTestName] + + if (prevTestCase) { + const testState = determineTestState(prevTestCase) + this.updateTestState(prevTest, testState, new Date(), parseFloat(prevTestCase.time || '0') * 1000) + this.markTestAsProcessed(testFile, this.lastKnownTestName) + } + } + } + } + + // Update last known test name + if (currentTestName !== DEFAULTS.TEST_NAME) { + this.lastKnownTestName = currentTestName + } + + return currentTestName + } + + /** + * Start a pending test if this is its first command + */ + startTestIfPending(currentTestName: string): void { + if (currentTestName === DEFAULTS.TEST_NAME || this.lastKnownTestName !== currentTestName) { + return + } + + const currentSuite = this.testReporter.getCurrentSuite() + if (!currentSuite) return + + const test = this.findTestInSuite(currentSuite, currentTestName) + if (test && test.state === TEST_STATE.PENDING) { + test.start = new Date() + test.end = null + this.updateTestState(test, TEST_STATE.RUNNING as TestStats['state']) + } + } + + /** + * Finalize all incomplete tests in a suite + */ + finalizeSuiteTests(suite: SuiteStats, testcases: Record): void { + suite.tests.forEach((test: any) => { + if (test.state === TEST_STATE.RUNNING && test.start) { + // Test was started but never finished - assume passed + test.state = TEST_STATE.PASSED + test.end = new Date() + test._duration = test.end.getTime() - (test.start?.getTime() || 0) + this.updateTestState(test, TEST_STATE.PASSED as TestStats['state']) + } else if (test.state === TEST_STATE.PENDING) { + const testcase = testcases[test.title] + if (testcase) { + const testState = determineTestState(testcase) + test.start = test.start || new Date() + this.updateTestState(test, testState, new Date(), parseFloat(testcase.time || '0') * 1000) + } else { + // Test never ran - mark as skipped + this.updateTestState(test, TEST_STATE.SKIPPED as TestStats['state'], new Date(), 0) + } + } + }) + } + + /** + * Reset internal state for new test files + */ + reset(): void { + this.lastKnownTestName = null + } +} diff --git a/packages/nightwatch-devtools/src/helpers/utils.ts b/packages/nightwatch-devtools/src/helpers/utils.ts new file mode 100644 index 0000000..f53a700 --- /dev/null +++ b/packages/nightwatch-devtools/src/helpers/utils.ts @@ -0,0 +1,180 @@ +/** + * Utility functions for test file discovery and metadata extraction + * Based on WDIO DevTools approach + */ + +import * as fs from 'node:fs' +import * as path from 'node:path' +import { parse as parseStackTrace } from 'stacktrace-parser' + +// File patterns for test file identification +const SPEC_FILE_PATTERN = /\/(test|spec|tests)\//i +const SPEC_FILE_RE = /\.(?:test|spec)\.[cm]?[jt]sx?$/i + +/** + * Find test file from stack trace (WDIO DevTools approach) + * Parses call stack to find the first frame that looks like a test file + */ +export function findTestFileFromStack(): string | undefined { + const stack = new Error().stack + if (!stack) { + return + } + + const frames = parseStackTrace(stack) + const testFrame = frames.find(frame => { + const file = frame.file + return file && + !file.includes('/node_modules/') && + !file.includes('') && + !file.includes('node:internal') && + !file.includes('/dist/') && + !file.includes('/index.js') && + (SPEC_FILE_PATTERN.test(file) || SPEC_FILE_RE.test(file)) + }) + + if (testFrame && testFrame.file) { + let filePath = testFrame.file + // Strip file:// protocol if present + if (filePath.startsWith('file://')) { + filePath = filePath.replace('file://', '') + } + // Remove line:col suffix + filePath = filePath.split(':')[0] + + // Verify file exists + if (fs.existsSync(filePath)) { + return filePath + } + } + return +} + +/** + * Extract suite and test names from test file using simple regex (lightweight approach) + * Falls back to simple regex matching instead of full AST parsing for performance + */ +export function extractTestMetadata(filePath: string): { + suiteTitle: string | null + testNames: string[] +} { + const result = { suiteTitle: null as string | null, testNames: [] as string[] } + + if (!fs.existsSync(filePath)) { + return result + } + + try { + const source = fs.readFileSync(filePath, 'utf-8') + + // Extract first describe() or suite() call + const suiteMatch = source.match(/(?:describe|suite|context)\s*\(\s*['"']([^'"']+)['"']/) + if (suiteMatch && suiteMatch[1]) { + result.suiteTitle = suiteMatch[1] + } + + // Extract all it() or test() calls + const testRegex = /(?:it|test|specify)\s*\(\s*['"']([^'"']+)['"']/g + let match + while ((match = testRegex.exec(source)) !== null) { + result.testNames.push(match[1]) + } + } catch (err) { + console.log(`[DEBUG] Failed to parse test file ${filePath}: ${(err as Error).message}`) + } + + return result +} + +/** + * Get call source info from stack trace (WDIO approach) + * Returns filename:line format for display + */ +export function getCallSourceFromStack(): { filePath: string | undefined, callSource: string } { + const stack = new Error().stack + if (!stack) { + return { filePath: undefined, callSource: 'unknown:0' } + } + + const frames = parseStackTrace(stack) + const userFrame = frames.find(frame => { + const file = frame.file + return file && + !file.includes('/node_modules/') && + !file.includes('') && + !file.includes('node:internal') && + !file.includes('/dist/') && + !file.includes('/index.js') + }) + + if (userFrame && userFrame.file) { + let filePath = userFrame.file + // Strip file:// protocol + if (filePath.startsWith('file://')) { + filePath = filePath.replace('file://', '') + } + + // Remove line:col from filePath + const cleanFilePath = filePath.split(':')[0] + + // Use full path with line number for callSource so Source tab can match it + const callSource = `${cleanFilePath}:${userFrame.lineNumber || 0}` + + return { filePath: cleanFilePath, callSource } + } + + return { filePath: undefined, callSource: 'unknown:0' } +} + +/** + * Find test file by searching workspace for matching filename + * Used when stack trace doesn't have the file yet (in beforeEach) + */ +export function findTestFileByName(filename: string, workspaceRoot?: string): string | undefined { + if (!filename || !workspaceRoot) { + return undefined + } + + // Clean up filename - remove extensions and normalize + const baseFilename = filename.replace(/\.[cm]?[jt]sx?$/, '') + + // Recursively search directories + function searchDir(dir: string, depth = 0): string | undefined { + if (depth > 5) return undefined // Limit recursion depth + + try { + const entries = fs.readdirSync(dir, { withFileTypes: true }) + + for (const entry of entries) { + const fullPath = path.join(dir, entry.name) + + // Skip node_modules and dist + if (entry.isDirectory()) { + if (entry.name === 'node_modules' || entry.name === 'dist' || entry.name === '.git') { + continue + } + // Recursively search subdirectories + const found = searchDir(fullPath, depth + 1) + if (found) return found + } else if (entry.isFile()) { + // Check if filename matches (with or without extension) + const nameMatch = entry.name === filename || + entry.name === `${baseFilename}.test.js` || + entry.name === `${baseFilename}.spec.js` || + entry.name === `${baseFilename}.test.ts` || + entry.name === `${baseFilename}.spec.ts` + + if (nameMatch) { + return fullPath + } + } + } + } catch (err) { + // Permission denied or other error, skip this directory + } + + return undefined + } + + return searchDir(workspaceRoot) +} diff --git a/packages/nightwatch-devtools/src/index.ts b/packages/nightwatch-devtools/src/index.ts new file mode 100644 index 0000000..9dc60f0 --- /dev/null +++ b/packages/nightwatch-devtools/src/index.ts @@ -0,0 +1,476 @@ +/** + * Nightwatch DevTools Plugin + * + * Integrates Nightwatch with WebdriverIO DevTools following the WDIO service pattern. + * Captures commands, network requests, and console logs during test execution in real-time. + */ + +import * as fs from 'node:fs' +import * as path from 'node:path' +import * as os from 'node:os' +import { start, stop } from '@wdio/devtools-backend' +import logger from '@wdio/logger' +import { remote } from 'webdriverio' +import { SessionCapturer } from './session.js' +import { TestReporter } from './reporter.js' +import { TestManager } from './helpers/testManager.js' +import { SuiteManager } from './helpers/suiteManager.js' +import { BrowserProxy } from './helpers/browserProxy.js' +import { + TraceType, + determineTestState, + type DevToolsOptions, + type NightwatchBrowser, + type TestStats +} from './types.js' +import { DEFAULTS, TIMING, TEST_STATE } from './constants.js' +import { findTestFileFromStack, findTestFileByName, extractTestMetadata } from './helpers/utils.js' + +const log = logger('@wdio/nightwatch-devtools') + +class NightwatchDevToolsPlugin { + private options: Required + private sessionCapturer!: SessionCapturer + private testReporter!: TestReporter + private testManager!: TestManager + private suiteManager!: SuiteManager + private browserProxy!: BrowserProxy + private isScriptInjected = false + #currentTest: any = null + #currentTestFile: string | null = null + #lastSessionId: string | null = null + #devtoolsBrowser?: WebdriverIO.Browser + #userDataDir?: string + + constructor(options: DevToolsOptions = {}) { + this.options = { + port: options.port ?? 3000, + hostname: options.hostname ?? 'localhost' + } + } + + /** + * Nightwatch Hook: before (global) + * Start the DevTools backend server + */ + async before() { + try { + log.info('🚀 Starting DevTools backend...') + const { server, port } = await start(this.options) + + // Update options with the actual port used (may differ if preferred port was busy) + this.options.port = port + const url = `http://${this.options.hostname}:${port}` + log.info(`✓ Backend started on port ${port}`) + log.info(``) + log.info(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`) + log.info(` 🌐 Opening DevTools UI in browser...`) + log.info(``) + log.info(` ${url}`) + log.info(``) + log.info(` ⏳ Waiting for UI to connect...`) + log.info(`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`) + log.info(``) + + // Open DevTools UI in a separate browser window using WDIO's method + try { + // Create unique user data directory for this instance to prevent conflicts + this.#userDataDir = path.join(os.tmpdir(), `nightwatch-devtools-${port}-${Date.now()}`) + + // Create the directory if it doesn't exist + if (!fs.existsSync(this.#userDataDir)) { + fs.mkdirSync(this.#userDataDir, { recursive: true }) + } + + this.#devtoolsBrowser = await remote({ + logLevel: 'error', // Show errors if browser fails to start + automationProtocol: 'devtools', + capabilities: { + browserName: 'chrome', + 'goog:chromeOptions': { + args: [ + '--window-size=1600,1200', + `--user-data-dir=${this.#userDataDir}`, + '--no-first-run', + '--no-default-browser-check' + ] + } + } + }) + + await this.#devtoolsBrowser.url(url) + log.info('✓ DevTools UI opened in separate browser window') + } catch (err) { + log.error(`Failed to open DevTools UI: ${(err as Error).message}`) + log.error(`Error stack: ${(err as Error).stack}`) + log.info(`Please manually open: ${url}`) + } + + // Wait for UI to connect + log.info(`Waiting ${TIMING.UI_CONNECTION_WAIT / 1000} seconds for UI to connect...`) + await new Promise(resolve => setTimeout(resolve, TIMING.UI_CONNECTION_WAIT)) + + log.info('Starting tests...') + + + } catch (err) { + log.error(`Failed to start backend: ${(err as Error).message}`) + throw err + } + } + + /** + * Nightwatch Hook: beforeEach + * Initialize session and inject script before each test + */ + async beforeEach(browser: NightwatchBrowser) { + const currentTest = (browser as any).currentTest + const testFile = (currentTest?.module || '').split('/').pop() || 'unknown' + const currentSessionId = (browser as any).sessionId + + // Check if browser session changed (happens with parallel workers) + if (currentSessionId && this.#lastSessionId && currentSessionId !== this.#lastSessionId) { + log.info(`Browser session changed - reinitializing for new worker`) + this.isScriptInjected = false + // Reset session capturer for new browser session + this.sessionCapturer = null as any + } + this.#lastSessionId = currentSessionId + + // Initialize on first test OR when browser session changes + if (!this.sessionCapturer) { + await new Promise(resolve => setTimeout(resolve, TIMING.INITIAL_CONNECTION_WAIT)) + + this.sessionCapturer = new SessionCapturer({ + port: this.options.port, + hostname: this.options.hostname + }, browser) + + const connected = await this.sessionCapturer.waitForConnection(3000) + if (!connected) { + log.error('❌ Worker WebSocket failed to connect!') + } else { + log.info('✓ Worker WebSocket connected') + } + + // Initialize managers + this.testReporter = new TestReporter((suitesData: any) => { + if (this.sessionCapturer) { + this.sessionCapturer.sendUpstream('suites', suitesData) + } + }) + + this.testManager = new TestManager(this.testReporter) + this.suiteManager = new SuiteManager(this.testReporter) + this.browserProxy = new BrowserProxy( + this.sessionCapturer, + this.testManager, + () => this.#currentTest + ) + + log.info('✓ Session initialized') + + // Send initial metadata + const capabilities = browser.capabilities || {} + this.sessionCapturer.sendUpstream('metadata', { + type: TraceType.Testrunner, + capabilities, + options: {}, + url: '' + }) + } + + // Get current test info and find test file + if (currentTest) { + const testFile = (currentTest.module || '').split('/').pop() || currentTest.module || DEFAULTS.FILE_NAME + + // Find test file: try stack trace first, then search workspace + let fullPath: string | null = findTestFileFromStack() || null + const cachedPath = this.browserProxy.getCurrentTestFullPath() + + // Only use cached path if it matches the current testFile + if (!fullPath && cachedPath && cachedPath.includes(testFile)) { + fullPath = cachedPath + } + + if (!fullPath && testFile) { + const workspaceRoot = process.cwd() + const possiblePaths = [ + path.join(workspaceRoot, 'example/tests', testFile + '.js'), + path.join(workspaceRoot, 'example/tests', testFile), + path.join(workspaceRoot, 'tests', testFile + '.js'), + path.join(workspaceRoot, 'test', testFile + '.js'), + path.join(workspaceRoot, testFile + '.js'), + ] + + for (const possiblePath of possiblePaths) { + if (fs.existsSync(possiblePath)) { + fullPath = possiblePath + break + } + } + } + + // Extract suite title and test metadata + let suiteTitle = testFile + let testNames: string[] = [] + if (fullPath) { + const metadata = extractTestMetadata(fullPath) + if (metadata.suiteTitle) { + suiteTitle = metadata.suiteTitle + } + testNames = metadata.testNames + } + + // Get or create suite for this test file + const currentSuite = this.suiteManager.getOrCreateSuite(testFile, suiteTitle, fullPath, testNames) + + // Capture source file for display + if (fullPath && fullPath.includes('/')) { + this.sessionCapturer.captureSource(fullPath).catch(() => {}) + } + + // Handle running test from previous beforeEach + const runningTest = currentSuite.tests.find( + (t: any) => typeof t !== 'string' && t.state === TEST_STATE.RUNNING + ) as TestStats | undefined + + if (runningTest) { + const currentTest = (browser as any).currentTest + const testcases = currentTest?.results?.testcases || {} + + if (testcases[runningTest.title]) { + const testcase = testcases[runningTest.title] + const testState = determineTestState(testcase) + runningTest.state = testState + runningTest.end = new Date() + runningTest._duration = parseFloat(testcase.time || '0') * 1000 + + this.testManager.updateTestState(runningTest, testState) + this.testManager.markTestAsProcessed(testFile, runningTest.title) + + await new Promise(resolve => setTimeout(resolve, TIMING.UI_RENDER_DELAY)) + } else { + const endTime = new Date() + const duration = endTime.getTime() - (runningTest.start?.getTime() || 0) + + this.testManager.updateTestState(runningTest, TEST_STATE.PASSED as TestStats['state'], endTime, duration) + this.testManager.markTestAsProcessed(testFile, runningTest.title) + + await new Promise(resolve => setTimeout(resolve, TIMING.UI_RENDER_DELAY)) + } + } + + // Find and start next test + const processedTests = this.testManager.getProcessedTests(testFile) + const currentTestName = testNames.find(name => !processedTests.has(name)) + + if (currentTestName) { + // Mark suite as running on first test + if (processedTests.size === 0) { + this.suiteManager.markSuiteAsRunning(currentSuite) + } + + const test = this.testManager.findTestInSuite(currentSuite, currentTestName) + if (test) { + test.state = TEST_STATE.RUNNING as TestStats['state'] + test.start = new Date() + test.end = null + this.testReporter.onTestStart(test) + + // Use the real test UID for command tracking (no temporary UIDs!) + this.#currentTest = test + + await new Promise(resolve => setTimeout(resolve, TIMING.TEST_START_DELAY)) + } else { + // This should never happen if suite was properly initialized + log.warn(`Test "${currentTestName}" not found in suite "${currentSuite.title}" - skipping command tracking`) + this.#currentTest = null + } + } + + this.#currentTestFile = testFile + + // Wrap browser.url for script injection + if (!this.isScriptInjected) { + this.browserProxy.wrapUrlMethod(browser) + this.isScriptInjected = true + } + + // Reset command tracking + this.browserProxy.resetCommandTracking() + } + + // Wrap browser commands + this.browserProxy.wrapBrowserCommands(browser) + } + + /** + * Nightwatch Hook: afterEach + * Capture trace data after each test + */ + async afterEach(browser: NightwatchBrowser) { + if (browser && this.sessionCapturer) { + try { + const currentTest = (browser as any).currentTest + const results = currentTest?.results || {} + const testFile = (currentTest.module || '').split('/').pop() || DEFAULTS.FILE_NAME + const testcases = results.testcases || {} + const testcaseNames = Object.keys(testcases) + + const currentSuite = this.suiteManager.getSuite(testFile) + if (currentSuite) { + const processedTests = this.testManager.getProcessedTests(testFile) + + // Handle case with no results yet + if (testcaseNames.length === 0) { + const runningTest = currentSuite.tests.find( + (t: any) => typeof t !== 'string' && t.state === TEST_STATE.RUNNING + ) as TestStats | undefined + + if (runningTest && !processedTests.has(runningTest.title)) { + const testState: TestStats['state'] = (results.errors > 0 || results.failed > 0) ? + TEST_STATE.FAILED : TEST_STATE.PASSED + const endTime = new Date() + const duration = endTime.getTime() - (runningTest.start?.getTime() || 0) + + this.testManager.updateTestState(runningTest, testState, endTime, duration) + this.testManager.markTestAsProcessed(testFile, runningTest.title) + } + } else { + // Process tests with results + const unprocessedTests = testcaseNames.filter(name => !processedTests.has(name)) + + for (const currentTestName of unprocessedTests) { + const testcase = testcases[currentTestName] + const testState = determineTestState(testcase) + + const test = this.testManager.findTestInSuite(currentSuite, currentTestName) + if (test) { + this.testManager.updateTestState(test, testState, new Date(), parseFloat(testcase.time || '0') * 1000) + } + + this.testManager.markTestAsProcessed(testFile, currentTestName) + } + + // Check if suite is complete + if (processedTests.size === testcaseNames.length) { + this.suiteManager.finalizeSuite(currentSuite) + await new Promise(resolve => setTimeout(resolve, TIMING.SUITE_COMPLETE_DELAY)) + } + } + } + + await this.sessionCapturer.captureTrace(browser) + } catch (err) { + log.error(`Failed to capture trace: ${(err as Error).message}`) + } + } + } + + /** + * Nightwatch Hook: after (global) + * Keep the application alive until the browser window is closed + */ + async after(browser?: NightwatchBrowser) { + try { + // Process any remaining incomplete suites + const currentTest = (browser as any)?.currentTest + const testcases = currentTest?.results?.testcases || {} + + for (const [testFile, suite] of this.suiteManager.getAllSuites().entries()) { + this.testManager.finalizeSuiteTests(suite, testcases) + await new Promise(resolve => setTimeout(resolve, TIMING.SUITE_COMPLETE_DELAY)) + this.suiteManager.finalizeSuite(suite) + } + + await new Promise(resolve => setTimeout(resolve, TIMING.SUITE_COMPLETE_DELAY)) + + log.info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━') + log.info('✅ Tests complete!') + log.info('') + log.info(` DevTools UI: http://${this.options.hostname}:${this.options.port}`) + log.info('') + log.info('💡 Please close the DevTools browser window to finish...') + log.info(' (or press Ctrl+C to exit and keep browser open)') + log.info('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━') + + // Keep the process alive by polling the devtools browser (WDIO pattern) + // When browser closes naturally, we clean up. + // When Ctrl+C happens, browser survives and we skip cleanup. + if (this.#devtoolsBrowser) { + logger.setLevel('devtools', 'warn') + let exitBySignal = false + + // Handle Ctrl+C: exit process but let browser survive + const signalHandler = () => { + exitBySignal = true + log.info('\n✓ Exiting... Browser window will remain open') + process.exit(0) + } + process.once('SIGINT', signalHandler) + process.once('SIGTERM', signalHandler) + + // Poll browser until it closes + while (true) { + try { + await this.#devtoolsBrowser.getTitle() + await new Promise((res) => setTimeout(res, TIMING.BROWSER_POLL_INTERVAL)) + } catch { + if (!exitBySignal) { + log.info('Browser window closed, stopping DevTools app') + break + } + } + } + + // Only clean up if browser was closed (not Ctrl+C) + if (!exitBySignal) { + try { + await this.#devtoolsBrowser.deleteSession() + } catch (err: any) { + log.warn('Session already closed or could not be deleted:', err.message) + } + + // Stop the backend + log.info('🛑 Stopping DevTools backend...') + await stop() + log.info('✓ Backend stopped') + } + } + } catch (err) { + log.error(`Failed to stop backend: ${(err as Error).message}`) + } + } +} + +/** + * Factory function to create the plugin instance + * This allows simple usage: require('@wdio/nightwatch-devtools').default + */ +export default function createNightwatchDevTools(options?: DevToolsOptions) { + const plugin = new NightwatchDevToolsPlugin(options) + + return { + // Set long timeout to allow user to review DevTools UI + // The after() hook waits for the browser window to be closed + asyncHookTimeout: 3600000, // 1 hour + + before: async function(this: any) { + await plugin.before() + }, + beforeEach: async function(this: any, browser: NightwatchBrowser) { + await plugin.beforeEach(browser) + }, + afterEach: async function(this: any, browser: NightwatchBrowser) { + await plugin.afterEach(browser) + }, + after: async function(this: any) { + await plugin.after() + } + } +} + +// Also export the class for advanced usage +export { NightwatchDevToolsPlugin } + diff --git a/packages/nightwatch-devtools/src/reporter.ts b/packages/nightwatch-devtools/src/reporter.ts new file mode 100644 index 0000000..c638288 --- /dev/null +++ b/packages/nightwatch-devtools/src/reporter.ts @@ -0,0 +1,289 @@ +import logger from '@wdio/logger' +import { extractTestMetadata } from './helpers/utils.js' +import type { SuiteStats, TestStats } from './types.js' + +const log = logger('@wdio/nightwatch-devtools:Reporter') + +// Track test occurrences to generate stable UIDs +const signatureCounters = new Map() + +/** + * Generate stable UID based on test/suite metadata (WDIO approach) + * Use only stable identifiers (file + fullTitle) that don't change between runs + */ +function generateStableUid(item: SuiteStats | TestStats): string { + const rawItem = item as any + + // Use file and fullTitle as stable identifiers + // DO NOT use cid or parent as they can vary based on run context + const parts = [rawItem.file || '', String(rawItem.fullTitle || item.title)] + + const signature = parts.join('::') + const count = signatureCounters.get(signature) || 0 + signatureCounters.set(signature, count + 1) + + if (count > 0) { + parts.push(String(count)) + } + + // Generate hash for stable, short UIDs + const hash = parts + .join('::') + .split('') + .reduce((acc, char) => { + return ((acc << 5) - acc + char.charCodeAt(0)) | 0 + }, 0) + + return `stable-${Math.abs(hash).toString(36)}` +} + +/** + * Reset counters at the start of each test run + */ +function resetSignatureCounters() { + signatureCounters.clear() +} + +export class TestReporter { + #report: (data: any) => void + #currentSpecFile?: string + #testNamesCache = new Map() + #currentSuite?: SuiteStats + #allSuites: SuiteStats[] = [] + + constructor(report: (data: any) => void) { + this.#report = report + resetSignatureCounters() + } + + /** + * Generate stable UID for test/suite - public method (WDIO approach) + */ + generateStableUid(filePath: string, name: string): string { + const parts = [filePath, name] + const signature = parts.join('::') + const count = signatureCounters.get(signature) || 0 + signatureCounters.set(signature, count + 1) + + if (count > 0) { + parts.push(String(count)) + } + + // Generate hash for stable, short UIDs + const hash = parts + .join('::') + .split('') + .reduce((acc, char) => { + return ((acc << 5) - acc + char.charCodeAt(0)) | 0 + }, 0) + + return `stable-${Math.abs(hash).toString(36)}` + } + + /** + * Called when a suite starts + */ + onSuiteStart(suiteStats: SuiteStats) { + this.#currentSpecFile = suiteStats.file + this.#currentSuite = suiteStats + + // Generate stable UID only if not already set + if (!suiteStats.uid) { + suiteStats.uid = generateStableUid(suiteStats) + } + + // Extract test names from source file + if (suiteStats.file && !this.#testNamesCache.has(suiteStats.file)) { + const metadata = extractTestMetadata(suiteStats.file) + const testNames = metadata.testNames + if (testNames.length > 0) { + this.#testNamesCache.set(suiteStats.file, testNames) + log.info(`📝 Extracted ${testNames.length} test names from ${suiteStats.file}`) + } + } + + this.#allSuites.push(suiteStats) + this.#sendUpstream() + } + + /** + * Update the suites data (send to UI) + */ + updateSuites() { + this.#sendUpstream() + } + + /** + * Get the current suite + */ + getCurrentSuite(): SuiteStats | undefined { + return this.#currentSuite + } + + /** + * Called when a test starts + */ + onTestStart(testStats: TestStats) { + // Generate stable UID (hashed, so consistent even if called multiple times) + if (!testStats.uid || testStats.uid.includes('temp-')) { + testStats.uid = generateStableUid(testStats) + } + + // Search for test by title within parent suite + for (const suite of this.#allSuites) { + const testIndex = suite.tests.findIndex( + (t) => { + if (typeof t === 'string') return false + // Match by title and parent suite + return t.title === testStats.title && t.parent === suite.uid + } + ) + if (testIndex !== -1) { + // Update existing test + suite.tests[testIndex] = testStats + this.#sendUpstream() + return + } + } + + // Test not found in any suite, add it to current suite (legacy behavior) + if (this.#currentSuite) { + this.#currentSuite.tests.push(testStats) + } + + this.#sendUpstream() + } + + /** + * Called when a test ends + */ + onTestEnd(testStats: TestStats) { + // Search all suites for this test (not just current suite) + for (const suite of this.#allSuites) { + const testIndex = suite.tests.findIndex( + (t) => (typeof t === 'string' ? t : t.uid) === testStats.uid + ) + if (testIndex !== -1) { + suite.tests[testIndex] = testStats + break + } + } + + this.#sendUpstream() + } + + /** + * Called when a test passes + */ + onTestPass(testStats: TestStats) { + // Search all suites for this test (not just current suite) + for (const suite of this.#allSuites) { + const testIndex = suite.tests.findIndex( + (t) => (typeof t === 'string' ? t : t.uid) === testStats.uid + ) + if (testIndex !== -1) { + suite.tests[testIndex] = testStats + break + } + } + + this.#sendUpstream() + } + + /** + * Called when a test fails + */ + onTestFail(testStats: TestStats) { + // Search all suites for this test (not just current suite) + for (const suite of this.#allSuites) { + const testIndex = suite.tests.findIndex( + (t) => (typeof t === 'string' ? t : t.uid) === testStats.uid + ) + if (testIndex !== -1) { + suite.tests[testIndex] = testStats + break + } + } + + this.#sendUpstream() + } + + /** + * Called when a suite ends - create skipped tests + */ + onSuiteEnd(suiteStats: SuiteStats) { + // Get all test names from cache + const cachedNames = this.#testNamesCache.get(suiteStats.file) || [] + const processedTestNames = new Set( + suiteStats.tests + .map((t) => (typeof t === 'string' ? t : t.title)) + .filter((title): title is string => Boolean(title)) + ) + + // Create skipped tests for tests that didn't run + cachedNames.forEach((testName) => { + if (!processedTestNames.has(testName)) { + const skippedTest: TestStats = { + uid: generateStableUid({ + file: suiteStats.file, + fullTitle: `${suiteStats.title} ${testName}` + } as TestStats), + cid: '0-0', + title: testName, + fullTitle: `${suiteStats.title} ${testName}`, + parent: suiteStats.uid, + state: 'skipped', + start: new Date(), + end: new Date(), + type: 'test', + file: suiteStats.file, + retries: 0, + _duration: 0, + hooks: [] + } + + suiteStats.tests.push(skippedTest) + log.info(`Created skipped test "${testName}" (never executed)`) + } + }) + + this.#sendUpstream() + } + + /** + * Update a specific suite and send to UI (used when updating suite title) + */ + updateSuite(suiteStats: SuiteStats) { + // Find and remove the old suite by file + const index = this.#allSuites.findIndex(s => s.file === suiteStats.file) + if (index !== -1) { + // Remove the old suite entry (with old UID) + this.#allSuites.splice(index, 1) + } + // Add the updated suite with new UID + this.#allSuites.push(suiteStats) + // Update current suite reference + this.#currentSuite = suiteStats + this.#sendUpstream() + } + + #sendUpstream() { + // Convert suites to WDIO format: array of objects with UID as key + const payload: Record[] = [] + + for (const suite of this.#allSuites) { + if (suite && suite.uid) { + // Each suite becomes an object with its UID as the key + payload.push({ [suite.uid]: suite }) + } + } + + if (payload.length > 0) { + this.#report(payload) + } + } + + get report() { + return this.#allSuites + } +} diff --git a/packages/nightwatch-devtools/src/session.ts b/packages/nightwatch-devtools/src/session.ts new file mode 100644 index 0000000..5bc586f --- /dev/null +++ b/packages/nightwatch-devtools/src/session.ts @@ -0,0 +1,573 @@ +import fs from 'node:fs/promises' +import path from 'node:path' +import { createRequire } from 'node:module' +import logger from '@wdio/logger' +import { WebSocket } from 'ws' +import { CONSOLE_METHODS, LOG_SOURCES, ANSI_REGEX, LOG_LEVEL_PATTERNS } from './constants.js' +import type { CommandLog, ConsoleLog, LogLevel, NightwatchBrowser } from './types.js' +import { getCapturePerformanceScript } from './helpers/capturePerformance.js' + +const require = createRequire(import.meta.url) +const log = logger('@wdio/nightwatch-devtools:SessionCapturer') + +/** + * Strip ANSI escape codes from text + */ +const stripAnsiCodes = (text: string): string => text.replace(ANSI_REGEX, '') + +/** + * Detect log level from text content + */ +const detectLogLevel = (text: string): LogLevel => { + const cleanText = stripAnsiCodes(text).toLowerCase() + + for (const { level, pattern } of LOG_LEVEL_PATTERNS) { + if (pattern.test(cleanText)) { + return level + } + } + + return 'log' +} + +/** + * Create a console log entry + */ +const createConsoleLogEntry = ( + type: LogLevel, + args: any[], + source: string +): ConsoleLog => ({ + timestamp: Date.now(), + type, + args, + source +}) + +export class SessionCapturer { + #ws: WebSocket | undefined + #originalConsoleMethods: Record< + (typeof CONSOLE_METHODS)[number], + typeof console.log + > + #originalProcessMethods: { + stdoutWrite: typeof process.stdout.write + stderrWrite: typeof process.stderr.write + } + #isCapturingConsole = false + #browser: NightwatchBrowser | undefined + #commandCounter = 0 // Sequential ID for commands + #sentCommandIds = new Set() // Track which commands have been sent + + commandsLog: CommandLog[] = [] + sources = new Map() + consoleLogs: ConsoleLog[] = [] + mutations: any[] = [] + traceLogs: string[] = [] + networkRequests: any[] = [] + metadata?: any + + constructor(devtoolsOptions: { hostname?: string; port?: number } = {}, browser?: NightwatchBrowser) { + const { port, hostname } = devtoolsOptions + this.#browser = browser + if (hostname && port) { + this.#ws = new WebSocket(`ws://${hostname}:${port}/worker`) + + this.#ws.on('open', () => { + log.info('✓ Worker WebSocket connected to backend') + }) + + this.#ws.on('error', (err: unknown) => + log.error( + `Couldn't connect to devtools backend: ${(err as Error).message}` + ) + ) + + this.#ws.on('close', () => { + log.info('Worker WebSocket disconnected') + }) + } + + this.#originalConsoleMethods = { + log: console.log, + info: console.info, + warn: console.warn, + error: console.error + } + + this.#originalProcessMethods = { + stdoutWrite: process.stdout.write.bind(process.stdout), + stderrWrite: process.stderr.write.bind(process.stderr) + } + + this.#patchConsole() + this.#interceptProcessStreams() + } + + #patchConsole() { + // Temporarily disable console patching to prevent infinite loops + // We can re-enable this later with better filtering + return + + CONSOLE_METHODS.forEach((method) => { + const originalMethod = this.#originalConsoleMethods[method] + console[method] = (...consoleArgs: any[]) => { + // Skip capturing if we're already in a capture operation (prevent infinite recursion) + if (this.#isCapturingConsole) { + return originalMethod.apply(console, consoleArgs) + } + + // Skip capturing internal framework logs (logger messages) + const firstArg = String(consoleArgs[0] || '') + if (firstArg.includes('@wdio/') || firstArg.includes('INFO') || firstArg.includes('WARN') || firstArg.includes('ERROR')) { + return originalMethod.apply(console, consoleArgs) + } + + const serializedArgs = consoleArgs.map((arg) => + typeof arg === 'object' && arg !== null + ? (() => { + try { + return JSON.stringify(arg) + } catch { + return String(arg) + } + })() + : String(arg) + ) + + // Set flag before capturing to prevent recursion + this.#isCapturingConsole = true + + const logEntry = createConsoleLogEntry( + method, + serializedArgs, + LOG_SOURCES.TEST + ) + this.consoleLogs.push(logEntry) + this.sendUpstream('consoleLogs', [logEntry]) + + const result = originalMethod.apply(console, consoleArgs) + + // Reset flag after everything is done + this.#isCapturingConsole = false + return result + } + }) + } + + #interceptProcessStreams() { + // Temporarily disable stream interception to prevent infinite loops + // We can re-enable this later with better filtering + return + + // Regex to detect spinner/progress characters + const spinnerRegex = /[\u280b\u2819\u2839\u2838\u283c\u2834\u2826\u2827\u2807\u280f]/ + + const captureTerminalOutput = (outputData: string | Uint8Array) => { + const outputText = + typeof outputData === 'string' ? outputData : outputData.toString() + if (!outputText?.trim()) { + return + } + + outputText + .split('\n') + .filter((line) => line.trim()) + .forEach((line) => { + // Skip lines with spinner characters to avoid flooding logs + if (spinnerRegex.test(line)) { + return + } + + // Strip ANSI codes and check if there's actual content + const cleanedLine = stripAnsiCodes(line).trim() + if (!cleanedLine) { + return + } + + const logEntry = createConsoleLogEntry( + detectLogLevel(cleanedLine), + [cleanedLine], + LOG_SOURCES.TERMINAL + ) + this.consoleLogs.push(logEntry) + this.sendUpstream('consoleLogs', [logEntry]) + }) + } + + const interceptStreamWrite = ( + stream: NodeJS.WriteStream, + originalWriteMethod: (...args: any[]) => boolean + ) => { + const capturer = this + stream.write = function (chunk: any, ...additionalArgs: any[]): boolean { + const writeResult = originalWriteMethod.call( + stream, + chunk, + ...additionalArgs + ) + if (chunk && !capturer.#isCapturingConsole) { + captureTerminalOutput(chunk) + } + return writeResult + } as any + } + + interceptStreamWrite( + process.stdout, + this.#originalProcessMethods.stdoutWrite + ) + interceptStreamWrite( + process.stderr, + this.#originalProcessMethods.stderrWrite + ) + } + + #restoreConsole() { + CONSOLE_METHODS.forEach((method) => { + console[method] = this.#originalConsoleMethods[method] + }) + } + + cleanup() { + this.#restoreConsole() + } + + get isReportingUpstream() { + return Boolean(this.#ws) && this.#ws?.readyState === WebSocket.OPEN + } + + /** + * Wait for WebSocket to connect + */ + async waitForConnection(timeoutMs: number = 5000): Promise { + if (!this.#ws) { + return false + } + + if (this.#ws.readyState === WebSocket.OPEN) { + return true + } + + return new Promise((resolve) => { + const timeout = setTimeout(() => { + log.warn(`WebSocket connection timeout after ${timeoutMs}ms`) + resolve(false) + }, timeoutMs) + + this.#ws!.once('open', () => { + clearTimeout(timeout) + resolve(true) + }) + + this.#ws!.once('error', () => { + clearTimeout(timeout) + resolve(false) + }) + }) + } + + /** + * Capture a command execution + * @returns true if command was captured, false if it was skipped as a duplicate + */ + async captureCommand( + command: string, + args: any[], + result: any, + error: Error | undefined, + testUid?: string, + callSource?: string, + timestamp?: number + ): Promise { + // Serialize error properly (Error objects don't JSON.stringify well) + const serializedError = error ? { + name: error.name, + message: error.message, + stack: error.stack + } : undefined + + const commandId = this.#commandCounter++ + const commandLogEntry: CommandLog & { _id?: number } = { + _id: commandId, // Internal ID for tracking + command, + args, + result, + error: serializedError as any, + timestamp: timestamp || Date.now(), + callSource, + testUid + } + + // IMPORTANT: Push to commandsLog FIRST (synchronously) + // so it's available immediately for sending + this.commandsLog.push(commandLogEntry) + + // THEN do async performance capture for navigation commands + const isNavigationCommand = ['url', 'navigate', 'navigateTo'].some(cmd => + command.toLowerCase().includes(cmd.toLowerCase()) + ) + + if (isNavigationCommand && this.#browser && !error) { + // Do this async work in the background without blocking + // Update the commandLogEntry that's already in the array + this.#capturePerformanceData(commandLogEntry, args).catch((err) => { + console.log(`⚠️ Failed to capture performance data: ${(err as Error).message}`) + }) + } + + return true + } + + async #capturePerformanceData(commandLogEntry: CommandLog & { _id?: number }, args: any[]) { + // Wait a bit for page to load + await new Promise(resolve => setTimeout(resolve, 500)) + + // Execute script to capture performance data + // Nightwatch's execute() requires a function, not a string + const performanceData = await this.#browser!.execute(function() { + // @ts-ignore - executed in browser context + const performance = window.performance; + // @ts-ignore + const navigation = performance.getEntriesByType?.('navigation')?.[0]; + // @ts-ignore + const resources = performance.getEntriesByType?.('resource') || []; + + return { + navigation: navigation ? { + // @ts-ignore + url: window.location.href, + timing: { + loadTime: navigation.loadEventEnd - navigation.fetchStart, + domContentLoaded: navigation.domContentLoadedEventEnd - navigation.fetchStart, + firstPaint: performance.getEntriesByType?.('paint')?.[0]?.startTime || 0 + } + } : null, + resources: resources.map((r: any) => ({ + name: r.name, + type: r.initiatorType, + size: r.transferSize || r.decodedBodySize || 0, + duration: r.duration + })), + // @ts-ignore + cookies: (function() { + // @ts-ignore - executed in browser context + try { return document.cookie; } catch (e) { return ''; } + })(), + documentInfo: { + // @ts-ignore + title: document.title, + // @ts-ignore + url: window.location.href, + // @ts-ignore + referrer: document.referrer + } + }; + }) + + // Nightwatch returns {value: result} or just the result directly + let data: any + if (performanceData && typeof performanceData === 'object') { + // Check if it has a 'value' property (WebDriver format) + if ('value' in performanceData) { + data = (performanceData as any).value + } else { + // It might be the data directly + data = performanceData + } + } + + if (data && data.navigation) { + commandLogEntry.performance = { + navigation: data.navigation, + resources: data.resources + } + commandLogEntry.cookies = data.cookies + commandLogEntry.documentInfo = data.documentInfo + + // Always set result with performance data for consistency + commandLogEntry.result = { + url: args[0], + loadTime: data.navigation?.timing?.loadTime, + resources: data.resources, + resourceCount: data.resources?.length, + cookies: data.cookies, + title: data.documentInfo?.title + } + + console.log(`✓ Captured performance data: ${data.resources?.length || 0} resources, load time: ${data.navigation?.timing?.loadTime || 0}ms`) + } + } + + /** + * Send a command to the UI (only if not already sent) + */ + sendCommand(command: CommandLog & { _id?: number }) { + if (command._id !== undefined && !this.#sentCommandIds.has(command._id)) { + this.#sentCommandIds.add(command._id) + // Remove internal ID before sending + const { _id, ...commandToSend } = command + this.sendUpstream('commands', [commandToSend]) + } + } + + /** + * Capture test source code + */ + async captureSource(filePath: string) { + if (!this.sources.has(filePath)) { + try { + const sourceCode = await fs.readFile(filePath, 'utf-8') + this.sources.set(filePath, sourceCode.toString()) + this.sendUpstream('sources', { [filePath]: sourceCode.toString() }) + } catch (err) { + log.warn(`Failed to read source file ${filePath}: ${(err as Error).message}`) + } + } + } + + /** + * Send data upstream to backend + */ + sendUpstream(event: string, data: any) { + // Check if WebSocket is open and ready + if (!this.#ws || this.#ws.readyState !== WebSocket.OPEN) { + // Use ORIGINAL process methods to avoid infinite recursion from console patching + this.#originalProcessMethods.stderrWrite(`[SESSION] WebSocket not ready (state: ${this.#ws?.readyState}), cannot send ${event}\n`) + return + } + + try { + // IMPORTANT: WDIO backend expects {scope, data} format, NOT {event, data} + const payload = JSON.stringify({ scope: event, data }) + // Use ORIGINAL process methods instead of console.log to avoid recursion + this.#originalProcessMethods.stdoutWrite(`[SESSION] Sending ${event} upstream, data size: ${payload.length} bytes\n`) + this.#ws.send(payload) + } catch (err) { + this.#originalProcessMethods.stderrWrite(`[SESSION] Failed to send ${event}: ${(err as Error).message}\n`) + } + } + + /** + * Check if WebSocket connection is still open + * Used by the after() hook to wait for browser window close + */ + isConnected(): boolean { + return this.#ws?.readyState === WebSocket.OPEN + } + + /** + * Inject the WDIO devtools script into the browser page + */ + async injectScript(browser: NightwatchBrowser) { + try { + // Load the preload script + const scriptPath = require.resolve('@wdio/devtools-script') + const scriptDir = path.dirname(scriptPath) + const preloadScriptPath = path.join(scriptDir, 'script.js') + let scriptContent = await fs.readFile(preloadScriptPath, 'utf-8') + + log.info(`Script path: ${preloadScriptPath}`) + log.info(`Script size: ${scriptContent.length} bytes`) + + // The script contains top-level await - wrap the entire script in async IIFE before injection + scriptContent = `(async function() { ${scriptContent} })()` + + // Inject using script element - synchronous check after timeout + const injectionScript = ` + const script = document.createElement('script'); + script.textContent = arguments[0]; + document.head.appendChild(script); + return true; + ` + + const injectResult = await browser.execute(injectionScript, [scriptContent]) + log.info(`Injection command executed: ${JSON.stringify(injectResult)}`) + + // Wait for script to execute + await browser.pause(300) + + // Check if collector exists using string-based execute + const checkScript = 'return typeof window.wdioTraceCollector !== "undefined"' + const checkResult = await browser.execute(checkScript) + + // Nightwatch wraps results in { value: ... } + const hasCollector = (checkResult as any)?.value === true + + log.info(`Collector check result: ${JSON.stringify(checkResult)}, hasCollector: ${hasCollector}`) + + if (hasCollector) { + log.info('✓ Devtools script injected successfully') + } else { + log.warn(`Script injection may have failed - collector not found`) + } + } catch (err) { + log.error(`Failed to inject script: ${(err as Error).message}`) + throw err + } + } + + /** + * Capture trace data from the browser (network requests, console logs, etc.) + */ + async captureTrace(browser: NightwatchBrowser) { + try { + // Check if the collector exists in the browser - access .value + const checkResult = await browser.execute('return typeof window.wdioTraceCollector !== "undefined"') + const collectorExists = (checkResult as any)?.value === true + + if (!collectorExists) { + log.warn('wdioTraceCollector not found - script may not have been injected') + return + } + + // Get trace data from the collector - access .value + const result = await browser.execute(` + if (typeof window.wdioTraceCollector === 'undefined') { + return null; + } + return window.wdioTraceCollector.getTraceData(); + `) + + const traceData = (result as any)?.value + + if (!traceData) { + return + } + + const { mutations, traceLogs, consoleLogs, networkRequests, metadata } = traceData + + // Send network requests + if (Array.isArray(networkRequests) && networkRequests.length > 0) { + this.networkRequests.push(...networkRequests) + this.sendUpstream('networkRequests', networkRequests) + log.info(`✓ Captured ${networkRequests.length} network requests`) + } + + // Send console logs from browser + if (Array.isArray(consoleLogs) && consoleLogs.length > 0) { + this.consoleLogs.push(...consoleLogs) + this.sendUpstream('consoleLogs', consoleLogs) + } + + // Send mutations + if (Array.isArray(mutations) && mutations.length > 0) { + this.mutations.push(...mutations) + this.sendUpstream('mutations', mutations) + } + + // Send trace logs + if (Array.isArray(traceLogs) && traceLogs.length > 0) { + this.traceLogs.push(...traceLogs) + this.sendUpstream('logs', traceLogs) + } + + // Update metadata + if (metadata) { + this.metadata = { ...this.metadata, ...metadata } + } + } catch (err) { + log.error(`Failed to capture trace: ${(err as Error).message}`) + } + } +} diff --git a/packages/nightwatch-devtools/src/types.ts b/packages/nightwatch-devtools/src/types.ts new file mode 100644 index 0000000..a6c01f5 --- /dev/null +++ b/packages/nightwatch-devtools/src/types.ts @@ -0,0 +1,182 @@ +export interface PerformanceData { + navigation?: { + url: string + timing: { + loadTime?: number + domReady?: number + responseTime?: number + dnsLookup?: number + tcpConnection?: number + serverResponse?: number + } + } + resources?: Array<{ + url: string + duration: number + size: number + type: string + startTime: number + responseEnd: number + }> +} + +export interface DocumentInfo { + url: string + title: string + headers: { + userAgent: string + language: string + platform: string + } + documentInfo: { + readyState: string + referrer: string + characterSet: string + } +} + +export interface CommandLog { + command: string + args: any[] + result?: any + error?: Error + timestamp: number + callSource?: string + screenshot?: string + testUid?: string + performance?: PerformanceData + cookies?: string + documentInfo?: DocumentInfo +} + +export enum TraceType { + Testrunner = 'testrunner' +} + +export type LogLevel = 'trace' | 'debug' | 'log' | 'info' | 'warn' | 'error' + +export interface ConsoleLog { + timestamp: number + type: LogLevel + args: any[] + source: string +} + +export interface TestStats { + uid: string + cid: string + title: string + fullTitle: string + parent: string + state: 'passed' | 'failed' | 'skipped' | 'pending' | 'running' + start: Date + end: Date | null + type: 'test' + file: string + retries: number + _duration: number + error?: Error + hooks?: any[] +} + +/** + * Nightwatch test case result from results.testcases + */ +export interface NightwatchTestCase { + passed: number + failed: number + errors: number + skipped: number + time: string + assertions: any[] +} + +/** + * Determine test state from Nightwatch testcase results + */ +export function determineTestState(testcase: NightwatchTestCase): 'passed' | 'failed' | 'skipped' { + if (testcase.passed === 0 && testcase.failed === 0) { + return 'skipped' + } + return testcase.passed > 0 && testcase.failed === 0 ? 'passed' : 'failed' +} + +export interface SuiteStats { + uid: string + cid: string + title: string + fullTitle: string + type: 'suite' + file: string + start: Date + state?: 'pending' | 'running' | 'passed' | 'failed' | 'skipped' + end?: Date | null + tests: (string | TestStats)[] + suites: SuiteStats[] + hooks: any[] + _duration: number +} + +export interface Metadata { + type: TraceType + url?: string + options?: any + capabilities?: any + viewport?: any +} + +export interface TraceLog { + mutations: any[] + logs: string[] + consoleLogs: ConsoleLog[] + networkRequests: any[] + metadata: Metadata + commands: CommandLog[] + sources: Record + suites: Record[] +} + +export interface DevToolsOptions { + port?: number + hostname?: string +} + +export interface NightwatchBrowser { + url: (url: string) => Promise + execute: (script: string | Function, args?: any[]) => Promise + executeAsync: (script: Function, args?: any[]) => Promise + pause: (ms: number) => Promise + capabilities?: Record + sessionId?: string + driver?: any +} + +export interface NetworkRequest { + id: string + url: string + method: string + headers?: Record + cookies?: any[] + status?: number + statusText?: string + timestamp: number + startTime: number + endTime?: number + time?: number + type: string + requestHeaders?: Record + responseHeaders?: Record + navigation?: string + redirectChain?: any[] + children?: NetworkRequest[] + response?: { + fromCache: boolean + headers: Record + mimeType: string + status: number + } + error?: string + requestBody?: string + responseBody?: string + size?: number +} diff --git a/packages/nightwatch-devtools/tsconfig.json b/packages/nightwatch-devtools/tsconfig.json new file mode 100644 index 0000000..25c4541 --- /dev/null +++ b/packages/nightwatch-devtools/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "module": "Node16", + "moduleResolution": "Node16", + "target": "ES2022", + "lib": ["ES2022"], + "declaration": true, + "sourceMap": true, + "strict": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "esModuleInterop": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/service/src/launcher.ts b/packages/service/src/launcher.ts index fd9494c..0769a1d 100644 --- a/packages/service/src/launcher.ts +++ b/packages/service/src/launcher.ts @@ -28,13 +28,10 @@ export class DevToolsAppLauncher { }) return } - const { server } = await start({ + const { port } = await start({ port: this.#options.port, hostname: this.#options.hostname }) - const address = server.address() - const port = - address && typeof address === 'object' ? address.port : undefined if (!port) { return console.log(`Failed to start server on port ${port}`) diff --git a/packages/service/src/types.ts b/packages/service/src/types.ts index 084152c..d8852d1 100644 --- a/packages/service/src/types.ts +++ b/packages/service/src/types.ts @@ -10,6 +10,7 @@ export interface CommandLog { timestamp: number callSource: string screenshot?: string + testUid?: string } export enum TraceType { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cba7ed8..825c770 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,7 +29,7 @@ importers: version: 8.56.0(eslint@9.39.3(jiti@2.6.1))(typescript@5.9.3) '@vitest/browser': specifier: ^4.0.16 - version: 4.0.18(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2))(vitest@4.0.18(@types/node@25.3.0)(happy-dom@20.7.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)) + version: 4.0.18(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2))(vitest@4.0.18(@types/node@25.3.0)(happy-dom@20.7.0)(jiti@2.6.1)(jsdom@24.1.3)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)) autoprefixer: specifier: ^10.4.21 version: 10.4.24(postcss@8.5.6) @@ -83,7 +83,7 @@ importers: version: 7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2) vitest: specifier: ^4.0.16 - version: 4.0.18(@types/node@25.3.0)(happy-dom@20.7.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2) + version: 4.0.18(@types/node@25.3.0)(happy-dom@20.7.0)(jiti@2.6.1)(jsdom@24.1.3)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2) webdriverio: specifier: ^9.19.1 version: 9.24.0(puppeteer-core@21.11.0) @@ -240,6 +240,43 @@ importers: specifier: ^8.18.3 version: 8.19.0 + packages/nightwatch-devtools: + dependencies: + '@wdio/devtools-backend': + specifier: workspace:* + version: link:../backend + '@wdio/logger': + specifier: ^9.6.0 + version: 9.18.0 + import-meta-resolve: + specifier: ^4.2.0 + version: 4.2.0 + stacktrace-parser: + specifier: ^0.1.10 + version: 0.1.10 + webdriverio: + specifier: ^9.18.0 + version: 9.24.0(puppeteer-core@21.11.0) + ws: + specifier: ^8.18.3 + version: 8.19.0 + devDependencies: + '@types/node': + specifier: ^22.10.5 + version: 22.19.11 + '@types/ws': + specifier: ^8.18.1 + version: 8.18.1 + chromedriver: + specifier: ^133.0.0 + version: 133.0.3 + nightwatch: + specifier: ^3.0.0 + version: 3.15.0(@cucumber/cucumber@10.9.0)(chromedriver@133.0.3) + typescript: + specifier: ^5.9.2 + version: 5.9.3 + packages/script: dependencies: htm: @@ -335,6 +372,9 @@ packages: '@antfu/install-pkg@1.1.0': resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==} + '@asamuzakjp/css-color@3.2.0': + resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} + '@babel/code-frame@7.29.0': resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} engines: {node: '>=6.9.0'} @@ -372,6 +412,9 @@ packages: resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} engines: {node: '>=6.9.0'} + '@bazel/runfiles@6.5.0': + resolution: {integrity: sha512-RzahvqTkfpY2jsDxo8YItPX+/iZ6hbiikw1YhE0bA9EKBR5Og8Pa6FHn9PO9M0zaXRVsr0GFQLKbB/0rzy9SzA==} + '@cacheable/memory@2.0.7': resolution: {integrity: sha512-RbxnxAMf89Tp1dLhXMS7ceft/PGsDl1Ip7T20z5nZ+pwIAsQ1p2izPjVG69oCLv/jfQ7HDPHTWK0c9rcAWXN3A==} @@ -413,6 +456,24 @@ packages: resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} + '@csstools/color-helpers@5.1.0': + resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==} + engines: {node: '>=18'} + + '@csstools/css-calc@2.1.4': + resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-color-parser@3.1.0': + resolution: {integrity: sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + '@csstools/css-parser-algorithms@3.0.5': resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==} engines: {node: '>=18'} @@ -973,6 +1034,122 @@ packages: '@microsoft/tsdoc@0.16.0': resolution: {integrity: sha512-xgAyonlVVS+q7Vc7qLW0UrJU7rSFcETRWsqdXZtjzRU8dF+6CkozTK4V4y1LwOX7j8r/vHphjDeMeGI4tNGeGA==} + '@napi-rs/nice-android-arm-eabi@1.1.1': + resolution: {integrity: sha512-kjirL3N6TnRPv5iuHw36wnucNqXAO46dzK9oPb0wj076R5Xm8PfUVA9nAFB5ZNMmfJQJVKACAPd/Z2KYMppthw==} + engines: {node: '>= 10'} + cpu: [arm] + os: [android] + + '@napi-rs/nice-android-arm64@1.1.1': + resolution: {integrity: sha512-blG0i7dXgbInN5urONoUCNf+DUEAavRffrO7fZSeoRMJc5qD+BJeNcpr54msPF6qfDD6kzs9AQJogZvT2KD5nw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@napi-rs/nice-darwin-arm64@1.1.1': + resolution: {integrity: sha512-s/E7w45NaLqTGuOjC2p96pct4jRfo61xb9bU1unM/MJ/RFkKlJyJDx7OJI/O0ll/hrfpqKopuAFDV8yo0hfT7A==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@napi-rs/nice-darwin-x64@1.1.1': + resolution: {integrity: sha512-dGoEBnVpsdcC+oHHmW1LRK5eiyzLwdgNQq3BmZIav+9/5WTZwBYX7r5ZkQC07Nxd3KHOCkgbHSh4wPkH1N1LiQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@napi-rs/nice-freebsd-x64@1.1.1': + resolution: {integrity: sha512-kHv4kEHAylMYmlNwcQcDtXjklYp4FCf0b05E+0h6nDHsZ+F0bDe04U/tXNOqrx5CmIAth4vwfkjjUmp4c4JktQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + + '@napi-rs/nice-linux-arm-gnueabihf@1.1.1': + resolution: {integrity: sha512-E1t7K0efyKXZDoZg1LzCOLxgolxV58HCkaEkEvIYQx12ht2pa8hoBo+4OB3qh7e+QiBlp1SRf+voWUZFxyhyqg==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@napi-rs/nice-linux-arm64-gnu@1.1.1': + resolution: {integrity: sha512-CIKLA12DTIZlmTaaKhQP88R3Xao+gyJxNWEn04wZwC2wmRapNnxCUZkVwggInMJvtVElA+D4ZzOU5sX4jV+SmQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@napi-rs/nice-linux-arm64-musl@1.1.1': + resolution: {integrity: sha512-+2Rzdb3nTIYZ0YJF43qf2twhqOCkiSrHx2Pg6DJaCPYhhaxbLcdlV8hCRMHghQ+EtZQWGNcS2xF4KxBhSGeutg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@napi-rs/nice-linux-ppc64-gnu@1.1.1': + resolution: {integrity: sha512-4FS8oc0GeHpwvv4tKciKkw3Y4jKsL7FRhaOeiPei0X9T4Jd619wHNe4xCLmN2EMgZoeGg+Q7GY7BsvwKpL22Tg==} + engines: {node: '>= 10'} + cpu: [ppc64] + os: [linux] + + '@napi-rs/nice-linux-riscv64-gnu@1.1.1': + resolution: {integrity: sha512-HU0nw9uD4FO/oGCCk409tCi5IzIZpH2agE6nN4fqpwVlCn5BOq0MS1dXGjXaG17JaAvrlpV5ZeyZwSon10XOXw==} + engines: {node: '>= 10'} + cpu: [riscv64] + os: [linux] + + '@napi-rs/nice-linux-s390x-gnu@1.1.1': + resolution: {integrity: sha512-2YqKJWWl24EwrX0DzCQgPLKQBxYDdBxOHot1KWEq7aY2uYeX+Uvtv4I8xFVVygJDgf6/92h9N3Y43WPx8+PAgQ==} + engines: {node: '>= 10'} + cpu: [s390x] + os: [linux] + + '@napi-rs/nice-linux-x64-gnu@1.1.1': + resolution: {integrity: sha512-/gaNz3R92t+dcrfCw/96pDopcmec7oCcAQ3l/M+Zxr82KT4DljD37CpgrnXV+pJC263JkW572pdbP3hP+KjcIg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@napi-rs/nice-linux-x64-musl@1.1.1': + resolution: {integrity: sha512-xScCGnyj/oppsNPMnevsBe3pvNaoK7FGvMjT35riz9YdhB2WtTG47ZlbxtOLpjeO9SqqQ2J2igCmz6IJOD5JYw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@napi-rs/nice-openharmony-arm64@1.1.1': + resolution: {integrity: sha512-6uJPRVwVCLDeoOaNyeiW0gp2kFIM4r7PL2MczdZQHkFi9gVlgm+Vn+V6nTWRcu856mJ2WjYJiumEajfSm7arPQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [openharmony] + + '@napi-rs/nice-win32-arm64-msvc@1.1.1': + resolution: {integrity: sha512-uoTb4eAvM5B2aj/z8j+Nv8OttPf2m+HVx3UjA5jcFxASvNhQriyCQF1OB1lHL43ZhW+VwZlgvjmP5qF3+59atA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@napi-rs/nice-win32-ia32-msvc@1.1.1': + resolution: {integrity: sha512-CNQqlQT9MwuCsg1Vd/oKXiuH+TcsSPJmlAFc5frFyX/KkOh0UpBLEj7aoY656d5UKZQMQFP7vJNa1DNUNORvug==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + + '@napi-rs/nice-win32-x64-msvc@1.1.1': + resolution: {integrity: sha512-vB+4G/jBQCAh0jelMTY3+kgFy00Hlx2f2/1zjMoH821IbplbWZOkLiTYXQkygNTzQJTq5cvwBDgn2ppHD+bglQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@napi-rs/nice@1.1.1': + resolution: {integrity: sha512-xJIPs+bYuc9ASBl+cvGsKbGrJmS6fAKaSZCnT0lhahT5rhA2VVy9/EcIgd2JhtEuFOJNx7UHNn/qiTPTY4nrQw==} + engines: {node: '>= 10'} + + '@nightwatch/chai@5.0.3': + resolution: {integrity: sha512-1OIkOf/7jswOC3/t+Add/HVQO8ib75kz6BVYSNeWGghTlmHUqYEfNJ6vcACbXrn/4v3+9iRlWixuhFkxXkU/RQ==} + engines: {node: '>=12'} + + '@nightwatch/html-reporter-template@0.3.0': + resolution: {integrity: sha512-Mze1z6pmUz2O8N9w1/h3QWz1lzMig45PGyh8PrL9ERs3FxVnIX0RCn37vjZUYiV4wgjZOg41JjdcpriZ3dJxkA==} + + '@nightwatch/nightwatch-inspector@1.0.1': + resolution: {integrity: sha512-/ax11EOB4eJXT5VioMztcalbCtsNeuFn6icfT75qPLBmkxLvThePSfyGTys+t9AULUR0ug0wMDMiLV1Oy586Fg==} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -1284,6 +1461,9 @@ packages: resolution: {integrity: sha512-ID7fosbc50TbT0MK0EG12O+gAP3W3Aa/Pz4DaTtQtEvlc9Odaqi0de+xuZ7Li2GtK4HzEX7IuRWS/JmZLksR3Q==} engines: {node: '>=14'} + '@testim/chrome-version@1.1.4': + resolution: {integrity: sha512-kIhULpw9TrGYnHp/8VfdcneIcxKnLixmADtukQRtJUmsVlMg0niMkwV0xZmi8hqa57xqilIHjWFA0GKvEjVU5g==} + '@tootallnate/quickjs-emscripten@0.23.0': resolution: {integrity: sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==} @@ -1314,6 +1494,9 @@ packages: '@types/babel__traverse@7.28.0': resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + '@types/chai@4.3.20': + resolution: {integrity: sha512-/pC9HAB5I/xMlc5FP77qjCnI16ChlJfW0tGa0IUcFn38VJrTV6DeZ60NU5KZBtaOZqjdpwTWohz5HU1RrhiYxQ==} + '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} @@ -1350,6 +1533,9 @@ packages: '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} + '@types/selenium-webdriver@4.35.5': + resolution: {integrity: sha512-wCQCjWmahRkUAO7S703UAvBFkxz4o/rjX4T2AOSWKXSi0sTQPsrXxR0GjtFUT0ompedLkYH4R5HO5Urz0hyeog==} + '@types/sinonjs__fake-timers@8.1.5': resolution: {integrity: sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==} @@ -1659,6 +1845,13 @@ packages: alien-signals@0.4.14: resolution: {integrity: sha512-itUAVzhczTmP2U5yX67xVpsbbOiquusbWVyA9N+sy6+r6YVbFkahXvNCeEPWEOMhwDYwbVbGHFkVL03N9I5g+Q==} + ansi-align@3.0.1: + resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==} + + ansi-colors@4.1.3: + resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} + engines: {node: '>=6'} + ansi-regex@4.1.1: resolution: {integrity: sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==} engines: {node: '>=6'} @@ -1687,6 +1880,11 @@ packages: resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} engines: {node: '>=12'} + ansi-to-html@0.7.2: + resolution: {integrity: sha512-v6MqmEpNlxF+POuyhKkidusCHWWkaLcGRURzivcU3I9tv7k4JVhFcnukrM5Rlk2rUywdZuzYAZ+kbZqWCnfN3g==} + engines: {node: '>=8.0.0'} + hasBin: true + any-promise@1.3.0: resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} @@ -1694,10 +1892,22 @@ packages: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} + archiver-utils@2.1.0: + resolution: {integrity: sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==} + engines: {node: '>= 6'} + + archiver-utils@3.0.4: + resolution: {integrity: sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==} + engines: {node: '>= 10'} + archiver-utils@5.0.2: resolution: {integrity: sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==} engines: {node: '>= 14'} + archiver@5.3.2: + resolution: {integrity: sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==} + engines: {node: '>= 10'} + archiver@7.0.1: resolution: {integrity: sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==} engines: {node: '>= 14'} @@ -1711,6 +1921,9 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + aria-query@5.1.3: + resolution: {integrity: sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==} + aria-query@5.3.2: resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} engines: {node: '>= 0.4'} @@ -1746,6 +1959,9 @@ packages: assertion-error-formatter@3.0.0: resolution: {integrity: sha512-6YyAVLrEze0kQ7CmJfUgrLHb+Y7XghmL2Ie7ijVa2Y9ynP3LV+VDiwFk62Dn0qtqbmY0BT0ss6p1xxpiF2PYbQ==} + assertion-error@1.1.0: + resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} + assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} @@ -1769,6 +1985,9 @@ packages: async@3.2.6: resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + atomic-sleep@1.0.0: resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} engines: {node: '>=8.0.0'} @@ -1787,6 +2006,13 @@ packages: avvio@9.2.0: resolution: {integrity: sha512-2t/sy01ArdHHE0vRH5Hsay+RtCZt3dLPji7W7/MMOCEgze5b7SNDC4j5H6FnVgPkI1MTNFGzHdHrVXDDl7QSSQ==} + axe-core@4.11.1: + resolution: {integrity: sha512-BASOg+YwO2C+346x3LZOeoovTIoTrRqEsqMa6fmfAV0P+U9mFr9NsyOEpiYvFjbc64NMrSswhV50WdXzdb/Z5A==} + engines: {node: '>=4'} + + axios@1.13.4: + resolution: {integrity: sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg==} + b4a@1.8.0: resolution: {integrity: sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==} peerDependencies: @@ -1866,12 +2092,19 @@ packages: binary@0.3.0: resolution: {integrity: sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==} + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + bluebird@3.4.7: resolution: {integrity: sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==} boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + boxen@5.1.2: + resolution: {integrity: sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==} + engines: {node: '>=10'} + brace-expansion@1.1.12: resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} @@ -1886,6 +2119,9 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} + browser-stdout@1.3.1: + resolution: {integrity: sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==} + browserslist@4.28.1: resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} @@ -1938,12 +2174,20 @@ packages: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} + camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + caniuse-lite@1.0.30001774: resolution: {integrity: sha512-DDdwPGz99nmIEv216hKSgLD+D4ikHQHjBC/seF98N9CPqRX4M5mSxT9eTV6oyisnJcuzxtZy4n17yKKQYmYQOA==} capital-case@1.0.4: resolution: {integrity: sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==} + chai-nightwatch@0.5.3: + resolution: {integrity: sha512-38ixH/mqpY6IwnZkz6xPqx8aB5/KVR+j6VPugcir3EGOsphnWXrPH/mUt8Jp+ninL6ghY0AaJDQ10hSfCPGy/g==} + engines: {node: '>= 12.0.0'} + chai@6.2.2: resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} engines: {node: '>=18'} @@ -1969,6 +2213,9 @@ packages: chardet@2.1.1: resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==} + check-error@1.0.2: + resolution: {integrity: sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==} + cheerio-select@2.1.0: resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==} @@ -1989,11 +2236,19 @@ packages: engines: {node: '>=12.13.0'} hasBin: true + chromedriver@133.0.3: + resolution: {integrity: sha512-wGZUtrSdyqnbweXEDIbn+ndu7memG4SEqG6/D+mSabKUEic0hveMYepAPAhlYtvyOc0X8JbsARYtEalVD3R/Vg==} + engines: {node: '>=18'} + hasBin: true + chromium-bidi@0.5.8: resolution: {integrity: sha512-blqh+1cEQbHBKmok3rVJkBlBxt9beKBgOsxbFgs7UJcoVbbeZ+K7+6liAsjgpc8l1Xd55cQUy14fXZdGSb4zIw==} peerDependencies: devtools-protocol: '*' + ci-info@3.3.0: + resolution: {integrity: sha512-riT/3vI5YpVH6/qomlDnJow6TBee2PBKSEpx3O32EGPYbWGIRsIlGRms3Sm74wYE1JMo8RnO04Hb12+v1J5ICw==} + ci-info@4.4.0: resolution: {integrity: sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==} engines: {node: '>=8'} @@ -2005,6 +2260,18 @@ packages: resolution: {integrity: sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==} engines: {node: '>=4'} + cli-boxes@2.2.1: + resolution: {integrity: sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==} + engines: {node: '>=6'} + + cli-cursor@3.1.0: + resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==} + engines: {node: '>=8'} + + cli-spinners@2.9.2: + resolution: {integrity: sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==} + engines: {node: '>=6'} + cli-table3@0.6.3: resolution: {integrity: sha512-w5Jac5SykAeZJKntOxJCrm63Eg5/4dhMWIcuTbo9rpE+brgaSZo0RuNJZeOyMgsUdhDeojvgyQLmjI+K50ZGyg==} engines: {node: 10.* || >= 12.*} @@ -2013,6 +2280,9 @@ packages: resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} engines: {node: '>= 12'} + cliui@7.0.4: + resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} + cliui@8.0.1: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} @@ -2040,6 +2310,10 @@ packages: colord@2.9.3: resolution: {integrity: sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==} + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + commander@10.0.1: resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} engines: {node: '>=14'} @@ -2063,6 +2337,10 @@ packages: compare-versions@6.1.1: resolution: {integrity: sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==} + compress-commons@4.1.2: + resolution: {integrity: sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==} + engines: {node: '>= 10'} + compress-commons@6.0.2: resolution: {integrity: sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==} engines: {node: '>= 14'} @@ -2104,6 +2382,10 @@ packages: engines: {node: '>=0.8'} hasBin: true + crc32-stream@4.0.3: + resolution: {integrity: sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==} + engines: {node: '>= 10'} + crc32-stream@6.0.0: resolution: {integrity: sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==} engines: {node: '>= 14'} @@ -2156,6 +2438,10 @@ packages: engines: {node: '>=4'} hasBin: true + cssstyle@4.6.0: + resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==} + engines: {node: '>=18'} + data-uri-to-buffer@4.0.1: resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} engines: {node: '>= 12'} @@ -2164,6 +2450,10 @@ packages: resolution: {integrity: sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==} engines: {node: '>= 14'} + data-urls@5.0.0: + resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} + engines: {node: '>=18'} + data-view-buffer@1.0.2: resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} engines: {node: '>= 0.4'} @@ -2187,6 +2477,15 @@ packages: supports-color: optional: true + debug@4.3.1: + resolution: {integrity: sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + debug@4.3.4: resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} engines: {node: '>=6.0'} @@ -2205,14 +2504,29 @@ packages: supports-color: optional: true + decamelize@4.0.0: + resolution: {integrity: sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==} + engines: {node: '>=10'} + decamelize@6.0.1: resolution: {integrity: sha512-G7Cqgaelq68XHJNGlZ7lrNQyhZGsFqpwtGFexqUv4IQdjKoSYF7ipZ9UuTJZUSQXFj/XaoBLuEVIVqr8EJngEQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + + deep-eql@4.0.1: + resolution: {integrity: sha512-D/Oxqobjr+kxaHsgiQBZq9b6iAWdEj5W/JdJm8deNduAPc9CwXQ3BJJCuEqlrPXcy45iOMkGPZ0T81Dnz7UDCA==} + engines: {node: '>=6'} + deep-eql@5.0.2: resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} engines: {node: '>=6'} + deep-equal@2.2.3: + resolution: {integrity: sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==} + engines: {node: '>= 0.4'} + deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -2231,6 +2545,10 @@ packages: resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} engines: {node: '>= 0.4'} + define-lazy-prop@2.0.0: + resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==} + engines: {node: '>=8'} + define-properties@1.2.1: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} @@ -2239,6 +2557,10 @@ packages: resolution: {integrity: sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==} engines: {node: '>= 14'} + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} @@ -2251,6 +2573,9 @@ packages: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} + devtools-protocol@0.0.1140464: + resolution: {integrity: sha512-I1jXnjpQh/6TBFyQ0A9dB2kXXk6DprpPFZoI8pUsxHtlNuOTQEdv9fUqYBsFtf8tOJCbdsZZyQrWeXu6GfK+Bw==} + devtools-protocol@0.0.1232444: resolution: {integrity: sha512-pM27vqEfxSxRkTMnF+XCmxSEb6duO5R+t8A9DEEJgy4Wz2RVanje2mmj99B6A3zv2r/qGfYlOvYznUhuokizmg==} @@ -2258,10 +2583,17 @@ packages: resolution: {integrity: sha512-Y9LRUJlGI0wjXLbeU6TEHufF9HnG2H22+/EABD0KtHlJt5AIRQnTGi8uLAJsE1aeQMF1YXd8l7ExaxBkfEBq8w==} engines: {node: ^16.13 || >=18} + didyoumean@1.2.2: + resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} + diff@4.0.4: resolution: {integrity: sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==} engines: {node: '>=0.3.1'} + diff@5.2.2: + resolution: {integrity: sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A==} + engines: {node: '>=0.3.1'} + diff@8.0.3: resolution: {integrity: sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==} engines: {node: '>=0.3.1'} @@ -2287,6 +2619,10 @@ packages: domutils@3.2.2: resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + dotenv@16.3.1: + resolution: {integrity: sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==} + engines: {node: '>=12'} + dotenv@17.3.1: resolution: {integrity: sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==} engines: {node: '>=12'} @@ -2344,6 +2680,9 @@ packages: resolution: {integrity: sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==} engines: {node: '>=10.13.0'} + entities@2.2.0: + resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==} + entities@4.5.0: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} @@ -2360,6 +2699,11 @@ packages: resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} engines: {node: '>=6'} + envinfo@7.11.0: + resolution: {integrity: sha512-G9/6xF1FPbIw0TtalAMaVPpiq2aDEuKLXM314jPVAO9r2fo2a4BLqMNkmRS7O/xPPZ+COAhGIz3ETvHEV3eUcg==} + engines: {node: '>=4'} + hasBin: true + error-ex@1.3.4: resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} @@ -2378,6 +2722,9 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} + es-get-iterator@1.1.3: + resolution: {integrity: sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==} + es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} @@ -2707,9 +3054,22 @@ packages: flat-cache@6.1.20: resolution: {integrity: sha512-AhHYqwvN62NVLp4lObVXGVluiABTHapoB57EyegZVmazN+hhGhLTn3uZbOofoTw4DSDvVCadzzyChXhOAvy8uQ==} + flat@5.0.2: + resolution: {integrity: sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==} + hasBin: true + flatted@3.3.3: resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + follow-redirects@1.15.11: + resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + for-each@0.3.5: resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} engines: {node: '>= 0.4'} @@ -2718,6 +3078,10 @@ packages: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} + formdata-polyfill@4.0.10: resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} engines: {node: '>=12.20.0'} @@ -2725,6 +3089,9 @@ packages: fraction.js@5.3.4: resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} + fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + fs-extra@11.3.3: resolution: {integrity: sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==} engines: {node: '>=14.14'} @@ -2770,6 +3137,9 @@ packages: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} + get-func-name@2.0.2: + resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} + get-intrinsic@1.3.0: resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} engines: {node: '>= 0.4'} @@ -2822,6 +3192,11 @@ packages: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + glob@8.1.0: + resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} + engines: {node: '>=12'} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + global-dirs@3.0.1: resolution: {integrity: sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==} engines: {node: '>=10'} @@ -2927,6 +3302,10 @@ packages: htm@3.1.1: resolution: {integrity: sha512-983Vyg8NwUE7JkZ6NmOqpCZ+sh1bKv2iYTlUkzlWmA5JD2acKoxd4KVxbMmxX/85mtfdnDmTFoNKcg5DGAvxNQ==} + html-encoding-sniffer@4.0.0: + resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} + engines: {node: '>=18'} + html-tags@3.3.1: resolution: {integrity: sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==} engines: {node: '>=8'} @@ -3032,10 +3411,18 @@ packages: resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==} engines: {node: '>= 12'} + ip-regex@4.3.0: + resolution: {integrity: sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==} + engines: {node: '>=8'} + ipaddr.js@2.3.0: resolution: {integrity: sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==} engines: {node: '>= 10'} + is-arguments@1.2.0: + resolution: {integrity: sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==} + engines: {node: '>= 0.4'} + is-array-buffer@3.0.5: resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} engines: {node: '>= 0.4'} @@ -3108,6 +3495,10 @@ packages: resolution: {integrity: sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==} engines: {node: '>=10'} + is-interactive@1.0.0: + resolution: {integrity: sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==} + engines: {node: '>=8'} + is-map@2.0.3: resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} engines: {node: '>= 0.4'} @@ -3128,6 +3519,10 @@ packages: resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} engines: {node: '>=8'} + is-plain-obj@2.1.0: + resolution: {integrity: sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==} + engines: {node: '>=8'} + is-plain-obj@4.1.0: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} @@ -3136,6 +3531,9 @@ packages: resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} engines: {node: '>=0.10.0'} + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + is-regex@1.2.1: resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} engines: {node: '>= 0.4'} @@ -3168,10 +3566,17 @@ packages: resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} engines: {node: '>= 0.4'} + is-unicode-supported@0.1.0: + resolution: {integrity: sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==} + engines: {node: '>=10'} + is-unicode-supported@2.1.0: resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} engines: {node: '>=18'} + is-url@1.2.4: + resolution: {integrity: sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==} + is-weakmap@2.0.2: resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} engines: {node: '>= 0.4'} @@ -3188,6 +3593,10 @@ packages: resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} engines: {node: '>=8'} + is2@2.0.9: + resolution: {integrity: sha512-rZkHeBn9Zzq52sd9IUIV3a5mfwBY+o2HePMh0wkGBM4z4qjvy2GwVxQ6nNXSfw6MmVP6gf1QIlWjiOavhM3x5g==} + engines: {node: '>=v0.10.0'} + isarray@1.0.0: resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} @@ -3251,6 +3660,15 @@ packages: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true + jsdom@24.1.3: + resolution: {integrity: sha512-MyL55p3Ut3cXbeBEG7Hcv0mVM8pp8PBNWxRqchZnSfAiES1v1mRnMeFfaHWIPULpwsYfvO+ZmMZz5tGCnjzDUQ==} + engines: {node: '>=18'} + peerDependencies: + canvas: ^2.11.2 + peerDependenciesMeta: + canvas: + optional: true + jsesc@3.1.0: resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} engines: {node: '>=6'} @@ -3451,9 +3869,21 @@ packages: lodash.clonedeep@4.5.0: resolution: {integrity: sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==} + lodash.defaults@4.2.0: + resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} + + lodash.difference@4.5.0: + resolution: {integrity: sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==} + + lodash.flatten@4.4.0: + resolution: {integrity: sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==} + lodash.flattendeep@4.4.0: resolution: {integrity: sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==} + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} @@ -3475,6 +3905,10 @@ packages: lodash@4.17.23: resolution: {integrity: sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==} + log-symbols@4.1.0: + resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} + engines: {node: '>=10'} + loglevel-plugin-prefix@0.8.4: resolution: {integrity: sha512-WpG9CcFAOjz/FtNht+QJeGpvVl/cdR6P0z6OcXSkr8wFJOsV2GRj2j10JLfjuA4aYkcKCNIEqRGCyTife9R8/g==} @@ -3482,6 +3916,9 @@ packages: resolution: {integrity: sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==} engines: {node: '>= 0.6.0'} + loupe@2.3.7: + resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==} + lower-case@2.0.2: resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} @@ -3539,11 +3976,23 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + mime@3.0.0: resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} engines: {node: '>=10.0.0'} hasBin: true + mimic-fn@2.1.0: + resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} + engines: {node: '>=6'} + minimatch@10.2.1: resolution: {integrity: sha512-MClCe8IL5nRRmawL6ib/eT4oLyeKMGCghibcDWK+J0hh0Q8kqSdia6BvbRMVk6mPa6WqUa5uR2oxt6C5jd533A==} engines: {node: 20 || >=22} @@ -3552,6 +4001,9 @@ packages: resolution: {integrity: sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw==} engines: {node: 18 || 20 || >=22} + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + minimatch@3.1.3: resolution: {integrity: sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==} @@ -3563,6 +4015,9 @@ packages: resolution: {integrity: sha512-kQAVowdR33euIqeA0+VZTDqU+qo1IeVY+hrKYtZMio3Pg0P0vuh/kwRylLUddJhB6pf3q/botcOvRtx4IN1wqQ==} engines: {node: '>=16 || 14 >=14.17'} + minimist@1.2.6: + resolution: {integrity: sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==} + minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} @@ -3588,6 +4043,11 @@ packages: mlly@1.8.0: resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} + mocha@10.8.2: + resolution: {integrity: sha512-VZlYo/WE8t1tstuRmqgeyBgCbJc/lEdopaa+axcKzTBJ+UIdlAB9XnmvTCAH4pwR4ElNInaedhEBmZD8iCSVEg==} + engines: {node: '>= 14.0.0'} + hasBin: true + modern-tar@0.7.5: resolution: {integrity: sha512-YTefgdpKKFgoTDbEUqXqgUJct2OG6/4hs4XWLsxcHkDLj/x/V8WmKIRppPnXP5feQ7d1vuYWSp3qKkxfwaFaxA==} engines: {node: '>=18.0.0'} @@ -3627,6 +4087,25 @@ packages: nice-try@1.0.5: resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==} + nightwatch-axe-verbose@2.4.0: + resolution: {integrity: sha512-ZWSygayLjyDNvkrL4LjfeBhCXCA5yRQ7Gf9ZkNVPba8VzBe/pa4yYuWgpMV+59uZpa+hMcGz7drTpRzT0RI/gw==} + + nightwatch@3.15.0: + resolution: {integrity: sha512-Vvh7TsDyEN1YzOsDNoafEUPJDQ6jfnmJPAsWo/EmygljZiRk1Ja/pEqNAhE5UdYJzF38SNO46gJS8IRk9mUNfA==} + engines: {node: '>= 16'} + hasBin: true + peerDependencies: + '@cucumber/cucumber': '*' + chromedriver: '*' + geckodriver: '*' + peerDependenciesMeta: + '@cucumber/cucumber': + optional: true + chromedriver: + optional: true + geckodriver: + optional: true + no-case@3.0.4: resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} @@ -3683,6 +4162,9 @@ packages: nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + nwsapi@2.2.23: + resolution: {integrity: sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==} + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -3691,6 +4173,10 @@ packages: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} + object-is@1.1.6: + resolution: {integrity: sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==} + engines: {node: '>= 0.4'} + object-keys@1.1.1: resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} engines: {node: '>= 0.4'} @@ -3721,11 +4207,23 @@ packages: once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + onetime@5.1.2: + resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} + engines: {node: '>=6'} + + open@8.4.2: + resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} + engines: {node: '>=12'} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} - own-keys@1.0.1: + ora@5.4.1: + resolution: {integrity: sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==} + engines: {node: '>=10'} + + own-keys@1.0.1: resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} engines: {node: '>= 0.4'} @@ -3862,6 +4360,9 @@ packages: pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + pathval@1.1.1: + resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} + pend@1.2.0: resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} @@ -3899,6 +4400,9 @@ packages: resolution: {integrity: sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==} hasBin: true + piscina@4.9.2: + resolution: {integrity: sha512-Fq0FERJWFEUpB4eSY59wSNwXD4RYqR+nR/WiEVcZW8IWfVBxJJafcgTEZDQo8k3w0sUarJ8RyVbbUF4GQ2LGbQ==} + pixelmatch@7.1.0: resolution: {integrity: sha512-1wrVzJ2STrpmONHKBy228LM1b84msXDUoAzVEl0R8Mz4Ce6EPr+IVtxm8+yvrqLYMHswREkjYFaMxnyGnaY3Ng==} hasBin: true @@ -4031,6 +4535,9 @@ packages: proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + psl@1.15.0: + resolution: {integrity: sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==} + pstree.remy@1.1.8: resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==} @@ -4055,12 +4562,18 @@ packages: query-selector-shadow-dom@1.0.1: resolution: {integrity: sha512-lT5yCqEBgfoMYpf3F2xQRK7zEr1rhIIZuceDK6+xRkJQ4NMbHTwXqk4NkwDwQMNqXgG9r9fyHnzwNVs6zV5KRw==} + querystringify@2.2.0: + resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} + queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} quick-format-unescaped@4.0.4: resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + randombytes@2.1.0: + resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} + react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} @@ -4155,6 +4668,9 @@ packages: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} + requires-port@1.0.0: + resolution: {integrity: sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -4178,6 +4694,10 @@ packages: resq@1.11.0: resolution: {integrity: sha512-G10EBz+zAAy3zUd/CDoBbXRL6ia9kOo3xRHrMDsHljI0GDkhYlyjwoCx5+3eCC4swi1uCoZQhskuJkj7Gp57Bw==} + restore-cursor@3.1.0: + resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} + engines: {node: '>=8'} + ret@0.5.0: resolution: {integrity: sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==} engines: {node: '>=10'} @@ -4202,6 +4722,12 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + rrweb-cssom@0.7.1: + resolution: {integrity: sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==} + + rrweb-cssom@0.8.0: + resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==} + run-async@4.0.6: resolution: {integrity: sha512-IoDlSLTs3Yq593mb3ZoKWKXMNu3UpObxhgA/Xuid5p4bbfi2jdY1Hj0m1K+0/tEuQTxIGMhQDqGjKb7RuxGpAQ==} engines: {node: '>=0.12.0'} @@ -4247,12 +4773,20 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + secure-json-parse@4.1.0: resolution: {integrity: sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==} seed-random@2.2.0: resolution: {integrity: sha512-34EQV6AAHQGhoc0tn/96a9Fsi6v2xdqe/dMUwljGRaFOzR3EgRmECvD0O8vi8X+/uQ50LGHfkNu/Eue5TPKZkQ==} + selenium-webdriver@4.27.0: + resolution: {integrity: sha512-LkTJrNz5socxpPnWPODQ2bQ65eYx9JK+DQMYNihpTjMCqHwgWGYQnQTCAAche2W3ZP87alA+1zYPvgS8tHNzMQ==} + engines: {node: '>= 14.21.0'} + semver@5.7.2: resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} hasBin: true @@ -4280,6 +4814,9 @@ packages: resolution: {integrity: sha512-ZYkZLAvKTKQXWuh5XpBw7CdbSzagarX39WyZ2H07CDLC5/KfsRGlIXV8d4+tfqX1M7916mRqR1QfNHSij+c9Pw==} engines: {node: '>=18'} + serialize-javascript@6.0.2: + resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} + set-cookie-parser@2.7.2: resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} @@ -4340,6 +4877,9 @@ packages: siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + signal-exit@4.1.0: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} @@ -4422,6 +4962,10 @@ packages: stackframe@1.3.4: resolution: {integrity: sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==} + stacktrace-parser@0.1.10: + resolution: {integrity: sha512-KJP1OCML99+8fhOHxwwzyWrlUuVX5GQ0ZpJTd1DFXhdkrvg1szxfHhawXUZ3g9TkXORQd4/WG68jMlQZ2p8wlg==} + engines: {node: '>=6'} + statuses@2.0.2: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} @@ -4554,6 +5098,9 @@ packages: svg-tags@1.0.0: resolution: {integrity: sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==} + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + synckit@0.11.12: resolution: {integrity: sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==} engines: {node: ^14.18.0 || >=16.0.0} @@ -4578,9 +5125,16 @@ packages: tar-fs@3.1.1: resolution: {integrity: sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==} + tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + tar-stream@3.1.7: resolution: {integrity: sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==} + tcp-port-used@1.0.2: + resolution: {integrity: sha512-l7ar8lLUD3XS1V2lfoJlCBaeoaWo/2xfYt81hM7VlvR4RrMVFqfmzfhLVk40hAb368uitje5gPtBRL1m/DGvLA==} + teex@1.0.1: resolution: {integrity: sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==} @@ -4650,9 +5204,17 @@ packages: resolution: {integrity: sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==} hasBin: true + tough-cookie@4.1.4: + resolution: {integrity: sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==} + engines: {node: '>=6'} + tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + tr46@5.1.1: + resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} + engines: {node: '>=18'} + traverse@0.3.9: resolution: {integrity: sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==} @@ -4699,10 +5261,22 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} + type-detect@4.0.8: + resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} + engines: {node: '>=4'} + + type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + type-fest@0.6.0: resolution: {integrity: sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==} engines: {node: '>=8'} + type-fest@0.7.1: + resolution: {integrity: sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==} + engines: {node: '>=8'} + type-fest@0.8.1: resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==} engines: {node: '>=8'} @@ -4784,6 +5358,10 @@ packages: resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} engines: {node: '>=18'} + universalify@0.2.0: + resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==} + engines: {node: '>= 4.0.0'} + universalify@2.0.1: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} @@ -4815,6 +5393,10 @@ packages: resolution: {integrity: sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==} engines: {node: '>=18.12.0'} + untildify@4.0.0: + resolution: {integrity: sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==} + engines: {node: '>=8'} + unzipper@0.10.14: resolution: {integrity: sha512-ti4wZj+0bQTiX2KmKWuwj7lhV+2n//uXEotUmGuQqrbVZSEGFMbI68+c6JCQ8aAmUWYvtHEz2A8K6wXvueR/6g==} @@ -4830,6 +5412,9 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + url-parse@1.5.10: + resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==} + urlpattern-polyfill@10.0.0: resolution: {integrity: sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg==} @@ -4850,6 +5435,10 @@ packages: resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} hasBin: true + uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + hasBin: true + uuid@9.0.1: resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} hasBin: true @@ -4956,6 +5545,10 @@ packages: w3c-keyname@2.2.8: resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + wait-port@1.1.0: resolution: {integrity: sha512-3e04qkoN3LxTMLakdqeWth8nih8usyg+sf1Bgdf9wwUkp05iuK1eSY/QpLvscT/+F/gA89+LpUmmgBtesbqI2Q==} engines: {node: '>=10'} @@ -4984,6 +5577,10 @@ packages: webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + webpack-virtual-modules@0.6.2: resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} @@ -5000,6 +5597,10 @@ packages: resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} engines: {node: '>=18'} + whatwg-url@14.2.0: + resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} + engines: {node: '>=18'} + whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} @@ -5043,10 +5644,17 @@ packages: engines: {node: '>=8'} hasBin: true + widest-line@3.1.0: + resolution: {integrity: sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==} + engines: {node: '>=8'} + word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} + workerpool@6.5.1: + resolution: {integrity: sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==} + wrap-ansi@6.2.0: resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} engines: {node: '>=8'} @@ -5090,10 +5698,17 @@ packages: utf-8-validate: optional: true + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + xmlbuilder@15.1.1: resolution: {integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==} engines: {node: '>=8.0'} + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -5106,10 +5721,22 @@ packages: engines: {node: '>= 14.6'} hasBin: true + yargs-parser@20.2.9: + resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} + engines: {node: '>=10'} + yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} + yargs-unparser@2.0.0: + resolution: {integrity: sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==} + engines: {node: '>=10'} + + yargs@16.2.0: + resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} + engines: {node: '>=10'} + yargs@17.7.2: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} @@ -5140,6 +5767,10 @@ packages: yup@1.2.0: resolution: {integrity: sha512-PPqYKSAXjpRCgLgLKVGPA33v5c/WgEx3wi6NFjIiegz90zSwyMpvTFp/uGcVnnbx6to28pgnzp/q8ih3QRjLMQ==} + zip-stream@4.1.1: + resolution: {integrity: sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==} + engines: {node: '>= 10'} + zip-stream@6.0.1: resolution: {integrity: sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==} engines: {node: '>= 14'} @@ -5153,6 +5784,14 @@ snapshots: package-manager-detector: 1.6.0 tinyexec: 1.0.2 + '@asamuzakjp/css-color@3.2.0': + dependencies: + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + lru-cache: 10.4.3 + '@babel/code-frame@7.29.0': dependencies: '@babel/helper-validator-identifier': 7.28.5 @@ -5200,6 +5839,8 @@ snapshots: '@babel/helper-string-parser': 7.27.1 '@babel/helper-validator-identifier': 7.28.5 + '@bazel/runfiles@6.5.0': {} + '@cacheable/memory@2.0.7': dependencies: '@cacheable/utils': 2.3.4 @@ -5282,6 +5923,20 @@ snapshots: dependencies: '@jridgewell/trace-mapping': 0.3.9 + '@csstools/color-helpers@5.1.0': {} + + '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-color-parser@3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/color-helpers': 5.1.0 + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)': dependencies: '@csstools/css-tokenizer': 3.0.4 @@ -5864,6 +6519,93 @@ snapshots: '@microsoft/tsdoc@0.16.0': {} + '@napi-rs/nice-android-arm-eabi@1.1.1': + optional: true + + '@napi-rs/nice-android-arm64@1.1.1': + optional: true + + '@napi-rs/nice-darwin-arm64@1.1.1': + optional: true + + '@napi-rs/nice-darwin-x64@1.1.1': + optional: true + + '@napi-rs/nice-freebsd-x64@1.1.1': + optional: true + + '@napi-rs/nice-linux-arm-gnueabihf@1.1.1': + optional: true + + '@napi-rs/nice-linux-arm64-gnu@1.1.1': + optional: true + + '@napi-rs/nice-linux-arm64-musl@1.1.1': + optional: true + + '@napi-rs/nice-linux-ppc64-gnu@1.1.1': + optional: true + + '@napi-rs/nice-linux-riscv64-gnu@1.1.1': + optional: true + + '@napi-rs/nice-linux-s390x-gnu@1.1.1': + optional: true + + '@napi-rs/nice-linux-x64-gnu@1.1.1': + optional: true + + '@napi-rs/nice-linux-x64-musl@1.1.1': + optional: true + + '@napi-rs/nice-openharmony-arm64@1.1.1': + optional: true + + '@napi-rs/nice-win32-arm64-msvc@1.1.1': + optional: true + + '@napi-rs/nice-win32-ia32-msvc@1.1.1': + optional: true + + '@napi-rs/nice-win32-x64-msvc@1.1.1': + optional: true + + '@napi-rs/nice@1.1.1': + optionalDependencies: + '@napi-rs/nice-android-arm-eabi': 1.1.1 + '@napi-rs/nice-android-arm64': 1.1.1 + '@napi-rs/nice-darwin-arm64': 1.1.1 + '@napi-rs/nice-darwin-x64': 1.1.1 + '@napi-rs/nice-freebsd-x64': 1.1.1 + '@napi-rs/nice-linux-arm-gnueabihf': 1.1.1 + '@napi-rs/nice-linux-arm64-gnu': 1.1.1 + '@napi-rs/nice-linux-arm64-musl': 1.1.1 + '@napi-rs/nice-linux-ppc64-gnu': 1.1.1 + '@napi-rs/nice-linux-riscv64-gnu': 1.1.1 + '@napi-rs/nice-linux-s390x-gnu': 1.1.1 + '@napi-rs/nice-linux-x64-gnu': 1.1.1 + '@napi-rs/nice-linux-x64-musl': 1.1.1 + '@napi-rs/nice-openharmony-arm64': 1.1.1 + '@napi-rs/nice-win32-arm64-msvc': 1.1.1 + '@napi-rs/nice-win32-ia32-msvc': 1.1.1 + '@napi-rs/nice-win32-x64-msvc': 1.1.1 + optional: true + + '@nightwatch/chai@5.0.3': + dependencies: + assertion-error: 1.1.0 + check-error: 1.0.2 + deep-eql: 4.0.1 + loupe: 2.3.7 + pathval: 1.1.1 + type-detect: 4.0.8 + + '@nightwatch/html-reporter-template@0.3.0': {} + + '@nightwatch/nightwatch-inspector@1.0.1': + dependencies: + archiver: 5.3.2 + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -6121,6 +6863,8 @@ snapshots: '@teppeis/multimaps@3.0.0': {} + '@testim/chrome-version@1.1.4': {} + '@tootallnate/quickjs-emscripten@0.23.0': {} '@tsconfig/node10@1.0.12': {} @@ -6154,6 +6898,8 @@ snapshots: dependencies: '@babel/types': 7.29.0 + '@types/chai@4.3.20': {} + '@types/chai@5.2.3': dependencies: '@types/deep-eql': 4.0.2 @@ -6191,6 +6937,11 @@ snapshots: '@types/normalize-package-data@2.4.4': {} + '@types/selenium-webdriver@4.35.5': + dependencies: + '@types/node': 25.3.0 + '@types/ws': 8.18.1 + '@types/sinonjs__fake-timers@8.1.5': {} '@types/stack-trace@0.0.33': {} @@ -6313,7 +7064,7 @@ snapshots: '@typescript-eslint/types': 8.56.0 eslint-visitor-keys: 5.0.1 - '@vitest/browser@4.0.18(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2))(vitest@4.0.18(@types/node@25.3.0)(happy-dom@20.7.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2))': + '@vitest/browser@4.0.18(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2))(vitest@4.0.18(@types/node@25.3.0)(happy-dom@20.7.0)(jiti@2.6.1)(jsdom@24.1.3)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2))': dependencies: '@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)) '@vitest/utils': 4.0.18 @@ -6322,7 +7073,7 @@ snapshots: pngjs: 7.0.0 sirv: 3.0.2 tinyrainbow: 3.0.3 - vitest: 4.0.18(@types/node@25.3.0)(happy-dom@20.7.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2) + vitest: 4.0.18(@types/node@25.3.0)(happy-dom@20.7.0)(jiti@2.6.1)(jsdom@24.1.3)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2) ws: 8.19.0 transitivePeerDependencies: - bufferutil @@ -6715,6 +7466,12 @@ snapshots: alien-signals@0.4.14: {} + ansi-align@3.0.1: + dependencies: + string-width: 4.2.3 + + ansi-colors@4.1.3: {} + ansi-regex@4.1.1: {} ansi-regex@5.0.1: {} @@ -6733,6 +7490,10 @@ snapshots: ansi-styles@6.2.3: {} + ansi-to-html@0.7.2: + dependencies: + entities: 2.2.0 + any-promise@1.3.0: {} anymatch@3.1.3: @@ -6740,6 +7501,32 @@ snapshots: normalize-path: 3.0.0 picomatch: 2.3.1 + archiver-utils@2.1.0: + dependencies: + glob: 7.2.3 + graceful-fs: 4.2.11 + lazystream: 1.0.1 + lodash.defaults: 4.2.0 + lodash.difference: 4.5.0 + lodash.flatten: 4.4.0 + lodash.isplainobject: 4.0.6 + lodash.union: 4.6.0 + normalize-path: 3.0.0 + readable-stream: 2.3.8 + + archiver-utils@3.0.4: + dependencies: + glob: 7.2.3 + graceful-fs: 4.2.11 + lazystream: 1.0.1 + lodash.defaults: 4.2.0 + lodash.difference: 4.5.0 + lodash.flatten: 4.4.0 + lodash.isplainobject: 4.0.6 + lodash.union: 4.6.0 + normalize-path: 3.0.0 + readable-stream: 3.6.2 + archiver-utils@5.0.2: dependencies: glob: 10.5.0 @@ -6750,6 +7537,16 @@ snapshots: normalize-path: 3.0.0 readable-stream: 4.7.0 + archiver@5.3.2: + dependencies: + archiver-utils: 2.1.0 + async: 3.2.6 + buffer-crc32: 0.2.13 + readable-stream: 3.6.2 + readdir-glob: 1.1.3 + tar-stream: 2.2.0 + zip-stream: 4.1.1 + archiver@7.0.1: dependencies: archiver-utils: 5.0.2 @@ -6771,6 +7568,10 @@ snapshots: argparse@2.0.1: {} + aria-query@5.1.3: + dependencies: + deep-equal: 2.2.3 + aria-query@5.3.2: {} array-buffer-byte-length@1.0.2: @@ -6831,6 +7632,8 @@ snapshots: pad-right: 0.2.2 repeat-string: 1.6.1 + assertion-error@1.1.0: {} + assertion-error@2.0.1: {} ast-types@0.13.4: @@ -6845,6 +7648,8 @@ snapshots: async@3.2.6: {} + asynckit@0.4.0: {} + atomic-sleep@1.0.0: {} autoprefixer@10.4.24(postcss@8.5.6): @@ -6865,6 +7670,16 @@ snapshots: '@fastify/error': 4.2.0 fastq: 1.20.1 + axe-core@4.11.1: {} + + axios@1.13.4: + dependencies: + follow-redirects: 1.15.11 + form-data: 4.0.5 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + b4a@1.8.0: {} balanced-match@1.0.2: {} @@ -6926,10 +7741,27 @@ snapshots: buffers: 0.1.1 chainsaw: 0.1.0 + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + bluebird@3.4.7: {} boolbase@1.0.0: {} + boxen@5.1.2: + dependencies: + ansi-align: 3.0.1 + camelcase: 6.3.0 + chalk: 4.1.2 + cli-boxes: 2.2.1 + string-width: 4.2.3 + type-fest: 0.20.2 + widest-line: 3.1.0 + wrap-ansi: 7.0.0 + brace-expansion@1.1.12: dependencies: balanced-match: 1.0.2 @@ -6947,6 +7779,8 @@ snapshots: dependencies: fill-range: 7.1.1 + browser-stdout@1.3.1: {} + browserslist@4.28.1: dependencies: baseline-browser-mapping: 2.10.0 @@ -7004,6 +7838,8 @@ snapshots: callsites@3.1.0: {} + camelcase@6.3.0: {} + caniuse-lite@1.0.30001774: {} capital-case@1.0.4: @@ -7012,6 +7848,10 @@ snapshots: tslib: 2.8.1 upper-case-first: 2.0.2 + chai-nightwatch@0.5.3: + dependencies: + assertion-error: 1.1.0 + chai@6.2.2: {} chainsaw@0.1.0: @@ -7035,6 +7875,8 @@ snapshots: chardet@2.1.1: {} + check-error@1.0.2: {} + cheerio-select@2.1.0: dependencies: boolbase: 1.0.0 @@ -7083,12 +7925,27 @@ snapshots: transitivePeerDependencies: - supports-color + chromedriver@133.0.3: + dependencies: + '@testim/chrome-version': 1.1.4 + axios: 1.13.4 + compare-versions: 6.1.1 + extract-zip: 2.0.1 + proxy-agent: 6.5.0 + proxy-from-env: 1.1.0 + tcp-port-used: 1.0.2 + transitivePeerDependencies: + - debug + - supports-color + chromium-bidi@0.5.8(devtools-protocol@0.0.1232444): dependencies: devtools-protocol: 0.0.1232444 mitt: 3.0.1 urlpattern-polyfill: 10.0.0 + ci-info@3.3.0: {} + ci-info@4.4.0: {} class-transformer@0.5.1: {} @@ -7097,6 +7954,14 @@ snapshots: dependencies: escape-string-regexp: 1.0.5 + cli-boxes@2.2.1: {} + + cli-cursor@3.1.0: + dependencies: + restore-cursor: 3.1.0 + + cli-spinners@2.9.2: {} + cli-table3@0.6.3: dependencies: string-width: 4.2.3 @@ -7105,14 +7970,19 @@ snapshots: cli-width@4.1.0: {} + cliui@7.0.4: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + cliui@8.0.1: dependencies: string-width: 4.2.3 strip-ansi: 6.0.1 wrap-ansi: 7.0.0 - clone@1.0.4: - optional: true + clone@1.0.4: {} codemirror@6.0.2: dependencies: @@ -7138,6 +8008,10 @@ snapshots: colord@2.9.3: {} + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + commander@10.0.1: {} commander@12.0.0: {} @@ -7150,6 +8024,13 @@ snapshots: compare-versions@6.1.1: {} + compress-commons@4.1.2: + dependencies: + buffer-crc32: 0.2.13 + crc32-stream: 4.0.3 + normalize-path: 3.0.0 + readable-stream: 3.6.2 + compress-commons@6.0.2: dependencies: crc-32: 1.2.2 @@ -7185,6 +8066,11 @@ snapshots: crc-32@1.2.2: {} + crc32-stream@4.0.3: + dependencies: + crc-32: 1.2.2 + readable-stream: 3.6.2 + crc32-stream@6.0.0: dependencies: crc-32: 1.2.2 @@ -7255,10 +8141,20 @@ snapshots: cssesc@3.0.0: {} + cssstyle@4.6.0: + dependencies: + '@asamuzakjp/css-color': 3.2.0 + rrweb-cssom: 0.8.0 + data-uri-to-buffer@4.0.1: {} data-uri-to-buffer@6.0.2: {} + data-urls@5.0.0: + dependencies: + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 + data-view-buffer@1.0.2: dependencies: call-bound: 1.0.4 @@ -7283,6 +8179,10 @@ snapshots: dependencies: ms: 2.1.3 + debug@4.3.1: + dependencies: + ms: 2.1.2 + debug@4.3.4: dependencies: ms: 2.1.2 @@ -7303,10 +8203,39 @@ snapshots: optionalDependencies: supports-color: 8.1.1 + decamelize@4.0.0: {} + decamelize@6.0.1: {} + decimal.js@10.6.0: {} + + deep-eql@4.0.1: + dependencies: + type-detect: 4.0.8 + deep-eql@5.0.2: {} + deep-equal@2.2.3: + dependencies: + array-buffer-byte-length: 1.0.2 + call-bind: 1.0.8 + es-get-iterator: 1.1.3 + get-intrinsic: 1.3.0 + is-arguments: 1.2.0 + is-array-buffer: 3.0.5 + is-date-object: 1.1.0 + is-regex: 1.2.1 + is-shared-array-buffer: 1.0.4 + isarray: 2.0.5 + object-is: 1.1.6 + object-keys: 1.1.1 + object.assign: 4.1.7 + regexp.prototype.flags: 1.5.4 + side-channel: 1.1.0 + which-boxed-primitive: 1.1.1 + which-collection: 1.0.2 + which-typed-array: 1.1.20 + deep-is@0.1.4: {} deepmerge-ts@5.1.0: {} @@ -7316,7 +8245,6 @@ snapshots: defaults@1.0.4: dependencies: clone: 1.0.4 - optional: true define-data-property@1.1.4: dependencies: @@ -7324,6 +8252,8 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 + define-lazy-prop@2.0.0: {} + define-properties@1.2.1: dependencies: define-data-property: 1.1.4 @@ -7336,12 +8266,16 @@ snapshots: escodegen: 2.1.0 esprima: 4.0.1 + delayed-stream@1.0.0: {} + depd@2.0.0: {} dequal@2.0.3: {} detect-libc@2.1.2: {} + devtools-protocol@0.0.1140464: {} + devtools-protocol@0.0.1232444: {} devtools@8.42.0: @@ -7369,8 +8303,12 @@ snapshots: - supports-color - utf-8-validate + didyoumean@1.2.2: {} + diff@4.0.4: {} + diff@5.2.2: {} + diff@8.0.3: {} dir-glob@3.0.1: @@ -7399,6 +8337,8 @@ snapshots: domelementtype: 2.3.0 domhandler: 5.0.3 + dotenv@16.3.1: {} + dotenv@17.3.1: {} dunder-proto@1.0.1: @@ -7478,6 +8418,8 @@ snapshots: graceful-fs: 4.2.11 tapable: 2.3.0 + entities@2.2.0: {} + entities@4.5.0: {} entities@6.0.1: {} @@ -7486,6 +8428,8 @@ snapshots: env-paths@2.2.1: {} + envinfo@7.11.0: {} + error-ex@1.3.4: dependencies: is-arrayish: 0.2.1 @@ -7555,6 +8499,18 @@ snapshots: es-errors@1.3.0: {} + es-get-iterator@1.1.3: + dependencies: + call-bind: 1.0.8 + get-intrinsic: 1.3.0 + has-symbols: 1.1.0 + is-arguments: 1.2.0 + is-map: 2.0.3 + is-set: 2.0.3 + is-string: 1.1.1 + isarray: 2.0.5 + stop-iteration-iterator: 1.1.0 + es-module-lexer@1.7.0: {} es-object-atoms@1.1.1: @@ -7987,8 +8943,12 @@ snapshots: flatted: 3.3.3 hookified: 1.15.1 + flat@5.0.2: {} + flatted@3.3.3: {} + follow-redirects@1.15.11: {} + for-each@0.3.5: dependencies: is-callable: 1.2.7 @@ -7998,12 +8958,22 @@ snapshots: cross-spawn: 7.0.6 signal-exit: 4.1.0 + form-data@4.0.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + formdata-polyfill@4.0.10: dependencies: fetch-blob: 3.2.0 fraction.js@5.3.4: {} + fs-constants@1.0.0: {} + fs-extra@11.3.3: dependencies: graceful-fs: 4.2.11 @@ -8066,6 +9036,8 @@ snapshots: get-caller-file@2.0.5: {} + get-func-name@2.0.2: {} + get-intrinsic@1.3.0: dependencies: call-bind-apply-helpers: 1.0.2 @@ -8145,6 +9117,14 @@ snapshots: once: 1.4.0 path-is-absolute: 1.0.1 + glob@8.1.0: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 5.1.7 + once: 1.4.0 + global-dirs@3.0.1: dependencies: ini: 2.0.0 @@ -8245,6 +9225,10 @@ snapshots: htm@3.1.1: {} + html-encoding-sniffer@4.0.0: + dependencies: + whatwg-encoding: 3.1.1 + html-tags@3.3.1: {} htmlfy@0.8.1: {} @@ -8344,8 +9328,15 @@ snapshots: ip-address@10.1.0: {} + ip-regex@4.3.0: {} + ipaddr.js@2.3.0: {} + is-arguments@1.2.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + is-array-buffer@3.0.5: dependencies: call-bind: 1.0.8 @@ -8423,6 +9414,8 @@ snapshots: global-dirs: 3.0.1 is-path-inside: 3.0.3 + is-interactive@1.0.0: {} + is-map@2.0.3: {} is-negative-zero@2.0.3: {} @@ -8436,10 +9429,14 @@ snapshots: is-path-inside@3.0.3: {} + is-plain-obj@2.1.0: {} + is-plain-obj@4.1.0: {} is-plain-object@5.0.0: {} + is-potential-custom-element-name@1.0.1: {} + is-regex@1.2.1: dependencies: call-bound: 1.0.4 @@ -8472,8 +9469,12 @@ snapshots: dependencies: which-typed-array: 1.1.20 + is-unicode-supported@0.1.0: {} + is-unicode-supported@2.1.0: {} + is-url@1.2.4: {} + is-weakmap@2.0.2: {} is-weakref@1.1.1: @@ -8489,6 +9490,12 @@ snapshots: dependencies: is-docker: 2.2.1 + is2@2.0.9: + dependencies: + deep-is: 0.1.4 + ip-regex: 4.3.0 + is-url: 1.2.4 + isarray@1.0.0: {} isarray@2.0.5: {} @@ -8564,6 +9571,34 @@ snapshots: dependencies: argparse: 2.0.1 + jsdom@24.1.3: + dependencies: + cssstyle: 4.6.0 + data-urls: 5.0.0 + decimal.js: 10.6.0 + form-data: 4.0.5 + html-encoding-sniffer: 4.0.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.23 + parse5: 7.3.0 + rrweb-cssom: 0.7.1 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 4.1.4 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 3.1.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 + ws: 8.19.0 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + jsesc@3.1.0: {} json-buffer@3.0.1: {} @@ -8753,8 +9788,16 @@ snapshots: lodash.clonedeep@4.5.0: {} + lodash.defaults@4.2.0: {} + + lodash.difference@4.5.0: {} + + lodash.flatten@4.4.0: {} + lodash.flattendeep@4.4.0: {} + lodash.isplainobject@4.0.6: {} + lodash.merge@4.6.2: {} lodash.mergewith@4.6.2: {} @@ -8769,10 +9812,19 @@ snapshots: lodash@4.17.23: {} + log-symbols@4.1.0: + dependencies: + chalk: 4.1.2 + is-unicode-supported: 0.1.0 + loglevel-plugin-prefix@0.8.4: {} loglevel@1.9.2: {} + loupe@2.3.7: + dependencies: + get-func-name: 2.0.2 + lower-case@2.0.2: dependencies: tslib: 2.8.1 @@ -8814,8 +9866,16 @@ snapshots: braces: 3.0.3 picomatch: 2.3.1 + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + mime@3.0.0: {} + mimic-fn@2.1.0: {} + minimatch@10.2.1: dependencies: brace-expansion: 5.0.3 @@ -8824,6 +9884,10 @@ snapshots: dependencies: brace-expansion: 5.0.3 + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.12 + minimatch@3.1.3: dependencies: brace-expansion: 1.1.12 @@ -8836,6 +9900,8 @@ snapshots: dependencies: brace-expansion: 5.0.3 + minimist@1.2.6: {} + minimist@1.2.8: {} minipass@7.1.3: {} @@ -8857,6 +9923,29 @@ snapshots: pkg-types: 1.3.1 ufo: 1.6.3 + mocha@10.8.2: + dependencies: + ansi-colors: 4.1.3 + browser-stdout: 1.3.1 + chokidar: 3.6.0 + debug: 4.4.3(supports-color@8.1.1) + diff: 5.2.2 + escape-string-regexp: 4.0.0 + find-up: 5.0.0 + glob: 8.1.0 + he: 1.2.0 + js-yaml: 4.1.1 + log-symbols: 4.1.0 + minimatch: 5.1.7 + ms: 2.1.3 + serialize-javascript: 6.0.2 + strip-json-comments: 3.1.1 + supports-color: 8.1.1 + workerpool: 6.5.1 + yargs: 16.2.0 + yargs-parser: 20.2.9 + yargs-unparser: 2.0.0 + modern-tar@0.7.5: {} mrmime@2.0.1: {} @@ -8883,6 +9972,55 @@ snapshots: nice-try@1.0.5: {} + nightwatch-axe-verbose@2.4.0: + dependencies: + axe-core: 4.11.1 + + nightwatch@3.15.0(@cucumber/cucumber@10.9.0)(chromedriver@133.0.3): + dependencies: + '@nightwatch/chai': 5.0.3 + '@nightwatch/html-reporter-template': 0.3.0 + '@nightwatch/nightwatch-inspector': 1.0.1 + '@types/chai': 4.3.20 + '@types/selenium-webdriver': 4.35.5 + ansi-to-html: 0.7.2 + aria-query: 5.1.3 + assertion-error: 1.1.0 + boxen: 5.1.2 + chai-nightwatch: 0.5.3 + chalk: 4.1.2 + ci-info: 3.3.0 + cli-table3: 0.6.3 + devtools-protocol: 0.0.1140464 + didyoumean: 1.2.2 + dotenv: 16.3.1 + ejs: 3.1.10 + envinfo: 7.11.0 + glob: 7.2.3 + jsdom: 24.1.3 + lodash: 4.17.23 + minimatch: 3.1.2 + minimist: 1.2.6 + mocha: 10.8.2 + nightwatch-axe-verbose: 2.4.0 + open: 8.4.2 + ora: 5.4.1 + piscina: 4.9.2 + selenium-webdriver: 4.27.0 + semver: 7.5.4 + stacktrace-parser: 0.1.10 + strip-ansi: 6.0.1 + untildify: 4.0.0 + uuid: 8.3.2 + optionalDependencies: + '@cucumber/cucumber': 10.9.0 + chromedriver: 133.0.3 + transitivePeerDependencies: + - bufferutil + - canvas + - supports-color + - utf-8-validate + no-case@3.0.4: dependencies: lower-case: 2.0.2 @@ -8957,10 +10095,17 @@ snapshots: dependencies: boolbase: 1.0.0 + nwsapi@2.2.23: {} + object-assign@4.1.1: {} object-inspect@1.13.4: {} + object-is@1.1.6: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + object-keys@1.1.1: {} object.assign@4.1.7: @@ -9000,6 +10145,16 @@ snapshots: dependencies: wrappy: 1.0.2 + onetime@5.1.2: + dependencies: + mimic-fn: 2.1.0 + + open@8.4.2: + dependencies: + define-lazy-prop: 2.0.0 + is-docker: 2.2.1 + is-wsl: 2.2.0 + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -9009,6 +10164,18 @@ snapshots: type-check: 0.4.0 word-wrap: 1.2.5 + ora@5.4.1: + dependencies: + bl: 4.1.0 + chalk: 4.1.2 + cli-cursor: 3.1.0 + cli-spinners: 2.9.2 + is-interactive: 1.0.0 + is-unicode-supported: 0.1.0 + log-symbols: 4.1.0 + strip-ansi: 6.0.1 + wcwidth: 1.0.1 + own-keys@1.0.1: dependencies: get-intrinsic: 1.3.0 @@ -9148,6 +10315,8 @@ snapshots: pathe@2.0.3: {} + pathval@1.1.1: {} + pend@1.2.0: {} picocolors@1.1.1: {} @@ -9182,6 +10351,10 @@ snapshots: sonic-boom: 4.2.1 thread-stream: 4.0.0 + piscina@4.9.2: + optionalDependencies: + '@napi-rs/nice': 1.1.1 + pixelmatch@7.1.0: dependencies: pngjs: 7.0.0 @@ -9317,6 +10490,10 @@ snapshots: proxy-from-env@1.1.0: {} + psl@1.15.0: + dependencies: + punycode: 2.3.1 + pstree.remy@1.1.8: {} pump@3.0.3: @@ -9350,10 +10527,16 @@ snapshots: query-selector-shadow-dom@1.0.1: {} + querystringify@2.2.0: {} + queue-microtask@1.2.3: {} quick-format-unescaped@4.0.4: {} + randombytes@2.1.0: + dependencies: + safe-buffer: 5.2.1 + react-is@18.3.1: {} read-cache@1.0.0: @@ -9472,6 +10655,8 @@ snapshots: require-from-string@2.0.2: {} + requires-port@1.0.0: {} + resolve-from@4.0.0: {} resolve-from@5.0.0: {} @@ -9492,6 +10677,11 @@ snapshots: dependencies: fast-deep-equal: 2.0.1 + restore-cursor@3.1.0: + dependencies: + onetime: 5.1.2 + signal-exit: 3.0.7 + ret@0.5.0: {} reusify@1.1.0: {} @@ -9535,6 +10725,10 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.59.0 fsevents: 2.3.3 + rrweb-cssom@0.7.1: {} + + rrweb-cssom@0.8.0: {} + run-async@4.0.6: {} run-parallel@1.2.0: @@ -9580,10 +10774,24 @@ snapshots: safer-buffer@2.1.2: {} + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + secure-json-parse@4.1.0: {} seed-random@2.2.0: {} + selenium-webdriver@4.27.0: + dependencies: + '@bazel/runfiles': 6.5.0 + jszip: 3.10.1 + tmp: 0.2.3 + ws: 8.19.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + semver@5.7.2: {} semver@6.3.1: {} @@ -9602,6 +10810,10 @@ snapshots: dependencies: type-fest: 4.41.0 + serialize-javascript@6.0.2: + dependencies: + randombytes: 2.1.0 + set-cookie-parser@2.7.2: {} set-function-length@1.2.2: @@ -9674,6 +10886,8 @@ snapshots: siginfo@2.0.0: {} + signal-exit@3.0.7: {} + signal-exit@4.1.0: {} simple-update-notifier@2.0.0: @@ -9752,6 +10966,10 @@ snapshots: stackframe@1.3.4: {} + stacktrace-parser@0.1.10: + dependencies: + type-fest: 0.7.1 + statuses@2.0.2: {} std-env@3.10.0: {} @@ -9925,6 +11143,8 @@ snapshots: svg-tags@1.0.0: {} + symbol-tree@3.2.4: {} + synckit@0.11.12: dependencies: '@pkgr/core': 0.2.9 @@ -9964,6 +11184,14 @@ snapshots: - bare-buffer - react-native-b4a + tar-stream@2.2.0: + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.5 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + tar-stream@3.1.7: dependencies: b4a: 1.8.0 @@ -9973,6 +11201,13 @@ snapshots: - bare-abort-controller - react-native-b4a + tcp-port-used@1.0.2: + dependencies: + debug: 4.3.1 + is2: 2.0.9 + transitivePeerDependencies: + - supports-color + teex@1.0.1: dependencies: streamx: 2.23.0 @@ -10032,8 +11267,19 @@ snapshots: touch@3.1.1: {} + tough-cookie@4.1.4: + dependencies: + psl: 1.15.0 + punycode: 2.3.1 + universalify: 0.2.0 + url-parse: 1.5.10 + tr46@0.0.3: {} + tr46@5.1.1: + dependencies: + punycode: 2.3.1 + traverse@0.3.9: {} tree-kill@1.2.2: {} @@ -10086,8 +11332,14 @@ snapshots: dependencies: prelude-ls: 1.2.1 + type-detect@4.0.8: {} + + type-fest@0.20.2: {} + type-fest@0.6.0: {} + type-fest@0.7.1: {} + type-fest@0.8.1: {} type-fest@2.19.0: {} @@ -10163,6 +11415,8 @@ snapshots: unicorn-magic@0.3.0: {} + universalify@0.2.0: {} + universalify@2.0.1: {} unplugin-icons@22.5.0: @@ -10182,6 +11436,8 @@ snapshots: picomatch: 4.0.3 webpack-virtual-modules: 0.6.2 + untildify@4.0.0: {} + unzipper@0.10.14: dependencies: big-integer: 1.6.52 @@ -10209,6 +11465,11 @@ snapshots: dependencies: punycode: 2.3.1 + url-parse@1.5.10: + dependencies: + querystringify: 2.2.0 + requires-port: 1.0.0 + urlpattern-polyfill@10.0.0: {} urlpattern-polyfill@10.1.0: {} @@ -10221,6 +11482,8 @@ snapshots: uuid@10.0.0: {} + uuid@8.3.2: {} + uuid@9.0.1: {} v8-compile-cache-lib@3.0.1: {} @@ -10271,7 +11534,7 @@ snapshots: tsx: 4.21.0 yaml: 2.8.2 - vitest@4.0.18(@types/node@25.3.0)(happy-dom@20.7.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2): + vitest@4.0.18(@types/node@25.3.0)(happy-dom@20.7.0)(jiti@2.6.1)(jsdom@24.1.3)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2): dependencies: '@vitest/expect': 4.0.18 '@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@25.3.0)(jiti@2.6.1)(lightningcss@1.31.1)(tsx@4.21.0)(yaml@2.8.2)) @@ -10296,6 +11559,7 @@ snapshots: optionalDependencies: '@types/node': 25.3.0 happy-dom: 20.7.0 + jsdom: 24.1.3 transitivePeerDependencies: - jiti - less @@ -10313,6 +11577,10 @@ snapshots: w3c-keyname@2.2.8: {} + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + wait-port@1.1.0: dependencies: chalk: 4.1.2 @@ -10324,7 +11592,6 @@ snapshots: wcwidth@1.0.1: dependencies: defaults: 1.0.4 - optional: true web-streams-polyfill@3.3.3: {} @@ -10388,6 +11655,8 @@ snapshots: webidl-conversions@3.0.1: {} + webidl-conversions@7.0.0: {} + webpack-virtual-modules@0.6.2: {} whatwg-encoding@3.1.1: @@ -10398,6 +11667,11 @@ snapshots: whatwg-mimetype@4.0.0: {} + whatwg-url@14.2.0: + dependencies: + tr46: 5.1.1 + webidl-conversions: 7.0.0 + whatwg-url@5.0.0: dependencies: tr46: 0.0.3 @@ -10465,8 +11739,14 @@ snapshots: siginfo: 2.0.0 stackback: 0.0.2 + widest-line@3.1.0: + dependencies: + string-width: 4.2.3 + word-wrap@1.2.5: {} + workerpool@6.5.1: {} + wrap-ansi@6.2.0: dependencies: ansi-styles: 4.3.0 @@ -10496,16 +11776,39 @@ snapshots: ws@8.19.0: {} + xml-name-validator@5.0.0: {} + xmlbuilder@15.1.1: {} + xmlchars@2.2.0: {} + y18n@5.0.8: {} yallist@4.0.0: {} yaml@2.8.2: {} + yargs-parser@20.2.9: {} + yargs-parser@21.1.1: {} + yargs-unparser@2.0.0: + dependencies: + camelcase: 6.3.0 + decamelize: 4.0.0 + flat: 5.0.2 + is-plain-obj: 2.1.0 + + yargs@16.2.0: + dependencies: + cliui: 7.0.4 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 20.2.9 + yargs@17.7.2: dependencies: cliui: 8.0.1 @@ -10538,6 +11841,12 @@ snapshots: toposort: 2.0.2 type-fest: 2.19.0 + zip-stream@4.1.1: + dependencies: + archiver-utils: 3.0.4 + compress-commons: 4.1.2 + readable-stream: 3.6.2 + zip-stream@6.0.1: dependencies: archiver-utils: 5.0.2 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 8b4edab..f0c8bb9 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -4,4 +4,5 @@ packages: - 'packages/script' - 'packages/service' - 'packages/app' + - 'packages/nightwatch-devtools' - 'example'