UNPKG

browser-debugger-cli

Version:

DevTools telemetry in your terminal. For humans and agents. Direct WebSocket to Chrome's debugging port.

235 lines 9.93 kB
/** * DOM Eval Command Smoke Tests * * Provides basic smoke test coverage for DOM eval helpers to verify core functionality. * These are lightweight tests that verify the helper functions work as expected without * requiring full integration testing with real sessions. * * Coverage: * 1. verifyTargetExists() - CDP target validation * 2. executeScript() - JavaScript execution via CDP * * Note: Session validation functions (validateActiveSession, getValidatedSessionMetadata) * are difficult to test in isolation due to ES module import constraints. They are covered * by integration tests instead. */ import assert from 'node:assert/strict'; import { afterEach, beforeEach, describe, it, mock } from 'node:test'; import { createResponse } from '../../__testfixtures__/cdpMessages.js'; import { FakeWebSocket } from '../../__testutils__/FakeWebSocket.js'; import { verifyTargetExists, executeScript } from '../../commands/dom/evalHelpers.js'; import { CDPConnection } from '../../connection/cdp.js'; /** * Mock CDP connection that simulates browser responses */ class MockCDPConnection extends CDPConnection { mockSocket; constructor() { const mockSocket = new FakeWebSocket(); super(console, () => mockSocket); this.mockSocket = mockSocket; } /** * Connect and open the mock WebSocket */ async connectAndOpen() { const connectPromise = this.connect('ws://localhost:9222/devtools/browser'); this.mockSocket.simulateOpen(); await connectPromise; } /** * Simulate CDP response for next command */ simulateNextResponse(result) { const messages = this.mockSocket.getSentMessages(); const lastMessage = messages[messages.length - 1]; if (!lastMessage) { throw new Error('No message sent to simulate response for'); } const request = JSON.parse(lastMessage); if (typeof request.id !== 'number') { throw new Error('Invalid request ID'); } const response = createResponse(request.id, result); this.mockSocket.simulateMessage(JSON.stringify(response)); } } describe('DOM Eval Command Smoke Tests', () => { let mockCDP; beforeEach(async () => { mockCDP = new MockCDPConnection(); await mockCDP.connectAndOpen(); }); afterEach(() => { if (mockCDP.isConnected()) { mockCDP.close(); } mock.restoreAll(); }); describe('verifyTargetExists', () => { it('throws error when target not found in CDP list', async () => { // Test CONTRACT: Missing target → error // Mock fetch to return targets without our targetId // eslint-disable-next-line @typescript-eslint/require-await globalThis.fetch = mock.fn(async () => { return { // eslint-disable-next-line @typescript-eslint/require-await json: async () => [ { id: 'page-456', type: 'page' }, { id: 'page-789', type: 'page' }, ], }; }); const metadata = { bdgPid: 12345, startTime: Date.now(), port: 9222, targetId: 'page-123', webSocketDebuggerUrl: 'ws://localhost:9222/devtools/page/123', }; await assert.rejects(async () => verifyTargetExists(metadata, 9222), { message: /Session target not found/, }); }); it('succeeds when target exists', async () => { // Test CONTRACT: Target exists → no error // eslint-disable-next-line @typescript-eslint/require-await globalThis.fetch = mock.fn(async () => { return { ok: true, // eslint-disable-next-line @typescript-eslint/require-await json: async () => [ { id: 'page-123', type: 'page' }, { id: 'page-456', type: 'page' }, ], }; }); const metadata = { bdgPid: 12345, startTime: Date.now(), port: 9222, targetId: 'page-123', webSocketDebuggerUrl: 'ws://localhost:9222/devtools/page/123', }; await assert.doesNotReject(async () => verifyTargetExists(metadata, 9222)); }); it('throws error when CDP response is invalid', async () => { // Test CONTRACT: Invalid CDP response → treated as target not found // After refactoring to use fetchCDPTargetById, invalid responses // are handled uniformly with missing targets (both return null) // eslint-disable-next-line @typescript-eslint/require-await globalThis.fetch = mock.fn(async () => { return { ok: true, // eslint-disable-next-line @typescript-eslint/require-await json: async () => ({ invalid: 'response' }), }; }); const metadata = { bdgPid: 12345, startTime: Date.now(), port: 9222, targetId: 'page-123', webSocketDebuggerUrl: 'ws://localhost:9222/devtools/page/123', }; await assert.rejects(async () => verifyTargetExists(metadata, 9222), { message: /Session target not found/, }); }); }); describe('executeScript', () => { it('executes simple JavaScript expression', async () => { // Test CONTRACT: Script execution → returns result const scriptPromise = executeScript(mockCDP, 'document.title'); mockCDP.simulateNextResponse({ result: { type: 'string', value: 'My Page Title', }, }); const result = await scriptPromise; assert.equal(result.result?.value, 'My Page Title'); }); it('executes complex JavaScript expression', async () => { // Test CONTRACT: Complex script → returns object result const scriptPromise = executeScript(mockCDP, 'window.location.href'); mockCDP.simulateNextResponse({ result: { type: 'string', value: 'https://example.com/page', }, }); const result = await scriptPromise; assert.equal(result.result?.value, 'https://example.com/page'); }); it('throws error when script has exception', async () => { // Test CONTRACT: Script exception → throws error const scriptPromise = executeScript(mockCDP, 'throw new Error("test error")'); mockCDP.simulateNextResponse({ exceptionDetails: { exception: { description: 'Error: test error', }, }, }); await assert.rejects(async () => scriptPromise, { message: /Error: test error/, }); }); it('throws error with default message when exception has no description', async () => { // Test CONTRACT: Exception without description → default error const scriptPromise = executeScript(mockCDP, 'invalid script'); mockCDP.simulateNextResponse({ exceptionDetails: { exception: {}, }, }); await assert.rejects(async () => scriptPromise, { message: /Unknown error executing script/, }); }); it('handles primitive return values', async () => { // Test CONTRACT: Different primitives preserved const tests = [ { script: '42', result: { type: 'number', value: 42 } }, { script: 'true', result: { type: 'boolean', value: true } }, { script: '"hello"', result: { type: 'string', value: 'hello' } }, { script: 'null', result: { type: 'object', subtype: 'null', value: null } }, ]; for (const test of tests) { const scriptPromise = executeScript(mockCDP, test.script); mockCDP.simulateNextResponse({ result: test.result }); const result = await scriptPromise; assert.equal(result.result?.value, test.result.value); } }); it('handles object return values', async () => { // Test CONTRACT: Objects returned by value const scriptPromise = executeScript(mockCDP, '({ url: "https://example.com", title: "Example" })'); mockCDP.simulateNextResponse({ result: { type: 'object', value: { url: 'https://example.com', title: 'Example' }, }, }); const result = await scriptPromise; assert.deepEqual(result.result?.value, { url: 'https://example.com', title: 'Example', }); }); it('handles array return values', async () => { // Test CONTRACT: Arrays returned by value const scriptPromise = executeScript(mockCDP, 'Array.from(document.querySelectorAll("div")).length'); mockCDP.simulateNextResponse({ result: { type: 'number', value: 5, }, }); const result = await scriptPromise; assert.equal(result.result?.value, 5); }); }); }); //# sourceMappingURL=domEval.contract.test.js.map