UNPKG

@wdio/runner

Version:

A WebdriverIO service that runs tests in arbitrary environments

927 lines (921 loc) 35 kB
// 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, SnapshotService } 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 matcher = "inverse" in arg && arg.inverse ? expect.not[matcherKey] : 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; /** * 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(); this._specFileRetryAttempts = (this._config.specFileRetries || 0) - (retries || 0); logger4.setLogLevelsConfig(this._config.logLevels, this._config.logLevel); 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 snapshotService = SnapshotService.initiate({ updateState: this._config.updateSnapshots, resolveSnapshotPath: this._config.resolveSnapshotPath }); 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._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); 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", 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; return framework.init(cid, config, specs, capabilities, reporter); } 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 || []; this._browser = await initializeInstance(config, caps, this._isMultiremote); _setGlobal("browser", this._browser, config.injectGlobals); _setGlobal("driver", this._browser, config.injectGlobals); if (config.framework !== "jasmine") { _setGlobal("expect", expect2, config.injectGlobals); } for (const params of customStubCommands) { this._browser.addCommand(...params); } for (const params of overwrittenCommands) { this._browser.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); } } catch (err) { log4.error(err); 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._configParser.getCapabilities() }, retry: this._specFileRetryAttempts }); } this._reporter.emit("runner:end", { failures, cid: this._cid, retries }); 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 };