/ src / browser / dom-helpers.test.ts
dom-helpers.test.ts
  1  import { describe, it, expect } from 'vitest';
  2  import { autoScrollJs, waitForCaptureJs, waitForSelectorJs } from './dom-helpers.js';
  3  
  4  describe('autoScrollJs', () => {
  5    it('returns early without error when document.body is null', async () => {
  6      const g = globalThis as unknown as Record<string, unknown>;
  7      const origDoc = g.document;
  8      g.document = { body: null, documentElement: {} };
  9      g.window = g;
 10      const code = autoScrollJs(3, 500);
 11      // Should resolve without throwing
 12      await expect(eval(code)).resolves.not.toThrow();
 13      g.document = origDoc;
 14      delete g.window;
 15    });
 16  });
 17  
 18  describe('waitForCaptureJs', () => {
 19    it('returns a non-empty string', () => {
 20      const code = waitForCaptureJs(1000);
 21      expect(typeof code).toBe('string');
 22      expect(code.length).toBeGreaterThan(0);
 23      expect(code).toContain('__opencli_xhr');
 24      expect(code).toContain('resolve');
 25      expect(code).toContain('reject');
 26    });
 27  
 28    it('resolves "captured" when __opencli_xhr is populated before deadline', async () => {
 29      const g = globalThis as unknown as Record<string, unknown>;
 30      const captured: unknown[] = [];
 31      g.__opencli_xhr = captured;
 32      g.window = g; // stub window for Node eval
 33      const code = waitForCaptureJs(1000);
 34      const promise = eval(code) as Promise<string>;
 35      captured.push({ data: 'test' });
 36      await expect(promise).resolves.toBe('captured');
 37      delete g.__opencli_xhr;
 38      delete g.window;
 39    });
 40  
 41    it('rejects when __opencli_xhr stays empty past deadline', async () => {
 42      const g = globalThis as unknown as Record<string, unknown>;
 43      g.__opencli_xhr = [];
 44      g.window = g;
 45      const code = waitForCaptureJs(50); // 50ms timeout
 46      const promise = eval(code) as Promise<string>;
 47      await expect(promise).rejects.toThrow('No network capture within 0.05s');
 48      delete g.__opencli_xhr;
 49      delete g.window;
 50    });
 51  
 52    it('resolves immediately when __opencli_xhr already has data', async () => {
 53      const g = globalThis as unknown as Record<string, unknown>;
 54      g.__opencli_xhr = [{ data: 'already here' }];
 55      g.window = g;
 56      const code = waitForCaptureJs(1000);
 57      await expect(eval(code) as Promise<string>).resolves.toBe('captured');
 58      delete g.__opencli_xhr;
 59      delete g.window;
 60    });
 61  });
 62  
 63  describe('waitForSelectorJs', () => {
 64    it('returns a non-empty string', () => {
 65      const code = waitForSelectorJs('#app', 1000);
 66      expect(typeof code).toBe('string');
 67      expect(code).toContain('#app');
 68      expect(code).toContain('querySelector');
 69      expect(code).toContain('MutationObserver');
 70    });
 71  
 72    it('resolves "found" immediately when selector already present', async () => {
 73      const g = globalThis as unknown as Record<string, unknown>;
 74      const fakeEl = { tagName: 'DIV' };
 75      g.document = { querySelector: (_: string) => fakeEl };
 76      const code = waitForSelectorJs('[data-testid="primaryColumn"]', 1000);
 77      await expect(eval(code) as Promise<string>).resolves.toBe('found');
 78      delete g.document;
 79    });
 80  
 81    it('resolves "found" when selector appears after DOM mutation', async () => {
 82      const g = globalThis as unknown as Record<string, unknown>;
 83      let mutationCallback!: () => void;
 84      g.MutationObserver = class {
 85        constructor(cb: () => void) { mutationCallback = cb; }
 86        observe() {}
 87        disconnect() {}
 88      };
 89      let calls = 0;
 90      g.document = {
 91        querySelector: (_: string) => (calls++ > 0 ? { tagName: 'DIV' } : null),
 92        body: {},
 93      };
 94      const code = waitForSelectorJs('#app', 1000);
 95      const promise = eval(code) as Promise<string>;
 96      mutationCallback(); // simulate DOM mutation
 97      await expect(promise).resolves.toBe('found');
 98      delete g.document;
 99      delete g.MutationObserver;
100    });
101  
102    it('rejects when selector never appears within timeout', async () => {
103      const g = globalThis as unknown as Record<string, unknown>;
104      g.MutationObserver = class {
105        constructor(_cb: () => void) {}
106        observe() {}
107        disconnect() {}
108      };
109      g.document = { querySelector: (_: string) => null, body: {} };
110      const code = waitForSelectorJs('#missing', 50);
111      await expect(eval(code) as Promise<string>).rejects.toThrow('Selector not found: #missing');
112      delete g.document;
113      delete g.MutationObserver;
114    });
115  });