reportage
Version:
Scenarist-wrapped mocha sessions on browsers to any reporters
922 lines (901 loc) • 31.6 kB
JavaScript
/*
@license https://github.com/t2ym/reportage/blob/master/LICENSE.md
Copyright (c) 2023 Tetsuya Mori <t2y3141592@gmail.com>. All rights reserved.
*/
import { _globalThis, sandbox } from './sandbox-global.js';
import { mochaInstaller } from './mocha-loader.js';
import { reporterInstaller } from './proxy-reporter.js';
import {
NAME_REPORTER,
NAME_MEDIATOR,
NAME_MEDIATOR_BRIDGE,
TYPE_READY,
TYPE_ERROR,
TYPE_DISCONNECT,
TYPE_TRANSFER_PORT,
TYPE_REQUEST_SESSION,
TYPE_START_SESSION,
TYPE_END_SESSION,
TYPE_NAVIGATE,
TYPE_NAVIGATING,
TYPE_CLOSING,
TYPE_CLOSE,
TYPE_COVERAGE,
DRIVER_STATE_CLOSED,
DRIVER_STATE_LOADING,
DRIVER_STATE_DISCONNECTED,
DRIVER_STATE_CONNECTING,
DRIVER_STATE_CONNECTED,
DRIVER_STATE_READY,
DRIVER_STATE_STARTING0,
DRIVER_STATE_RUNNING,
DRIVER_STATE_STOPPING,
DRIVER_STATE_STOPPED,
DRIVER_STATE_ABORTING,
DRIVER_STATE_CLOSING0,
DRIVER_STATE_CLOSING1,
DRIVER_STATE_CLOSING2,
DRIVER_STATE_NAVIGATING0,
DRIVER_STATE_NAVIGATING1,
DRIVER_STATE_NAVIGATING2,
DRIVER_STATE_NAVIGATING3,
DRIVER_STATE_BEFOREUNLOAD,
DRIVER_STATE_UNLOADING,
DRIVER_STATE_ERROR,
SESSION_STORAGE_DRIVER_NAVIGATING,
// location
NAVI_LOCATION_HREF,
NAVI_LOCATION_RELOAD,
NAVI_LOCATION_REPLACE,
NAVI_LOCATION_ASSIGN,
// history
NAVI_HISTORY_GO,
NAVI_HISTORY_BACK,
NAVI_HISTORY_FORWARD,
NAVI_HISTORY_REPLACE,
NAVI_HISTORY_PUSH,
// click link or programmatic
NAVI_DEFERRED,
} from './constants.js';
try {
/*
{
let w = window.open("about:blank"); // open a new window without user interaction
//console.log(`window.open("about:blank") = `, w);
if (w) {
//console.log('closing the window');
w.close();
//launcherButton.click();
}
else {
console.error(`Popup blocking on the target app host domain ${location.hostname} has to be disabled for the site to open mediator.html window` +
`The instruction on how to allow pop-ups and redirects from a site is found at ` +
`https://support.google.com/chrome/answer/95472?hl=en&co=GENIE.Platform%3DDesktop#zippy=%2Callow-pop-ups-and-redirects-from-a-site`);
console.error(`The command line option --disable-popup-blocking is also effective if it is applied properly to Chrome browser processes`);
throw new Error('popup blocking must be disabled for the host domain ' + location.hostname);
}
}
*/
const driverURL = import.meta.url;
//console.log(`driver.js: driverURL = ${driverURL}`);
const testConfigPath = new URL(driverURL).hash.substring(1) || '/test/reportage.config.js';
let Config;
let success = false;
let retryCount = 0;
let maxRetries = 5;
do {
try {
const { default: _Config } = await import(testConfigPath + (retryCount > 0 ? '#' + retryCount : ''));
Config = _Config;
Config.importedBy(driverURL);
success = true;
}
catch (e) {
console.error(e);
}
if (!success) {
await new Promise(resolve => setTimeout(resolve, 1000));
retryCount++;
if (retryCount > maxRetries) {
// no way to recover
console.error(`driver.js: failed to load ${testConfigPath}`);
window.close();
}
}
}
while (!success);
//window.Config = Config; // for debugging
let Suite;
success = false;
retryCount = 0;
const SESSION_STORAGE_RELOADING = 'SessionStorage:reloading';
do {
if (Config.importOnlyTargetScope) break;
try {
const { default: _Suite } = await import(Config.suitesLoaderPath + (retryCount > 0 ? '#' + retryCount : ''));
Suite = _Suite;
success = true;
}
catch (e) {
console.error(e);
}
if (!success) {
await new Promise(resolve => setTimeout(resolve, 1000));
retryCount++;
if (retryCount > Config.suitesLoaderRetries) {
const reloading = sessionStorage.getItem(SESSION_STORAGE_RELOADING);
if (reloading) {
sessionStorage.removeItem(SESSION_STORAGE_RELOADING);
console.error(`driver.js: closing as loading suites failed`);
window.close();
throw new Error(`driver.js: loading suites failed`)
}
else {
sessionStorage.setItem(SESSION_STORAGE_RELOADING, '1');
console.error(`driver.js: reloading page to load suites`);
history.go();
}
}
}
}
while (!success);
class Driver extends EventTarget {
constructor() {
super();
this.reset();
this.restore();
this.setUpListeners();
}
reset() {
this.state = DRIVER_STATE_LOADING;
this.mediatorPort = null;
this.pageId = '';
this.sessionId = '';
this.url = '';
this.timeoutId = 0;
this.reason = null;
}
restore() {
const navigatingStateRaw = sessionStorage.getItem(SESSION_STORAGE_DRIVER_NAVIGATING);
if (navigatingStateRaw) {
sessionStorage.removeItem(SESSION_STORAGE_DRIVER_NAVIGATING);
const navigatingState = JSON.parse(navigatingStateRaw);
const { eventData, state, history: _history } = navigatingState;
if (state >= 10) {
return;
}
// navigation in progress
//console.log(`driver.js:restore: navigating ${navigatingStateRaw}`);
const event = {
data: eventData,
};
this.navigating = event;
}
}
setUpListeners() {
this.addEventListener('start', (event) => { this.start(event); });
}
setUpMessageListener() {
this._onMessage = (event) => { return this.onMessage(event); };
this.mediatorPort.addEventListener('message', this._onMessage);
}
setUpListenerDelayed() {
this._onBeforeUnload = (event) => { return this.onBeforeUnload(event); };
window.addEventListener('beforeunload', this._onBeforeUnload);
}
start(event) {
switch (this.state) {
case DRIVER_STATE_LOADING:
this.state = DRIVER_STATE_DISCONNECTED;
this.connect();
break;
default:
// TODO: error handling
break;
}
}
onBeforeUnload(event) {
//console.log(`${this.constructor.name}.onBeforeUnload:`);
if (this.__onBeforeUnload) {
this.__onBeforeUnload(event);
}
if (globalThis.__coverage__) {
let coverage = [ globalThis.__coverage__ ];
if (Array.isArray(this.bridgeCoverage)) {
coverage.splice(coverage.length, 0, ...this.bridgeCoverage);
}
this.sendCoverage(coverage);
}
}
sendCoverage(_coverage) {
if (this.mediatorPort && Array.isArray(_coverage) && _coverage.length > 0) {
//console.log(`${this.constructor.name}.sendCoverage`);
const message = {
type: TYPE_COVERAGE,
source: this.pageId,
target: NAME_REPORTER,
url: this.url,
__coverage__: _coverage,
};
this.mediatorPort.postMessage(message);
}
}
async connect() {
switch (this.state) {
case DRIVER_STATE_DISCONNECTED:
try {
this.doConnect();
}
catch (e) {
if (typeof this.onTransferPortMessage.reject === 'function') {
this.onTransferPortMessage.reject();
}
this.error(e);
}
break;
default:
// TODO: error handling
break;
}
}
async doConnect() {
// request port via mediator.html in a new _blank tab
// popup blocking feature has to be disabled on the target app domain
const This = this;
this.onTransferPortMessage = function onTransferPortMessage (_event) {
if (_event.origin === Config.reporterOrigin) {
const { type, source, transfer } = _event.data;
if (source === NAME_MEDIATOR_BRIDGE) {
//window.removeEventListener('message', This._onTransferPortMessage);
if (type === TYPE_TRANSFER_PORT) {
if (Array.isArray(transfer) && transfer[0] instanceof MessagePort) {
onTransferPortMessage.resolve(transfer[0]);
}
else {
onTransferPortMessage.reject(new Error('transfer[0] is not a MessagePort in TYPE_TRANSFER_PORT message from mediator.html'));
}
}
else if (type === TYPE_COVERAGE) {
if (_event.data.__coverage__) {
This.bridgeCoverage = This.bridgeCoverage || [];
This.bridgeCoverage.splice(This.bridgeCoverage.length, 0, ..._event.data.__coverage__);
}
}
else if (type === TYPE_ERROR) {
onTransferPortMessage.reject(new Error('error from mediator.html', { cause: _event.data.errorMessage }));
}
else {
onTransferPortMessage.reject(new Error('unexpected message type from mediator.html type: ' + type));
}
}
/*
else {
console.warn('discarding an uninteresting message source from origin:type:source ' + _event.origin + ':' + type + ':' + source, _event);
}
*/
}
/*
else {
console.warn('discarding an uninteresting message from origin ' + _event.origin, _event);
}
*/
}
this.state = DRIVER_STATE_CONNECTING;
while (!this.mediatorPort) {
try {
this.mediatorPort = await Promise.race([
new Promise((resolve, reject) => {
this.onTransferPortMessage.resolve = resolve;
this.onTransferPortMessage.reject = reject;
if (!this._onTransferPortMessage) {
window.addEventListener('message', this._onTransferPortMessage = (event) => {
return this.onTransferPortMessage(event);
});
}
this.mediatorWindow = window.open(Config.mediatorHtmlURL, NAME_MEDIATOR_BRIDGE);
}),
new Promise((resolve, reject) => {
setTimeout(() => {
if (this.mediatorPort) {
resolve(this.mediatorPort); // the resolved value is not used
return;
}
//window.removeEventListener('message', this._onTransferPortMessage);
if (this.mediatorWindow) {
this.mediatorWindow.close();
}
reject(new Error(`${this.state}.timeout: timeout for receiving ready`));
}, Config.mediatorPortTimeout || Config.timeout);
})
]);
}
catch (error) {
console.error(error);
}
}
this.onTransferPortMessage.resolve = this.onTransferPortMessage.reject = () => null;
this.mediatorPort.start();
this.onConnect();
}
createPageId() {
return crypto.randomUUID
? crypto.randomUUID()
: (() => {
const hex = Array.prototype.map.call(
crypto.getRandomValues(new Uint16Array(8)),
(v) => v.toString(16).padStart(4, '0'));
return `${hex[0]}${hex[1]}-${hex[2]}-${hex[3]}-${hex[4]}-${hex[5]}${hex[6]}${hex[7]}`
})();
}
onConnect() {
switch (this.state) {
case DRIVER_STATE_CONNECTING:
this.state = DRIVER_STATE_CONNECTED;
if (this.navigating) {
this.setUpListenerDelayed();
this.onNavigate(this.navigating);
this.navigate();
}
else {
this.pageId = this.createPageId();
this.setUpMessageListener();
this.sendReady();
}
break;
default:
// TODO: error handling
break;
}
}
sendReady() {
switch (this.state) {
case DRIVER_STATE_CONNECTED:
case DRIVER_STATE_STOPPED:
this.url = location.href;
// notify reporter of the ready status of this target app page
const message = {
type: TYPE_READY,
source: this.pageId,
target: NAME_REPORTER,
url: this.url,
};
//console.log(`${this.state}.sendReady:`, message);
this.mediatorPort.postMessage(message);
this.state = DRIVER_STATE_READY;
break;
default:
// TODO: error handling
break;
}
}
onMessage(event) {
//console.log(`Driver.onMessage: `, event.data);
const { type } = event.data;
switch (type) {
case TYPE_ERROR:
if (event.data.source === NAME_MEDIATOR && event.data.errorMessage === 'Error: source is blocked') {
console.error(`Driver.onMessage: ${type} ${event.data.errorMessage}`);
this.disconnect();
}
return;
}
switch (this.state) {
case DRIVER_STATE_READY:
switch (type) {
case TYPE_REQUEST_SESSION:
this.onRequestSession(event);
break;
case TYPE_NAVIGATE:
this.onNavigate(event);
break;
case TYPE_CLOSE:
this.sendClosing();
break;
default:
break;
}
break;
case DRIVER_STATE_ERROR:
switch (type) {
case TYPE_CLOSE:
this.sendClosing();
break;
default:
break;
}
break;
default:
// TODO: error handling
break;
}
}
onRequestSession(event) {
this.state = DRIVER_STATE_STARTING0;
const { sessionId, suite, suiteParameters } = event.data;
this.sessionId = sessionId;
this.suite = suite;
this.suiteParameters = suiteParameters || { suite: { ...suite }, phase: 0 };
//console.log('onRequestSession', event);
this.startSession(event);
}
onNavigate(event) {
let restoring = false;
if (this.navigating === event) {
restoring = true;
}
else {
this.state = DRIVER_STATE_NAVIGATING0;
}
/*
event.data = {
type: TYPE_NAVIGATE,
source: NAME_REPORTER,
target: pageId,
suite: suite,
url: url,
navigationType: navigationType,
}
*/
const { suite, url, navigationType } = event.data;
this.suite = suite;
this.url = url;
switch (navigationType) {
// location
/*
case NAVI_LOCATION_HREF:
this.suiteParameters = {
url: url,
navigationType: navigationType,
deferredNavigation: () => {
location.href = url;
},
};
break;
case NAVI_LOCATION_RELOAD:
this.suiteParameters = {
url: url,
navigationType: navigationType,
deferredNavigation: () => {
// TODO: check the current URL against the url parameter
location.reload();
},
};
break;
case NAVI_LOCATION_REPLACE:
this.suiteParameters = {
url: url,
navigationType: navigationType,
deferredNavigation: () => {
if (location.href === url) {
history.go();
}
else {
window.addEventListener('popstate', (event) => {
location.reload(); // hash-only URL changes need reloading to load the url
});
location.replace(url);
}
},
};
break;
case NAVI_LOCATION_ASSIGN:
this.suiteParameters = {
url: url,
navigationType: navigationType,
deferredNavigation: () => {
if (location.href === url) {
history.go();
}
else {
window.addEventListener('popstate', (event) => {
location.reload(); // hash-only URL changes need reloading to load the url
});
location.replace(url);
}
},
};
break;
*/
case NAVI_HISTORY_GO: // navigate to the target via cleanup.html
this.suiteParameters = {
url: url,
navigationType: navigationType,
deferredNavigation: async () => {
const DUMMY_HISTORY_STACK_TOP_PATH = '/test/history-stack-top';
const storeNavigatingState = (state) => {
sessionStorage.setItem(SESSION_STORAGE_DRIVER_NAVIGATING, JSON.stringify({
/*
event.data = {
type: TYPE_NAVIGATE,
source: NAME_REPORTER,
target: this.pageId,
suite: suite,
url: url,
navigationType: NAVI_HISTORY_GO,
}
*/
eventData: event.data,
history: {
length: history.length,
state: history.state,
},
state: state,
}, null, 2));
};
const waitUntil = async (condition) => {
if (!condition()) {
await new Promise(resolve => {
const intervalId = setInterval(() => {
if (condition()) {
clearInterval(intervalId);
resolve();
}
}, 100);
});
}
};
storeNavigatingState(0);
//console.log(`pushState '${DUMMY_HISTORY_STACK_TOP_PATH}'`);
history.pushState(null, '', DUMMY_HISTORY_STACK_TOP_PATH); // push a dummy state at the top of session stack
storeNavigatingState(1);
if (history.length > 1) {
//console.log(`history.go(${1 - history.length}) when history.length = ${history.length}`);
history.go(1 - history.length); // back to the bottom of the stack
await new Promise(resolve => setTimeout(resolve, 100));
}
storeNavigatingState(2);
//console.log(`replaceState ${url}`);
history.replaceState(null, '', url);
storeNavigatingState(3);
await waitUntil(() => location.href === url);
storeNavigatingState(4);
//console.log(`pushState ''`);
history.pushState(null, '', DUMMY_HISTORY_STACK_TOP_PATH); // push a dummy state at the top of session stack to reset the history length to 2
storeNavigatingState(5);
await waitUntil(() => history.length === 2);
storeNavigatingState(6);
//console.log(`history.go(-1)`);
history.go(-1); // back to the bottom of the stack
storeNavigatingState(7);
await waitUntil(() => location.href === url);
storeNavigatingState(8);
// partial cleanup
const cleanupOptions = Config.cleanupOptions;
const removalOptions_raw = cleanupOptions.RemovalOptions;
const partialCleanupTargets_raw = cleanupOptions.dataToRemove.suite;
const partialCleanupTargets = Object.assign({}, partialCleanupTargets_raw);
const removalTimeout = cleanupOptions.timeout;
//console.log(`cleanup.js: partial cleanup: ${JSON.stringify(partialCleanupTargets, null, 2)}`);
const removalOptions = {
since: removalOptions_raw.since || 0,
origins: [new URL(import.meta.url).origin],
};
try {
const cleanupStart = Date.now();
let cleanupFinishedCallback;
if (partialCleanupTargets.sessionStorage) {
sessionStorage.clear();
}
delete partialCleanupTargets.sessionStorage;
const result = await new Promise((resolve, reject) => {
window.addEventListener('cleanup-finished', cleanupFinishedCallback = (event) => {
let cleanupFinished = Date.now();
window.removeEventListener('cleanup-finished', cleanupFinishedCallback);
//console.log(`cleanup finished in ${cleanupFinished - cleanupStart}ms removalOptions: ${JSON.stringify(removalOptions)}, dataToRemove: ${JSON.stringify(partialCleanupTargets_raw)}`);
resolve(event.detail);
});
window.dispatchEvent(new CustomEvent('cleanup', { detail: {
removalOptions: removalOptions,
dataToRemove: partialCleanupTargets,
}}));
setTimeout(() => { reject('timed out') }, removalTimeout);
});
}
catch (e) {
console.error(`driver.js: cleanup timeout`, e);
}
storeNavigatingState(9);
this.__onBeforeUnload = (event) => {
//console.log(`driver.js: beforeunload`);
setTimeout(() => {
console.warn(`driver.js: recalling history.go() 100ms after beforeunload`);
history.go(); // recover from flaky history.go() calls
storeNavigatingState(12);
}, 100);
};
storeNavigatingState(10);
//console.log(`driver.js: navigating to ${url}, location: ${location.href}`);
history.go();
storeNavigatingState(11);
},
};
break;
// history: TODO: handle properly
/*
case NAVI_HISTORY_BACK:
case NAVI_HISTORY_FORWARD:
break;
case NAVI_HISTORY_REPLACE:
this.suiteParameters = {
url: url,
navigationType: navigationType,
deferredNavigation: () => {
history.replaceState(null, undefined, url);
location.reload();
},
};
break;
case NAVI_HISTORY_PUSH:
this.suiteParameters = {
url: url,
navigationType: navigationType,
deferredNavigation: () => {
history.pushState(null, undefined, url);
location.reload();
},
};
break;
// click link or programmatic
//NAVI_DEFERRED,
default:
this.suiteParameters = {
url: url,
navigationType: navigationType,
deferredNavigation: () => {
location.href = url;
},
};
break;
*/
}
if (!restoring) {
this.sendNavigating();
}
}
async startSession(event) {
// start a new mocha session
// (re-)install mocha and proxy reporter as disposed mocha cannot be reused
// In a browser, globalThis.mocha object instance is created by mocha-es2018.js and new Mocha() cannot be used for instantiation
//console.log('============ mochaInstaller');
mochaInstaller(_globalThis /* , _globalThis, console */); // console logging facilities can be hooked with a custom one
//console.log('============ reporterInstaller');
reporterInstaller(_globalThis.Mocha, Config);
//console.log('============ mocha.setup'); // TODO: hand parameters
_globalThis.mocha.setup(Config.mochaOptions);
let testDoneResolve;
const testDonePromise = new Promise(resolve => {
testDoneResolve = resolve;
});
//console.log('============ mocha.reporter');
this.reporterOptions = {
start: (runner, reporter) => {
this.runner = runner;
this.reporter = reporter;
this.reporter.driver = this;
this.sendStartSession();
this.setUpListenerDelayed();
},
end: (err, proxy) => {
testDonePromise.then(() => {
this.onReporterPortClosed(err, proxy);
});
},
port: event.data.transfer[0],
};
_globalThis.mocha.reporter('proxy-reporter', this.reporterOptions);
//console.log('============ Suite.scopes[scope].run');
const { scope, testIndex, file } = this.suite;
if (!Suite) {
if (file) {
do {
try {
const { default: _Suite } = await import(file + (retryCount > 0 ? '#' + retryCount : ''));
Suite = _Suite;
success = true;
}
catch (e) {
console.error(e);
}
if (!success) {
await new Promise(resolve => setTimeout(resolve, 1000));
retryCount++;
if (retryCount > Config.suitesLoaderRetries) {
throw new Error(`failed to import ${file} for scope ${scope} with retryCount ${retryCount - 1}`);
}
}
}
while (!success);
}
else {
throw new Error(`file property is not available for scope ${scope}`);
}
}
Suite.scopes[scope].run(testIndex, this.suiteParameters, sandbox)
.then(() => {})
.catch(error => { throw error }); // use _globalThis.describe, etc.
//console.log('============ mocha.run');
_globalThis.mocha.run(() => {
//console.log('mocha.run finished');
testDoneResolve();
});
}
sendStartSession() {
const reporterOptions = Object.assign({}, this.reporterOptions);
reporterOptions.start = reporterOptions.start.toString();
reporterOptions.end = reporterOptions.end.toString();
//reporterOptions.abort = reporterOptions.abort.toString();
const message = {
type: TYPE_START_SESSION,
source: this.pageId,
target: NAME_REPORTER,
sessionId: this.sessionId,
url: this.url,
runner: {
total: this.runner.total,
stats: this.runner.stats,
},
options: { // TODO: send options in a clonable object
reporterOptions: reporterOptions,
},
};
//console.log(`${this.state}.sendStartSession:`, message);
this.mediatorPort.postMessage(message);
this.state = DRIVER_STATE_RUNNING;
}
onReporterPortClosed(err, proxy) {
this.state = DRIVER_STATE_STOPPING;
//console.log(`${this.state}.onReporterStreamClosed`);
//console.log('============ mocha.dispose');
try {
_globalThis.mocha.dispose(); // mocha is disposed after the reporter stream is closed
}
catch (e) {
console.error('In mocha.dispose():', e); // exception while disposal
}
//console.log('mocha-driver.js: done for suite ', this.suite);
this.sendEndSession();
}
sendEndSession() {
const message = {
type: TYPE_END_SESSION,
source: this.pageId,
target: NAME_REPORTER,
sessionId: this.sessionId,
url: this.url,
};
//console.log(`${this.state}.sendEndSession:`, message);
this.mediatorPort.postMessage(message);
this.state = DRIVER_STATE_STOPPED;
this.handleSuiteParameters();
}
handleSuiteParameters() {
if (this.suiteParameters &&
typeof this.suiteParameters.phase === 'number' &&
this.suiteParameters.phase > 0 &&
typeof this.suiteParameters.deferredNavigation === 'function') {
// deferred navigation is set by the suite
/*
Format:
this.suiteParameters = {
suite: { ...suite },
phase: phase, // in a positive integer
navigationType: NAVI_DEFERRED, // TODO: support other types
url: '*', // [optional]: url after deferredNavigation may not be specified
deferredNavigation: function operationToNavigate() {
// click a link, a button, etc.
},
}
*/
this.suiteParameters.navigationType = NAVI_DEFERRED;
if (!this.suiteParameters.url) {
this.suiteParameters.url = '*'; // In general, url is unspecified on deferred navigation
}
this.sendNavigatingDeferred();
}
else {
this.suite = null;
this.suiteParameters = null;
this.sessionId = null;
this.sendReady();
}
}
sendNavigating() {
const clonableSuiteParameters = Object.assign({}, this.suiteParameters);
clonableSuiteParameters.deferredNavigation = clonableSuiteParameters.deferredNavigation.toString();
const { url, navigationType } = this.suiteParameters;
const message = {
type: TYPE_NAVIGATING,
source: this.pageId,
target: NAME_REPORTER,
url: url,
navigationType: navigationType,
suite: this.suite,
suiteParameters: clonableSuiteParameters,
timestamp: Date.now(),
};
//console.log(`${this.state}.sendNavigating`, message);
this.mediatorPort.postMessage(message);
this.sendDisconnectForNavigation();
}
sendNavigatingDeferred() {
const clonableSuiteParameters = Object.assign({}, this.suiteParameters);
clonableSuiteParameters.deferredNavigation = clonableSuiteParameters.deferredNavigation.toString();
const { url, navigationType } = this.suiteParameters;
const message = {
type: TYPE_NAVIGATING,
source: this.pageId,
target: NAME_REPORTER,
url: url,
navigationType: navigationType, // = NAVI_DEFERRED, // TODO: other types
suite: this.suite,
suiteParameters: clonableSuiteParameters,
timestamp: Date.now(),
};
//console.log(`${this.state}.sendNavigatingDeferred`, message);
this.mediatorPort.postMessage(message);
this.state = DRIVER_STATE_NAVIGATING1;
this.sendDisconnectForNavigation();
}
sendDisconnectForNavigation() {
const message = {
type: TYPE_DISCONNECT,
source: this.pageId,
target: NAME_MEDIATOR,
url: this.url,
coverageAvailable: !!globalThis.__coverage__,
};
//console.log(`${this.state}.sendDisconnectForNavigation`, message);
this.mediatorPort.postMessage(message);
this.state = DRIVER_STATE_NAVIGATING2;
this.navigate();
}
navigate() {
//console.log(`${this.state}.navigate: suiteParameters: `, this.suiteParameters);
this.state = DRIVER_STATE_NAVIGATING3;
//console.log(`${this.state}.navigate: deferredNavigation called`);
this.suiteParameters.deferredNavigation();
}
error(e) {
this.reason = reason || new Error(`${this.state}: Unknown Error`);
this.state = DRIVER_STATE_ERROR;
console.error(`Driver.error: `, this.reason);
if (!(this.mediatorPort instanceof MessagePort && this.pageId)) {
this.closeWindow();
}
}
sendClosing() {
this.state = DRIVER_STATE_CLOSING0;
this.mediatorPort.postMessage({
type: TYPE_CLOSING,
source: this.pageId,
target: NAME_REPORTER,
url: this.url,
});
this.disconnect();
}
disconnect() {
this.state = DRIVER_STATE_CLOSING1;
this.mediatorPort.postMessage({
type: TYPE_DISCONNECT,
source: this.pageId,
target: NAME_MEDIATOR,
url: this.url,
coverageAvailable: !!globalThis.__coverage__,
});
this.closeWindow();
}
closeWindow() {
this.state = DRIVER_STATE_CLOSING2;
console.warn('TODO: Driver.closeWindow() has to wait for cleanup');
if (this.mediatorWindow) {
this.mediatorWindow.close();
}
window.close();
}
}
const driver = new Driver();
driver.dispatchEvent(new CustomEvent('start'));
}
catch (e) {
console.error('Fatal Exception:', e);
}