UNPKG

reportage

Version:

Scenarist-wrapped mocha sessions on browsers to any reporters

537 lines (516 loc) 18.1 kB
/* @license https://github.com/t2ym/reportage/blob/master/LICENSE.md Copyright (c) 2022, 2023 Tetsuya Mori <t2y3141592@gmail.com>. All rights reserved. */ /* Note: initial obsoleted design target.html <--- window.open()--------------------------------------- reporter.html +- mocha.run() +- ReceiverRunner(origin, Mocha.reporters.HTML) +- Runner +- startListening() +- ProxyReporter(origin) +- window.addEventListener('message', listener) +- window.opener.postMessage({ type: 'openReporter' }, origin) -(open)> +- createStream() +- window.addEventListener('message') +- onOpen(): reporter = new Mocha.reporters.HTML() +- writer = stream.getWriter() <------------(stream)-------------- +- event.source.postMessage(['transferWritableStream', stream]) +- transferEvent('suite', arg) ----{ type: 'suite', arg: arg }----> +- reporter.suiteHandler(arg)) +- transferEvent('pass', arg) ----{ type: 'pass', arg: arg }-----> +- reporter.passHandler(arg)) ... +- transferEvent('suite end',arg) -{type: 'suite end', arg: arg}--> +- reporter.suiteEndHandler(arg)) ... +- transferEvent('end', arg) ----{ type: 'end', arg: arg }-------> +- onEnd() +- writer.close() -------------(close)----------------> +- onClose() */ /** * Installer of the custom reporter to Mocha.reporters */ export function reporterInstaller(Mocha, Config) { const namespace = {}; const Base = Mocha.reporters.Base; const { EVENT_RUN_BEGIN, EVENT_RUN_END, EVENT_SUITE_BEGIN, EVENT_SUITE_END, EVENT_HOOK_BEGIN, EVENT_HOOK_END, EVENT_TEST_PASS, EVENT_TEST_FAIL, EVENT_TEST_PENDING, STATE_IDLE, STATE_RUNNING, STATE_STOPPED, } = Mocha.Runner.constants; // Pseudo-Runner class which transfers received events from Aggregator to HTML reporter // Note: ReceiverRunner is NOT instantiated via mocha.run() namespace.ReceiverRunner = class ReceiverRunner { constructor(reporterType = Mocha.reporters.HTML) { this.state = STATE_IDLE; this.reporterType = reporterType; this.suite = { title: '', suites: [], }; this.listeners = { [EVENT_RUN_BEGIN]: [{ listener: this.onStart, once: true, }], [EVENT_SUITE_BEGIN]: [{ listener: this.onSuite, once: false, }], [EVENT_TEST_PASS]: [{ listener: this.onTest, once: false, }], [EVENT_TEST_FAIL]: [{ listener: this.onTest, once: false, }], [EVENT_TEST_PENDING]: [{ listener: this.onTest, once: false, }], }; } registerOnEnd() { this.on(EVENT_RUN_END, this.onEnd); } onMochaEvent(mEvent) { if (this.listeners[mEvent.type]) { Object.assign(this.stats, mEvent.stats); const arg = this.decodeArg(mEvent.arg); for (let { listener, once } of this.listeners[mEvent.type]) { listener.call(this, arg, arg.err); // Base reporter fail listener expects the 2nd argument as an error object } } else { //console.warn('ReceiverRunner.onChunk: unhandled mocha event', mEvent); } } onOpen(eventData) { /* { type: "open", runner: { total: num, stats: { } }, options: { reporterOptions: {} } } */ if (eventData && typeof eventData === 'object' /* && eventData.type === TYPE_OPEN_REPORTER */ && typeof eventData.runner === 'object') { this.total = eventData.runner.total; this.stats = eventData.runner.stats; this.options = eventData.options; this.reporter = new this.reporterType(this, this.options); this.registerOnEnd(); //console.log('ReceiverRunner.onOpen: ', eventData); } else { console.error('ReceiverRunner.onOpen: invalid event.data', eventData); } } onStart() { //console.log('ReceiverRunner.onStart()'); this.suiteMap = new Map(); this.state = STATE_RUNNING; } onSuite(suite) { suite.suites = suite.suites || []; suite.tests = suite.tests || []; suite._beforeAll = suite._beforeAll || []; suite._beforeEach = suite._beforeEach || []; suite._afterAll = suite._afterAll || []; suite._afterEach = suite._afterEach || []; this.suiteMap.set(suite.__mocha_id__, suite); //console.log(`onSuite ${suite.title} id:${suite.__mocha_id__}, parent id: ${suite.parent ? suite.parent.__mocha_id__ : null}`) //console.log(this.suiteMap); if (suite.root) { this.suite = suite; } else { let parentSuite = this.suiteMap.get(suite.parent.__mocha_id__); if (parentSuite) { parentSuite.suites.push(suite); } else { this.suite.suites.push(suite); // TODO: error? //console.error('parentSuite not found', suite); } } } onTest(test) { let parentSuite = this.suiteMap.get(test.parent.__mocha_id__); if (parentSuite) { parentSuite.tests.push(test); } else { this.suite.tests.push(test); // TODO: error? //console.error('parentSuite not found', test); } } onEnd(arg) { //console.log(`ReceiverRunner.onEnd()`); this.state = STATE_STOPPED; if (typeof this.reporter.done === 'function') { this.reporter.done(); } } on(type, listener) { if (!this.listeners[type]) { this.listeners[type] = []; } this.listeners[type].push({ listener: listener, once: false, }); } once(type, listener) { // TODO: once feature is not functional if (!this.listeners[type]) { this.listeners[type] = []; } this.listeners[type].push({ listener: listener, once: true, }); } decodeArg(arg) { //console.log('decodeArg:original', arg); Object.keys(arg).forEach(key => { if (key.startsWith('$$')) { arg[key.substring(2)] = () => arg[key]; } if (arg[key] && typeof arg[key] === 'object' && !Array.isArray(arg[key])) { Object.keys(arg[key]).forEach(_key => { if (_key.startsWith('$$')) { arg[key][_key.substring(2)] = () => arg[key][_key]; } }); } }); //console.log('decodeArg', arg); return arg; } } namespace.ProxyReporter = class ProxyReporter extends Base { constructor(runner, options) { super(runner, options); const This = this; if (options && options.reporterOptions && options.reporterOptions.start && options.reporterOptions.end && options.reporterOptions.port instanceof MessagePort) { this.start(runner, options); [ EVENT_RUN_BEGIN, EVENT_SUITE_BEGIN, EVENT_SUITE_END, EVENT_TEST_PASS, EVENT_TEST_FAIL, EVENT_TEST_PENDING ].forEach((eventType) => { runner.on(eventType, (arg) => { //console.log('transferring chunk', arg); This.enqueueEvent(eventType, arg); This.transferEvent(); }); }); runner.once(EVENT_RUN_END, () => { //console.log('transferring event ', EVENT_RUN_END); This.enqueueEvent(EVENT_RUN_END, { $$fulltitle: '' }); This.transferEvent(); }); } else { throw new Error(`${this.constructor.name}: invalid reporterOptions ${JSON.stringify(options ? options.reporterOptions : null)}`); } } start(runner, options) { this.reporterOrigin = options.reporterOptions.reporterOrigin; this.pendingQueue = []; this.mochaTestIdInventory = {}; this.port = options.reporterOptions.port; options.reporterOptions.port = null; this.onTransferPort(this.port); this.runner = runner; this.options = options; this._onBeforeUnload = (event) => { this.onBeforeUnload(event); }; window.addEventListener('beforeunload', this._onBeforeUnload, false); this.options.reporterOptions.start(runner, this); } onBeforeUnload(event) { if (this.writer) { if (this.writerClosed) { //this.writer.close(); } else { const stack = (new Error().stack); const reason = stack + '\nunexpected unload of target page'; this.writer.abort(reason); console.error('writer.abort() on unexpected unload of target page'); } } } onTransferPort(port) { if (port instanceof MessagePort) { this.portReceived = true; this.port = port; const This = this; this.writer = { write(mEvent) { This.port.postMessage({ op: 'write', payload: mEvent, sent: Date.now(), }); }, close() { This.port.postMessage({ op: 'close', sent: Date.now(), }); }, abort(err) { This.port.postMessage({ op: 'abort', err: err, sent: Date.now(), }); }, beacon() { This.port.postMessage({ op: 'beacon', sent: Date.now(), }); }, }; this.port.start(); this.writer.beacon(); setInterval(() => { this.writer.beacon(); }, 1000); this.writerClosed = false; //console.log('onTransferPort: MessagePort received'); } } enqueueEvent(eventType, arg = {}) { //console.log('enqueueEvent called', eventType, arg); /* Suite.prototype.serialize = function serialize() { return { _bail: this._bail, $$fullTitle: this.fullTitle(), $$isPending: Boolean(this.isPending()), root: this.root, title: this.title, [MOCHA_ID_PROP_NAME]: this.id, parent: this.parent ? {[MOCHA_ID_PROP_NAME]: this.parent.id} : null }; }; Test.prototype.serialize = function serialize() { return { $$currentRetry: this._currentRetry, $$fullTitle: this.fullTitle(), $$isPending: Boolean(this.pending), $$retriedTest: this._retriedTest || null, $$slow: this._slow, $$titlePath: this.titlePath(), body: this.body, duration: this.duration, err: this.err, parent: { $$fullTitle: this.parent.fullTitle(), [MOCHA_ID_PROP_NAME]: this.parent.id }, speed: this.speed, state: this.state, title: this.title, type: this.type, file: this.file, [MOCHA_ID_PROP_NAME]: this.id }; }; Hook.prototype.serialize = function serialize() { return { $$currentRetry: this.currentRetry(), $$fullTitle: this.fullTitle(), $$isPending: Boolean(this.isPending()), $$titlePath: this.titlePath(), ctx: this.ctx && this.ctx.currentTest ? { currentTest: { title: this.ctx.currentTest.title, [MOCHA_ID_PROP_NAME$1]: this.ctx.currentTest.id } } : {}, duration: this.duration, file: this.file, parent: { $$fullTitle: this.parent.fullTitle(), [MOCHA_ID_PROP_NAME$1]: this.parent.id }, state: this.state, title: this.title, type: this.type, [MOCHA_ID_PROP_NAME$1]: this.id }; }; */ const chunk = { type: eventType, stats: this.runner.stats, timings: {} }; if (arg instanceof Mocha.Suite) { chunk.arg = arg.serialize(); chunk.arg.context = []; if (eventType === EVENT_SUITE_BEGIN) { const id = chunk.arg.__mocha_id__; if (chunk.arg.root) { this.suiteIdMap = {}; this.suiteIdMap[id] = Symbol.for('root'); this.testClassIndex = -2; // root } else { const parentId = chunk.arg.parent.__mocha_id__; if (this.suiteIdMap[parentId] === Symbol.for('root')) { this.suiteIdMap[id] = Symbol.for('scope'); this.testClassIndex = -1; // scope } else if (this.suiteIdMap[parentId] === Symbol.for('scope')) { this.suiteIdMap[id] = Symbol.for('class'); if (this.testClassIndex < 0) { this.testClassIndex = 0; // first } else { this.testClassIndex++; // subsequent } } } const reporterURL = new URL(Config.reporterURL); const configPath = (reporterURL.hash.substring(1) || '/test/reportage.config.js').split('?')[0]; let url = `${reporterURL.origin}${reporterURL.pathname}${reporterURL.search}#${configPath}`; if (!chunk.arg.root) { url += `?scope=${encodeURIComponent(this.driver.suite.scope)}`; if (this.testClassIndex >= 0) { url += `&testIndex=${encodeURIComponent(this.driver.suite.testIndex)}` + `&testClass=${encodeURIComponent(this.driver.suite.tests.split(',')[this.testClassIndex])}`; } } chunk.arg.context.push({ title: 'suiteURL', value: url, }); } } else if (arg instanceof Mocha.Test) { const id = arg.__mocha_id__; if (id && this.mochaTestIdInventory[id]) { return; // skip enqueueing redundant tests such as unhandled exceptions } this.mochaTestIdInventory[id] = eventType; let err = arg.err; if (err) { err = { $$toString: err.toString(), actual: err.actual, expected: err.expected, message: err.message, operator: err.operator, showDiff: err.showDiff, stack: err.stack, }; } else { // arg.err is mandatory for failures but is missing // supply err for a failure err = { $$toString: arg.title, actual: '', expected: '', message: arg.title, operator: '', showDiff: '', stack: '', }; } chunk.arg = arg.serialize(); chunk.arg.body = arg.body; chunk.arg.err = err; if (chunk.arg.$$retriedTest instanceof Mocha.Test) { chunk.arg.$$retriedTest = chunk.arg.$$retriedTest.serialize(); } chunk.arg.context = JSON.parse(JSON.stringify(arg.context || [])); // mochawesome addContext } else if (arg instanceof Mocha.Hook) { let err = arg.err; if (err) { err = { $$toString: err.toString(), actual: err.actual, expected: err.expected, message: err.message, operator: err.operator, showDiff: err.showDiff, stack: err.stack, }; } /* else { // arg.err is mandatory for failures but is missing // supply err for a failure err = { $$toString: arg.title, actual: '', expected: '', message: arg.title, operator: '', showDiff: '', stack: '', }; } */ chunk.arg = arg.serialize(); chunk.arg.body = arg.body; chunk.arg.err = err; } else { chunk.arg = arg; } chunk.timings.enqueue = Date.now(); this.pendingQueue.push(chunk); //console.log("enqueueEvent: Chunk enqueued", chunk); } onClose(err) { //console.log('ProxyReporter.onClose() called'); this.writerClosed = true; this.writer.close(); if (this.options && this.options.reporterOptions && typeof this.options.reporterOptions.end === 'function') { this.options.reporterOptions.end(err, this); } if (typeof this._onBeforeUnload === 'function') { window.removeEventListener('beforeunload', this._onBeforeUnload); } } transferEvent() { const writable = !!(this.writer && !this.writerClosed); //console.log('transferEvent called writable = ', writable, 'writer', this.writer, 'writerClosed', this.writerClosed); if (writable) { let chunk; try { while (!this.writerClosed && this.pendingQueue.length > 0) { chunk = this.pendingQueue.shift(); //console.log(`About to write a chunk from pending queue to sink (${this.pendingQueue.length} remaining)`, chunk); chunk.timings.write = Date.now(); this.writer.write(chunk); //console.log(`Chunk written from pending queue (${this.pendingQueue.length} remaining) to sink in ${Date.now() - chunk.timings.write}ms`, chunk); if (chunk.type === EVENT_RUN_END) { this.onClose(); } } } catch (err) { console.error('catching error', err, chunk); this.onClose(err); } } } } namespace.ProxyReporter.description = 'Proxy reporter events to ReceiverRunner via MessagePort'; Mocha.reporters.ProxyReporter = Mocha.reporters['proxy-reporter'] = namespace.ProxyReporter; return namespace; }