@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/browser.d.ts +3 -4
- package/build/browser.d.ts.map +1 -1
- package/build/index.d.ts +0 -5
- package/build/index.d.ts.map +1 -1
- package/build/index.js +874 -432
- package/build/reporter.d.ts +2 -2
- package/build/reporter.d.ts.map +1 -1
- package/build/types.d.ts +2 -10
- package/build/types.d.ts.map +1 -1
- package/build/utils.d.ts +4 -4
- package/build/utils.d.ts.map +1 -1
- package/package.json +15 -13
- package/build/browser.js +0 -385
- package/build/reporter.js +0 -192
- package/build/types.js +0 -1
- package/build/utils.js +0 -152
- /package/{LICENSE-MIT → LICENSE} +0 -0
package/build/index.js
CHANGED
|
@@ -1,448 +1,890 @@
|
|
|
1
|
-
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
import
|
|
12
|
-
import
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
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
|
-
|
|
373
|
-
|
|
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
|
-
|
|
376
|
-
|
|
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
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
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
|
-
*
|
|
400
|
-
*
|
|
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
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
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
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
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
|
-
|
|
426
|
-
|
|
427
|
-
|
|
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
|
-
|
|
441
|
-
|
|
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
|
-
|
|
444
|
-
|
|
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
|
-
|
|
448
|
-
|
|
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
|
+
};
|