'use strict'; const puppeteer = require('puppeteer'), { AppInputError } = require('./app_error'), debug = require('debug')('agm:web-util'); /** * Take screenshot of a screen using a headless webdriver * @param {*} params screenshot options { url: url, type: 'jpeg', 'png' (default), quality: 1-100 (75% default, jpeg only), width: number, height: number, path: path to save the output image } */ async function webShot(params, ops = { logTime: false, timeout: 30000 }) { const logTime = !!(ops && ops.logTime); if (logTime) console.time('webShot'); const type = params.type || 'png'; const quality = params.quality || 75; const width = params.width || 800; const height = params.height || 600; if (!params || !params.url || !params.path) AppInputError.throw(); let browser; try { browser = await puppeteer.launch({ headless: 'new', // headless: false, slowMo: 250, args: ['--incognito'], ignoreHTTPSErrors: true, ignoreDefaultArgs: ['--disable-dev-shm-usage'], defaultViewport: { width: width, height: height }, fullPage: true }); const pages = await browser.pages(); const page = pages.length ? pages[0] : await browser.newPage(); await page.goto(params.url); // const selector = 'div.gm-style-cc a'; // await page.waitForFunction(selector => !!document.querySelector(selector), { timeout: 10000 }, selector); // Generic wait condition when tiles all finished loading, the page set loaded can add some delay to make sure they all loaded visually perfect await page.waitForFunction('window.loaded == true', { timeout: ops.timeout }); const shotOps = { type: type, clip: { x: 0, y: 0, width: width, height: height }, path: params.path }; if (type == 'jpeg') shotOps['quality'] = quality; await page.screenshot(shotOps); } catch (err) { debug("input:", params); throw err; } finally { if (browser) await browser.close(); if (logTime) console.timeEnd('webShot'); } } /** * Capture multiple screenshots from ONE page in ONE Chromium instance (NFR-1.3 — * the Advanced Report takes a mission map, thumbnails and zone maps per request; * launching a browser per image the way webShot does would dominate the budget). * * @param {*} params { url, width, height } — page to load and viewport size * @param {Array} shots executed in order; each is one of: * { extract: '' } -> push evaluated value into results * { path, type?, quality?, clip?, clipExpr?, skipIf?, evaluate?, waitFor?, optional? } -> screenshot * skipIf: page-side condition; when truthy the shot is skipped (null result) — * lets one batch branch on page state (e.g. locator vs polygon mode) * evaluate: JS to run before the shot (e.g. 'window.focusZone(2)') * waitFor: condition to await before the shot (defaults to none) * clipExpr: page-side expression returning {x, y, width, height} — clip computed * by the page itself (e.g. a zone's pixel rect for thumbnail crops) * optional: on failure push null and continue instead of throwing (NFR-3.1 — * zone-map captures degrade, the mission map does not) * @returns {Array} one entry per shot: saved path, extracted value, or null */ async function webShotBatch(params, shots, ops = { logTime: false, timeout: 30000 }) { const logTime = !!(ops && ops.logTime); if (logTime) console.time('webShotBatch'); if (!params || !params.url || !Array.isArray(shots)) AppInputError.throw(); const timeout = (ops && ops.timeout) || 30000; const width = params.width || 800; const height = params.height || 600; let browser; const results = []; try { browser = await puppeteer.launch({ headless: 'new', args: ['--incognito'], ignoreHTTPSErrors: true, ignoreDefaultArgs: ['--disable-dev-shm-usage'], defaultViewport: { width: width, height: height }, fullPage: true }); const pages = await browser.pages(); const page = pages.length ? pages[0] : await browser.newPage(); await page.goto(params.url); await page.waitForFunction('window.loaded == true', { timeout }); for (const shot of shots) { try { if (shot.extract !== undefined) { results.push(await page.evaluate(shot.extract)); continue; } if (shot.skipIf && await page.evaluate(shot.skipIf)) { results.push(null); continue; } if (shot.evaluate) await page.evaluate(shot.evaluate); if (shot.waitFor) await page.waitForFunction(shot.waitFor, { timeout }); const type = shot.type || 'jpeg'; const shotOps = { type: type, clip: shot.clip || (shot.clipExpr ? await page.evaluate(shot.clipExpr) : { x: 0, y: 0, width: width, height: height }), path: shot.path }; if (type == 'jpeg') shotOps['quality'] = shot.quality || 75; await page.screenshot(shotOps); results.push(shot.path); } catch (err) { if (!shot.optional) throw err; debug('webShotBatch optional shot failed:', shot.path || shot.extract, err.message); results.push(null); } } return results; } catch (err) { debug("input:", params); throw err; } finally { if (browser) await browser.close(); if (logTime) console.timeEnd('webShotBatch'); } } module.exports = { webShot, webShotBatch, }