@wdio/runner
Version:
A WebdriverIO service that runs tests in arbitrary environments
953 lines (947 loc) • 36.4 kB
JavaScript
// src/index.ts
import { EventEmitter } from "node:events";
import logger4 from "@wdio/logger";
import { initializeWorkerService, initializePlugin as initializePlugin2, executeHooksWithArgs as executeHooksWithArgs2 } from "@wdio/utils";
import { ConfigParser } from "@wdio/config/node";
import { _setGlobal } from "@wdio/globals";
import { expect as expect2, setOptions, getConfig, matchers as matchers2, SnapshotService, SoftAssertionService } from "expect-webdriverio";
import { attach as attach2 } from "webdriverio";
// src/browser.ts
import url from "node:url";
import path from "node:path";
import logger2 from "@wdio/logger";
import { browser } from "@wdio/globals";
import { executeHooksWithArgs } from "@wdio/utils";
import { matchers } from "expect-webdriverio";
import { ELEMENT_KEY } from "webdriver";
import { MESSAGE_TYPES } from "@wdio/types";
// src/utils.ts
import { deepmerge } from "deepmerge-ts";
import logger from "@wdio/logger";
import { remote, multiremote, attach } from "webdriverio";
import { DEFAULTS } from "webdriver";
import { DEFAULT_CONFIGS } from "@wdio/config";
import { enableFileLogging } from "@wdio/utils";
var log = logger("@wdio/runner");
function sanitizeCaps(capabilities, filterOut) {
const caps = "alwaysMatch" in capabilities ? capabilities.alwaysMatch : capabilities;
const defaultConfigsKeys = [
// WDIO config keys
...Object.keys(DEFAULT_CONFIGS()),
// WebDriver config keys
...Object.keys(DEFAULTS)
];
return Object.keys(caps).filter((key) => (
/**
* filter out all wdio config keys
*/
!defaultConfigsKeys.includes(key) === !filterOut
)).reduce((obj, key) => {
obj[key] = caps[key];
return obj;
}, {});
}
async function initializeInstance(config, capabilities, isMultiremote) {
await enableFileLogging(config.outputDir);
if ("sessionId" in config) {
log.debug(`attach to session with id ${config.sessionId}`);
config.capabilities = sanitizeCaps(capabilities);
const caps = capabilities;
const connectionProps = {
protocol: caps.protocol || config.protocol,
hostname: caps.hostname || config.hostname,
port: caps.port || config.port,
path: caps.path || config.path
};
const params = { ...config, ...connectionProps, capabilities };
return attach({ ...params, options: params });
}
if (!isMultiremote) {
log.debug("init remote session");
const sessionConfig = {
...config,
/**
* allow to overwrite connection details by user through capabilities
*/
...sanitizeCaps(capabilities, true),
capabilities: sanitizeCaps(capabilities)
};
return remote(sessionConfig);
}
const options = {};
log.debug("init multiremote session");
delete config.capabilities;
for (const browserName of Object.keys(capabilities)) {
options[browserName] = deepmerge(
config,
capabilities[browserName]
);
}
const browser2 = await multiremote(options, config);
const browserNames = config.injectGlobals ? Object.keys(capabilities) : [];
for (const browserName of browserNames) {
global[browserName] = browser2[browserName];
}
return browser2;
}
function getInstancesData(browser2, isMultiremote) {
if (!isMultiremote) {
return;
}
const multiRemoteBrowser = browser2;
const instances = {};
multiRemoteBrowser.instances.forEach((browserName) => {
const { protocol, hostname, port, path: path3, queryParams } = multiRemoteBrowser.getInstance(browserName).options;
const { isW3C, sessionId } = multiRemoteBrowser.getInstance(browserName);
instances[browserName] = { sessionId, isW3C, protocol, hostname, port, path: path3, queryParams };
});
return instances;
}
var SUPPORTED_ASYMMETRIC_MATCHER = {
Any: "any",
Anything: "anything",
ArrayContaining: "arrayContaining",
ObjectContaining: "objectContaining",
StringContaining: "stringContaining",
StringMatching: "stringMatching",
CloseTo: "closeTo"
};
function transformExpectArgs(arg) {
if (typeof arg === "object" && arg && "$$typeof" in arg && typeof arg.$$typeof === "string" && Object.keys(SUPPORTED_ASYMMETRIC_MATCHER).includes(arg.$$typeof)) {
const matcherKey = SUPPORTED_ASYMMETRIC_MATCHER[arg.$$typeof];
const inverseMatcherKey = SUPPORTED_ASYMMETRIC_MATCHER[arg.$$typeof];
const matcher = "inverse" in arg && arg.inverse ? expect.not[inverseMatcherKey] : expect[matcherKey];
if (!matcher) {
throw new Error(`Matcher "${matcherKey}" is not supported by expect-webdriverio`);
}
return matcher(arg.sample);
}
return arg;
}
// src/browser.ts
var log2 = logger2("@wdio/runner");
var sep = "\n - ";
var ERROR_CHECK_INTERVAL = 500;
var DEFAULT_TIMEOUT = 60 * 1e3;
var BrowserFramework = class _BrowserFramework {
constructor(_cid, _config, _specs, _reporter) {
this._cid = _cid;
this._config = _config;
this._specs = _specs;
this._reporter = _reporter;
process.on("message", this.#processMessage.bind(this));
const [, runnerOptions] = Array.isArray(_config.runner) ? _config.runner : [];
this.#runnerOptions = runnerOptions || {};
}
#retryOutdatedOptimizeDep = false;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
#runnerOptions;
// `any` here because we don't want to create a dependency to @wdio/browser-runner
#resolveTestStatePromise;
/**
* always return true as it is irrelevant for component testing
*/
hasTests() {
return true;
}
init() {
return void 0;
}
async run() {
try {
const failures = await this.#loop();
return failures;
} catch (_err) {
const err = _err;
if (err.message.includes("net::ERR_CONNECTION_REFUSE")) {
err.message = `Failed to load test page to run tests, make sure your browser can access "${browser.options.baseUrl}"`;
}
log2.error(`Failed to run browser tests with cid ${this._cid}: ${err.stack}`);
process.send({
origin: "worker",
name: "error",
content: { name: err.name, message: err.message, stack: err.stack }
});
return 1;
}
}
async #loop() {
let failures = 0;
for (const spec of this._specs) {
failures += await this.#runSpec(spec);
}
return failures;
}
async #runSpec(spec, retried = false) {
this.#retryOutdatedOptimizeDep = false;
const timeout = this._config.mochaOpts?.timeout || DEFAULT_TIMEOUT;
log2.info(`Run spec file ${spec} for cid ${this._cid}`);
const testStatePromise = new Promise((resolve) => {
this.#resolveTestStatePromise = resolve;
});
if (!this._config.sessionId) {
await browser.url(`/?cid=${this._cid}&spec=${new URL(spec).pathname}`);
}
await browser.setCookies([
{ name: "WDIO_SPEC", value: url.fileURLToPath(spec) },
{ name: "WDIO_CID", value: this._cid }
]);
const testTimeout = setTimeout(
() => this.#onTestTimeout(`Timed out after ${timeout / 1e3}s waiting for test results`),
timeout
);
const errorInterval = setInterval(
this.#checkForTestError.bind(this),
ERROR_CHECK_INTERVAL
);
const state = await testStatePromise;
clearTimeout(testTimeout);
clearInterval(errorInterval);
if (this.#runnerOptions.coverage?.enabled && process.send) {
const coverageMap = await browser.execute(
() => window.__coverage__ || {}
);
const workerEvent = {
origin: "worker",
name: "workerEvent",
args: {
type: MESSAGE_TYPES.coverageMap,
value: coverageMap
}
};
process.send(workerEvent);
}
if (state.errors?.length) {
const errors = state.errors.map((ev) => state.hasViteError ? `${ev.message}
${ev.error ? ev.error.split("\n").slice(1).join("\n") : ""}` : `${path.basename(ev.filename || spec)}: ${ev.message}
${ev.error ? ev.error.split("\n").slice(1).join("\n") : ""}`);
if (!retried && errors.some((err) => err.includes("Failed to fetch dynamically imported module") || err.includes("the server responded with a status of 504 (Outdated Optimize Dep)") || /**
* this is specific to Preact and can bre resolved by rerunning the spec
*/
err.includes("undefined is not an object (evaluating 'r.__H')"))) {
log2.info("Retry test run due to dynamic import error");
return this.#runSpec(spec, true);
}
const { name, message, stack } = new Error(state.hasViteError ? `Test failed due to the following error: ${errors.join("\n\n")}` : `Test failed due to following error(s):${sep}${errors.join(sep)}`);
process.send({
origin: "worker",
name: "error",
content: { name, message, stack }
});
return 1;
}
for (const ev of state.events || []) {
if ((ev.type === "suite:start" || ev.type === "suite:end") && ev.title === "") {
continue;
}
this._reporter.emit(ev.type, {
...ev,
file: spec,
uid: `${this._cid}-${Buffer.from(ev.fullTitle).toString("base64")}`,
cid: this._cid
});
}
return state.failures || 0;
}
async #processMessage(cmd) {
if (cmd.command !== "workerRequest" || !process.send) {
return;
}
const { message, id } = cmd.args;
if (message.type === MESSAGE_TYPES.hookTriggerMessage) {
return this.#handleHook(id, message.value);
}
if (message.type === MESSAGE_TYPES.consoleMessage) {
return this.#handleConsole(message.value);
}
if (message.type === MESSAGE_TYPES.commandRequestMessage) {
return this.#handleCommand(id, message.value);
}
if (message.type === MESSAGE_TYPES.expectRequestMessage) {
return this.#handleExpectation(id, message.value);
}
if (message.type === MESSAGE_TYPES.browserTestResult) {
return this.#handleTestFinish(message.value);
}
if (message.type === MESSAGE_TYPES.expectMatchersRequest) {
return this.#sendWorkerResponse(
id,
this.#expectMatcherResponse({ matchers: Array.from(matchers.keys()) })
);
}
}
async #handleHook(id, payload) {
const error = await executeHooksWithArgs(
payload.name,
this._config[payload.name],
payload.args
).then(() => void 0, (err) => err);
if (error) {
log2.warn(`Failed running "${payload.name}" hook for cid ${payload.cid}: ${error.message}`);
}
return this.#sendWorkerResponse(id, this.#hookResponse({ id: payload.id, error }));
}
#expectMatcherResponse(value) {
return {
type: MESSAGE_TYPES.expectMatchersResponse,
value
};
}
#hookResponse(value) {
return {
type: MESSAGE_TYPES.hookResultMessage,
value
};
}
#sendWorkerResponse(id, message) {
if (!process.send) {
return;
}
const response = {
origin: "worker",
name: "workerResponse",
args: { id, message }
};
process.send(response);
}
/**
* Print console message executed in browser to the terminal
* @param message console.log message args
* @returns void
*/
#handleConsole(message) {
const isWDIOLog = Boolean(typeof message.args[0] === "string" && message.args[0].startsWith("[WDIO]") && message.type !== "error");
if (message.name !== "consoleEvent" || isWDIOLog) {
return;
}
console[message.type](...message.args || []);
}
async #handleCommand(id, payload) {
log2.debug(`Received browser message: ${JSON.stringify(payload)}`);
const cid = payload.cid;
if (typeof cid !== "string") {
const { message, stack } = new Error(`No "cid" property passed into command message with id "${payload.id}"`);
const error = { message, stack, name: "Error" };
return this.#sendWorkerResponse(id, this.#commandResponse({ id: payload.id, error }));
}
try {
const scope = payload.scope ? await browser.$({ [ELEMENT_KEY]: payload.scope }) : browser;
if (typeof scope[payload.commandName] !== "function") {
throw new Error(`${payload.scope ? "element" : "browser"}.${payload.commandName} is not a function`);
}
let result = await scope[payload.commandName](...payload.args);
if (result?.constructor?.name === "Element") {
result = result.elementId ? { [ELEMENT_KEY]: result.elementId } : result.error ? { message: result.error.message, stack: result.error.stack, name: result.error.name } : void 0;
} else if (result?.foundWith) {
result = (await result.map((res) => ({
[ELEMENT_KEY]: res.elementId
}))).filter(Boolean);
}
const resultMsg = this.#commandResponse({ id: payload.id, result });
log2.debug(`Return command result: ${resultMsg}`);
return this.#sendWorkerResponse(id, resultMsg);
} catch (error) {
const { message, stack, name } = error;
return this.#sendWorkerResponse(id, this.#commandResponse({ id: payload.id, error: { message, stack, name } }));
}
}
#commandResponse(value) {
return {
type: MESSAGE_TYPES.commandResponseMessage,
value
};
}
/**
* handle expectation assertions within the worker process
* @param id message id from communicator
* @param payload information about the expectation to run
* @returns void
*/
async #handleExpectation(id, payload) {
log2.debug(`Received expectation message: ${JSON.stringify(payload)}`);
const cid = payload.cid;
if (typeof cid !== "string") {
const message = `No "cid" property passed into expect request message with id "${payload.id}"`;
return this.#sendWorkerResponse(id, this.#expectResponse({ id: payload.id, pass: false, message }));
}
const matcher = matchers.get(payload.matcherName);
if (!matcher) {
const message = `Couldn't find matcher with name "${payload.matcherName}"`;
return this.#sendWorkerResponse(id, this.#expectResponse({ id: payload.id, pass: false, message }));
}
try {
const context = payload.element ? Array.isArray(payload.element) ? await browser.$$(payload.element) : payload.element.elementId ? await browser.$(payload.element) : await browser.$(payload.element.selector) : payload.context || browser;
const result = await matcher.apply(payload.scope, [context, ...payload.args.map(transformExpectArgs)]);
return this.#sendWorkerResponse(id, this.#expectResponse({
id: payload.id,
pass: result.pass,
message: result.message()
}));
} catch (err) {
const errorMessage = err instanceof Error ? err.stack : err;
const message = `Failed to execute expect command "${payload.matcherName}": ${errorMessage}`;
return this.#sendWorkerResponse(id, this.#expectResponse({ id: payload.id, pass: false, message }));
}
}
#expectResponse(value) {
return {
type: MESSAGE_TYPES.expectResponseMessage,
value
};
}
#handleTestFinish(payload) {
this.#resolveTestStatePromise({ failures: payload.failures, events: payload.events });
}
#onTestTimeout(message) {
return this.#resolveTestStatePromise?.({
events: [],
failures: 1,
errors: [{ message }]
});
}
async #checkForTestError() {
const testError = await browser.execute(function fetchExecutionState() {
let viteError;
const viteErrorElem = document.querySelector("vite-error-overlay");
if (viteErrorElem && viteErrorElem.shadowRoot) {
const errorElements = Array.from(viteErrorElem.shadowRoot.querySelectorAll("pre"));
if (errorElements.length) {
viteError = [{ message: errorElements.map((elem) => elem.innerText).join("\n") }];
}
}
const loadError = typeof window.__wdioErrors__ === "undefined" && document.title !== "WebdriverIO Browser Test" && !document.querySelector("mocha-framework") ? [{ message: `Failed to load test page (title = "${document.title}", source: ${document.documentElement.innerHTML})` }] : null;
const errors = viteError || window.__wdioErrors__ || loadError;
return { errors, hasViteError: Boolean(viteError) };
}).catch((err) => {
if (err.message.includes("Cannot find context with specified id")) {
return;
}
throw err;
});
if (!testError) {
return;
}
if (testError.errors && testError.errors.length > 0 || testError.hasViteError) {
this.#resolveTestStatePromise?.({
events: [],
failures: 1,
...testError
});
}
const logs = typeof browser.getLogs === "function" ? await browser.getLogs("browser").catch(() => []) : [];
const severeLogs = logs.filter((log5) => log5.level === "SEVERE" && log5.source !== "deprecation");
if (severeLogs.length) {
if (!this.#retryOutdatedOptimizeDep && severeLogs.some((log5) => log5.message?.includes("(Outdated Optimize Dep)"))) {
log2.info("Retry test run due to outdated optimize dep");
this.#retryOutdatedOptimizeDep = true;
return browser.refresh();
}
this.#resolveTestStatePromise?.({
events: [],
failures: 1,
hasViteError: false,
/**
* error messages often look like:
* "http://localhost:40167/node_modules/.vite/deps/expect.js?v=bca8e2f3 - Failed to load resource: the server responded with a status of 504 (Outdated Optimize Dep)"
*/
errors: severeLogs.map((log5) => {
const [filename, message] = log5.message.split(" - ");
return {
filename: filename.startsWith("http") ? filename : void 0,
message
};
})
});
}
}
static init(cid, config, specs, _, reporter) {
const framework = new _BrowserFramework(cid, config, specs, reporter);
return framework;
}
};
// src/reporter.ts
import path2 from "node:path";
import logger3 from "@wdio/logger";
import DotReporter from "@wdio/dot-reporter";
import { initializePlugin } from "@wdio/utils";
var log3 = logger3("@wdio/runner");
var mochaAllHooks = ['"before all" hook', '"after all" hook'];
var BaseReporter = class {
constructor(_config, _cid, caps) {
this._config = _config;
this._cid = _cid;
this.caps = caps;
this._config.reporters = this._config.reporters || [];
if (this._config.reporters.length === 0) {
this._config.reporters.push([DotReporter, {}]);
}
}
_reporters = [];
listeners = [];
async initReporters() {
this._reporters = await Promise.all(
this._config.reporters.map(this._loadReporter.bind(this))
);
}
/**
* emit events to all registered reporter and wdio launcer
*
* @param {string} e event name
* @param {object} payload event payload
*/
emit(e, payload) {
payload.cid = this._cid;
const isTestError = e === "test:fail";
const isHookError = e === "hook:end" && payload.error && mochaAllHooks.some((hook) => payload.title?.startsWith(hook));
if (isTestError || isHookError) {
this.#emitData({
origin: "reporter",
name: "printFailureMessage",
content: payload
});
}
this._reporters.forEach((reporter) => {
try {
reporter.emit(e, payload);
} catch (err) {
const error = err instanceof Error ? err : new Error(`An unknown error occurred: ${err}`);
this.#emitData({
origin: "reporter",
name: "printFailureMessage",
content: {
cid: this._cid,
// Destructing of message and stack is required else nothing is outputted
error: { message: error.message, stack: error.stack },
fullTitle: `reporter ${reporter.constructor.name}`
}
});
}
});
}
onMessage(listener) {
this.listeners.push(listener);
}
getLogFile(name) {
const options = Object.assign({}, this._config);
let filename = `wdio-${this._cid}-${name}-reporter.log`;
const reporterOptions = this._config.reporters.find((reporter) => Array.isArray(reporter) && (reporter[0] === name || typeof reporter[0] === "function" && reporter[0].name === name));
if (reporterOptions && Array.isArray(reporterOptions)) {
const fileformat = reporterOptions[1].outputFileFormat;
options.cid = this._cid;
options.capabilities = this.caps;
Object.assign(options, reporterOptions[1]);
if (fileformat) {
if (typeof fileformat !== "function") {
throw new Error("outputFileFormat must be a function");
}
filename = fileformat(options);
}
}
if (!options.outputDir) {
return;
}
return path2.join(options.outputDir, filename);
}
/**
* return write stream object based on reporter name
*/
getWriteStreamObject(reporter) {
return {
write: (
/* istanbul ignore next */
(content) => this.#emitData({
origin: "reporter",
name: reporter,
content
})
)
};
}
/**
* emit data either through process or listener
*/
#emitData(payload) {
if (typeof process.send === "function") {
return process.send(payload);
}
this.listeners.forEach((fn) => fn(payload));
return true;
}
/**
* wait for reporter to finish synchronization, e.g. when sending data asynchronous
* to a server (e.g. sumo reporter)
*/
waitForSync() {
const startTime = Date.now();
return new Promise((resolve, reject) => {
const interval = setInterval(() => {
const unsyncedReporter = this._reporters.filter((reporter) => !reporter.isSynchronised).map((reporter) => reporter.constructor.name);
if (Date.now() - startTime > this._config.reporterSyncTimeout && unsyncedReporter.length) {
clearInterval(interval);
return reject(new Error(`Some reporters are still unsynced: ${unsyncedReporter.join(", ")}`));
}
if (!unsyncedReporter.length) {
clearInterval(interval);
return resolve(true);
}
log3.info(`Wait for ${unsyncedReporter.length} reporter to synchronize`);
}, this._config.reporterSyncInterval);
});
}
/**
* initialize reporters
*/
async _loadReporter(reporter) {
let ReporterClass;
let options = {};
if (Array.isArray(reporter)) {
options = Object.assign({}, options, reporter[1]);
reporter = reporter[0];
}
if (typeof reporter === "function") {
ReporterClass = reporter;
options.logFile = options.setLogFile ? options.setLogFile(this._cid, ReporterClass.name) : typeof options.logFile === "string" ? options.logFile : this.getLogFile(ReporterClass.name);
options.writeStream = this.getWriteStreamObject(ReporterClass.name);
return new ReporterClass(options);
}
if (typeof reporter === "string") {
ReporterClass = (await initializePlugin(reporter, "reporter")).default;
options.logFile = options.setLogFile ? options.setLogFile(this._cid, reporter) : typeof options.logFile === "string" ? options.logFile : this.getLogFile(reporter);
options.writeStream = this.getWriteStreamObject(reporter);
return new ReporterClass(options);
}
throw new Error("Invalid reporters config");
}
};
// src/index.ts
var log4 = logger4("@wdio/runner");
var Runner = class extends EventEmitter {
_browser;
_configParser;
_sigintWasCalled = false;
_isMultiremote = false;
_specFileRetryAttempts = 0;
_reporter;
_framework;
_config;
_cid;
_specs;
_caps;
_sessionInitError;
/**
* run test suite
* @param {string} cid worker id (e.g. `0-0`)
* @param {Object} args config arguments passed into worker process
* @param {string[]} specs list of spec files to run
* @param {Object} caps capabilities to run session with
* @param {string} configFile path to config file to get config from
* @param {number} retries number of retries remaining
* @return {Promise} resolves in number of failures for testrun
*/
async run({ cid, args, specs, caps, configFile, retries }) {
this._configParser = new ConfigParser(configFile, args);
this._cid = cid;
this._specs = specs;
try {
await this._configParser.initialize(args);
} catch (err) {
log4.error(`Failed to read config file: ${err.stack}`);
return this._shutdown(1, retries, true);
}
this._config = this._configParser.getConfig();
logger4.setLogLevelsConfig(this._config.logLevels, this._config.logLevel);
if (this._config.maskingPatterns) {
logger4.setMaskingPatterns(this._config.maskingPatterns);
}
const capabilities = this._configParser.getCapabilities();
const isMultiremote = this._isMultiremote = !Array.isArray(capabilities) || Object.values(caps).length > 0 && Object.values(caps).every((c) => typeof c === "object" && c.capabilities);
const softAssertionService = new SoftAssertionService({
autoAssertOnTestEnd: this._config.autoAssertOnTestEnd || true
}, this._caps, this._config);
const snapshotService = SnapshotService.initiate({
updateState: this._config.updateSnapshots,
resolveSnapshotPath: this._config.resolveSnapshotPath
});
this._configParser.addService(softAssertionService);
this._configParser.addService(snapshotService);
this._caps = this._isMultiremote ? Object.entries(caps).reduce((filteredCaps, [browserName, browserCaps]) => {
const ex = browserCaps.capabilities["wdio:exclude"];
if (ex) {
const sp = this._configParser?.getSpecs(specs, ex);
if (sp && sp.length === 0) {
return filteredCaps;
}
}
filteredCaps[browserName] = browserCaps;
return filteredCaps;
}, {}) : caps;
let browser2 = await this._startSession({
...this._config,
// @ts-ignore used in `/packages/webdriverio/src/protocol-stub.ts`
_automationProtocol: this._config.automationProtocol,
automationProtocol: "./protocol-stub.js"
}, this._caps);
(await initializeWorkerService(
this._config,
this._caps,
args.ignoredWorkerServices
)).map(this._configParser.addService.bind(this._configParser));
const beforeSessionParams = [this._config, this._caps, this._specs, this._cid];
await executeHooksWithArgs2("beforeSession", this._config.beforeSession, beforeSessionParams);
this._specFileRetryAttempts = (this._config.specFileRetries || 0) - (retries || 0);
this._reporter = new BaseReporter(this._config, this._cid, { ...this._caps });
await this._reporter.initReporters();
this._framework = await this.#initFramework(cid, this._config, this._caps, this._reporter, specs);
process.send({ name: "testFrameworkInit", content: { cid, caps: this._caps, specs, hasTests: this._framework.hasTests() } });
if (!this._framework.hasTests()) {
return this._shutdown(0, retries, true);
}
browser2 = await this._initSession(this._config, this._caps);
if (!browser2) {
const afterArgs = [1, this._caps, this._specs];
await executeHooksWithArgs2("after", this._config.after, afterArgs);
await this.endSession();
return this._shutdown(1, retries, true);
}
this._reporter.caps = browser2.capabilities;
const beforeArgs = [this._caps, this._specs, browser2];
await executeHooksWithArgs2("before", this._config.before, beforeArgs);
if (this._sigintWasCalled) {
log4.info("SIGINT signal detected while starting session, shutting down...");
await this.endSession();
return this._shutdown(0, retries, true);
}
const multiRemoteBrowser = browser2;
this._reporter.emit("runner:start", {
cid,
specs,
config: browser2.options,
isMultiremote,
instanceOptions: isMultiremote ? multiRemoteBrowser.instances.reduce((prev, browserName) => {
prev[multiRemoteBrowser.getInstance(browserName).sessionId] = multiRemoteBrowser.getInstance(browserName).options;
return prev;
}, {}) : {
[browser2.sessionId]: browser2.options
},
sessionId: browser2.sessionId,
capabilities: isMultiremote ? multiRemoteBrowser.instances.reduce((caps2, browserName) => {
caps2[browserName] = multiRemoteBrowser.getInstance(browserName).capabilities;
caps2[browserName].sessionId = multiRemoteBrowser.getInstance(browserName).sessionId;
return caps2;
}, {}) : { ...browser2.capabilities, sessionId: browser2.sessionId },
retry: this._specFileRetryAttempts
});
const { protocol, hostname, port, path: path3, queryParams, automationProtocol, headers } = browser2.options;
const { isW3C, sessionId } = browser2;
const instances = getInstancesData(browser2, isMultiremote);
process.send({
origin: "worker",
name: "sessionStarted",
specFileRetries: this._specFileRetryAttempts,
content: {
automationProtocol,
sessionId,
isW3C,
protocol,
hostname,
port,
path: path3,
queryParams,
isMultiremote,
instances,
capabilities: browser2.capabilities,
injectGlobals: this._config.injectGlobals,
headers
}
});
let failures = 0;
try {
failures = await this._framework.run();
} catch (err) {
log4.error(err);
this.emit("error", err);
failures = 1;
}
if (!args.watch) {
await this.endSession();
}
process.send({
origin: "worker",
name: "snapshot",
content: snapshotService.results
});
return this._shutdown(failures, retries);
}
async #initFramework(cid, config, capabilities, reporter, specs) {
const runner = Array.isArray(config.runner) ? config.runner[0] : config.runner;
if (runner === "local") {
const framework = (await initializePlugin2(config.framework, "framework")).default;
const frameworkInstance = await framework.init(cid, config, specs, capabilities, reporter);
if (frameworkInstance.setupExpect) {
await frameworkInstance.setupExpect(expect2, matchers2, getConfig);
}
return frameworkInstance;
}
if (runner === "browser") {
return BrowserFramework.init(cid, config, specs, capabilities, reporter);
}
throw new Error(`Unknown runner "${runner}"`);
}
/**
* init protocol session
* @param {object} config configuration of sessions
* @param {Object} caps desired capabilities of session
* @param {Object} browserStub stubbed `browser` object with only capabilities, config and env flags
* @return {Promise} resolves with browser object or null if session couldn't get established
*/
async _initSession(config, caps) {
const browser2 = await this._startSession(config, caps);
if (!browser2) {
return;
}
_setGlobal("$", (selector) => browser2.$(selector), config.injectGlobals);
_setGlobal("$$", (selector) => browser2.$$(selector), config.injectGlobals);
browser2.on("command", (command) => this._reporter?.emit(
"client:beforeCommand",
Object.assign(command, { sessionId: browser2.sessionId })
));
browser2.on("result", (result) => this._reporter?.emit(
"client:afterCommand",
Object.assign(result, { sessionId: browser2.sessionId })
));
return browser2;
}
/**
* start protocol session
* @param {object} config configuration of sessions
* @param {Object} caps desired capabilities of session
* @return {Promise} resolves with browser object or null if session couldn't get established
*/
async _startSession(config, caps) {
try {
const customStubCommands = this._browser?.customCommands || [];
const overwrittenCommands = this._browser?.overwrittenCommands || [];
const browser2 = await initializeInstance(config, caps, this._isMultiremote);
this._browser = browser2;
_setGlobal("browser", this._browser, config.injectGlobals);
_setGlobal("driver", this._browser, config.injectGlobals);
if (config.framework !== "jasmine") {
_setGlobal("expect", expect2, config.injectGlobals);
}
const commandTarget = browser2;
for (const [name, func, thirdArg, proto, instances] of customStubCommands) {
if (typeof thirdArg === "object" && thirdArg !== null) {
commandTarget.addCommand(name, func, thirdArg);
} else if (typeof thirdArg === "boolean") {
commandTarget.addCommand(name, func, thirdArg, proto, instances);
} else {
commandTarget.addCommand(name, func);
}
}
for (const params of overwrittenCommands) {
browser2.overwriteCommand(...params);
}
setOptions({
wait: config.waitforTimeout,
// ms to wait for expectation to succeed
interval: config.waitforInterval,
// interval between attempts
beforeAssertion: async (params) => {
await Promise.all([
this._reporter?.emit("client:beforeAssertion", { ...params, sessionId: this._browser?.sessionId }),
executeHooksWithArgs2("beforeAssertion", config.beforeAssertion, [params])
]);
},
afterAssertion: async (params) => {
await Promise.all([
this._reporter?.emit("client:afterAssertion", { ...params, sessionId: this._browser?.sessionId }),
executeHooksWithArgs2("afterAssertion", config.afterAssertion, [params])
]);
}
});
if (this._isMultiremote) {
_setGlobal("multiremotebrowser", this._browser, config.injectGlobals);
_setGlobal("multiRemoteBrowser", this._browser, config.injectGlobals);
}
} catch (error) {
log4.error(error);
this._sessionInitError = error;
return;
}
return this._browser;
}
/**
* kill worker session
*/
async _shutdown(failures, retries, initiationFailed = false) {
if (this._reporter && initiationFailed) {
this._reporter.emit("runner:start", {
cid: this._cid,
specs: this._specs,
config: this._config,
isMultiremote: this._isMultiremote,
instanceOptions: {},
capabilities: this._caps,
retry: this._specFileRetryAttempts
});
}
this._reporter.emit("runner:end", {
failures,
cid: this._cid,
retries,
...this._sessionInitError instanceof Error ? { error: this._sessionInitError.message } : {}
});
try {
await this._reporter.waitForSync();
} catch (err) {
log4.error(err);
}
this.emit("exit", failures === 0 ? 0 : 1);
return failures;
}
/**
* end WebDriver session, a config object can be applied if object has changed
* within a hook by the user
*/
async endSession(payload) {
const multiRemoteBrowser = this._browser;
const browser2 = this._browser;
const hasSessionId = Boolean(this._browser) && (this._isMultiremote ? !multiRemoteBrowser.instances.some(
(browserName) => multiRemoteBrowser.getInstance(browserName) && !multiRemoteBrowser.getInstance(browserName).sessionId
) : browser2.sessionId);
if (!hasSessionId && payload?.args.config.sessionId) {
this._browser = await attach2({
...payload.args.config,
capabilities: payload?.args.capabilities
});
} else if (!hasSessionId) {
return;
}
const capabilities = this._browser?.capabilities || {};
if (this._isMultiremote) {
const multiRemoteBrowser2 = this._browser;
multiRemoteBrowser2.instances.forEach((browserName) => {
capabilities[browserName] = multiRemoteBrowser2.getInstance(browserName).capabilities;
});
}
await this._browser?.deleteSession();
process.send({
origin: "worker",
name: "sessionEnded",
cid: this._cid
});
if (this._isMultiremote) {
multiRemoteBrowser.instances.forEach((browserName) => {
delete multiRemoteBrowser.getInstance(browserName).sessionId;
});
} else if (browser2) {
browser2.sessionId = void 0;
}
const afterSessionArgs = [this._config, capabilities, this._specs];
await executeHooksWithArgs2("afterSession", this._config.afterSession, afterSessionArgs);
}
};
export {
BaseReporter,
Runner as default
};