@wdio/runner 9.0.0-alpha.78 → 9.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/index.js CHANGED
@@ -1,448 +1,890 @@
1
- import fs from 'node:fs/promises';
2
- import path from 'node:path';
3
- import { EventEmitter } from 'node:events';
4
- import logger from '@wdio/logger';
5
- import { initializeWorkerService, initializePlugin, executeHooksWithArgs } from '@wdio/utils';
6
- import { ConfigParser } from '@wdio/config/node';
7
- import { _setGlobal } from '@wdio/globals';
8
- import { expect, setOptions, SnapshotService } from 'expect-webdriverio';
9
- import { attach } from 'webdriverio';
10
- import BrowserFramework from './browser.js';
11
- import BaseReporter from './reporter.js';
12
- import { initializeInstance, filterLogTypes, getInstancesData } from './utils.js';
13
- const log = logger('@wdio/runner');
14
- export default class Runner extends EventEmitter {
15
- _browser;
16
- _configParser;
17
- _sigintWasCalled = false;
18
- _isMultiremote = false;
19
- _specFileRetryAttempts = 0;
20
- _reporter;
21
- _framework;
22
- _config;
23
- _cid;
24
- _specs;
25
- _caps;
1
+ // src/index.ts
2
+ import { EventEmitter } from "node:events";
3
+ import logger4 from "@wdio/logger";
4
+ import { initializeWorkerService, initializePlugin as initializePlugin2, executeHooksWithArgs as executeHooksWithArgs2 } from "@wdio/utils";
5
+ import { ConfigParser } from "@wdio/config/node";
6
+ import { _setGlobal } from "@wdio/globals";
7
+ import { expect as expect2, setOptions, SnapshotService } from "expect-webdriverio";
8
+ import { attach as attach2 } from "webdriverio";
9
+
10
+ // src/browser.ts
11
+ import url from "node:url";
12
+ import path from "node:path";
13
+ import logger2 from "@wdio/logger";
14
+ import { browser } from "@wdio/globals";
15
+ import { executeHooksWithArgs } from "@wdio/utils";
16
+ import { matchers } from "expect-webdriverio";
17
+ import { ELEMENT_KEY } from "webdriver";
18
+ import { MESSAGE_TYPES } from "@wdio/types";
19
+
20
+ // src/utils.ts
21
+ import { deepmerge } from "deepmerge-ts";
22
+ import logger from "@wdio/logger";
23
+ import { remote, multiremote, attach } from "webdriverio";
24
+ import { DEFAULTS } from "webdriver";
25
+ import { DEFAULT_CONFIGS } from "@wdio/config";
26
+ import { enableFileLogging } from "@wdio/utils";
27
+ var log = logger("@wdio/runner");
28
+ function sanitizeCaps(capabilities, filterOut) {
29
+ const caps = "alwaysMatch" in capabilities ? capabilities.alwaysMatch : capabilities;
30
+ const defaultConfigsKeys = [
31
+ // WDIO config keys
32
+ ...Object.keys(DEFAULT_CONFIGS()),
33
+ // WebDriver config keys
34
+ ...Object.keys(DEFAULTS)
35
+ ];
36
+ return Object.keys(caps).filter((key) => (
26
37
  /**
27
- * run test suite
28
- * @param {string} cid worker id (e.g. `0-0`)
29
- * @param {Object} args config arguments passed into worker process
30
- * @param {string[]} specs list of spec files to run
31
- * @param {Object} caps capabilities to run session with
32
- * @param {string} configFile path to config file to get config from
33
- * @param {number} retries number of retries remaining
34
- * @return {Promise} resolves in number of failures for testrun
38
+ * filter out all wdio config keys
35
39
  */
36
- async run({ cid, args, specs, caps, configFile, retries }) {
37
- this._configParser = new ConfigParser(configFile, args);
38
- this._cid = cid;
39
- this._specs = specs;
40
- this._caps = caps;
41
- /**
42
- * add config file
43
- */
44
- try {
45
- await this._configParser.initialize(args);
46
- }
47
- catch (err) {
48
- log.error(`Failed to read config file: ${err.stack}`);
49
- return this._shutdown(1, retries, true);
50
- }
51
- this._config = this._configParser.getConfig();
52
- this._specFileRetryAttempts = (this._config.specFileRetries || 0) - (retries || 0);
53
- logger.setLogLevelsConfig(this._config.logLevels, this._config.logLevel);
54
- const capabilities = this._configParser.getCapabilities();
55
- const isMultiremote = this._isMultiremote = !Array.isArray(capabilities) ||
56
- (Object.values(caps).length > 0 && Object.values(caps).every(c => typeof c === 'object' && c.capabilities));
57
- /**
58
- * add built-in services
59
- */
60
- const snapshotService = SnapshotService.initiate({
61
- updateState: this._config.updateSnapshots,
62
- resolveSnapshotPath: this._config.resolveSnapshotPath
63
- });
64
- // ToDo(Christian): resolve type incompatibility between v8 and v9
65
- this._configParser.addService(snapshotService);
66
- /**
67
- * create `browser` stub only if `specFiltering` feature is enabled
68
- */
69
- let browser = await this._startSession({
70
- ...this._config,
71
- // @ts-ignore used in `/packages/webdriverio/src/protocol-stub.ts`
72
- _automationProtocol: this._config.automationProtocol,
73
- automationProtocol: './protocol-stub.js'
74
- }, caps);
75
- (await initializeWorkerService(this._config, caps, args.ignoredWorkerServices)).map(this._configParser.addService.bind(this._configParser));
76
- const beforeSessionParams = [this._config, this._caps, this._specs, this._cid];
77
- await executeHooksWithArgs('beforeSession', this._config.beforeSession, beforeSessionParams);
78
- this._reporter = new BaseReporter(this._config, this._cid, { ...caps });
79
- await this._reporter.initReporters();
80
- /**
81
- * initialize framework
82
- */
83
- this._framework = await this.#initFramework(cid, this._config, caps, this._reporter, specs);
84
- process.send({ name: 'testFrameworkInit', content: { cid, caps, specs, hasTests: this._framework.hasTests() } });
85
- if (!this._framework.hasTests()) {
86
- return this._shutdown(0, retries, true);
87
- }
88
- browser = await this._initSession(this._config, this._caps);
89
- /**
90
- * return if session initialization failed
91
- */
92
- if (!browser) {
93
- const afterArgs = [1, this._caps, this._specs];
94
- await executeHooksWithArgs('after', this._config.after, afterArgs);
95
- return this._shutdown(1, retries, true);
96
- }
97
- this._reporter.caps = browser.capabilities;
98
- const beforeArgs = [this._caps, this._specs, browser];
99
- await executeHooksWithArgs('before', this._config.before, beforeArgs);
100
- /**
101
- * kill session of SIGINT signal showed up while trying to
102
- * get a session ID
103
- */
104
- if (this._sigintWasCalled) {
105
- log.info('SIGINT signal detected while starting session, shutting down...');
106
- await this.endSession();
107
- return this._shutdown(0, retries, true);
108
- }
109
- /**
110
- * initialization successful, send start message
111
- */
112
- const multiRemoteBrowser = browser;
113
- this._reporter.emit('runner:start', {
114
- cid,
115
- specs,
116
- config: browser.options,
117
- isMultiremote,
118
- instanceOptions: isMultiremote
119
- ? multiRemoteBrowser.instances.reduce((prev, browserName) => {
120
- prev[multiRemoteBrowser.getInstance(browserName).sessionId] = multiRemoteBrowser.getInstance(browserName).options;
121
- return prev;
122
- }, {})
123
- : {
124
- [browser.sessionId]: browser.options
125
- },
126
- sessionId: browser.sessionId,
127
- capabilities: isMultiremote
128
- ? multiRemoteBrowser.instances.reduce((caps, browserName) => {
129
- caps[browserName] = multiRemoteBrowser.getInstance(browserName).capabilities;
130
- caps[browserName].sessionId = multiRemoteBrowser.getInstance(browserName).sessionId;
131
- return caps;
132
- }, {})
133
- : { ...browser.capabilities, sessionId: browser.sessionId },
134
- retry: this._specFileRetryAttempts
135
- });
136
- /**
137
- * report sessionId and target connection information to worker
138
- */
139
- const { protocol, hostname, port, path, queryParams, automationProtocol, headers } = browser.options;
140
- const { isW3C, sessionId } = browser;
141
- const instances = getInstancesData(browser, isMultiremote);
142
- process.send({
143
- origin: 'worker',
144
- name: 'sessionStarted',
145
- content: {
146
- automationProtocol, sessionId, isW3C, protocol, hostname, port, path, queryParams, isMultiremote, instances,
147
- capabilities: browser.capabilities,
148
- injectGlobals: this._config.injectGlobals,
149
- headers
150
- }
151
- });
152
- /**
153
- * kick off tests in framework
154
- */
155
- let failures = 0;
156
- try {
157
- failures = await this._framework.run();
158
- await this._fetchDriverLogs(this._config, caps.excludeDriverLogs);
159
- }
160
- catch (err) {
161
- log.error(err);
162
- this.emit('error', err);
163
- failures = 1;
164
- }
165
- /**
166
- * in watch mode we don't close the session and leave current page opened
167
- */
168
- if (!args.watch) {
169
- await this.endSession();
170
- }
171
- /**
172
- * send snapshot result upstream
173
- */
174
- process.send({
175
- origin: 'worker',
176
- name: 'snapshot',
177
- content: snapshotService.results
178
- });
179
- return this._shutdown(failures, retries);
180
- }
181
- async #initFramework(cid, config, capabilities, reporter, specs) {
182
- const runner = Array.isArray(config.runner) ? config.runner[0] : config.runner;
183
- /**
184
- * initialize framework adapter when running remote browser tests
185
- */
186
- if (runner === 'local') {
187
- const framework = (await initializePlugin(config.framework, 'framework')).default;
188
- return framework.init(cid, config, specs, capabilities, reporter);
189
- }
190
- /**
191
- * for embedded browser tests the `@wdio/browser-runner` already has the environment
192
- * setup so we can just run through the tests
193
- */
194
- if (runner === 'browser') {
195
- return BrowserFramework.init(cid, config, specs, capabilities, reporter);
196
- }
197
- throw new Error(`Unknown runner "${runner}"`);
40
+ !defaultConfigsKeys.includes(key) === !filterOut
41
+ )).reduce((obj, key) => {
42
+ obj[key] = caps[key];
43
+ return obj;
44
+ }, {});
45
+ }
46
+ async function initializeInstance(config, capabilities, isMultiremote) {
47
+ await enableFileLogging(config.outputDir);
48
+ if ("sessionId" in config) {
49
+ log.debug(`attach to session with id ${config.sessionId}`);
50
+ config.capabilities = sanitizeCaps(capabilities);
51
+ const caps = capabilities;
52
+ const connectionProps = {
53
+ protocol: caps.protocol || config.protocol,
54
+ hostname: caps.hostname || config.hostname,
55
+ port: caps.port || config.port,
56
+ path: caps.path || config.path
57
+ };
58
+ const params = { ...config, ...connectionProps, capabilities };
59
+ return attach({ ...params, options: params });
60
+ }
61
+ if (!isMultiremote) {
62
+ log.debug("init remote session");
63
+ const sessionConfig = {
64
+ ...config,
65
+ /**
66
+ * allow to overwrite connection details by user through capabilities
67
+ */
68
+ ...sanitizeCaps(capabilities, true),
69
+ capabilities: sanitizeCaps(capabilities)
70
+ };
71
+ return remote(sessionConfig);
72
+ }
73
+ const options = {};
74
+ log.debug("init multiremote session");
75
+ delete config.capabilities;
76
+ for (const browserName of Object.keys(capabilities)) {
77
+ options[browserName] = deepmerge(
78
+ config,
79
+ capabilities[browserName]
80
+ );
81
+ }
82
+ const browser2 = await multiremote(options, config);
83
+ const browserNames = config.injectGlobals ? Object.keys(capabilities) : [];
84
+ for (const browserName of browserNames) {
85
+ global[browserName] = browser2[browserName];
86
+ }
87
+ return browser2;
88
+ }
89
+ function getInstancesData(browser2, isMultiremote) {
90
+ if (!isMultiremote) {
91
+ return;
92
+ }
93
+ const multiRemoteBrowser = browser2;
94
+ const instances = {};
95
+ multiRemoteBrowser.instances.forEach((browserName) => {
96
+ const { protocol, hostname, port, path: path3, queryParams } = multiRemoteBrowser.getInstance(browserName).options;
97
+ const { isW3C, sessionId } = multiRemoteBrowser.getInstance(browserName);
98
+ instances[browserName] = { sessionId, isW3C, protocol, hostname, port, path: path3, queryParams };
99
+ });
100
+ return instances;
101
+ }
102
+ var SUPPORTED_ASYMMETRIC_MATCHER = {
103
+ Any: "any",
104
+ Anything: "anything",
105
+ ArrayContaining: "arrayContaining",
106
+ ObjectContaining: "objectContaining",
107
+ StringContaining: "stringContaining",
108
+ StringMatching: "stringMatching",
109
+ CloseTo: "closeTo"
110
+ };
111
+ function transformExpectArgs(arg) {
112
+ if (typeof arg === "object" && "$$typeof" in arg && Object.keys(SUPPORTED_ASYMMETRIC_MATCHER).includes(arg.$$typeof)) {
113
+ const matcherKey = SUPPORTED_ASYMMETRIC_MATCHER[arg.$$typeof];
114
+ const matcher = arg.inverse ? expect.not[matcherKey] : expect[matcherKey];
115
+ if (!matcher) {
116
+ throw new Error(`Matcher "${matcherKey}" is not supported by expect-webdriverio`);
198
117
  }
199
- /**
200
- * init protocol session
201
- * @param {object} config configuration of sessions
202
- * @param {Object} caps desired capabilities of session
203
- * @param {Object} browserStub stubbed `browser` object with only capabilities, config and env flags
204
- * @return {Promise} resolves with browser object or null if session couldn't get established
205
- */
206
- async _initSession(config, caps) {
207
- const browser = await this._startSession(config, caps);
208
- // return null if session couldn't get established
209
- if (!browser) {
210
- return;
211
- }
212
- /**
213
- * register global helper method to fetch elements
214
- */
215
- _setGlobal('$', (selector) => browser.$(selector), config.injectGlobals);
216
- _setGlobal('$$', (selector) => browser.$$(selector), config.injectGlobals);
217
- /**
218
- * register command event
219
- */
220
- browser.on('command', (command) => this._reporter?.emit('client:beforeCommand', Object.assign(command, { sessionId: browser.sessionId })));
221
- /**
222
- * register result event
223
- */
224
- browser.on('result', (result) => this._reporter?.emit('client:afterCommand', Object.assign(result, { sessionId: browser.sessionId })));
225
- return browser;
118
+ return matcher(arg.sample);
119
+ }
120
+ return arg;
121
+ }
122
+
123
+ // src/browser.ts
124
+ var log2 = logger2("@wdio/runner");
125
+ var sep = "\n - ";
126
+ var ERROR_CHECK_INTERVAL = 500;
127
+ var DEFAULT_TIMEOUT = 60 * 1e3;
128
+ var BrowserFramework = class _BrowserFramework {
129
+ constructor(_cid, _config, _specs, _reporter) {
130
+ this._cid = _cid;
131
+ this._config = _config;
132
+ this._specs = _specs;
133
+ this._reporter = _reporter;
134
+ process.on("message", this.#processMessage.bind(this));
135
+ const [, runnerOptions] = Array.isArray(_config.runner) ? _config.runner : [];
136
+ this.#runnerOptions = runnerOptions || {};
137
+ }
138
+ #retryOutdatedOptimizeDep = false;
139
+ #runnerOptions;
140
+ // `any` here because we don't want to create a dependency to @wdio/browser-runner
141
+ #resolveTestStatePromise;
142
+ /**
143
+ * always return true as it is irrelevant for component testing
144
+ */
145
+ hasTests() {
146
+ return true;
147
+ }
148
+ init() {
149
+ return void 0;
150
+ }
151
+ async run() {
152
+ try {
153
+ const failures = await this.#loop();
154
+ return failures;
155
+ } catch (err) {
156
+ if (err.message.includes("net::ERR_CONNECTION_REFUSE")) {
157
+ err.message = `Failed to load test page to run tests, make sure your browser can access "${browser.options.baseUrl}"`;
158
+ }
159
+ log2.error(`Failed to run browser tests with cid ${this._cid}: ${err.stack}`);
160
+ process.send({
161
+ origin: "worker",
162
+ name: "error",
163
+ content: { name: err.name, message: err.message, stack: err.stack }
164
+ });
165
+ return 1;
226
166
  }
227
- /**
228
- * start protocol session
229
- * @param {object} config configuration of sessions
230
- * @param {Object} caps desired capabilities of session
231
- * @return {Promise} resolves with browser object or null if session couldn't get established
232
- */
233
- async _startSession(config, caps) {
234
- try {
235
- /**
236
- * get all custom or overwritten commands users tried to register before the
237
- * test started, e.g. after all imports
238
- */
239
- const customStubCommands = this._browser?.customCommands || [];
240
- const overwrittenCommands = this._browser?.overwrittenCommands || [];
241
- this._browser = await initializeInstance(config, caps, this._isMultiremote);
242
- _setGlobal('browser', this._browser, config.injectGlobals);
243
- _setGlobal('driver', this._browser, config.injectGlobals);
244
- /**
245
- * for Jasmine we extend the Jasmine matchers instead of injecting the assertion
246
- * library ourselves
247
- */
248
- if (config.framework !== 'jasmine') {
249
- _setGlobal('expect', expect, config.injectGlobals);
250
- }
251
- /**
252
- * re-assign previously registered custom commands to the actual instance
253
- */
254
- for (const params of customStubCommands) {
255
- this._browser.addCommand(...params);
256
- }
257
- for (const params of overwrittenCommands) {
258
- this._browser.overwriteCommand(...params);
259
- }
260
- /**
261
- * import and set options for `expect-webdriverio` assertion lib once
262
- * the browser was initiated
263
- */
264
- setOptions({
265
- wait: config.waitforTimeout, // ms to wait for expectation to succeed
266
- interval: config.waitforInterval, // interval between attempts
267
- beforeAssertion: async (params) => {
268
- await Promise.all([
269
- this._reporter?.emit('client:beforeAssertion', { ...params, sessionId: this._browser?.sessionId }),
270
- executeHooksWithArgs('beforeAssertion', config.beforeAssertion, [params])
271
- ]);
272
- },
273
- afterAssertion: async (params) => {
274
- await Promise.all([
275
- this._reporter?.emit('client:afterAssertion', { ...params, sessionId: this._browser?.sessionId }),
276
- executeHooksWithArgs('afterAssertion', config.afterAssertion, [params])
277
- ]);
278
- }
279
- });
280
- /**
281
- * attach browser to `multiremotebrowser` so user have better typing support
282
- */
283
- if (this._isMultiremote) {
284
- _setGlobal('multiremotebrowser', this._browser, config.injectGlobals);
285
- }
286
- }
287
- catch (err) {
288
- log.error(err);
289
- return;
290
- }
291
- return this._browser;
167
+ }
168
+ async #loop() {
169
+ let failures = 0;
170
+ for (const spec of this._specs) {
171
+ failures += await this.#runSpec(spec);
292
172
  }
293
- /**
294
- * fetch logs provided by browser driver
295
- */
296
- async _fetchDriverLogs(config, excludeDriverLogs) {
297
- /**
298
- * only fetch logs if
299
- */
300
- if (
301
- /**
302
- * a log directory is given in config
303
- */
304
- !config.outputDir ||
305
- /**
306
- * the session wasn't killed during start up phase
307
- */
308
- !this._browser?.sessionId ||
309
- /**
310
- * driver supports it
311
- */
312
- typeof this._browser?.getLogs === 'undefined') {
313
- return;
314
- }
315
- let logTypes;
316
- try {
317
- logTypes = await this._browser.getLogTypes();
318
- }
319
- catch (errIgnored) {
320
- /**
321
- * getLogTypes is not supported by browser
322
- */
323
- return;
324
- }
325
- logTypes = filterLogTypes(excludeDriverLogs, logTypes);
326
- log.debug(`Fetching logs for ${logTypes.join(', ')}`);
327
- return Promise.all(logTypes.map(async (logType) => {
328
- let logs;
329
- try {
330
- logs = await this._browser?.getLogs(logType);
331
- }
332
- catch (e) {
333
- return log.warn(`Couldn't fetch logs for ${logType}: ${e.message}`);
334
- }
335
- /**
336
- * don't write to file if no logs were captured
337
- */
338
- if (!Array.isArray(logs) || logs.length === 0) {
339
- return;
340
- }
341
- const stringLogs = logs.map((log) => JSON.stringify(log)).join('\n');
342
- return fs.writeFile(path.join(config.outputDir, `wdio-${this._cid}-${logType}.log`), stringLogs, 'utf-8');
343
- }));
173
+ return failures;
174
+ }
175
+ async #runSpec(spec, retried = false) {
176
+ this.#retryOutdatedOptimizeDep = false;
177
+ const timeout = this._config.mochaOpts?.timeout || DEFAULT_TIMEOUT;
178
+ log2.info(`Run spec file ${spec} for cid ${this._cid}`);
179
+ const testStatePromise = new Promise((resolve) => {
180
+ this.#resolveTestStatePromise = resolve;
181
+ });
182
+ if (!this._config.sessionId) {
183
+ await browser.url(`/?cid=${this._cid}&spec=${new URL(spec).pathname}`);
344
184
  }
345
- /**
346
- * kill worker session
347
- */
348
- async _shutdown(failures, retries, initiationFailed = false) {
349
- /**
350
- * In case of initialization failed, the sessionId is undefined and the runner:start is not triggered.
351
- * So, to be able to perform the runner:end into the reporters, we need to launch the runner:start just before the runner:end.
352
- */
353
- if (this._reporter && initiationFailed) {
354
- this._reporter.emit('runner:start', {
355
- cid: this._cid,
356
- specs: this._specs,
357
- config: this._config,
358
- isMultiremote: this._isMultiremote,
359
- instanceOptions: {},
360
- capabilities: { ...this._configParser.getCapabilities() },
361
- retry: this._specFileRetryAttempts
362
- });
363
- }
364
- this._reporter.emit('runner:end', {
365
- failures,
366
- cid: this._cid,
367
- retries
368
- });
369
- try {
370
- await this._reporter.waitForSync();
185
+ await browser.setCookies([
186
+ { name: "WDIO_SPEC", value: url.fileURLToPath(spec) },
187
+ { name: "WDIO_CID", value: this._cid }
188
+ ]);
189
+ const testTimeout = setTimeout(
190
+ () => this.#onTestTimeout(`Timed out after ${timeout / 1e3}s waiting for test results`),
191
+ timeout
192
+ );
193
+ const errorInterval = setInterval(
194
+ this.#checkForTestError.bind(this),
195
+ ERROR_CHECK_INTERVAL
196
+ );
197
+ const state = await testStatePromise;
198
+ clearTimeout(testTimeout);
199
+ clearInterval(errorInterval);
200
+ if (this.#runnerOptions.coverage?.enabled && process.send) {
201
+ const coverageMap = await browser.execute(
202
+ () => window.__coverage__ || {}
203
+ );
204
+ const workerEvent = {
205
+ origin: "worker",
206
+ name: "workerEvent",
207
+ args: {
208
+ type: MESSAGE_TYPES.coverageMap,
209
+ value: coverageMap
371
210
  }
372
- catch (err) {
373
- log.error(err);
211
+ };
212
+ process.send(workerEvent);
213
+ }
214
+ if (state.errors?.length) {
215
+ const errors = state.errors.map((ev) => state.hasViteError ? `${ev.message}
216
+ ${ev.error ? ev.error.split("\n").slice(1).join("\n") : ""}` : `${path.basename(ev.filename || spec)}: ${ev.message}
217
+ ${ev.error ? ev.error.split("\n").slice(1).join("\n") : ""}`);
218
+ 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)"))) {
219
+ log2.info("Retry test run due to dynamic import error");
220
+ return this.#runSpec(spec, true);
221
+ }
222
+ 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)}`);
223
+ process.send({
224
+ origin: "worker",
225
+ name: "error",
226
+ content: { name, message, stack }
227
+ });
228
+ return 1;
229
+ }
230
+ for (const ev of state.events || []) {
231
+ if ((ev.type === "suite:start" || ev.type === "suite:end") && ev.title === "") {
232
+ continue;
233
+ }
234
+ this._reporter.emit(ev.type, {
235
+ ...ev,
236
+ file: spec,
237
+ uid: `${this._cid}-${Buffer.from(ev.fullTitle).toString("base64")}`,
238
+ cid: this._cid
239
+ });
240
+ }
241
+ return state.failures || 0;
242
+ }
243
+ async #processMessage(cmd) {
244
+ if (cmd.command !== "workerRequest" || !process.send) {
245
+ return;
246
+ }
247
+ const { message, id } = cmd.args;
248
+ if (message.type === MESSAGE_TYPES.hookTriggerMessage) {
249
+ return this.#handleHook(id, message.value);
250
+ }
251
+ if (message.type === MESSAGE_TYPES.consoleMessage) {
252
+ return this.#handleConsole(message.value);
253
+ }
254
+ if (message.type === MESSAGE_TYPES.commandRequestMessage) {
255
+ return this.#handleCommand(id, message.value);
256
+ }
257
+ if (message.type === MESSAGE_TYPES.expectRequestMessage) {
258
+ return this.#handleExpectation(id, message.value);
259
+ }
260
+ if (message.type === MESSAGE_TYPES.browserTestResult) {
261
+ return this.#handleTestFinish(message.value);
262
+ }
263
+ if (message.type === MESSAGE_TYPES.expectMatchersRequest) {
264
+ return this.#sendWorkerResponse(
265
+ id,
266
+ this.#expectMatcherResponse({ matchers: Array.from(matchers.keys()) })
267
+ );
268
+ }
269
+ }
270
+ async #handleHook(id, payload) {
271
+ const error = await executeHooksWithArgs(
272
+ payload.name,
273
+ this._config[payload.name],
274
+ payload.args
275
+ ).then(() => void 0, (err) => err);
276
+ if (error) {
277
+ log2.warn(`Failed running "${payload.name}" hook for cid ${payload.cid}: ${error.message}`);
278
+ }
279
+ return this.#sendWorkerResponse(id, this.#hookResponse({ id: payload.id, error }));
280
+ }
281
+ #expectMatcherResponse(value) {
282
+ return {
283
+ type: MESSAGE_TYPES.expectMatchersResponse,
284
+ value
285
+ };
286
+ }
287
+ #hookResponse(value) {
288
+ return {
289
+ type: MESSAGE_TYPES.hookResultMessage,
290
+ value
291
+ };
292
+ }
293
+ #sendWorkerResponse(id, message) {
294
+ if (!process.send) {
295
+ return;
296
+ }
297
+ const response = {
298
+ origin: "worker",
299
+ name: "workerResponse",
300
+ args: { id, message }
301
+ };
302
+ process.send(response);
303
+ }
304
+ /**
305
+ * Print console message executed in browser to the terminal
306
+ * @param message console.log message args
307
+ * @returns void
308
+ */
309
+ #handleConsole(message) {
310
+ const isWDIOLog = Boolean(typeof message.args[0] === "string" && message.args[0].startsWith("[WDIO]") && message.type !== "error");
311
+ if (message.name !== "consoleEvent" || isWDIOLog) {
312
+ return;
313
+ }
314
+ console[message.type](...message.args || []);
315
+ }
316
+ async #handleCommand(id, payload) {
317
+ log2.debug(`Received browser message: ${JSON.stringify(payload)}`);
318
+ const cid = payload.cid;
319
+ if (typeof cid !== "string") {
320
+ const { message, stack } = new Error(`No "cid" property passed into command message with id "${payload.id}"`);
321
+ const error = { message, stack, name: "Error" };
322
+ return this.#sendWorkerResponse(id, this.#commandResponse({ id: payload.id, error }));
323
+ }
324
+ try {
325
+ const scope = payload.scope ? await browser.$({ [ELEMENT_KEY]: payload.scope }) : browser;
326
+ if (typeof scope[payload.commandName] !== "function") {
327
+ throw new Error(`${payload.scope ? "element" : "browser"}.${payload.commandName} is not a function`);
328
+ }
329
+ let result = await scope[payload.commandName](...payload.args);
330
+ if (result?.constructor?.name === "Element") {
331
+ result = result.elementId ? { [ELEMENT_KEY]: result.elementId } : result.error ? { message: result.error.message, stack: result.error.stack, name: result.error.name } : void 0;
332
+ } else if (result?.foundWith) {
333
+ result = (await result.map((res) => ({
334
+ [ELEMENT_KEY]: res.elementId
335
+ }))).filter(Boolean);
336
+ }
337
+ const resultMsg = this.#commandResponse({ id: payload.id, result });
338
+ log2.debug(`Return command result: ${resultMsg}`);
339
+ return this.#sendWorkerResponse(id, resultMsg);
340
+ } catch (error) {
341
+ const { message, stack, name } = error;
342
+ return this.#sendWorkerResponse(id, this.#commandResponse({ id: payload.id, error: { message, stack, name } }));
343
+ }
344
+ }
345
+ #commandResponse(value) {
346
+ return {
347
+ type: MESSAGE_TYPES.commandResponseMessage,
348
+ value
349
+ };
350
+ }
351
+ /**
352
+ * handle expectation assertions within the worker process
353
+ * @param id message id from communicator
354
+ * @param payload information about the expectation to run
355
+ * @returns void
356
+ */
357
+ async #handleExpectation(id, payload) {
358
+ log2.debug(`Received expectation message: ${JSON.stringify(payload)}`);
359
+ const cid = payload.cid;
360
+ if (typeof cid !== "string") {
361
+ const message = `No "cid" property passed into expect request message with id "${payload.id}"`;
362
+ return this.#sendWorkerResponse(id, this.#expectResponse({ id: payload.id, pass: false, message }));
363
+ }
364
+ const matcher = matchers.get(payload.matcherName);
365
+ if (!matcher) {
366
+ const message = `Couldn't find matcher with name "${payload.matcherName}"`;
367
+ return this.#sendWorkerResponse(id, this.#expectResponse({ id: payload.id, pass: false, message }));
368
+ }
369
+ try {
370
+ 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;
371
+ const result = await matcher.apply(payload.scope, [context, ...payload.args.map(transformExpectArgs)]);
372
+ return this.#sendWorkerResponse(id, this.#expectResponse({
373
+ id: payload.id,
374
+ pass: result.pass,
375
+ message: result.message()
376
+ }));
377
+ } catch (err) {
378
+ const errorMessage = err instanceof Error ? err.stack : err;
379
+ const message = `Failed to execute expect command "${payload.matcherName}": ${errorMessage}`;
380
+ return this.#sendWorkerResponse(id, this.#expectResponse({ id: payload.id, pass: false, message }));
381
+ }
382
+ }
383
+ #expectResponse(value) {
384
+ return {
385
+ type: MESSAGE_TYPES.expectResponseMessage,
386
+ value
387
+ };
388
+ }
389
+ #handleTestFinish(payload) {
390
+ this.#resolveTestStatePromise({ failures: payload.failures, events: payload.events });
391
+ }
392
+ #onTestTimeout(message) {
393
+ return this.#resolveTestStatePromise?.({
394
+ events: [],
395
+ failures: 1,
396
+ errors: [{ message }]
397
+ });
398
+ }
399
+ async #checkForTestError() {
400
+ const testError = await browser.execute(function fetchExecutionState() {
401
+ let viteError;
402
+ const viteErrorElem = document.querySelector("vite-error-overlay");
403
+ if (viteErrorElem && viteErrorElem.shadowRoot) {
404
+ const errorElements = Array.from(viteErrorElem.shadowRoot.querySelectorAll("pre"));
405
+ if (errorElements.length) {
406
+ viteError = [{ message: errorElements.map((elem) => elem.innerText).join("\n") }];
374
407
  }
375
- this.emit('exit', failures === 0 ? 0 : 1);
376
- return failures;
408
+ }
409
+ 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;
410
+ const errors = viteError || window.__wdioErrors__ || loadError;
411
+ return { errors, hasViteError: Boolean(viteError) };
412
+ }).catch((err) => {
413
+ if (err.message.includes("Cannot find context with specified id")) {
414
+ return;
415
+ }
416
+ throw err;
417
+ });
418
+ if (!testError) {
419
+ return;
377
420
  }
