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 });