/ extension / src / cdp.test.ts
cdp.test.ts
  1  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
  2  
  3  function createChromeMock() {
  4    const debuggerEventListeners: Array<(source: { tabId?: number }, method: string, params: any) => void> = [];
  5    const tabRemovedListeners: Array<(tabId: number) => void> = [];
  6    const tabs = {
  7      get: vi.fn(async (_tabId: number) => ({
  8        id: 1,
  9        windowId: 1,
 10        url: 'https://x.com/home',
 11      })),
 12      onRemoved: { addListener: vi.fn((fn: (tabId: number) => void) => { tabRemovedListeners.push(fn); }) },
 13      onUpdated: { addListener: vi.fn() },
 14    };
 15  
 16    const debuggerApi = {
 17      attach: vi.fn(async () => {}),
 18      detach: vi.fn(async () => {}),
 19      sendCommand: vi.fn(async (_target: unknown, method: string) => {
 20        if (method === 'Runtime.evaluate') return { result: { value: 'ok' } };
 21        return {};
 22      }),
 23      onDetach: { addListener: vi.fn() },
 24      onEvent: { addListener: vi.fn((fn: (source: { tabId?: number }, method: string, params: any) => void) => { debuggerEventListeners.push(fn); }) },
 25    };
 26  
 27    const scripting = {
 28      executeScript: vi.fn(async () => [{ result: { removed: 1 } }]),
 29    };
 30  
 31    return {
 32      chrome: {
 33        tabs,
 34        debugger: debuggerApi,
 35        scripting,
 36        runtime: { id: 'opencli-test' },
 37      },
 38      debuggerApi,
 39      scripting,
 40      debuggerEventListeners,
 41      tabRemovedListeners,
 42    };
 43  }
 44  
 45  describe('cdp attach recovery', () => {
 46    beforeEach(() => {
 47      vi.resetModules();
 48    });
 49  
 50    afterEach(() => {
 51      vi.unstubAllGlobals();
 52    });
 53  
 54    it('does not mutate the DOM before a successful attach', async () => {
 55      const { chrome, debuggerApi, scripting } = createChromeMock();
 56      vi.stubGlobal('chrome', chrome);
 57  
 58      const mod = await import('./cdp');
 59      const result = await mod.evaluate(1, '1');
 60  
 61      expect(result).toBe('ok');
 62      expect(debuggerApi.attach).toHaveBeenCalledTimes(1);
 63      expect(scripting.executeScript).not.toHaveBeenCalled();
 64    });
 65  
 66    it('uses the default execution context for a frame when isolated worlds also exist', async () => {
 67      const { chrome, debuggerApi, debuggerEventListeners } = createChromeMock();
 68      vi.stubGlobal('chrome', chrome);
 69  
 70      const mod = await import('./cdp');
 71      mod.registerFrameTracking();
 72  
 73      expect(debuggerEventListeners).toHaveLength(1);
 74      debuggerEventListeners[0](
 75        { tabId: 1 },
 76        'Runtime.executionContextCreated',
 77        { context: { id: 11, auxData: { frameId: 'frame-1', isDefault: false } } },
 78      );
 79      debuggerEventListeners[0](
 80        { tabId: 1 },
 81        'Runtime.executionContextCreated',
 82        { context: { id: 22, auxData: { frameId: 'frame-1', isDefault: true } } },
 83      );
 84  
 85      await mod.evaluateInFrame(1, 'document.title', 'frame-1');
 86  
 87      expect(debuggerApi.sendCommand).toHaveBeenCalledWith(
 88        { tabId: 1 },
 89        'Runtime.evaluate',
 90        expect.objectContaining({ contextId: 22 }),
 91      );
 92    });
 93  
 94    // Dead test: chrome.scripting.executeScript was removed from cdp.ts;
 95    // this test references functionality that no longer exists. Delete or rewrite
 96    // when cdp attach-recovery logic is next updated.
 97    it.skip('retries after cleanup when attach fails with a foreign extension error', async () => {
 98      const { chrome, debuggerApi, scripting } = createChromeMock();
 99      debuggerApi.attach
100        .mockRejectedValueOnce(new Error('Cannot access a chrome-extension:// URL of different extension'))
101        .mockResolvedValueOnce(undefined);
102      vi.stubGlobal('chrome', chrome);
103  
104      const mod = await import('./cdp');
105      const result = await mod.evaluate(1, '1');
106  
107      expect(result).toBe('ok');
108      expect(scripting.executeScript).toHaveBeenCalledTimes(1);
109      expect(debuggerApi.attach).toHaveBeenCalledTimes(2);
110    });
111  });