try { require('puppeteer') } catch (e) { console.error( "\nFATAL ERROR:" + "\n Package 'puppeteer' is not available." + "\n Run 'npm install --no-save puppeteer' before running this script" + "\n * You may use PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=1 environment variable to avoid automatic Chromium downloading" + "\n (specify own Chromium/Chrome version through PUPPETEER_EXECUTABLE_PATH=`which google-chrome` environment variable)" + "\n"); process.exit(1); } const puppeteer = require('puppeteer') const colors = require("ansi-colors") const path = require("path"); const fs = require("fs"); const http = require("http"); run_main(require('minimist')(process.argv.slice(2))); async function run_main(o = {}) { try { await main(o); console.magenta("FATAL: Unexpected exit!"); process.exit(1); } catch (e) { console.error(colors.magenta("FATAL: Unexpected exception!")); console.error(e); process.exit(1); } } async function main(o = {}) { o = Object.assign({}, { buildFolder: __dirname, port: 8080, debug: false, noHeadless: false, serverPrefix: `http://localhost`, noExit: false, screenshot: undefined, help: false, noTryCatch: false, maxBlockDuration: 30000 }, o) if (typeof o.screenshot == 'string' && o.screenshot == 'false') { console.log(colors.red('ERROR: misused screenshot option, use --no-screenshot instead')); } if (o.noExit) { o.maxBlockDuration = 999999999 } o.debug && console.log('Current Options', o); if (o.help) { printHelpAndExit(); } const serverAddress = `${o.serverPrefix}:${o.port}` const url = `${serverAddress}/tests.html${o.noTryCatch ? '?notrycatch=1' : ''}`; if (!fs.existsSync(o.buildFolder)) { console.error(`Expected folder "${o.buildFolder}" to exists. Aborting`); } o.debug && debug('Server Listening at ' + url); const server = await staticServer(o.buildFolder, o.port, m => debug, m => error); o.debug && debug(`Browser launching ${!o.noHeadless ? 'headless' : 'not headless'}`); const browser = await puppeteer.launch({ headless: !o.noHeadless }); const page = await browser.newPage(); page.on('console', e => { locationMsg = formatMessage(`${e.location().url}:${e.location().lineNumber}:${e.location().columnNumber}`); if (e.type() === 'error') { console.log(colors.red(formatMessage('' + e.text(), `-- ERROR:${locationMsg}: `, ))); } else if (o.debug) { o.debug && console.log(colors.grey(formatMessage('' + e.text(), `-- ${locationMsg}: `))); } }); o.debug && debug(`Opening page address ${url}`); await page.goto(url); await page.waitForFunction(() => (document.querySelector(`#qunit-testresult`) && document.querySelector(`#qunit-testresult`).textContent || '').trim().toLowerCase().startsWith('tests completed')); const text = await getText(`#qunit-testresult`); if (!text) { return await fail(`An error occurred extracting test results. Check the build folder ${o.buildFolder} is correct and has build with tests enabled.`); } o.debug && debug(colors.blackBright("* UserAgent: " + await getText('#qunit-userAgent'))); const testFailed = !text.includes(' 0 failed'); if (testFailed && !o.debug) { process.stdout.write(colors.grey("* Use '--debug' parameter to see details of failed tests.\n")); } if (o.screenshot || (o.screenshot === undefined && testFailed)) { await page.screenshot({ path: 'screenshot.png', fullPage: 'true' }); process.stdout.write(colors.grey(`* Screenshot taken: ${o.buildFolder}/screenshot.png\n`)); } if (testFailed) { const report = await failReport(); process.stdout.write(` ${colors.red.bold.underline('Failed tests ! :(')} ${colors.redBright(colors.symbols.cross + ' ' + report.join(`\n${colors.symbols.cross} `))} ${colors.redBright(`=== Summary ===\n${text}`)} `); } else { process.stdout.write(colors.green(` ${colors.symbols.check} No Errors :) === Summary ===\n${text} `)); } if (o.noExit) { while (true) { await new Promise(r => setTimeout(r, 5000)); } } await server && server.close(); await browser.close(); process.exit(testFailed ? 1 : 0); async function getText(s) { return await page.evaluate((s) => (document.querySelector(s) && document.querySelector(s).innerText) || ''.trim(), s); } async function failReport() { const failures = await page.evaluate(() => Array.from(document.querySelectorAll('#qunit-tests .fail')).filter(e => e.querySelector('.module-name')).map(e => ({ moduleName: e.querySelector('.module-name') && e.querySelector('.module-name').textContent, testName: e.querySelector('.test-name') && e.querySelector('.test-name').textContent, expected: e.querySelector('.test-expected pre') && e.querySelector('.test-expected pre').textContent, actual: e.querySelector('.test-actual pre') && e.querySelector('.test-actual pre').textContent, code: e.querySelector('.test-source') && e.querySelector('.test-source').textContent.replace("Source: at ", ""), }))); return failures.map(f => `${f.moduleName}: ${f.testName} (${formatMessage(f.code)})`); } async function fail(s) { await failReport(); process.stdout.write(colors.red(s) + '\n'); if (o.screenshot || o.screenshot === undefined) { await page.screenshot({ path: 'screenshot.png', fullPage: 'true' }); process.stdout.write(colors.grey(`* Screenshot taken: ${o.buildFolder}/screenshot.png\n`)); } process.exit(1); } async function debug(s) { process.stdout.write(s + '\n'); } async function error(s) { process.stdout.write(s + '\n'); } function formatMessage(message, prefix) { prefix = prefix || ''; return prefix + ('' + message).split('\n').map(l => l.replace(serverAddress, o.buildFolder)).join('\n' + prefix); } } function printHelpAndExit() { console.log(` Usage: # First, remember to build opencv.js with tests enabled: ${colors.blueBright(`python ./platforms/js/build_js.py build_js --build_test`)} # Install the tool locally (needed only once) and run it ${colors.blueBright(`cd build_js/bin`)} ${colors.blueBright(`npm install`)} ${colors.blueBright(`node run_puppeteer`)} By default will run a headless browser silently printing a small report in the terminal. But it could used to debug the tests in the browser, take screenshots, global tool or targeting external servers exposing the tests. TIP: you could install the tool globally (npm install --global build_js/bin) to execute it from any local folder. # Options * port?: number. Default 8080 * buildFolder?: string. Default __dirname (this folder) * debug?: boolean. Default false * noHeadless?: boolean. Default false * serverPrefix?: string . Default http://localhost * help?: boolean * screenshot?: boolean . Make screenshot on failure by default. Use --no-screenshot to disable screenshots completely. * noExit?: boolean default false. If true it will keep running the server - together with noHeadless you can debug in the browser. * noTryCatch?: boolean will disable Qunit tryCatch - so exceptions are dump to stdout rather than in the browser. * maxBlockDuration: QUnit timeout. If noExit is given then is infinity. `); process.exit(0); } async function staticServer(basePath, port, onFound, onNotFound) { return new Promise(async (resolve) => { const server = http.createServer((req, res) => { var url = resolveUrl(req.url); onFound && onFound(url); var stream = fs.createReadStream(path.join(basePath, url || '')); stream.on('error', function () { onNotFound && onNotFound(url); res.writeHead(404); res.end(); }); stream.pipe(res); }).listen(port); server.on('listening', () => { resolve(server); }); }); function resolveUrl(url = '') { var i = url.indexOf('?'); if (i != -1) { url = url.substr(0, i); } i = url.indexOf('#'); if (i != -1) { url = url.substr(0, i); } return url; } }