378
- /**
379
- * end WebDriver session, a config object can be applied if object has changed
380
- * within a hook by the user
381
- */
382
- async endSession(payload) {
383
- /**
384
- * make sure instance(s) exist and have `sessionId`
385
- */
386
- const multiremoteBrowser = this._browser;
387
- const browser = this._browser;
388
- const hasSessionId = Boolean(this._browser) && (this._isMultiremote
389
- /**
390
- * every multiremote instance should exist and should have `sessionId`
391
- */
392
- ? !multiremoteBrowser.instances.some((browserName) => (multiremoteBrowser.getInstance(browserName) &&
393
- !multiremoteBrowser.getInstance(browserName).sessionId))
394
- /**
395
- * browser object should have `sessionId` in regular mode
396
- */
397
- : browser.sessionId);
421
+ if (testError.errors && testError.errors.length > 0 || testError.hasViteError) {
422
+ this.#resolveTestStatePromise?.({
423
+ events: [],
424
+ failures: 1,
425
+ ...testError
426
+ });
427
+ }
428
+ const logs = typeof browser.getLogs === "function" ? await browser.getLogs("browser").catch(() => []) : [];
429
+ const severeLogs = logs.filter((log5) => log5.level === "SEVERE" && log5.source !== "deprecation");
430
+ if (severeLogs.length) {
431
+ if (!this.#retryOutdatedOptimizeDep && severeLogs.some((log5) => log5.message?.includes("(Outdated Optimize Dep)"))) {
432
+ log2.info("Retry test run due to outdated optimize dep");
433
+ this.#retryOutdatedOptimizeDep = true;
434
+ return browser.refresh();
435
+ }
436
+ this.#resolveTestStatePromise?.({
437
+ events: [],
438
+ failures: 1,
439
+ hasViteError: false,
398
440
  /**
399
- * in watch mode we create a new child process to kill the
400
- * session, see packages/wdio-local-runner/src/index.ts,
401
- * therefore we need to attach to the session to kill it
441
+ * error messages often look like:
442
+ * "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)"
402
443
  */
403
- if (!hasSessionId && payload?.args.config.sessionId) {
404
- this._browser = await attach({
405
- ...payload.args.config,
406
- capabilities: payload?.args.capabilities
407
- });
408
- }
409
- else if (!hasSessionId) {
410
- /**
411
- * don't do anything if test framework returns after SIGINT
412
- * if endSession is called without payload we expect a session id
413
- */
414
- return;
444
+ errors: severeLogs.map((log5) => {
445
+ const [filename, message] = log5.message.split(" - ");
446
+ return {
447
+ filename: filename.startsWith("http") ? filename : void 0,
448
+ message
449
+ };
450
+ })
451
+ });
452
+ }
453
+ }
454
+ static init(cid, config, specs, _, reporter) {
455
+ const framework = new _BrowserFramework(cid, config, specs, reporter);
456
+ return framework;
457
+ }
458
+ };
459
+
460
+ // src/reporter.ts
461
+ import path2 from "node:path";
462
+ import logger3 from "@wdio/logger";
463
+ import { initializePlugin } from "@wdio/utils";
464
+ var log3 = logger3("@wdio/runner");
465
+ var mochaAllHooks = ['"before all" hook', '"after all" hook'];
466
+ var BaseReporter = class {
467
+ constructor(_config, _cid, caps) {
468
+ this._config = _config;
469
+ this._cid = _cid;
470
+ this.caps = caps;
471
+ }
472
+ _reporters = [];
473
+ listeners = [];
474
+ async initReporters() {
475
+ this._reporters = await Promise.all(
476
+ this._config.reporters.map(this._loadReporter.bind(this))
477
+ );
478
+ }
479
+ /**
480
+ * emit events to all registered reporter and wdio launcer
481
+ *
482
+ * @param {string} e event name
483
+ * @param {object} payload event payload
484
+ */
485
+ emit(e, payload) {
486
+ payload.cid = this._cid;
487
+ const isTestError = e === "test:fail";
488
+ const isHookError = e === "hook:end" && payload.error && mochaAllHooks.some((hook) => payload.title.startsWith(hook));
489
+ if (isTestError || isHookError) {
490
+ this.#emitData({
491
+ origin: "reporter",
492
+ name: "printFailureMessage",
493
+ content: payload
494
+ });
495
+ }
496
+ this._reporters.forEach((reporter) => reporter.emit(e, payload));
497
+ }
498
+ onMessage(listener) {
499
+ this.listeners.push(listener);
500
+ }
501
+ getLogFile(name) {
502
+ const options = Object.assign({}, this._config);
503
+ let filename = `wdio-${this._cid}-${name}-reporter.log`;
504
+ const reporterOptions = this._config.reporters.find((reporter) => Array.isArray(reporter) && (reporter[0] === name || typeof reporter[0] === "function" && reporter[0].name === name));
505
+ if (reporterOptions && Array.isArray(reporterOptions)) {
506
+ const fileformat = reporterOptions[1].outputFileFormat;
507
+ options.cid = this._cid;
508
+ options.capabilities = this.caps;
509
+ Object.assign(options, reporterOptions[1]);
510
+ if (fileformat) {
511
+ if (typeof fileformat !== "function") {
512
+ throw new Error("outputFileFormat must be a function");
415
513
  }
416
- /**
417
- * store capabilities for afterSession hook
418
- */
419
- const capabilities = this._browser?.capabilities || {};
420
- if (this._isMultiremote) {
421
- multiremoteBrowser.instances.forEach((browserName) => {
422
- capabilities[browserName] = multiremoteBrowser.getInstance(browserName).capabilities;
423
- });
514
+ filename = fileformat(options);
515
+ }
516
+ }
517
+ if (!options.outputDir) {
518
+ return;
519
+ }
520
+ return path2.join(options.outputDir, filename);
521
+ }
522
+ /**
523
+ * return write stream object based on reporter name
524
+ */
525
+ getWriteStreamObject(reporter) {
526
+ return {
527
+ write: (
528
+ /* istanbul ignore next */
529
+ (content) => this.#emitData({
530
+ origin: "reporter",
531
+ name: reporter,
532
+ content
533
+ })
534
+ )
535
+ };
536
+ }
537
+ /**
538
+ * emit data either through process or listener
539
+ */
540
+ #emitData(payload) {
541
+ if (typeof process.send === "function") {
542
+ return process.send(payload);
543
+ }
544
+ this.listeners.forEach((fn) => fn(payload));
545
+ return true;
546
+ }
547
+ /**
548
+ * wait for reporter to finish synchronization, e.g. when sending data asynchronous
549
+ * to a server (e.g. sumo reporter)
550
+ */
551
+ waitForSync() {
552
+ const startTime = Date.now();
553
+ return new Promise((resolve, reject) => {
554
+ const interval = setInterval(() => {
555
+ const unsyncedReporter = this._reporters.filter((reporter) => !reporter.isSynchronised).map((reporter) => reporter.constructor.name);
556
+ if (Date.now() - startTime > this._config.reporterSyncTimeout && unsyncedReporter.length) {
557
+ clearInterval(interval);
558
+ return reject(new Error(`Some reporters are still unsynced: ${unsyncedReporter.join(", ")}`));
424
559
  }
425
- await this._browser?.deleteSession();
426
- process.send({
427
- origin: 'worker',
428
- name: 'sessionEnded',
429
- cid: this._cid
430
- });
431
- /**
432
- * delete session(s)
433
- */
434
- if (this._isMultiremote) {
435
- multiremoteBrowser.instances.forEach((browserName) => {
436
- // @ts-ignore sessionId is usually required
437
- delete multiremoteBrowser.getInstance(browserName).sessionId;
438
- });
560
+ if (!unsyncedReporter.length) {
561
+ clearInterval(interval);
562
+ return resolve(true);
439
563
  }
440
- else if (browser) {
441
- browser.sessionId = undefined;
564
+ log3.info(`Wait for ${unsyncedReporter.length} reporter to synchronise`);
565
+ }, this._config.reporterSyncInterval);
566
+ });
567
+ }
568
+ /**
569
+ * initialize reporters
570
+ */
571
+ async _loadReporter(reporter) {
572
+ let ReporterClass;
573
+ let options = {};
574
+ if (Array.isArray(reporter)) {
575
+ options = Object.assign({}, options, reporter[1]);
576
+ reporter = reporter[0];
577
+ }
578
+ if (typeof reporter === "function") {
579
+ ReporterClass = reporter;
580
+ options.logFile = options.setLogFile ? options.setLogFile(this._cid, ReporterClass.name) : typeof options.logFile === "string" ? options.logFile : this.getLogFile(ReporterClass.name);
581
+ options.writeStream = this.getWriteStreamObject(ReporterClass.name);
582
+ return new ReporterClass(options);
583
+ }
584
+ if (typeof reporter === "string") {
585
+ ReporterClass = (await initializePlugin(reporter, "reporter")).default;
586
+ options.logFile = options.setLogFile ? options.setLogFile(this._cid, reporter) : typeof options.logFile === "string" ? options.logFile : this.getLogFile(reporter);
587
+ options.writeStream = this.getWriteStreamObject(reporter);
588
+ return new ReporterClass(options);
589
+ }
590
+ throw new Error("Invalid reporters config");
591
+ }
592
+ };
593
+
594
+ // src/index.ts
595
+ var log4 = logger4("@wdio/runner");
596
+ var Runner = class extends EventEmitter {
597
+ _browser;
598
+ _configParser;
599
+ _sigintWasCalled = false;
600
+ _isMultiremote = false;
601
+ _specFileRetryAttempts = 0;
602
+ _reporter;
603
+ _framework;
604
+ _config;
605
+ _cid;
606
+ _specs;
607
+ _caps;
608
+ /**
609
+ * run test suite
610
+ * @param {string} cid worker id (e.g. `0-0`)
611
+ * @param {Object} args config arguments passed into worker process
612
+ * @param {string[]} specs list of spec files to run
613
+ * @param {Object} caps capabilities to run session with
614
+ * @param {string} configFile path to config file to get config from
615
+ * @param {number} retries number of retries remaining
616
+ * @return {Promise} resolves in number of failures for testrun
617
+ */
618
+ async run({ cid, args, specs, caps, configFile, retries }) {
619
+ this._configParser = new ConfigParser(configFile, args);
620
+ this._cid = cid;
621
+ this._specs = specs;
622
+ this._caps = caps;
623
+ try {
624
+ await this._configParser.initialize(args);
625
+ } catch (err) {
626
+ log4.error(`Failed to read config file: ${err.stack}`);
627
+ return this._shutdown(1, retries, true);
628
+ }
629
+ this._config = this._configParser.getConfig();
630
+ this._specFileRetryAttempts = (this._config.specFileRetries || 0) - (retries || 0);
631
+ logger4.setLogLevelsConfig(this._config.logLevels, this._config.logLevel);
632
+ const capabilities = this._configParser.getCapabilities();
633
+ const isMultiremote = this._isMultiremote = !Array.isArray(capabilities) || Object.values(caps).length > 0 && Object.values(caps).every((c) => typeof c === "object" && c.capabilities);
634
+ const snapshotService = SnapshotService.initiate({
635
+ updateState: this._config.updateSnapshots,
636
+ resolveSnapshotPath: this._config.resolveSnapshotPath
637
+ });
638
+ this._configParser.addService(snapshotService);
639
+ let browser2 = await this._startSession({
640
+ ...this._config,
641
+ // @ts-ignore used in `/packages/webdriverio/src/protocol-stub.ts`
642
+ _automationProtocol: this._config.automationProtocol,
643
+ automationProtocol: "./protocol-stub.js"
644
+ }, caps);
645
+ (await initializeWorkerService(
646
+ this._config,
647
+ caps,
648
+ args.ignoredWorkerServices
649
+ )).map(this._configParser.addService.bind(this._configParser));
650
+ const beforeSessionParams = [this._config, this._caps, this._specs, this._cid];
651
+ await executeHooksWithArgs2("beforeSession", this._config.beforeSession, beforeSessionParams);
652
+ this._reporter = new BaseReporter(this._config, this._cid, { ...caps });
653
+ await this._reporter.initReporters();
654
+ this._framework = await this.#initFramework(cid, this._config, caps, this._reporter, specs);
655
+ process.send({ name: "testFrameworkInit", content: { cid, caps, specs, hasTests: this._framework.hasTests() } });
656
+ if (!this._framework.hasTests()) {
657
+ return this._shutdown(0, retries, true);
658
+ }
659
+ browser2 = await this._initSession(this._config, this._caps);
660
+ if (!browser2) {
661
+ const afterArgs = [1, this._caps, this._specs];
662
+ await executeHooksWithArgs2("after", this._config.after, afterArgs);
663
+ return this._shutdown(1, retries, true);
664
+ }
665
+ this._reporter.caps = browser2.capabilities;
666
+ const beforeArgs = [this._caps, this._specs, browser2];
667
+ await executeHooksWithArgs2("before", this._config.before, beforeArgs);
668
+ if (this._sigintWasCalled) {
669
+ log4.info("SIGINT signal detected while starting session, shutting down...");
670
+ await this.endSession();
671
+ return this._shutdown(0, retries, true);
672
+ }
673
+ const multiRemoteBrowser = browser2;
674
+ this._reporter.emit("runner:start", {
675
+ cid,
676
+ specs,
677
+ config: browser2.options,
678
+ isMultiremote,
679
+ instanceOptions: isMultiremote ? multiRemoteBrowser.instances.reduce((prev, browserName) => {
680
+ prev[multiRemoteBrowser.getInstance(browserName).sessionId] = multiRemoteBrowser.getInstance(browserName).options;
681
+ return prev;
682
+ }, {}) : {
683
+ [browser2.sessionId]: browser2.options
684
+ },
685
+ sessionId: browser2.sessionId,
686
+ capabilities: isMultiremote ? multiRemoteBrowser.instances.reduce((caps2, browserName) => {
687
+ caps2[browserName] = multiRemoteBrowser.getInstance(browserName).capabilities;
688
+ caps2[browserName].sessionId = multiRemoteBrowser.getInstance(browserName).sessionId;
689
+ return caps2;
690
+ }, {}) : { ...browser2.capabilities, sessionId: browser2.sessionId },
691
+ retry: this._specFileRetryAttempts
692
+ });
693
+ const { protocol, hostname, port, path: path3, queryParams, automationProtocol, headers } = browser2.options;
694
+ const { isW3C, sessionId } = browser2;
695
+ const instances = getInstancesData(browser2, isMultiremote);
696
+ process.send({
697
+ origin: "worker",
698
+ name: "sessionStarted",
699
+ content: {
700
+ automationProtocol,
701
+ sessionId,
702
+ isW3C,
703
+ protocol,
704
+ hostname,
705
+ port,
706
+ path: path3,
707
+ queryParams,
708
+ isMultiremote,
709
+ instances,
710
+ capabilities: browser2.capabilities,
711
+ injectGlobals: this._config.injectGlobals,
712
+ headers
713
+ }
714
+ });
715
+ let failures = 0;
716
+ try {
717
+ failures = await this._framework.run();
718
+ } catch (err) {
719
+ log4.error(err);
720
+ this.emit("error", err);
721
+ failures = 1;
722
+ }
723
+ if (!args.watch) {
724
+ await this.endSession();
725
+ }
726
+ process.send({
727
+ origin: "worker",
728
+ name: "snapshot",
729
+ content: snapshotService.results
730
+ });
731
+ return this._shutdown(failures, retries);
732
+ }
733
+ async #initFramework(cid, config, capabilities, reporter, specs) {
734
+ const runner = Array.isArray(config.runner) ? config.runner[0] : config.runner;
735
+ if (runner === "local") {
736
+ const framework = (await initializePlugin2(config.framework, "framework")).default;
737
+ return framework.init(cid, config, specs, capabilities, reporter);
738
+ }
739
+ if (runner === "browser") {
740
+ return BrowserFramework.init(cid, config, specs, capabilities, reporter);
741
+ }
742
+ throw new Error(`Unknown runner "${runner}"`);
743
+ }
744
+ /**
745
+ * init protocol session
746
+ * @param {object} config configuration of sessions
747
+ * @param {Object} caps desired capabilities of session
748
+ * @param {Object} browserStub stubbed `browser` object with only capabilities, config and env flags
749
+ * @return {Promise} resolves with browser object or null if session couldn't get established
750
+ */
751
+ async _initSession(config, caps) {
752
+ const browser2 = await this._startSession(config, caps);
753
+ if (!browser2) {
754
+ return;
755
+ }
756
+ _setGlobal("$", (selector) => browser2.$(selector), config.injectGlobals);
757
+ _setGlobal("$$", (selector) => browser2.$$(selector), config.injectGlobals);
758
+ browser2.on("command", (command) => this._reporter?.emit(
759
+ "client:beforeCommand",
760
+ Object.assign(command, { sessionId: browser2.sessionId })
761
+ ));
762
+ browser2.on("result", (result) => this._reporter?.emit(
763
+ "client:afterCommand",
764
+ Object.assign(result, { sessionId: browser2.sessionId })
765
+ ));
766
+ return browser2;
767
+ }
768
+ /**
769
+ * start protocol session
770
+ * @param {object} config configuration of sessions
771
+ * @param {Object} caps desired capabilities of session
772
+ * @return {Promise} resolves with browser object or null if session couldn't get established
773
+ */
774
+ async _startSession(config, caps) {
775
+ try {
776
+ const customStubCommands = this._browser?.customCommands || [];
777
+ const overwrittenCommands = this._browser?.overwrittenCommands || [];
778
+ this._browser = await initializeInstance(config, caps, this._isMultiremote);
779
+ _setGlobal("browser", this._browser, config.injectGlobals);
780
+ _setGlobal("driver", this._browser, config.injectGlobals);
781
+ if (config.framework !== "jasmine") {
782
+ _setGlobal("expect", expect2, config.injectGlobals);
783
+ }
784
+ for (const params of customStubCommands) {
785
+ this._browser.addCommand(...params);
786
+ }
787
+ for (const params of overwrittenCommands) {
788
+ this._browser.overwriteCommand(...params);
789
+ }
790
+ setOptions({
791
+ wait: config.waitforTimeout,
792
+ // ms to wait for expectation to succeed
793
+ interval: config.waitforInterval,
794
+ // interval between attempts
795
+ beforeAssertion: async (params) => {
796
+ await Promise.all([
797
+ this._reporter?.emit("client:beforeAssertion", { ...params, sessionId: this._browser?.sessionId }),
798
+ executeHooksWithArgs2("beforeAssertion", config.beforeAssertion, [params])
799
+ ]);
800
+ },
801
+ afterAssertion: async (params) => {
802
+ await Promise.all([
803
+ this._reporter?.emit("client:afterAssertion", { ...params, sessionId: this._browser?.sessionId }),
804
+ executeHooksWithArgs2("afterAssertion", config.afterAssertion, [params])
805
+ ]);
442
806
  }
443
- const afterSessionArgs = [this._config, capabilities, this._specs];
444
- await executeHooksWithArgs('afterSession', this._config.afterSession, afterSessionArgs);
807
+ });
808
+ if (this._isMultiremote) {
809
+ _setGlobal("multiremotebrowser", this._browser, config.injectGlobals);
810
+ }
811
+ } catch (err) {
812
+ log4.error(err);
813
+ return;
445
814
  }
446
- }
447
- export { default as BaseReporter } from './reporter.js';
448
- export * from './types.js';
815
+ return this._browser;
816
+ }
817
+ /**
818
+ * kill worker session
819
+ */
820
+ async _shutdown(failures, retries, initiationFailed = false) {
821
+ if (this._reporter && initiationFailed) {
822
+ this._reporter.emit("runner:start", {
823
+ cid: this._cid,
824
+ specs: this._specs,
825
+ config: this._config,
826
+ isMultiremote: this._isMultiremote,
827
+ instanceOptions: {},
828
+ capabilities: { ...this._configParser.getCapabilities() },
829
+ retry: this._specFileRetryAttempts
830
+ });
831
+ }
832
+ this._reporter.emit("runner:end", {
833
+ failures,
834
+ cid: this._cid,
835
+ retries
836
+ });
837
+ try {
838
+ await this._reporter.waitForSync();
839
+ } catch (err) {
840
+ log4.error(err);
841
+ }
842
+ this.emit("exit", failures === 0 ? 0 : 1);
843
+ return failures;
844
+ }
845
+ /**
846
+ * end WebDriver session, a config object can be applied if object has changed
847
+ * within a hook by the user
848
+ */
849
+ async endSession(payload) {
850
+ const multiremoteBrowser = this._browser;
851
+ const browser2 = this._browser;
852
+ const hasSessionId = Boolean(this._browser) && (this._isMultiremote ? !multiremoteBrowser.instances.some(
853
+ (browserName) => multiremoteBrowser.getInstance(browserName) && !multiremoteBrowser.getInstance(browserName).sessionId
854
+ ) : browser2.sessionId);
855
+ if (!hasSessionId && payload?.args.config.sessionId) {
856
+ this._browser = await attach2({
857
+ ...payload.args.config,
858
+ capabilities: payload?.args.capabilities
859
+ });
860
+ } else if (!hasSessionId) {
861
+ return;
862
+ }
863
+ const capabilities = this._browser?.capabilities || {};
864
+ if (this._isMultiremote) {
865
+ const multiremoteBrowser2 = this._browser;
866
+ multiremoteBrowser2.instances.forEach((browserName) => {
867
+ capabilities[browserName] = multiremoteBrowser2.getInstance(browserName).capabilities;
868
+ });
869
+ }
870
+ await this._browser?.deleteSession();
871
+ process.send({
872
+ origin: "worker",
873
+ name: "sessionEnded",
874
+ cid: this._cid
875
+ });
876
+ if (this._isMultiremote) {
877
+ multiremoteBrowser.instances.forEach((browserName) => {
878
+ delete multiremoteBrowser.getInstance(browserName).sessionId;
879
+ });
880
+ } else if (browser2) {
881
+ browser2.sessionId = void 0;
882
+ }
883
+ const afterSessionArgs = [this._config, capabilities, this._specs];
884
+ await executeHooksWithArgs2("afterSession", this._config.afterSession, afterSessionArgs);
885
+ }
886
+ };
887
+ export {
888
+ BaseReporter,
889
+ Runner as default
890
+ };