| #!/usr/bin/env node |
| 'use strict'; |
| |
| const fs = require('node:fs'); |
| const playwright = require('playwright'); |
| const { identity, pickBy } = require('lodash'); |
| const { SourceMapConsumer } = require('source-map'); |
| const stacktraceParser = require('stacktrace-parser'); |
| |
| var MochaSpecReporter = require('mocha').reporters.Spec; |
| const createMochaStatsCollector = require('mocha/lib/stats-collector'); |
| |
| // BAIL=0 to disable bailing |
| var bail = process.env.BAIL !== '0'; |
| |
| // Track if the browser has closed at the request of this script, or due to an external event. |
| let closeRequested; |
| |
| // Playwright BrowserType whitelist. |
| // See: https://playwright.dev/docs/api/class-playwright |
| const SUPPORTED_BROWSERS = [ 'chromium', 'firefox', 'webkit' ]; |
| const browserName = process.env.CLIENT || 'firefox'; |
| if (!SUPPORTED_BROWSERS.includes(browserName)) { |
| console.log(` |
| !!! Requested browser not supported: '${browserName}'. |
| !!! Available browsers: ${SUPPORTED_BROWSERS.map(b => `'${b}'`).join(', ')} |
| `); |
| process.exit(1); |
| } |
| |
| var testRoot = 'http://127.0.0.1:8000/tests/'; |
| var testUrl; |
| if (process.env.TYPE === 'performance') { |
| testUrl = testRoot + 'performance/index.html'; |
| } else if (process.env.TYPE === 'fuzzy') { |
| testUrl = testRoot + 'fuzzy/index.html'; |
| } else if (process.env.TYPE === 'mapreduce') { |
| testUrl = testRoot + 'mapreduce/index.html'; |
| } else if (process.env.TYPE === 'find') { |
| testUrl = testRoot + 'find/index.html'; |
| } else { |
| testUrl = testRoot + 'integration/index.html'; |
| } |
| |
| const qs = { |
| remote: 1, |
| invert: process.env.INVERT, |
| grep: process.env.GREP, |
| adapters: process.env.ADAPTERS, |
| viewAdapters: process.env.VIEW_ADAPTERS, |
| autoCompaction: process.AUTO_COMPACTION, |
| SERVER: process.env.SERVER, |
| SKIP_MIGRATION: process.env.SKIP_MIGRATION, |
| srcRoot: process.env.SRC_ROOT, |
| src: process.env.POUCHDB_SRC, |
| useMinified: process.env.USE_MINIFIED, |
| plugins: process.env.PLUGINS, |
| couchHost: process.env.COUCH_HOST, |
| iterations: process.env.ITERATIONS, |
| }; |
| |
| testUrl += '?'; |
| testUrl += new URLSearchParams(pickBy(qs, identity)); |
| |
| let stackConsumer; |
| |
| class ArrayMap extends Map { |
| get(key) { |
| if (!this.has(key)) { |
| this.set(key, []); |
| } |
| return super.get(key); |
| } |
| } |
| |
| class RemoteRunner { |
| constructor(browser) { |
| this.failed = false; |
| this.browser = browser; |
| this.handlers = new ArrayMap(); |
| this.onceHandlers = new ArrayMap(); |
| this.handleEvent = this.handleEvent.bind(this); |
| createMochaStatsCollector(this); |
| } |
| |
| once(name, handler) { |
| this.onceHandlers.get(name).push(handler); |
| } |
| |
| on(name, handler) { |
| this.handlers.get(name).push(handler); |
| } |
| |
| triggerHandlers(eventName, handlerArgs) { |
| const triggerHandler = handler => handler.apply(null, handlerArgs); |
| |
| this.onceHandlers.get(eventName).forEach(triggerHandler); |
| this.onceHandlers.delete(eventName); |
| |
| this.handlers.get(eventName).forEach(triggerHandler); |
| } |
| |
| async handleEvent(event) { |
| try { |
| var additionalProps = ['pass', 'fail', 'pending'].indexOf(event.name) === -1 ? {} : { |
| slow: event.obj.slow ? function () { return event.obj.slow; } : function () { return 60; }, |
| fullTitle: event.obj.fullTitle ? function () { return event.obj.fullTitle; } : undefined, |
| titlePath: event.obj.titlePath ? function () { return event.obj.titlePath; } : undefined, |
| }; |
| var obj = Object.assign({}, event.obj, additionalProps); |
| |
| this.triggerHandlers(event.name, [ obj, event.err ]); |
| |
| if (event.err && stackConsumer) { |
| let stackMapped; |
| const mappedStack = stacktraceParser |
| .parse(event.err.stack) |
| .map(v => { |
| if (v.file === 'http://127.0.0.1:8000/packages/node_modules/pouchdb/dist/pouchdb.min.js') { |
| const NON_UGLIFIED_HEADER_LENGTH = 6; // number of lines of header added in build-pouchdb.js |
| const target = { line:v.lineNumber-NON_UGLIFIED_HEADER_LENGTH, column:v.column-1 }; |
| const mapped = stackConsumer.originalPositionFor(target); |
| v.file = 'packages/node_modules/pouchdb/dist/pouchdb.js'; |
| v.lineNumber = mapped.line; |
| v.column = mapped.column+1; |
| if (mapped.name !== null) { |
| v.methodName = mapped.name; |
| } |
| stackMapped = true; |
| } |
| return v; |
| }) |
| // NodeJS stack frame format: https://nodejs.org/docs/latest/api/errors.html#errorstack |
| .map(v => `at ${v.methodName} (${v.file}:${v.lineNumber}:${v.column})`) |
| .join('\n '); |
| if (stackMapped) { |
| console.log(` [${obj.title}] Minified error stacktrace mapped to:`); |
| console.log(` ${event.err.name||'Error'}: ${event.err.message}`); |
| console.log(` ${mappedStack}`); |
| } |
| } |
| |
| switch (event.name) { |
| case 'fail': this.handleFailed(); break; |
| case 'end': this.handleEnd(); break; |
| } |
| } catch (e) { |
| console.error('Tests failed:', e); |
| |
| closeRequested = true; |
| await this.browser.close(); |
| process.exit(3); |
| } |
| } |
| |
| async handleEnd() { |
| closeRequested = true; |
| await this.browser.close(); |
| process.exit(this.failed ? 1 : 0); |
| } |
| |
| handleFailed() { |
| this.failed = true; |
| if (bail) { |
| try { |
| this.triggerHandlers('end'); |
| } catch (e) { |
| console.log('An error occurred while bailing:', e); |
| } finally { |
| this.handleEnd(); |
| } |
| } |
| } |
| } |
| |
| function BenchmarkConsoleReporter(runner) { |
| runner.on('benchmark:result', function (obj) { |
| console.log(' ', obj); |
| }); |
| } |
| |
| function BenchmarkJsonReporter(runner) { |
| runner.on('end', results => { |
| if (runner.failed) { |
| console.log('Runner failed; JSON will not be writted.'); |
| } else { |
| results.srcRoot = process.env.SRC_ROOT; |
| |
| const resultsDir = 'perf-test-results'; |
| fs.mkdirSync(resultsDir, { recursive: true }); |
| |
| const jsonPath = `${resultsDir}/${new Date().toISOString()}.json`; |
| fs.writeFileSync(jsonPath, JSON.stringify(results, null, 2)); |
| console.log('Wrote JSON results to:', jsonPath); |
| } |
| }); |
| } |
| |
| async function startTest() { |
| if (qs.src === '../../packages/node_modules/pouchdb/dist/pouchdb.min.js') { |
| const mapPath = './packages/node_modules/pouchdb/dist/pouchdb.min.js.map'; |
| const rawMap = fs.readFileSync(mapPath, { encoding:'utf8' }); |
| const jsonMap = JSON.parse(rawMap); |
| stackConsumer = await new SourceMapConsumer(jsonMap); |
| } |
| |
| try { |
| console.log('Starting', browserName, 'on', testUrl); |
| |
| const options = { |
| headless: true, |
| }; |
| const browser = await playwright[browserName].launch(options); |
| |
| const runner = new RemoteRunner(browser); |
| new MochaSpecReporter(runner); |
| new BenchmarkConsoleReporter(runner); |
| |
| if (process.env.JSON_REPORTER) { |
| if (process.env.TYPE !== 'performance') { |
| console.log('!!! JSON_REPORTER should only be set if TYPE is set to "performance".'); |
| process.exit(1); |
| } |
| new BenchmarkJsonReporter(runner); |
| } |
| |
| // Workaround: create a BrowserContext to handle init scripts. In Chromium in |
| // Playwright v1.39.0, v1.40.1 and v1.41.1, page.addInitScript() did not appear to work. |
| const ctx = await browser.newContext(); |
| |
| // Playwright's Browser.on('close') event handler would be the more obvious |
| // choice here, but it does not seem to be triggered if the browser is closed |
| // by an external event (e.g. process is killed, user closes non-headless |
| // browser window). |
| ctx.on('close', () => { |
| if (!closeRequested) { |
| console.log('!!! Browser closed by external event.'); |
| process.exit(1); |
| } |
| }); |
| |
| ctx.exposeFunction('handleMochaEvent', runner.handleEvent); |
| ctx.addInitScript(() => { |
| window.addEventListener('message', (e) => { |
| if (e.data.type === 'mocha') { |
| window.handleMochaEvent(e.data.details); |
| } |
| }); |
| }); |
| |
| ctx.on('pageerror', err => { |
| if (browserName === 'webkit' && err.toString() |
| .match(/^Fetch API cannot load http.* due to access control checks.$/)) { |
| // This is an _uncatchable_, error seen in playwright v1.36.1 webkit. If |
| // it is ignored, fetch() will also throw a _catchable_: |
| // `TypeError: Load failed` |
| console.log('Ignoring error:', err); |
| return; |
| } |
| |
| console.log('Unhandled error in test page:', err); |
| console.log(' stack:', err.stack); |
| console.log(' cause:', err.cause); |
| process.exit(1); |
| }); |
| |
| ctx.on('console', message => { |
| console.log(message.text()); |
| }); |
| |
| const page = await ctx.newPage(); |
| await page.goto(testUrl); |
| |
| const userAgent = await page.evaluate('navigator.userAgent'); |
| console.log('Testing on:', userAgent); |
| } catch (err) { |
| console.log('Error starting tests:', err); |
| process.exit(1); |
| } |
| } |
| |
| if (process.env.MANUAL_DEV_SERVER) { |
| startTest(); |
| } else { |
| // dev-server.js rebuilds bundles when required |
| const devserver = require('./dev-server.js'); |
| devserver.start(startTest); |
| } |