agmission/server/helpers/web_util.js

139 lines
5.4 KiB
JavaScript

'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: '<js expression>' } -> 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,
}