/ tests / capture / capture-augmented.test.js
capture-augmented.test.js
  1  /**
  2   * Capture Module - Additional Unit Tests
  3   * Covers captureWebsite, VIEWPORTS and launchBrowser not in capture-mocked.test.js
  4   */
  5  
  6  import { describe, test, mock, beforeEach } from 'node:test';
  7  import assert from 'node:assert/strict';
  8  
  9  const mockPage = {
 10    goto: mock.fn(async () => ({
 11      status: () => 200,
 12      ok: () => true,
 13      headers: () => ({ 'content-type': 'text/html' }),
 14      url: () => 'https://example.com',
 15    })),
 16    setViewportSize: mock.fn(async () => {}),
 17    evaluate: mock.fn(async fn => {
 18      const fnStr = typeof fn === 'function' ? fn.toString() : '';
 19      if (fnStr.includes('innerWidth') && fnStr.includes('scrollWidth')) {
 20        return {
 21          viewport: { width: 1440, height: 900 },
 22          document: { width: 1440, height: 3000 },
 23          devicePixelRatio: 1,
 24        };
 25      }
 26      if (fnStr.includes('scrollY') && fnStr.includes('innerHeight') && fnStr.includes('target'))
 27        return 900;
 28      if (fnStr.includes('scrollY') && !fnStr.includes('innerHeight')) return 900;
 29      if (fnStr.includes('navigator.language') || fnStr.includes('htmlLang')) {
 30        return { lang: 'en', htmlLang: 'en', metaLang: null, ogLocale: null, hreflangTags: [] };
 31      }
 32      if (fnStr.includes('querySelectorAll')) return 0;
 33      return undefined;
 34    }),
 35    screenshot: mock.fn(async () => Buffer.from('fake-screenshot-data')),
 36    content: mock.fn(async () => '<html><body>Test content</body></html>'),
 37    close: mock.fn(async () => {}),
 38    on: mock.fn(),
 39    locator: mock.fn(() => ({ count: async () => 0, all: async () => [] })),
 40    click: mock.fn(async () => {}),
 41    waitForFunction: mock.fn(async () => {}),
 42    addStyleTag: mock.fn(async () => {}),
 43    keyboard: { press: mock.fn(async () => {}) },
 44    waitForTimeout: mock.fn(async () => {}),
 45    waitForLoadState: mock.fn(async () => {}),
 46    url: () => 'https://example.com',
 47  };
 48  
 49  const mockContext = {
 50    newPage: mock.fn(async () => mockPage),
 51    close: mock.fn(async () => {}),
 52  };
 53  
 54  const mockBrowser = {
 55    newContext: mock.fn(async () => mockContext),
 56    close: mock.fn(async () => {}),
 57  };
 58  
 59  const launchStealthBrowserMock = mock.fn(async () => mockBrowser);
 60  const createStealthContextMock = mock.fn(async () => mockContext);
 61  
 62  mock.module('../../src/utils/stealth-browser.js', {
 63    namedExports: {
 64      launchStealthBrowser: launchStealthBrowserMock,
 65      createStealthContext: createStealthContextMock,
 66      humanScroll: mock.fn(async () => {}),
 67      randomDelay: mock.fn(async () => {}),
 68    },
 69  });
 70  
 71  const optimizedResult = {
 72    cropped: Buffer.from('optimized-cropped'),
 73    uncropped: Buffer.from('optimized-uncropped'),
 74    metadata: { uncroppedSkipped: false },
 75  };
 76  
 77  mock.module('../../src/utils/image-optimizer.js', {
 78    namedExports: {
 79      optimizeScreenshot: mock.fn(async () => optimizedResult),
 80      calculateSavings: mock.fn(() => ({ originalKB: 100, optimizedKB: 50, savingsPercent: 50 })),
 81    },
 82  });
 83  
 84  mock.module('../../src/utils/dom-crop-analyzer.js', {
 85    namedExports: {
 86      analyzeCropBoundaries: mock.fn(() => ({
 87        left: 0,
 88        top: 100,
 89        width: 1440,
 90        height: 700,
 91        metadata: { navReasoning: 'mock nav detected', navHeight: 60 },
 92      })),
 93    },
 94  });
 95  
 96  const { VIEWPORTS, captureWebsite, launchBrowser } = await import('../../src/capture.js');
 97  
 98  describe('VIEWPORTS configuration details', () => {
 99    test('desktop width is at least 1200px', () => {
100      assert.ok(VIEWPORTS.desktop.width >= 1200);
101    });
102    test('mobile width is 400px or less', () => {
103      assert.ok(VIEWPORTS.mobile.width <= 400);
104    });
105    test('both viewports have positive height', () => {
106      assert.ok(VIEWPORTS.desktop.height > 0);
107      assert.ok(VIEWPORTS.mobile.height > 0);
108    });
109    test('mobile width is smaller than desktop width', () => {
110      assert.ok(VIEWPORTS.mobile.width < VIEWPORTS.desktop.width);
111    });
112  });
113  
114  describe('captureWebsite', () => {
115    beforeEach(() => {
116      launchStealthBrowserMock.mock.resetCalls();
117      createStealthContextMock.mock.resetCalls();
118      mockPage.goto.mock.resetCalls();
119      mockContext.close.mock.resetCalls();
120      mockBrowser.close.mock.resetCalls();
121      mockPage.goto.mock.mockImplementation(async () => ({
122        status: () => 200,
123        ok: () => true,
124        headers: () => ({ 'content-type': 'text/html' }),
125        url: () => 'https://example.com',
126      }));
127    });
128  
129    test('launches browser and returns capture results', async () => {
130      const result = await captureWebsite('https://example.com');
131      assert.ok(result, 'should return capture results');
132      assert.ok(launchStealthBrowserMock.mock.calls.length > 0, 'should launch stealth browser');
133    });
134  
135    test('uses headless mode', async () => {
136      await captureWebsite('https://example.com');
137      const launchCall = launchStealthBrowserMock.mock.calls[0];
138      const opts = launchCall?.arguments[0];
139      assert.equal(opts?.headless, true, 'should launch in headless mode');
140    });
141  
142    test('closes context and browser after capture', async () => {
143      await captureWebsite('https://example.com');
144      assert.ok(mockContext.close.mock.calls.length > 0, 'should close context');
145      assert.ok(mockBrowser.close.mock.calls.length > 0, 'should close browser');
146    });
147  
148    test('closes context and browser even when capture fails', async () => {
149      mockPage.goto.mock.mockImplementation(async () => {
150        throw new Error('Navigation failed');
151      });
152      await assert.rejects(() => captureWebsite('https://example.com'), {
153        message: 'Navigation failed',
154      });
155      assert.ok(mockContext.close.mock.calls.length > 0, 'should close context on error');
156      assert.ok(mockBrowser.close.mock.calls.length > 0, 'should close browser on error');
157    });
158  
159    test('returns object with screenshots key', async () => {
160      const result = await captureWebsite('https://example.com');
161      assert.ok(typeof result === 'object');
162      assert.ok('screenshots' in result, 'should have screenshots key');
163    });
164  });
165  
166  describe('launchBrowser options', () => {
167    beforeEach(() => {
168      launchStealthBrowserMock.mock.resetCalls();
169    });
170  
171    test('passes slowMo option to stealth browser', async () => {
172      await launchBrowser({ headless: true, slowMo: 200 });
173      const call = launchStealthBrowserMock.mock.calls[0];
174      assert.equal(call?.arguments[0]?.slowMo, 200);
175    });
176  
177    test('uses headless=false by default', async () => {
178      await launchBrowser({});
179      const call = launchStealthBrowserMock.mock.calls[0];
180      assert.equal(call?.arguments[0]?.headless, false);
181    });
182  
183    test('uses slowMo=100 by default', async () => {
184      await launchBrowser({});
185      const call = launchStealthBrowserMock.mock.calls[0];
186      assert.equal(call?.arguments[0]?.slowMo, 100);
187    });
188  
189    test('always uses minimal stealth level', async () => {
190      await launchBrowser({ headless: true });
191      const call = launchStealthBrowserMock.mock.calls[0];
192      assert.equal(call?.arguments[0]?.stealthLevel, 'minimal');
193    });
194  });