@wdio/browser-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/driver.js +249 -234
- package/build/browser/expect.js +107 -148
- package/build/browser/frameworks/mocha.d.ts.map +1 -1
- package/build/browser/integrations/stencil.js +370 -407
- package/build/browser/mock.d.ts +3 -2
- package/build/browser/mock.d.ts.map +1 -1
- package/build/browser/mock.js +78 -34
- package/build/browser/setup.js +313 -37
- package/build/browser/spy.d.ts.map +1 -1
- package/build/browser/spy.js +29 -40
- package/build/browser/utils.d.ts +7 -0
- package/build/browser/utils.d.ts.map +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1465 -171
- package/build/types.d.ts +19 -2
- package/build/types.d.ts.map +1 -1
- package/build/utils.d.ts +3 -3
- package/build/utils.d.ts.map +1 -1
- package/build/vite/constants.d.ts +3 -0
- package/build/vite/constants.d.ts.map +1 -1
- package/build/vite/plugins/esbuild.d.ts.map +1 -1
- package/build/vite/plugins/testrunner.d.ts.map +1 -1
- package/build/vite/server.d.ts +0 -1
- package/build/vite/server.d.ts.map +1 -1
- package/build/vite/utils.d.ts.map +1 -1
- package/package.json +57 -26
- package/build/browser/commands/debug.js +0 -6
- package/build/browser/frameworks/mocha.js +0 -320
- package/build/browser/utils.js +0 -61
- package/build/communicator.js +0 -82
- package/build/constants.js +0 -89
- package/build/types.js +0 -1
- package/build/utils.js +0 -86
- package/build/vite/constants.js +0 -55
- package/build/vite/frameworks/index.js +0 -19
- package/build/vite/frameworks/nuxt.js +0 -61
- package/build/vite/frameworks/stencil.js +0 -165
- package/build/vite/frameworks/tailwindcss.js +0 -28
- package/build/vite/mock.js +0 -50
- package/build/vite/plugins/esbuild.js +0 -25
- package/build/vite/plugins/mockHoisting.js +0 -312
- package/build/vite/plugins/testrunner.js +0 -152
- package/build/vite/plugins/worker.js +0 -12
- package/build/vite/server.js +0 -104
- package/build/vite/types.js +0 -1
- package/build/vite/utils.js +0 -223
- /package/{LICENSE-MIT → LICENSE} +0 -0
package/build/index.js
CHANGED
|
@@ -1,189 +1,1483 @@
|
|
|
1
|
-
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
import
|
|
9
|
-
import
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
import
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import fs4 from "node:fs/promises";
|
|
3
|
+
import url7 from "node:url";
|
|
4
|
+
import path9 from "node:path";
|
|
5
|
+
import logger8 from "@wdio/logger";
|
|
6
|
+
import LocalRunner from "@wdio/local-runner";
|
|
7
|
+
import libCoverage2 from "istanbul-lib-coverage";
|
|
8
|
+
import libReport from "istanbul-lib-report";
|
|
9
|
+
import reports from "istanbul-reports";
|
|
10
|
+
|
|
11
|
+
// src/vite/server.ts
|
|
12
|
+
import path5 from "node:path";
|
|
13
|
+
import { EventEmitter } from "node:events";
|
|
14
|
+
import getPort from "get-port";
|
|
15
|
+
import logger4 from "@wdio/logger";
|
|
16
|
+
import istanbulPlugin from "vite-plugin-istanbul";
|
|
17
|
+
import { deepmerge as deepmerge2 } from "deepmerge-ts";
|
|
18
|
+
import { createServer } from "vite";
|
|
19
|
+
|
|
20
|
+
// src/vite/plugins/testrunner.ts
|
|
21
|
+
import url2 from "node:url";
|
|
22
|
+
import path2 from "node:path";
|
|
23
|
+
import { builtinModules } from "node:module";
|
|
24
|
+
import logger2 from "@wdio/logger";
|
|
25
|
+
import { polyfillPath } from "modern-node-polyfills";
|
|
26
|
+
import { deepmerge } from "deepmerge-ts";
|
|
27
|
+
import { resolve as resolve2 } from "import-meta-resolve";
|
|
28
|
+
import {
|
|
29
|
+
WebDriverProtocol,
|
|
30
|
+
MJsonWProtocol,
|
|
31
|
+
AppiumProtocol,
|
|
32
|
+
ChromiumProtocol,
|
|
33
|
+
SauceLabsProtocol,
|
|
34
|
+
SeleniumProtocol,
|
|
35
|
+
GeckoProtocol
|
|
36
|
+
} from "@wdio/protocols";
|
|
37
|
+
|
|
38
|
+
// src/constants.ts
|
|
39
|
+
var SESSIONS = /* @__PURE__ */ new Map();
|
|
40
|
+
var WDIO_EVENT_NAME = "wdio:workerMessage";
|
|
41
|
+
var FRAMEWORK_SUPPORT_ERROR = 'Currently only "mocha" is supported as framework when using @wdio/browser-runner.';
|
|
42
|
+
var DEFAULT_INCLUDE = ["**"];
|
|
43
|
+
var DEFAULT_FILE_EXTENSIONS = [".js", ".cjs", ".mjs", ".ts", ".mts", ".cts", ".tsx", ".jsx", ".vue", ".svelte"];
|
|
44
|
+
var DEFAULT_REPORTS_DIRECTORY = "coverage";
|
|
45
|
+
var DEFAULT_AUTOMOCK = true;
|
|
46
|
+
var DEFAULT_MOCK_DIRECTORY = "__mocks__";
|
|
47
|
+
var SUMMARY_REPORTER = "json-summary";
|
|
48
|
+
var COVERAGE_FACTORS = ["lines", "functions", "branches", "statements"];
|
|
49
|
+
var DEFAULT_COVERAGE_REPORTS = ["text", "html", "clover", SUMMARY_REPORTER];
|
|
50
|
+
var GLOBAL_TRESHOLD_REPORTING = "ERROR: Coverage for %s (%s%) does not meet global threshold (%s%)";
|
|
51
|
+
var FILE_TRESHOLD_REPORTING = "ERROR: Coverage for %s (%s%) does not meet threshold (%s%) for %s";
|
|
52
|
+
var MOCHA_VARIABELS = (
|
|
53
|
+
/*css*/
|
|
54
|
+
`:root {
|
|
55
|
+
--mocha-color: #000;
|
|
56
|
+
--mocha-bg-color: #fff;
|
|
57
|
+
--mocha-pass-icon-color: #00d6b2;
|
|
58
|
+
--mocha-pass-color: #fff;
|
|
59
|
+
--mocha-pass-shadow-color: rgba(0, 0, 0, .2);
|
|
60
|
+
--mocha-pass-mediump-color: #c09853;
|
|
61
|
+
--mocha-pass-slow-color: #b94a48;
|
|
62
|
+
--mocha-test-pending-color: #0b97c4;
|
|
63
|
+
--mocha-test-pending-icon-color: #0b97c4;
|
|
64
|
+
--mocha-test-fail-color: #c00;
|
|
65
|
+
--mocha-test-fail-icon-color: #c00;
|
|
66
|
+
--mocha-test-fail-pre-color: #000;
|
|
67
|
+
--mocha-test-fail-pre-error-color: #c00;
|
|
68
|
+
--mocha-test-html-error-color: #000;
|
|
69
|
+
--mocha-box-shadow-color: #eee;
|
|
70
|
+
--mocha-box-bottom-color: #ddd;
|
|
71
|
+
--mocha-test-replay-color: #000;
|
|
72
|
+
--mocha-test-replay-bg-color: #eee;
|
|
73
|
+
--mocha-stats-color: #888;
|
|
74
|
+
--mocha-stats-em-color: #000;
|
|
75
|
+
--mocha-stats-hover-color: #eee;
|
|
76
|
+
--mocha-error-color: #c00;
|
|
77
|
+
|
|
78
|
+
--mocha-code-comment: #ddd;
|
|
79
|
+
--mocha-code-init: #2f6fad;
|
|
80
|
+
--mocha-code-string: #5890ad;
|
|
81
|
+
--mocha-code-keyword: #8a6343;
|
|
82
|
+
--mocha-code-number: #2f6fad;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
@media (prefers-color-scheme: dark) {
|
|
86
|
+
:root {
|
|
87
|
+
--mocha-color: #fff;
|
|
88
|
+
--mocha-bg-color: #222;
|
|
89
|
+
--mocha-pass-icon-color: #00d6b2;
|
|
90
|
+
--mocha-pass-color: #222;
|
|
91
|
+
--mocha-pass-shadow-color: rgba(255, 255, 255, .2);
|
|
92
|
+
--mocha-pass-mediump-color: #f1be67;
|
|
93
|
+
--mocha-pass-slow-color: #f49896;
|
|
94
|
+
--mocha-test-pending-color: #0b97c4;
|
|
95
|
+
--mocha-test-pending-icon-color: #0b97c4;
|
|
96
|
+
--mocha-test-fail-color: #f44;
|
|
97
|
+
--mocha-test-fail-icon-color: #f44;
|
|
98
|
+
--mocha-test-fail-pre-color: #fff;
|
|
99
|
+
--mocha-test-fail-pre-error-color: #f44;
|
|
100
|
+
--mocha-test-html-error-color: #fff;
|
|
101
|
+
--mocha-box-shadow-color: #444;
|
|
102
|
+
--mocha-box-bottom-color: #555;
|
|
103
|
+
--mocha-test-replay-color: #fff;
|
|
104
|
+
--mocha-test-replay-bg-color: #444;
|
|
105
|
+
--mocha-stats-color: #aaa;
|
|
106
|
+
--mocha-stats-em-color: #fff;
|
|
107
|
+
--mocha-stats-hover-color: #444;
|
|
108
|
+
--mocha-error-color: #f44;
|
|
109
|
+
|
|
110
|
+
--mocha-code-comment: #ddd;
|
|
111
|
+
--mocha-code-init: #9cc7f1;
|
|
112
|
+
--mocha-code-string: #80d4ff;
|
|
113
|
+
--mocha-code-keyword: #e3a470;
|
|
114
|
+
--mocha-code-number: #4ca7ff;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
`
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
// src/vite/utils.ts
|
|
121
|
+
import fs from "node:fs/promises";
|
|
122
|
+
import url from "node:url";
|
|
123
|
+
import path from "node:path";
|
|
124
|
+
import logger from "@wdio/logger";
|
|
125
|
+
import { resolve } from "import-meta-resolve";
|
|
126
|
+
var log = logger("@wdio/browser-runner");
|
|
127
|
+
async function getTemplate(options, env, spec, p = process) {
|
|
128
|
+
const root = options.rootDir || process.cwd();
|
|
129
|
+
const rootFileUrl = url.pathToFileURL(root).href;
|
|
130
|
+
const isHeadless = options.headless || Boolean(process.env.CI);
|
|
131
|
+
const alias = options.viteConfig?.resolve?.alias || {};
|
|
132
|
+
const usesTailwindCSS = await hasFileByExtensions(path.join(root, "tailwind.config"));
|
|
133
|
+
if ("runner" in env.config) {
|
|
134
|
+
delete env.config.runner;
|
|
135
|
+
}
|
|
136
|
+
let sourceMapScript = "";
|
|
137
|
+
let sourceMapSetupCommand = "";
|
|
138
|
+
try {
|
|
139
|
+
const sourceMapSupportDir = await resolve("source-map-support", import.meta.url);
|
|
140
|
+
sourceMapScript = /*html*/
|
|
141
|
+
`<script src="/@fs/${url.fileURLToPath(path.dirname(sourceMapSupportDir))}/browser-source-map-support.js"></script>`;
|
|
142
|
+
sourceMapSetupCommand = "sourceMapSupport.install()";
|
|
143
|
+
} catch (err) {
|
|
144
|
+
log.error(`Failed to setup source-map-support: ${err.message}`);
|
|
145
|
+
}
|
|
146
|
+
const mochaPath = await resolve("mocha", `${rootFileUrl}/node_modules`);
|
|
147
|
+
const mochaCSSHref = path.join(url.fileURLToPath(path.dirname(mochaPath)), "mocha.css");
|
|
148
|
+
const mochaJSSrc = path.join(url.fileURLToPath(path.dirname(mochaPath)), "mocha.js");
|
|
149
|
+
return (
|
|
150
|
+
/* html */
|
|
151
|
+
`
|
|
152
|
+
<!doctype html>
|
|
153
|
+
<html>
|
|
154
|
+
<head>
|
|
155
|
+
<title>WebdriverIO Browser Test</title>
|
|
156
|
+
<link rel="icon" type="image/x-icon" href="https://webdriver.io/img/favicon.png">
|
|
157
|
+
${usesTailwindCSS ? (
|
|
158
|
+
/*html*/
|
|
159
|
+
`<link rel="stylesheet" href="/node_modules/tailwindcss/tailwind.css">`
|
|
160
|
+
) : ""}
|
|
161
|
+
<script type="module">
|
|
162
|
+
const alias = ${JSON.stringify(alias)}
|
|
163
|
+
window.__wdioMockCache__ = new Map()
|
|
164
|
+
window.WDIO_EVENT_NAME = '${WDIO_EVENT_NAME}'
|
|
165
|
+
window.wdioImport = function (modName, mod) {
|
|
166
|
+
/**
|
|
167
|
+
* attempt to resolve direct import
|
|
168
|
+
*/
|
|
169
|
+
if (window.__wdioMockCache__.get(modName)) {
|
|
170
|
+
return window.__wdioMockCache__.get(modName)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* if above fails, check if we have an alias for it
|
|
175
|
+
*/
|
|
176
|
+
for (const [aliasName, aliasPath] of Object.entries(alias)) {
|
|
177
|
+
if (modName.slice(0, aliasName.length) === aliasName) {
|
|
178
|
+
modName = modName.replace(aliasName, aliasPath)
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (window.__wdioMockCache__.get(modName)) {
|
|
182
|
+
return window.__wdioMockCache__.get(modName)
|
|
183
|
+
}
|
|
184
|
+
return mod
|
|
185
|
+
}
|
|
186
|
+
</script>
|
|
187
|
+
<link rel="stylesheet" href="/@fs/${mochaCSSHref}">
|
|
188
|
+
<script type="module" src="/@fs/${mochaJSSrc}"></script>
|
|
189
|
+
${sourceMapScript}
|
|
190
|
+
<script type="module">
|
|
191
|
+
${sourceMapSetupCommand}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Inject environment variables
|
|
195
|
+
*/
|
|
196
|
+
window.__wdioEnv__ = ${JSON.stringify(env)}
|
|
197
|
+
window.__wdioSpec__ = '${spec}'
|
|
198
|
+
window.__wdioEvents__ = []
|
|
199
|
+
/**
|
|
200
|
+
* listen to window errors during bootstrap phase
|
|
201
|
+
*/
|
|
202
|
+
window.__wdioErrors__ = []
|
|
203
|
+
addEventListener('error', (ev) => window.__wdioErrors__.push({
|
|
204
|
+
filename: ev.filename,
|
|
205
|
+
message: ev.message,
|
|
206
|
+
error: ev.error.stack
|
|
207
|
+
}))
|
|
208
|
+
/**
|
|
209
|
+
* mock process
|
|
210
|
+
*/
|
|
211
|
+
window.process = window.process || {
|
|
212
|
+
platform: 'browser',
|
|
213
|
+
env: ${JSON.stringify(p.env)},
|
|
214
|
+
stdout: {},
|
|
215
|
+
stderr: {},
|
|
216
|
+
cwd: () => ${JSON.stringify(p.cwd())},
|
|
217
|
+
}
|
|
218
|
+
</script>
|
|
219
|
+
<script type="module" src="@wdio/browser-runner/setup"></script>
|
|
220
|
+
<style>
|
|
221
|
+
${MOCHA_VARIABELS}
|
|
222
|
+
|
|
223
|
+
body {
|
|
224
|
+
width: calc(100% - 500px);
|
|
225
|
+
padding: 0;
|
|
226
|
+
margin: 0;
|
|
227
|
+
}
|
|
228
|
+
</style>
|
|
229
|
+
</head>
|
|
230
|
+
<body>
|
|
231
|
+
<mocha-framework spec="${spec}" ${isHeadless ? 'style="display: none"' : ""}></mocha-framework>
|
|
232
|
+
</body>
|
|
233
|
+
</html>`
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
async function userfriendlyImport(preset, pkg) {
|
|
237
|
+
if (!pkg) {
|
|
238
|
+
return {};
|
|
239
|
+
}
|
|
240
|
+
try {
|
|
241
|
+
return await import(pkg);
|
|
242
|
+
} catch (err) {
|
|
243
|
+
throw new Error(
|
|
244
|
+
`Couldn't load preset "${preset}" given important dependency ("${pkg}") is not installed.
|
|
245
|
+
Please run:
|
|
246
|
+
|
|
247
|
+
npm install ${pkg}
|
|
248
|
+
or
|
|
249
|
+
yarn add --dev ${pkg}`
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
function normalizeId(id, base) {
|
|
254
|
+
if (base && id.startsWith(base)) {
|
|
255
|
+
id = `/${id.slice(base.length)}`;
|
|
256
|
+
}
|
|
257
|
+
return id.replace(/^\/@id\/__x00__/, "\0").replace(/^\/@id\//, "").replace(/^__vite-browser-external:/, "").replace(/^node:/, "").replace(/[?&]v=\w+/, "?").replace(/\?$/, "");
|
|
258
|
+
}
|
|
259
|
+
async function getFilesFromDirectory(dir) {
|
|
260
|
+
const isExisting = await fs.access(dir).then(() => true, () => false);
|
|
261
|
+
if (!isExisting) {
|
|
262
|
+
return [];
|
|
263
|
+
}
|
|
264
|
+
let files = await fs.readdir(dir);
|
|
265
|
+
files = (await Promise.all(files.map(async (file) => {
|
|
266
|
+
const filePath = path.join(dir, file);
|
|
267
|
+
const stats = await fs.stat(filePath);
|
|
268
|
+
if (stats.isDirectory()) {
|
|
269
|
+
return getFilesFromDirectory(filePath);
|
|
270
|
+
} else if (stats.isFile()) {
|
|
271
|
+
return filePath;
|
|
272
|
+
}
|
|
273
|
+
}))).filter(Boolean);
|
|
274
|
+
return files.reduce((all, folderContents) => all.concat(folderContents), []);
|
|
275
|
+
}
|
|
276
|
+
var mockedModulesList;
|
|
277
|
+
async function getManualMocks(automockDir) {
|
|
278
|
+
if (!mockedModulesList) {
|
|
279
|
+
mockedModulesList = (await getFilesFromDirectory(automockDir)).map((filePath) => [
|
|
280
|
+
filePath,
|
|
281
|
+
filePath.slice(automockDir.length + 1).slice(0, -path.extname(filePath).length)
|
|
282
|
+
]);
|
|
283
|
+
}
|
|
284
|
+
return mockedModulesList;
|
|
285
|
+
}
|
|
286
|
+
var EXTENSION = [".js", ".ts", ".mjs", ".cjs", ".mts"];
|
|
287
|
+
async function hasFileByExtensions(p, extensions = EXTENSION) {
|
|
288
|
+
return (await Promise.all([
|
|
289
|
+
fs.access(p).then(() => p, () => void 0),
|
|
290
|
+
...extensions.map((ext) => fs.access(p + ext).then(() => p + ext, () => void 0))
|
|
291
|
+
])).filter(Boolean)[0];
|
|
292
|
+
}
|
|
293
|
+
function hasDir(p) {
|
|
294
|
+
return fs.stat(p).then((s) => s.isDirectory(), () => false);
|
|
295
|
+
}
|
|
296
|
+
function getErrorTemplate(filename, error) {
|
|
297
|
+
return (
|
|
298
|
+
/*html*/
|
|
299
|
+
`
|
|
300
|
+
<pre>${error.stack}</pre>
|
|
301
|
+
<script type="module">
|
|
302
|
+
window.__wdioErrors__ = [{
|
|
303
|
+
filename: "${filename}",
|
|
304
|
+
message: \`${error.message}\`
|
|
305
|
+
}]
|
|
306
|
+
</script>
|
|
307
|
+
`
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// src/vite/plugins/testrunner.ts
|
|
312
|
+
var log2 = logger2("@wdio/browser-runner:plugin");
|
|
313
|
+
var __dirname = url2.fileURLToPath(new URL(".", import.meta.url));
|
|
314
|
+
var commands = deepmerge(
|
|
315
|
+
WebDriverProtocol,
|
|
316
|
+
MJsonWProtocol,
|
|
317
|
+
AppiumProtocol,
|
|
318
|
+
ChromiumProtocol,
|
|
319
|
+
SauceLabsProtocol,
|
|
320
|
+
SeleniumProtocol,
|
|
321
|
+
GeckoProtocol
|
|
322
|
+
);
|
|
323
|
+
var protocolCommandList = Object.values(commands).map(
|
|
324
|
+
(endpoint) => Object.values(endpoint).map(
|
|
325
|
+
({ command }) => command
|
|
326
|
+
)
|
|
327
|
+
).flat();
|
|
328
|
+
var WDIO_PACKAGES = ["webdriverio", "expect-webdriverio"];
|
|
329
|
+
var virtualModuleId = "virtual:wdio";
|
|
330
|
+
var resolvedVirtualModuleId = "\0" + virtualModuleId;
|
|
331
|
+
var MODULES_TO_MOCK = [
|
|
332
|
+
"import-meta-resolve",
|
|
333
|
+
"puppeteer-core",
|
|
334
|
+
"archiver",
|
|
335
|
+
"glob",
|
|
336
|
+
"ws",
|
|
337
|
+
"decamelize",
|
|
338
|
+
"geckodriver",
|
|
339
|
+
"safaridriver",
|
|
340
|
+
"edgedriver",
|
|
341
|
+
"@puppeteer/browsers",
|
|
342
|
+
"locate-app",
|
|
343
|
+
"wait-port",
|
|
344
|
+
"lodash.isequal",
|
|
345
|
+
"@wdio/repl"
|
|
346
|
+
];
|
|
347
|
+
var POLYFILLS = [
|
|
348
|
+
...builtinModules,
|
|
349
|
+
...builtinModules.map((m) => `node:${m}`)
|
|
350
|
+
];
|
|
351
|
+
function testrunner(options) {
|
|
352
|
+
const browserModules = path2.resolve(__dirname, "browser");
|
|
353
|
+
const automationProtocolPath = `/@fs${url2.pathToFileURL(path2.resolve(browserModules, "driver.js")).pathname}`;
|
|
354
|
+
const mockModulePath = path2.resolve(browserModules, "mock.js");
|
|
355
|
+
const setupModulePath = path2.resolve(browserModules, "setup.js");
|
|
356
|
+
const spyModulePath = path2.resolve(browserModules, "spy.js");
|
|
357
|
+
const wdioExpectModulePath = path2.resolve(browserModules, "expect.js");
|
|
358
|
+
return [{
|
|
359
|
+
name: "wdio:testrunner",
|
|
360
|
+
enforce: "pre",
|
|
361
|
+
resolveId: async (id) => {
|
|
362
|
+
if (id === virtualModuleId) {
|
|
363
|
+
return resolvedVirtualModuleId;
|
|
364
|
+
}
|
|
365
|
+
if (POLYFILLS.includes(id)) {
|
|
366
|
+
return polyfillPath(normalizeId(id.replace("/promises", "")));
|
|
367
|
+
}
|
|
368
|
+
if (id === "@wdio/browser-runner") {
|
|
369
|
+
return spyModulePath;
|
|
370
|
+
}
|
|
371
|
+
if (id.endsWith("@wdio/browser-runner/setup")) {
|
|
372
|
+
return setupModulePath;
|
|
373
|
+
}
|
|
374
|
+
if (id === "expect-webdriverio") {
|
|
375
|
+
return wdioExpectModulePath;
|
|
376
|
+
}
|
|
377
|
+
if (id === "@wdio/logger") {
|
|
378
|
+
const newId = url2.fileURLToPath(await resolve2(id, import.meta.url));
|
|
379
|
+
return path2.resolve(path2.dirname(newId), "browser.js");
|
|
380
|
+
}
|
|
381
|
+
if (id.startsWith("@wdio") || WDIO_PACKAGES.includes(id)) {
|
|
382
|
+
return url2.fileURLToPath(await resolve2(id, import.meta.url));
|
|
383
|
+
}
|
|
384
|
+
if (MODULES_TO_MOCK.includes(id)) {
|
|
385
|
+
return mockModulePath;
|
|
386
|
+
}
|
|
387
|
+
},
|
|
388
|
+
load(id) {
|
|
389
|
+
if (id === resolvedVirtualModuleId) {
|
|
390
|
+
return (
|
|
391
|
+
/*js*/
|
|
392
|
+
`
|
|
393
|
+
import { fn } from '@wdio/browser-runner'
|
|
394
|
+
export const commands = ${JSON.stringify(protocolCommandList)}
|
|
395
|
+
export const automationProtocolPath = ${JSON.stringify(automationProtocolPath)}
|
|
396
|
+
export const wrappedFn = (...args) => fn()(...args)
|
|
397
|
+
`
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
},
|
|
401
|
+
transform(code, id) {
|
|
402
|
+
if (id.includes(".vite/deps/expect.js")) {
|
|
403
|
+
return {
|
|
404
|
+
code: code.replace(
|
|
405
|
+
"var fs = _interopRequireWildcard(require_graceful_fs());",
|
|
406
|
+
"var fs = {};"
|
|
407
|
+
).replace(
|
|
408
|
+
"var expect_default = require_build11();",
|
|
409
|
+
"var expect_default = require_build11();\nwindow.expect = expect_default.default;"
|
|
410
|
+
).replace(
|
|
411
|
+
"process.stdout.isTTY",
|
|
412
|
+
"false"
|
|
413
|
+
)
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
return { code };
|
|
417
|
+
},
|
|
418
|
+
configureServer(server) {
|
|
419
|
+
return () => {
|
|
420
|
+
server.middlewares.use(async (req, res, next) => {
|
|
421
|
+
log2.info(`Received request for: ${req.originalUrl}`);
|
|
422
|
+
if (!req.originalUrl || req.url?.endsWith(".map") || req.url?.endsWith(".wasm")) {
|
|
423
|
+
return next();
|
|
424
|
+
}
|
|
425
|
+
const cookies = (req.headers.cookie && req.headers.cookie.split(";") || []).map((c) => c.trim());
|
|
426
|
+
const urlParsed = url2.parse(req.originalUrl);
|
|
427
|
+
const urlParamString = new URLSearchParams(urlParsed.query || "");
|
|
428
|
+
const cid = urlParamString.get("cid") || cookies.find((c) => c.includes("WDIO_CID"))?.split("=").pop();
|
|
429
|
+
const spec = urlParamString.get("spec") || cookies.find((c) => c.includes("WDIO_SPEC"))?.split("=").pop();
|
|
430
|
+
if (!cid || !SESSIONS.has(cid)) {
|
|
431
|
+
log2.error(`No environment found for ${cid || "non determined environment"}`);
|
|
432
|
+
return next();
|
|
433
|
+
}
|
|
434
|
+
if (!spec) {
|
|
435
|
+
log2.error("No spec file was defined to run for this environment");
|
|
436
|
+
return next();
|
|
437
|
+
}
|
|
438
|
+
const env = SESSIONS.get(cid);
|
|
439
|
+
try {
|
|
440
|
+
const template = await getTemplate(options, env, spec);
|
|
441
|
+
log2.debug(`Render template for ${req.originalUrl}`);
|
|
442
|
+
res.end(await server.transformIndexHtml(`${req.originalUrl}`, template));
|
|
443
|
+
} catch (err) {
|
|
444
|
+
const template = getErrorTemplate(req.originalUrl, err);
|
|
445
|
+
log2.error(`Failed to render template: ${err.message}`);
|
|
446
|
+
res.end(await server.transformIndexHtml(`${req.originalUrl}`, template));
|
|
447
|
+
}
|
|
448
|
+
return next();
|
|
449
|
+
});
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
}, {
|
|
453
|
+
name: "modern-node-polyfills",
|
|
454
|
+
async resolveId(id, _, ctx) {
|
|
455
|
+
if (ctx.ssr || !builtinModules.includes(id)) {
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
id = normalizeId(id);
|
|
459
|
+
return { id: await polyfillPath(id), moduleSideEffects: false };
|
|
460
|
+
}
|
|
461
|
+
}];
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// src/vite/plugins/mockHoisting.ts
|
|
465
|
+
import os from "node:os";
|
|
466
|
+
import url3 from "node:url";
|
|
467
|
+
import path3 from "node:path";
|
|
468
|
+
import fs2 from "node:fs/promises";
|
|
469
|
+
import logger3 from "@wdio/logger";
|
|
470
|
+
import { parse, print, visit, types } from "recast";
|
|
471
|
+
import typescriptParser from "recast/parsers/typescript.js";
|
|
472
|
+
var log3 = logger3("@wdio/browser-runner:mockHoisting");
|
|
473
|
+
var INTERNALS_TO_IGNORE = [
|
|
474
|
+
"@vite/client",
|
|
475
|
+
"vite/dist/client",
|
|
476
|
+
"/webdriverio/build/",
|
|
477
|
+
"/@wdio/",
|
|
478
|
+
"/webdriverio/node_modules/",
|
|
479
|
+
"virtual:wdio",
|
|
480
|
+
"?html-proxy",
|
|
481
|
+
"/__fixtures__/",
|
|
482
|
+
"/__mocks__/",
|
|
483
|
+
"/.vite/deps/@testing-library_vue.js"
|
|
484
|
+
];
|
|
485
|
+
var b = types.builders;
|
|
486
|
+
var MOCK_PREFIX = "/@mock";
|
|
487
|
+
function mockHoisting(mockHandler) {
|
|
488
|
+
let spec = null;
|
|
489
|
+
let isTestDependency = false;
|
|
490
|
+
const sessionMocks = /* @__PURE__ */ new Set();
|
|
491
|
+
const importMap = /* @__PURE__ */ new Map();
|
|
492
|
+
return [{
|
|
493
|
+
name: "wdio:mockHoisting:pre",
|
|
494
|
+
enforce: "pre",
|
|
495
|
+
resolveId: mockHandler.resolveId.bind(mockHandler),
|
|
496
|
+
load: async function(id) {
|
|
497
|
+
if (id.startsWith(MOCK_PREFIX)) {
|
|
498
|
+
try {
|
|
499
|
+
const orig = await fs2.readFile(id.slice(MOCK_PREFIX.length + (os.platform() === "win32" ? 1 : 0)));
|
|
500
|
+
return orig.toString();
|
|
501
|
+
} catch (err) {
|
|
502
|
+
log3.error(`Failed to read file (${id}) for mocking: ${err.message}`);
|
|
503
|
+
return "";
|
|
31
504
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}, {
|
|
508
|
+
name: "wdio:mockHoisting",
|
|
509
|
+
enforce: "post",
|
|
510
|
+
transform(code, id) {
|
|
511
|
+
const isSpecFile = id === spec;
|
|
512
|
+
if (isSpecFile) {
|
|
513
|
+
isTestDependency = true;
|
|
514
|
+
}
|
|
515
|
+
if (
|
|
516
|
+
// where loading was inititated through the test file
|
|
517
|
+
!isTestDependency && // however when files are inlined they will be loaded when parsing file under test
|
|
518
|
+
// in this case we want to transform them, but make sure we exclude these paths
|
|
519
|
+
(id.includes("/node_modules/") || id.includes("/?cid=") || id.startsWith("virtual:")) || // are not Vite or WebdriverIO internals
|
|
520
|
+
INTERNALS_TO_IGNORE.find((f) => id.includes(f)) || // when the spec file is actually mocking any dependencies
|
|
521
|
+
!isSpecFile && sessionMocks.size === 0
|
|
522
|
+
) {
|
|
523
|
+
return { code };
|
|
524
|
+
}
|
|
525
|
+
let ast;
|
|
526
|
+
const start = Date.now();
|
|
527
|
+
try {
|
|
528
|
+
ast = parse(code, {
|
|
529
|
+
parser: typescriptParser,
|
|
530
|
+
sourceFileName: id,
|
|
531
|
+
sourceRoot: path3.dirname(id)
|
|
532
|
+
});
|
|
533
|
+
log3.trace(`Parsed file for mocking: ${id} in ${Date.now() - start}ms`);
|
|
534
|
+
} catch (err) {
|
|
535
|
+
return { code };
|
|
536
|
+
}
|
|
537
|
+
let importIndex = 0;
|
|
538
|
+
let mockFunctionName;
|
|
539
|
+
let unmockFunctionName;
|
|
540
|
+
const mockCalls = [];
|
|
541
|
+
visit(ast, {
|
|
37
542
|
/**
|
|
38
|
-
*
|
|
543
|
+
* find function name for mock and unmock calls
|
|
39
544
|
*/
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
545
|
+
visitImportDeclaration: function(path10) {
|
|
546
|
+
const dec = path10.value;
|
|
547
|
+
const source = dec.source.value;
|
|
548
|
+
if (!dec.specifiers || dec.specifiers.length === 0 || source !== "@wdio/browser-runner") {
|
|
549
|
+
return this.traverse(path10);
|
|
550
|
+
}
|
|
551
|
+
const mockSpecifier = dec.specifiers.filter((s) => s.type === types.namedTypes.ImportSpecifier.toString()).find((s) => s.imported.name === "mock");
|
|
552
|
+
if (mockSpecifier && mockSpecifier.local) {
|
|
553
|
+
mockFunctionName = mockSpecifier.local.name;
|
|
554
|
+
}
|
|
555
|
+
const unmockSpecifier = dec.specifiers.filter((s) => s.type === types.namedTypes.ImportSpecifier.toString()).find((s) => s.imported.name === "unmock");
|
|
556
|
+
if (unmockSpecifier && unmockSpecifier.local) {
|
|
557
|
+
unmockFunctionName = unmockSpecifier.local.name;
|
|
558
|
+
}
|
|
559
|
+
mockCalls.push(dec);
|
|
560
|
+
path10.prune();
|
|
561
|
+
return this.traverse(path10);
|
|
562
|
+
},
|
|
563
|
+
/**
|
|
564
|
+
* detect which modules are supposed to be mocked
|
|
565
|
+
*/
|
|
566
|
+
...isSpecFile ? {
|
|
567
|
+
visitExpressionStatement: function(path10) {
|
|
568
|
+
const exp = path10.value;
|
|
569
|
+
if (exp.expression.type !== types.namedTypes.CallExpression.toString()) {
|
|
570
|
+
return this.traverse(path10);
|
|
60
571
|
}
|
|
61
|
-
|
|
572
|
+
const callExp = exp.expression;
|
|
573
|
+
const isUnmockCall = unmockFunctionName && callExp.callee.name === unmockFunctionName;
|
|
574
|
+
const isMockCall = mockFunctionName && callExp.callee.name === mockFunctionName;
|
|
575
|
+
if (!isMockCall && !isUnmockCall) {
|
|
576
|
+
return this.traverse(path10);
|
|
577
|
+
}
|
|
578
|
+
if (isUnmockCall && callExp.arguments[0] && typeof callExp.arguments[0].value === "string") {
|
|
579
|
+
mockHandler.unmock(callExp.arguments[0].value);
|
|
580
|
+
} else if (isMockCall) {
|
|
581
|
+
const mockCall = exp.expression;
|
|
582
|
+
if (mockCall.arguments.length === 1) {
|
|
583
|
+
mockHandler.manualMocks.push(mockCall.arguments[0].value);
|
|
584
|
+
} else {
|
|
585
|
+
if (exp.expression.arguments.length) {
|
|
586
|
+
sessionMocks.add(exp.expression.arguments[0].value);
|
|
587
|
+
}
|
|
588
|
+
mockCalls.push(exp);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
path10.prune();
|
|
592
|
+
this.traverse(path10);
|
|
593
|
+
}
|
|
594
|
+
} : {}
|
|
595
|
+
});
|
|
596
|
+
visit(ast, {
|
|
62
597
|
/**
|
|
63
|
-
*
|
|
598
|
+
* rewrite import statements
|
|
64
599
|
*/
|
|
65
|
-
|
|
66
|
-
|
|
600
|
+
visitImportDeclaration: function(nodePath) {
|
|
601
|
+
const dec = nodePath.value;
|
|
602
|
+
const source = dec.source.value;
|
|
603
|
+
if (!dec.specifiers || dec.specifiers.length === 0) {
|
|
604
|
+
return this.traverse(nodePath);
|
|
605
|
+
}
|
|
606
|
+
const newImportIdentifier = `__wdio_import${importIndex++}`;
|
|
607
|
+
const isMockedModule = Boolean(
|
|
608
|
+
// matches if a dependency is mocked
|
|
609
|
+
sessionMocks.has(source) || // matches if a relative file is mocked
|
|
610
|
+
source.startsWith(".") && [...sessionMocks.values()].find((m) => {
|
|
611
|
+
const fileImportPath = path3.resolve(path3.dirname(id), source);
|
|
612
|
+
const fileImportPathSliced = fileImportPath.slice(0, path3.extname(fileImportPath).length * -1);
|
|
613
|
+
const testMockPath = path3.resolve(path3.dirname(spec || "/"), m);
|
|
614
|
+
const testMockPathSliced = testMockPath.slice(0, path3.extname(testMockPath).length * -1);
|
|
615
|
+
return fileImportPathSliced === testMockPathSliced && fileImportPathSliced.length > 0;
|
|
616
|
+
})
|
|
617
|
+
);
|
|
618
|
+
if (isMockedModule && isSpecFile) {
|
|
619
|
+
importMap.set(source, newImportIdentifier);
|
|
620
|
+
}
|
|
621
|
+
if (!isSpecFile || isMockedModule) {
|
|
622
|
+
const newNode = b.importDeclaration(
|
|
623
|
+
[b.importNamespaceSpecifier(b.identifier(newImportIdentifier))],
|
|
624
|
+
b.literal(source)
|
|
625
|
+
);
|
|
626
|
+
mockCalls.unshift(newNode);
|
|
627
|
+
}
|
|
628
|
+
const mockExtensionLengh = path3.extname(source).length * -1 || Infinity;
|
|
629
|
+
const wdioImportModuleIdentifier = source.startsWith(".") || source.startsWith("/") ? url3.pathToFileURL(path3.resolve(path3.dirname(id), source).slice(0, mockExtensionLengh)).pathname : source;
|
|
630
|
+
const isNamespaceImport = dec.specifiers.length === 1 && dec.specifiers[0].type === types.namedTypes.ImportNamespaceSpecifier.toString();
|
|
631
|
+
const mockImport = isSpecFile && !isMockedModule ? b.variableDeclaration("const", [
|
|
632
|
+
b.variableDeclarator(
|
|
633
|
+
isNamespaceImport ? dec.specifiers[0].local : b.objectPattern(dec.specifiers.map((s) => {
|
|
634
|
+
if (s.type === types.namedTypes.ImportDefaultSpecifier.toString()) {
|
|
635
|
+
return b.property("init", b.identifier("default"), b.identifier(s.local.name));
|
|
636
|
+
}
|
|
637
|
+
return b.property("init", b.identifier(s.imported.name), b.identifier(s.local.name));
|
|
638
|
+
})),
|
|
639
|
+
b.awaitExpression(b.importExpression(b.literal(source)))
|
|
640
|
+
)
|
|
641
|
+
]) : b.variableDeclaration("const", [
|
|
642
|
+
b.variableDeclarator(
|
|
643
|
+
dec.specifiers.length === 1 && dec.specifiers[0].type === types.namedTypes.ImportNamespaceSpecifier.toString() ? b.identifier(dec.specifiers[0].local.name) : b.objectPattern(dec.specifiers.map((s) => {
|
|
644
|
+
if (s.type === types.namedTypes.ImportDefaultSpecifier.toString()) {
|
|
645
|
+
return b.property("init", b.identifier("default"), b.identifier(s.local.name));
|
|
646
|
+
}
|
|
647
|
+
return b.property("init", b.identifier(s.imported.name), b.identifier(s.local.name));
|
|
648
|
+
})),
|
|
649
|
+
b.callExpression(
|
|
650
|
+
b.identifier("wdioImport"),
|
|
651
|
+
[
|
|
652
|
+
b.literal(wdioImportModuleIdentifier),
|
|
653
|
+
b.identifier(newImportIdentifier)
|
|
654
|
+
]
|
|
655
|
+
)
|
|
656
|
+
)
|
|
657
|
+
]);
|
|
658
|
+
nodePath.replace(mockImport);
|
|
659
|
+
this.traverse(nodePath);
|
|
67
660
|
}
|
|
68
|
-
|
|
69
|
-
|
|
661
|
+
});
|
|
662
|
+
ast.program.body.unshift(...mockCalls.map((mc) => {
|
|
663
|
+
const exp = mc;
|
|
664
|
+
if (exp.expression && exp.expression.type === types.namedTypes.CallExpression.toString()) {
|
|
665
|
+
const mockCallExpression = exp.expression;
|
|
666
|
+
const mockedModule = mockCallExpression.arguments[0].value;
|
|
667
|
+
const mockFactory = mockCallExpression.arguments[1];
|
|
668
|
+
if (importMap.has(mockedModule)) {
|
|
669
|
+
mockCallExpression.arguments.push(b.identifier(importMap.get(mockedModule)));
|
|
670
|
+
} else if (mockFactory.params.length > 0) {
|
|
671
|
+
const newImportIdentifier = `__wdio_import${importIndex++}`;
|
|
672
|
+
ast.program.body.unshift(b.importDeclaration(
|
|
673
|
+
[b.importNamespaceSpecifier(b.identifier(newImportIdentifier))],
|
|
674
|
+
b.literal(mockedModule)
|
|
675
|
+
));
|
|
676
|
+
mockCallExpression.arguments.push(b.identifier(newImportIdentifier));
|
|
677
|
+
}
|
|
678
|
+
return b.expressionStatement(b.awaitExpression(mockCallExpression));
|
|
70
679
|
}
|
|
71
|
-
|
|
680
|
+
return mc;
|
|
681
|
+
}));
|
|
682
|
+
try {
|
|
683
|
+
const newCode = print(ast, { sourceMapName: id });
|
|
684
|
+
log3.trace(`Transformed file for mocking: ${id} in ${Date.now() - start}ms`);
|
|
685
|
+
return newCode;
|
|
686
|
+
} catch (err) {
|
|
687
|
+
log3.trace(`Failed to transformed file (${id}) for mocking: ${err.stack}`);
|
|
688
|
+
return { code };
|
|
689
|
+
}
|
|
690
|
+
},
|
|
691
|
+
configureServer(server) {
|
|
692
|
+
return () => {
|
|
693
|
+
server.middlewares.use("/", async (req, res, next) => {
|
|
694
|
+
if (!req.originalUrl) {
|
|
695
|
+
return next();
|
|
696
|
+
}
|
|
697
|
+
const urlParsed = url3.parse(req.originalUrl);
|
|
698
|
+
const urlParamString = new URLSearchParams(urlParsed.query || "");
|
|
699
|
+
const specParam = urlParamString.get("spec");
|
|
700
|
+
if (specParam) {
|
|
701
|
+
mockHandler.resetMocks();
|
|
702
|
+
isTestDependency = false;
|
|
703
|
+
spec = os.platform() === "win32" ? specParam.slice(1) : specParam;
|
|
704
|
+
}
|
|
705
|
+
return next();
|
|
706
|
+
});
|
|
707
|
+
};
|
|
72
708
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
709
|
+
}];
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// src/vite/plugins/worker.ts
|
|
713
|
+
function workerPlugin(onSocketEvent) {
|
|
714
|
+
return {
|
|
715
|
+
name: "wdio:worker",
|
|
716
|
+
configureServer({ ws }) {
|
|
717
|
+
ws.on(WDIO_EVENT_NAME, onSocketEvent);
|
|
718
|
+
}
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// src/vite/mock.ts
|
|
723
|
+
import path4 from "node:path";
|
|
724
|
+
var FIXTURE_PREFIX = "/@fixture/";
|
|
725
|
+
var MockHandler = class {
|
|
726
|
+
#automock;
|
|
727
|
+
#automockDir;
|
|
728
|
+
#manualMocksList;
|
|
729
|
+
#mocks = /* @__PURE__ */ new Map();
|
|
730
|
+
#unmocked = [];
|
|
731
|
+
manualMocks = [];
|
|
732
|
+
constructor(options, config) {
|
|
733
|
+
this.#automock = typeof options.automock === "boolean" ? options.automock : DEFAULT_AUTOMOCK;
|
|
734
|
+
this.#automockDir = path4.resolve(config.rootDir, options.automockDir || DEFAULT_MOCK_DIRECTORY);
|
|
735
|
+
this.#manualMocksList = getManualMocks(this.#automockDir);
|
|
736
|
+
}
|
|
737
|
+
get mocks() {
|
|
738
|
+
return this.#mocks;
|
|
739
|
+
}
|
|
740
|
+
unmock(moduleName) {
|
|
741
|
+
this.#unmocked.push(moduleName);
|
|
742
|
+
}
|
|
743
|
+
async resolveId(id) {
|
|
744
|
+
const manualMocksList = await this.#manualMocksList;
|
|
745
|
+
const mockPath = manualMocksList.find((m) => (
|
|
746
|
+
// e.g. someModule
|
|
747
|
+
id === m[1].replace(path4.sep, "/") || // e.g. @some/module
|
|
748
|
+
id.slice(1) === m[1].replace(path4.sep, "/")
|
|
749
|
+
));
|
|
750
|
+
if ((this.manualMocks.includes(id) || this.#automock) && mockPath && !this.#unmocked.includes(id)) {
|
|
751
|
+
return mockPath[0];
|
|
752
|
+
}
|
|
753
|
+
if (id.startsWith(FIXTURE_PREFIX)) {
|
|
754
|
+
return path4.resolve(this.#automockDir, id.slice(FIXTURE_PREFIX.length));
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
/**
|
|
758
|
+
* reset manual mocks between tests
|
|
759
|
+
*/
|
|
760
|
+
resetMocks() {
|
|
761
|
+
this.manualMocks = [];
|
|
762
|
+
this.#unmocked = [];
|
|
763
|
+
}
|
|
764
|
+
};
|
|
765
|
+
|
|
766
|
+
// src/vite/constants.ts
|
|
767
|
+
import topLevelAwait from "vite-plugin-top-level-await";
|
|
768
|
+
import { esbuildCommonjs } from "@originjs/vite-plugin-commonjs";
|
|
769
|
+
|
|
770
|
+
// src/vite/plugins/esbuild.ts
|
|
771
|
+
import fs3 from "node:fs/promises";
|
|
772
|
+
function codeFrameFix() {
|
|
773
|
+
return {
|
|
774
|
+
name: "wdio:codeFrameFix",
|
|
775
|
+
setup(build) {
|
|
776
|
+
build.onLoad(
|
|
777
|
+
{ filter: /@babel\/code-frame/, namespace: "file" },
|
|
778
|
+
/**
|
|
779
|
+
* mock @babel/code-frame as it fails in Safari due
|
|
780
|
+
* to usage of chalk
|
|
781
|
+
*/
|
|
782
|
+
async ({ path: id }) => {
|
|
783
|
+
const code = await fs3.readFile(id).then(
|
|
784
|
+
(buf) => buf.toString(),
|
|
785
|
+
() => void 0
|
|
786
|
+
);
|
|
787
|
+
if (!code) {
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
790
|
+
return {
|
|
791
|
+
contents: code.replace(
|
|
792
|
+
'require("@babel/highlight");',
|
|
793
|
+
/*js*/
|
|
794
|
+
`{
|
|
795
|
+
shouldHighlight: false,
|
|
796
|
+
reset: () => {}
|
|
797
|
+
}`
|
|
798
|
+
)
|
|
799
|
+
};
|
|
87
800
|
}
|
|
88
|
-
|
|
89
|
-
this.#communicator.register(server, worker);
|
|
90
|
-
return worker;
|
|
801
|
+
);
|
|
91
802
|
}
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// src/vite/constants.ts
|
|
807
|
+
var DEFAULT_PROTOCOL = "http";
|
|
808
|
+
var DEFAULT_HOSTNAME = "localhost";
|
|
809
|
+
var DEFAULT_HOST = `${DEFAULT_PROTOCOL}://${DEFAULT_HOSTNAME}`;
|
|
810
|
+
var PRESET_DEPENDENCIES = {
|
|
811
|
+
react: ["@vitejs/plugin-react", "default", {
|
|
812
|
+
babel: {
|
|
813
|
+
assumptions: {
|
|
814
|
+
setPublicClassFields: true
|
|
815
|
+
},
|
|
816
|
+
parserOpts: {
|
|
817
|
+
plugins: ["decorators-legacy", "classProperties"]
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
}],
|
|
821
|
+
preact: ["@preact/preset-vite", "default", void 0],
|
|
822
|
+
vue: ["@vitejs/plugin-vue", "default", void 0],
|
|
823
|
+
svelte: ["@sveltejs/vite-plugin-svelte", "svelte", void 0],
|
|
824
|
+
solid: ["vite-plugin-solid", "default", void 0],
|
|
825
|
+
stencil: void 0,
|
|
826
|
+
lit: void 0
|
|
827
|
+
};
|
|
828
|
+
var DEFAULT_VITE_CONFIG = {
|
|
829
|
+
configFile: false,
|
|
830
|
+
server: { host: DEFAULT_HOSTNAME },
|
|
831
|
+
logLevel: "silent",
|
|
832
|
+
plugins: [topLevelAwait()],
|
|
833
|
+
build: {
|
|
834
|
+
sourcemap: "inline",
|
|
835
|
+
commonjsOptions: {
|
|
836
|
+
include: [/node_modules/]
|
|
837
|
+
}
|
|
838
|
+
},
|
|
839
|
+
optimizeDeps: {
|
|
92
840
|
/**
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
* @return {Promise} resolves when vite server has been shutdown
|
|
841
|
+
* the following deps are CJS packages and need to be optimized (compiled to ESM) by Vite
|
|
96
842
|
*/
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
843
|
+
include: [
|
|
844
|
+
"expect",
|
|
845
|
+
"minimatch",
|
|
846
|
+
"css-shorthand-properties",
|
|
847
|
+
"lodash.merge",
|
|
848
|
+
"lodash.zip",
|
|
849
|
+
"ws",
|
|
850
|
+
"lodash.clonedeep",
|
|
851
|
+
"lodash.pickby",
|
|
852
|
+
"lodash.flattendeep",
|
|
853
|
+
"aria-query",
|
|
854
|
+
"grapheme-splitter",
|
|
855
|
+
"css-value",
|
|
856
|
+
"rgb2hex",
|
|
857
|
+
"p-iteration",
|
|
858
|
+
"deepmerge-ts",
|
|
859
|
+
"jest-util",
|
|
860
|
+
"jest-matcher-utils",
|
|
861
|
+
"split2"
|
|
862
|
+
],
|
|
863
|
+
esbuildOptions: {
|
|
864
|
+
logLevel: "silent",
|
|
865
|
+
// Node.js global to browser globalThis
|
|
866
|
+
define: {
|
|
867
|
+
global: "globalThis"
|
|
868
|
+
},
|
|
869
|
+
// Enable esbuild polyfill plugins
|
|
870
|
+
plugins: [
|
|
871
|
+
esbuildCommonjs(["@testing-library/vue"]),
|
|
872
|
+
codeFrameFix()
|
|
873
|
+
]
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
};
|
|
877
|
+
|
|
878
|
+
// src/vite/server.ts
|
|
879
|
+
var log4 = logger4("@wdio/browser-runner:ViteServer");
|
|
880
|
+
var DEFAULT_CONFIG_ENV = {
|
|
881
|
+
command: "serve",
|
|
882
|
+
mode: process.env.NODE_ENV === "production" ? "production" : "development"
|
|
883
|
+
};
|
|
884
|
+
var ViteServer = class extends EventEmitter {
|
|
885
|
+
#options;
|
|
886
|
+
#config;
|
|
887
|
+
#viteConfig;
|
|
888
|
+
#server;
|
|
889
|
+
#mockHandler;
|
|
890
|
+
#socketEventHandler = [];
|
|
891
|
+
get config() {
|
|
892
|
+
return this.#viteConfig;
|
|
893
|
+
}
|
|
894
|
+
constructor(options, config, optimizations) {
|
|
895
|
+
super();
|
|
896
|
+
this.#options = options;
|
|
897
|
+
this.#config = config;
|
|
898
|
+
this.#mockHandler = new MockHandler(options, config);
|
|
899
|
+
const root = options.rootDir || config.rootDir || process.cwd();
|
|
900
|
+
this.#viteConfig = deepmerge2(DEFAULT_VITE_CONFIG, optimizations, {
|
|
901
|
+
root,
|
|
902
|
+
plugins: [
|
|
903
|
+
testrunner(options),
|
|
904
|
+
mockHoisting(this.#mockHandler),
|
|
905
|
+
workerPlugin((payload, client) => this.#socketEventHandler.forEach(
|
|
906
|
+
(handler) => handler(payload, client)
|
|
907
|
+
))
|
|
908
|
+
]
|
|
909
|
+
});
|
|
910
|
+
if (options.coverage && options.coverage.enabled) {
|
|
911
|
+
log4.info("Capturing test coverage enabled");
|
|
912
|
+
const plugin = istanbulPlugin;
|
|
913
|
+
this.#viteConfig.plugins?.push(plugin({
|
|
914
|
+
cwd: config.rootDir,
|
|
915
|
+
include: DEFAULT_INCLUDE,
|
|
916
|
+
extension: DEFAULT_FILE_EXTENSIONS,
|
|
917
|
+
forceBuildInstrument: true,
|
|
918
|
+
...options.coverage
|
|
919
|
+
}));
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
onBrowserEvent(handler) {
|
|
923
|
+
this.#socketEventHandler.push(handler);
|
|
924
|
+
}
|
|
925
|
+
async start() {
|
|
926
|
+
const vitePort = await getPort();
|
|
927
|
+
this.#viteConfig = deepmerge2(this.#viteConfig, {
|
|
928
|
+
server: {
|
|
929
|
+
...this.#viteConfig.server,
|
|
930
|
+
port: vitePort
|
|
931
|
+
}
|
|
932
|
+
});
|
|
933
|
+
if (this.#options.preset) {
|
|
934
|
+
const [pkg, importProp, opts] = PRESET_DEPENDENCIES[this.#options.preset] || [];
|
|
935
|
+
const plugin = (await userfriendlyImport(this.#options.preset, pkg))[importProp || "default"];
|
|
936
|
+
if (plugin) {
|
|
937
|
+
this.#viteConfig.plugins.push(plugin(opts));
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
if (this.#options.viteConfig) {
|
|
941
|
+
const { plugins, ...configToMerge } = typeof this.#options.viteConfig === "string" ? (await import(path5.resolve(this.#config.rootDir || process.cwd(), this.#options.viteConfig))).default : typeof this.#options.viteConfig === "function" ? await this.#options.viteConfig(DEFAULT_CONFIG_ENV) : this.#options.viteConfig;
|
|
942
|
+
this.#viteConfig = deepmerge2(this.#viteConfig, configToMerge);
|
|
943
|
+
this.#viteConfig.plugins = [...plugins || [], ...this.#viteConfig.plugins];
|
|
944
|
+
}
|
|
945
|
+
this.#server = await createServer(this.#viteConfig);
|
|
946
|
+
await this.#server.listen();
|
|
947
|
+
log4.info(`Vite server started successfully on port ${vitePort}, root directory: ${this.#viteConfig.root}`);
|
|
948
|
+
}
|
|
949
|
+
async close() {
|
|
950
|
+
await this.#server?.close();
|
|
951
|
+
}
|
|
952
|
+
};
|
|
953
|
+
|
|
954
|
+
// src/vite/frameworks/nuxt.ts
|
|
955
|
+
import url4 from "node:url";
|
|
956
|
+
import path6 from "node:path";
|
|
957
|
+
import logger5 from "@wdio/logger";
|
|
958
|
+
import { resolve as resolve3 } from "import-meta-resolve";
|
|
959
|
+
var log5 = logger5("@wdio/browser-runner:NuxtOptimization");
|
|
960
|
+
async function isNuxtFramework(rootDir) {
|
|
961
|
+
return (await Promise.all([
|
|
962
|
+
hasFileByExtensions(path6.join(rootDir, "nuxt.config")),
|
|
963
|
+
hasDir(path6.join(rootDir, ".nuxt"))
|
|
964
|
+
])).filter(Boolean).length > 0;
|
|
965
|
+
}
|
|
966
|
+
async function optimizeForNuxt(options, config) {
|
|
967
|
+
const Unimport = (await import("unimport/unplugin")).default;
|
|
968
|
+
const { scanDirExports, scanExports } = await import("unimport");
|
|
969
|
+
const { loadNuxtConfig } = await import("@nuxt/kit");
|
|
970
|
+
const rootDir = config.rootDir || process.cwd();
|
|
971
|
+
const nuxtOptions = await loadNuxtConfig({ rootDir });
|
|
972
|
+
if (nuxtOptions.imports?.autoImport === false) {
|
|
973
|
+
return {};
|
|
974
|
+
}
|
|
975
|
+
const nuxtDepPath = await resolve3("nuxt", import.meta.url);
|
|
976
|
+
const composablesDirs = [];
|
|
977
|
+
for (const layer of nuxtOptions._layers) {
|
|
978
|
+
composablesDirs.push(path6.resolve(layer.config.srcDir, "composables"));
|
|
979
|
+
composablesDirs.push(path6.resolve(layer.config.srcDir, "utils"));
|
|
980
|
+
for (const dir of layer.config.imports?.dirs ?? []) {
|
|
981
|
+
if (!dir) {
|
|
982
|
+
continue;
|
|
983
|
+
}
|
|
984
|
+
composablesDirs.push(path6.resolve(layer.config.srcDir, dir));
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
const composableImports = await Promise.all([
|
|
988
|
+
scanDirExports([
|
|
989
|
+
...composablesDirs,
|
|
990
|
+
path6.resolve(path6.dirname(url4.fileURLToPath(nuxtDepPath)), "app", "components")
|
|
991
|
+
]),
|
|
992
|
+
scanExports(path6.resolve(path6.dirname(url4.fileURLToPath(nuxtDepPath)), "app", "composables", "index.js"))
|
|
993
|
+
]).then((scannedExports) => scannedExports.flat().map((ci) => {
|
|
994
|
+
if (ci.from.includes("/nuxt/dist/app/composables")) {
|
|
995
|
+
ci.from = "virtual:wdio";
|
|
996
|
+
ci.name = "wrappedFn";
|
|
997
|
+
}
|
|
998
|
+
return ci;
|
|
999
|
+
}));
|
|
1000
|
+
const viteConfig = {
|
|
1001
|
+
resolve: {
|
|
1002
|
+
alias: nuxtOptions.alias || {}
|
|
1003
|
+
},
|
|
1004
|
+
plugins: [Unimport.vite({
|
|
1005
|
+
presets: ["vue"],
|
|
1006
|
+
imports: composableImports
|
|
1007
|
+
})]
|
|
1008
|
+
};
|
|
1009
|
+
log5.info(`Optimized Vite config for Nuxt project at ${rootDir}`);
|
|
1010
|
+
return viteConfig;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
// src/vite/frameworks/tailwindcss.ts
|
|
1014
|
+
import url5 from "node:url";
|
|
1015
|
+
import path7 from "node:path";
|
|
1016
|
+
import { resolve as resolve4 } from "import-meta-resolve";
|
|
1017
|
+
function isUsingTailwindCSS(rootDir) {
|
|
1018
|
+
return Promise.all([
|
|
1019
|
+
hasFileByExtensions(path7.join(rootDir, "tailwind.config")),
|
|
1020
|
+
hasFileByExtensions(path7.join(rootDir, "postcss.config"))
|
|
1021
|
+
]).then(([hasTailwindConfig, hasPostCSSConfig]) => {
|
|
1022
|
+
return hasTailwindConfig && !hasPostCSSConfig;
|
|
1023
|
+
});
|
|
1024
|
+
}
|
|
1025
|
+
async function optimizeForTailwindCSS(rootDir) {
|
|
1026
|
+
const viteConfig = {};
|
|
1027
|
+
const tailwindcssPath = await resolve4("tailwindcss", url5.pathToFileURL(path7.resolve(rootDir, "index.js")).href);
|
|
1028
|
+
const tailwindcss = (await import(tailwindcssPath)).default;
|
|
1029
|
+
viteConfig.css = {
|
|
1030
|
+
postcss: { plugins: [tailwindcss] }
|
|
1031
|
+
};
|
|
1032
|
+
return viteConfig;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
// src/vite/frameworks/stencil.ts
|
|
1036
|
+
import path8 from "node:path";
|
|
1037
|
+
import url6 from "node:url";
|
|
1038
|
+
import { findStaticImports, parseStaticImport } from "mlly";
|
|
1039
|
+
var __dirname2 = url6.fileURLToPath(new URL(".", import.meta.url));
|
|
1040
|
+
var STENCIL_IMPORT = "@stencil/core";
|
|
1041
|
+
async function isUsingStencilJS(rootDir, options) {
|
|
1042
|
+
return Boolean(options.preset === "stencil" || await hasFileByExtensions(path8.join(rootDir, "stencil.config")));
|
|
1043
|
+
}
|
|
1044
|
+
async function optimizeForStencil(rootDir) {
|
|
1045
|
+
const stencilConfig = await importStencilConfig(rootDir);
|
|
1046
|
+
const stencilPlugins = stencilConfig.config.plugins;
|
|
1047
|
+
const stencilOptimizations = {
|
|
1048
|
+
plugins: [await stencilVitePlugin(rootDir)],
|
|
1049
|
+
optimizeDeps: { include: [] }
|
|
1050
|
+
};
|
|
1051
|
+
if (stencilPlugins) {
|
|
1052
|
+
const esbuildPlugin = stencilPlugins.find((plugin) => plugin.name === "esbuild-plugin");
|
|
1053
|
+
if (esbuildPlugin) {
|
|
1054
|
+
stencilOptimizations.optimizeDeps?.include?.push(...esbuildPlugin.options.include);
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
stencilOptimizations.optimizeDeps?.include?.push(
|
|
1058
|
+
"@wdio/browser-runner/stencil > @stencil/core/internal/testing/index.js"
|
|
1059
|
+
);
|
|
1060
|
+
return stencilOptimizations;
|
|
1061
|
+
}
|
|
1062
|
+
async function stencilVitePlugin(rootDir) {
|
|
1063
|
+
const { transpileSync, ts } = await import("@stencil/core/compiler/stencil.js");
|
|
1064
|
+
const stencilHelperPath = path8.resolve(__dirname2, "browser", "integrations", "stencil.js");
|
|
1065
|
+
return {
|
|
1066
|
+
name: "wdio-stencil",
|
|
1067
|
+
enforce: "pre",
|
|
1068
|
+
resolveId(source) {
|
|
1069
|
+
if (source === "@wdio/browser-runner/stencil") {
|
|
1070
|
+
return stencilHelperPath;
|
|
1071
|
+
}
|
|
1072
|
+
},
|
|
1073
|
+
transform: function(code, id) {
|
|
1074
|
+
const staticImports = findStaticImports(code);
|
|
1075
|
+
const stencilImports = staticImports.filter((imp) => imp.specifier === STENCIL_IMPORT).map((imp) => parseStaticImport(imp));
|
|
1076
|
+
const isStencilComponent = stencilImports.some((imp) => "Component" in (imp.namedImports || {}));
|
|
1077
|
+
if (!isStencilComponent) {
|
|
1078
|
+
const stencilHelperImport = staticImports.find((imp) => imp.specifier === "@wdio/browser-runner/stencil");
|
|
1079
|
+
if (stencilHelperImport) {
|
|
1080
|
+
const imports = parseStaticImport(stencilHelperImport);
|
|
1081
|
+
if ("render" in (imports.namedImports || {})) {
|
|
1082
|
+
code = injectStencilImports(code, stencilImports);
|
|
1083
|
+
}
|
|
101
1084
|
}
|
|
102
|
-
return
|
|
1085
|
+
return { code };
|
|
1086
|
+
}
|
|
1087
|
+
const tsCompilerOptions = getCompilerOptions(ts, rootDir);
|
|
1088
|
+
const opts = {
|
|
1089
|
+
componentExport: "module",
|
|
1090
|
+
componentMetadata: "compilerstatic",
|
|
1091
|
+
coreImportPath: "@stencil/core/internal/client",
|
|
1092
|
+
currentDirectory: rootDir,
|
|
1093
|
+
file: path8.basename(id),
|
|
1094
|
+
module: "esm",
|
|
1095
|
+
sourceMap: "inline",
|
|
1096
|
+
style: "static",
|
|
1097
|
+
proxy: "defineproperty",
|
|
1098
|
+
styleImportData: "queryparams",
|
|
1099
|
+
transformAliasedImportPaths: process.env.__STENCIL_TRANSPILE_PATHS__ === "true",
|
|
1100
|
+
target: tsCompilerOptions?.target || "es2018",
|
|
1101
|
+
paths: tsCompilerOptions?.paths,
|
|
1102
|
+
baseUrl: tsCompilerOptions?.baseUrl
|
|
1103
|
+
};
|
|
1104
|
+
const transpiledCode = transpileSync(code, opts);
|
|
1105
|
+
let transformedCode = transpiledCode.code.replace(
|
|
1106
|
+
"static get style()",
|
|
1107
|
+
"static set style(_) {}\n static get style()"
|
|
1108
|
+
);
|
|
1109
|
+
transformedCode = injectStencilImports(transformedCode, stencilImports);
|
|
1110
|
+
findStaticImports(transformedCode).filter((imp) => imp.specifier.includes("&encapsulation=shadow")).forEach((imp) => {
|
|
1111
|
+
const cssPath = path8.resolve(path8.dirname(id), imp.specifier);
|
|
1112
|
+
transformedCode = transformedCode.replace(
|
|
1113
|
+
imp.code,
|
|
1114
|
+
`import ${imp.imports.trim()} from '/@fs/${cssPath}&inline';
|
|
1115
|
+
`
|
|
1116
|
+
);
|
|
1117
|
+
});
|
|
1118
|
+
return {
|
|
1119
|
+
...transpiledCode,
|
|
1120
|
+
code: transformedCode,
|
|
1121
|
+
inputFilePath: id
|
|
1122
|
+
};
|
|
103
1123
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
1124
|
+
};
|
|
1125
|
+
}
|
|
1126
|
+
function injectStencilImports(code, imports) {
|
|
1127
|
+
const hasRenderFunctionImport = imports.some((imp) => "h" in (imp.namedImports || {}));
|
|
1128
|
+
if (!hasRenderFunctionImport) {
|
|
1129
|
+
code = `import { h } from '@stencil/core/internal/client';
|
|
1130
|
+
${code}`;
|
|
1131
|
+
}
|
|
1132
|
+
const hasFragmentImport = imports.some((imp) => "Fragment" in (imp.namedImports || {}));
|
|
1133
|
+
if (!hasFragmentImport) {
|
|
1134
|
+
code = `import { Fragment } from '@stencil/core/internal/client';
|
|
1135
|
+
${code}`;
|
|
1136
|
+
}
|
|
1137
|
+
return code;
|
|
1138
|
+
}
|
|
1139
|
+
var _tsCompilerOptions = null;
|
|
1140
|
+
function getCompilerOptions(ts, rootDir) {
|
|
1141
|
+
if (_tsCompilerOptions) {
|
|
1142
|
+
return _tsCompilerOptions;
|
|
1143
|
+
}
|
|
1144
|
+
if (typeof rootDir !== "string") {
|
|
1145
|
+
return null;
|
|
1146
|
+
}
|
|
1147
|
+
const tsconfigFilePath = ts.findConfigFile(rootDir, ts.sys.fileExists);
|
|
1148
|
+
if (!tsconfigFilePath) {
|
|
1149
|
+
return null;
|
|
1150
|
+
}
|
|
1151
|
+
const tsconfigResults = ts.readConfigFile(tsconfigFilePath, ts.sys.readFile);
|
|
1152
|
+
if (tsconfigResults.error) {
|
|
1153
|
+
throw new Error(tsconfigResults.error);
|
|
1154
|
+
}
|
|
1155
|
+
const parseResult = ts.parseJsonConfigFileContent(
|
|
1156
|
+
tsconfigResults.config,
|
|
1157
|
+
ts.sys,
|
|
1158
|
+
rootDir,
|
|
1159
|
+
void 0,
|
|
1160
|
+
tsconfigFilePath
|
|
1161
|
+
);
|
|
1162
|
+
_tsCompilerOptions = parseResult.options;
|
|
1163
|
+
return _tsCompilerOptions;
|
|
1164
|
+
}
|
|
1165
|
+
async function importStencilConfig(rootDir) {
|
|
1166
|
+
const configPath = path8.join(rootDir, "stencil.config.ts");
|
|
1167
|
+
const config = await import(configPath).catch(() => ({ config: {} }));
|
|
1168
|
+
if ("default" in config) {
|
|
1169
|
+
return config.default;
|
|
1170
|
+
}
|
|
1171
|
+
return config;
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
// src/vite/frameworks/index.ts
|
|
1175
|
+
async function updateViteConfig(options, config) {
|
|
1176
|
+
const optimizations = {};
|
|
1177
|
+
const rootDir = options.rootDir || config.rootDir || process.cwd();
|
|
1178
|
+
const isNuxt = await isNuxtFramework(rootDir);
|
|
1179
|
+
if (isNuxt) {
|
|
1180
|
+
Object.assign(optimizations, await optimizeForNuxt(options, config));
|
|
1181
|
+
}
|
|
1182
|
+
const isTailwind = await isUsingTailwindCSS(rootDir);
|
|
1183
|
+
if (isTailwind) {
|
|
1184
|
+
Object.assign(optimizations, await optimizeForTailwindCSS(rootDir));
|
|
1185
|
+
}
|
|
1186
|
+
if (await isUsingStencilJS(rootDir, options)) {
|
|
1187
|
+
Object.assign(optimizations, await optimizeForStencil(rootDir));
|
|
1188
|
+
}
|
|
1189
|
+
return optimizations;
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
// src/communicator.ts
|
|
1193
|
+
import libSourceMap from "istanbul-lib-source-maps";
|
|
1194
|
+
import libCoverage from "istanbul-lib-coverage";
|
|
1195
|
+
import logger6 from "@wdio/logger";
|
|
1196
|
+
import { MESSAGE_TYPES } from "@wdio/types";
|
|
1197
|
+
var log6 = logger6("@wdio/browser-runner");
|
|
1198
|
+
var ServerWorkerCommunicator = class {
|
|
1199
|
+
#mapStore = libSourceMap.createSourceMapStore();
|
|
1200
|
+
#config;
|
|
1201
|
+
#msgId = 0;
|
|
1202
|
+
/**
|
|
1203
|
+
* keep track of custom commands per session
|
|
1204
|
+
*/
|
|
1205
|
+
#customCommands = /* @__PURE__ */ new Map();
|
|
1206
|
+
/**
|
|
1207
|
+
* keep track of request/response messages on browser/worker level
|
|
1208
|
+
*/
|
|
1209
|
+
#pendingMessages = /* @__PURE__ */ new Map();
|
|
1210
|
+
coverageMaps = [];
|
|
1211
|
+
constructor(config) {
|
|
1212
|
+
this.#config = config;
|
|
1213
|
+
}
|
|
1214
|
+
register(server, worker) {
|
|
1215
|
+
server.onBrowserEvent((data, client) => this.#onBrowserEvent(data, client, worker));
|
|
1216
|
+
worker.on("message", this.#onWorkerMessage.bind(this));
|
|
1217
|
+
}
|
|
1218
|
+
async #onWorkerMessage(payload) {
|
|
1219
|
+
if (payload.name === "sessionStarted" && !SESSIONS.has(payload.cid)) {
|
|
1220
|
+
SESSIONS.set(payload.cid, {
|
|
1221
|
+
args: this.#config.mochaOpts || {},
|
|
1222
|
+
config: this.#config,
|
|
1223
|
+
capabilities: payload.content.capabilities,
|
|
1224
|
+
sessionId: payload.content.sessionId,
|
|
1225
|
+
injectGlobals: payload.content.injectGlobals
|
|
1226
|
+
});
|
|
1227
|
+
}
|
|
1228
|
+
if (payload.name === "sessionEnded") {
|
|
1229
|
+
SESSIONS.delete(payload.cid);
|
|
1230
|
+
}
|
|
1231
|
+
if (payload.name === "workerEvent" && payload.args.type === MESSAGE_TYPES.coverageMap) {
|
|
1232
|
+
const coverageMapData = payload.args.value;
|
|
1233
|
+
this.coverageMaps.push(
|
|
1234
|
+
await this.#mapStore.transformCoverage(libCoverage.createCoverageMap(coverageMapData))
|
|
1235
|
+
);
|
|
1236
|
+
}
|
|
1237
|
+
if (payload.name === "workerEvent" && payload.args.type === MESSAGE_TYPES.customCommand) {
|
|
1238
|
+
const { commandName, cid } = payload.args.value;
|
|
1239
|
+
if (!this.#customCommands.has(cid)) {
|
|
1240
|
+
this.#customCommands.set(cid, /* @__PURE__ */ new Set());
|
|
1241
|
+
}
|
|
1242
|
+
const customCommands = this.#customCommands.get(cid) || /* @__PURE__ */ new Set();
|
|
1243
|
+
customCommands.add(commandName);
|
|
1244
|
+
return;
|
|
1245
|
+
}
|
|
1246
|
+
if (payload.name === "workerResponse") {
|
|
1247
|
+
const msg = this.#pendingMessages.get(payload.args.id);
|
|
1248
|
+
if (!msg) {
|
|
1249
|
+
return log6.error(`Couldn't find message with id ${payload.args.id} from type ${payload.args.message.type}`);
|
|
1250
|
+
}
|
|
1251
|
+
this.#pendingMessages.delete(payload.args.id);
|
|
1252
|
+
return msg.client.send(WDIO_EVENT_NAME, payload.args.message);
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
#onBrowserEvent(message, client, worker) {
|
|
1256
|
+
if (message.type === MESSAGE_TYPES.initiateBrowserStateRequest) {
|
|
1257
|
+
const result = {
|
|
1258
|
+
type: MESSAGE_TYPES.initiateBrowserStateResponse,
|
|
1259
|
+
value: {
|
|
1260
|
+
customCommands: [...this.#customCommands.get(message.value.cid) || []]
|
|
112
1261
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
1262
|
+
};
|
|
1263
|
+
return client.send(WDIO_EVENT_NAME, result);
|
|
1264
|
+
}
|
|
1265
|
+
const id = this.#msgId++;
|
|
1266
|
+
const msg = { id, client };
|
|
1267
|
+
this.#pendingMessages.set(id, msg);
|
|
1268
|
+
const args = { id, message };
|
|
1269
|
+
return worker.postMessage("workerRequest", args, true);
|
|
1270
|
+
}
|
|
1271
|
+
};
|
|
1272
|
+
|
|
1273
|
+
// src/utils.ts
|
|
1274
|
+
import util from "node:util";
|
|
1275
|
+
import { deepmerge as deepmerge3 } from "deepmerge-ts";
|
|
1276
|
+
import logger7 from "@wdio/logger";
|
|
1277
|
+
var log7 = logger7("@wdio/browser-runner");
|
|
1278
|
+
function makeHeadless(options, caps) {
|
|
1279
|
+
const capability = caps.alwaysMatch || caps;
|
|
1280
|
+
if (!capability.browserName) {
|
|
1281
|
+
throw new Error(
|
|
1282
|
+
'No "browserName" defined in capability object. It seems you are trying to run tests in a non web environment, however WebdriverIOs browser runner only supports web environments'
|
|
1283
|
+
);
|
|
1284
|
+
}
|
|
1285
|
+
if (
|
|
1286
|
+
// either user sets headless option implicitly
|
|
1287
|
+
typeof options.headless === "boolean" && !options.headless || // or CI environment is set
|
|
1288
|
+
typeof process.env.CI !== "undefined" && !process.env.CI || // or non are set
|
|
1289
|
+
typeof options.headless !== "boolean" && typeof process.env.CI === "undefined"
|
|
1290
|
+
) {
|
|
1291
|
+
return caps;
|
|
1292
|
+
}
|
|
1293
|
+
if (capability.browserName === "chrome") {
|
|
1294
|
+
return deepmerge3(capability, {
|
|
1295
|
+
"goog:chromeOptions": {
|
|
1296
|
+
args: ["headless", "disable-gpu"]
|
|
1297
|
+
}
|
|
1298
|
+
});
|
|
1299
|
+
} else if (capability.browserName === "firefox") {
|
|
1300
|
+
return deepmerge3(capability, {
|
|
1301
|
+
"moz:firefoxOptions": {
|
|
1302
|
+
args: ["-headless"]
|
|
1303
|
+
}
|
|
1304
|
+
});
|
|
1305
|
+
} else if (capability.browserName === "msedge" || capability.browserName === "edge") {
|
|
1306
|
+
return deepmerge3(capability, {
|
|
1307
|
+
"ms:edgeOptions": {
|
|
1308
|
+
args: ["--headless"]
|
|
1309
|
+
}
|
|
1310
|
+
});
|
|
1311
|
+
}
|
|
1312
|
+
log7.error(`Headless mode not supported for browser "${capability.browserName}"`);
|
|
1313
|
+
return caps;
|
|
1314
|
+
}
|
|
1315
|
+
function adjustWindowInWatchMode(config, caps) {
|
|
1316
|
+
if (!config.watch) {
|
|
1317
|
+
return caps;
|
|
1318
|
+
}
|
|
1319
|
+
const capability = caps.alwaysMatch || caps;
|
|
1320
|
+
if (config.watch && capability.browserName === "chrome") {
|
|
1321
|
+
return deepmerge3(capability, {
|
|
1322
|
+
"goog:chromeOptions": {
|
|
1323
|
+
args: ["auto-open-devtools-for-tabs", "window-size=1600,1200"],
|
|
1324
|
+
prefs: {
|
|
1325
|
+
devtools: {
|
|
1326
|
+
preferences: {
|
|
1327
|
+
"panel-selectedTab": '"console"'
|
|
131
1328
|
}
|
|
132
|
-
|
|
133
|
-
projectRoot: this.#config.rootDir,
|
|
134
|
-
subdir: 'html'
|
|
135
|
-
}));
|
|
136
|
-
reportBases.map((reportBase) => reportBase.execute(context));
|
|
137
|
-
log.info(`Successfully created coverage reports for ${reporter.join(', ')}`);
|
|
138
|
-
const summaryFilePath = path.join(this.#reportsDirectory, 'coverage-summary.json');
|
|
139
|
-
const summary = JSON.parse((await fs.readFile(summaryFilePath)).toString());
|
|
140
|
-
coverageIssues.push(...this.#coverageOptions.perFile
|
|
141
|
-
? Object.entries(summary)
|
|
142
|
-
.filter(([source]) => source !== 'total')
|
|
143
|
-
.map(([source, summary]) => (getCoverageByFactor(this.#coverageOptions, summary, source.replace(this.#config.rootDir, ''))))
|
|
144
|
-
.flat()
|
|
145
|
-
: getCoverageByFactor(this.#coverageOptions, summary.total));
|
|
1329
|
+
}
|
|
146
1330
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
1331
|
+
}
|
|
1332
|
+
});
|
|
1333
|
+
}
|
|
1334
|
+
return caps;
|
|
1335
|
+
}
|
|
1336
|
+
function getCoverageByFactor(options, summary, fileName) {
|
|
1337
|
+
return COVERAGE_FACTORS.map((factor) => {
|
|
1338
|
+
const treshold = options[factor];
|
|
1339
|
+
if (!treshold) {
|
|
1340
|
+
return;
|
|
1341
|
+
}
|
|
1342
|
+
if (summary[factor].pct >= treshold) {
|
|
1343
|
+
return;
|
|
1344
|
+
}
|
|
1345
|
+
return fileName ? util.format(FILE_TRESHOLD_REPORTING, factor, summary[factor].pct, treshold, fileName) : util.format(GLOBAL_TRESHOLD_REPORTING, factor, summary[factor].pct, treshold);
|
|
1346
|
+
}).filter(Boolean);
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
// src/index.ts
|
|
1350
|
+
export * from "@vitest/spy";
|
|
1351
|
+
var log8 = logger8("@wdio/browser-runner");
|
|
1352
|
+
var BrowserRunner = class extends LocalRunner {
|
|
1353
|
+
constructor(options, _config) {
|
|
1354
|
+
super(options, _config);
|
|
1355
|
+
this.options = options;
|
|
1356
|
+
this._config = _config;
|
|
1357
|
+
if (_config.framework !== "mocha") {
|
|
1358
|
+
throw new Error(FRAMEWORK_SUPPORT_ERROR);
|
|
1359
|
+
}
|
|
1360
|
+
this.#options = options;
|
|
1361
|
+
this.#config = _config;
|
|
1362
|
+
this.#communicator = new ServerWorkerCommunicator(this.#config);
|
|
1363
|
+
this.#coverageOptions = options.coverage || {};
|
|
1364
|
+
this.#reportsDirectory = this.#coverageOptions.reportsDirectory || path9.join(this.#config.rootDir, DEFAULT_REPORTS_DIRECTORY);
|
|
1365
|
+
if (this.#config.mochaOpts) {
|
|
1366
|
+
this.#config.mochaOpts.require = (this.#config.mochaOpts.require || []).map((r) => path9.join(this.#config.rootDir || process.cwd(), r)).map((r) => url7.pathToFileURL(r).pathname);
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
#options;
|
|
1370
|
+
#config;
|
|
1371
|
+
#servers = /* @__PURE__ */ new Set();
|
|
1372
|
+
#coverageOptions;
|
|
1373
|
+
#reportsDirectory;
|
|
1374
|
+
#viteOptimizations = {};
|
|
1375
|
+
#communicator;
|
|
1376
|
+
/**
|
|
1377
|
+
* for testing purposes
|
|
1378
|
+
*/
|
|
1379
|
+
_servers = this.#servers;
|
|
1380
|
+
/**
|
|
1381
|
+
* nothing to initialize when running locally
|
|
1382
|
+
*/
|
|
1383
|
+
async initialize() {
|
|
1384
|
+
log8.info("Initiate browser environment");
|
|
1385
|
+
if (typeof this.#coverageOptions.clean === "undefined" || this.#coverageOptions.clean) {
|
|
1386
|
+
const reportsDirectoryExist = await fs4.access(this.#reportsDirectory).then(() => true, () => false);
|
|
1387
|
+
if (reportsDirectoryExist) {
|
|
1388
|
+
await fs4.rm(this.#reportsDirectory, { recursive: true });
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
try {
|
|
1392
|
+
this.#viteOptimizations = await updateViteConfig(this.#options, this.#config);
|
|
1393
|
+
} catch (err) {
|
|
1394
|
+
log8.error(`Failed to optimize Vite config: ${err.stack}`);
|
|
1395
|
+
}
|
|
1396
|
+
await super.initialize();
|
|
1397
|
+
}
|
|
1398
|
+
async run(runArgs) {
|
|
1399
|
+
runArgs.caps = makeHeadless(this.options, runArgs.caps);
|
|
1400
|
+
runArgs.caps = adjustWindowInWatchMode(this.#config, runArgs.caps);
|
|
1401
|
+
const server = new ViteServer(this.#options, this.#config, this.#viteOptimizations);
|
|
1402
|
+
this.#servers.add(server);
|
|
1403
|
+
try {
|
|
1404
|
+
await server.start();
|
|
1405
|
+
const host = this.#options.host || DEFAULT_HOST;
|
|
1406
|
+
runArgs.args.baseUrl = `${host}:${server.config.server?.port}`;
|
|
1407
|
+
} catch (err) {
|
|
1408
|
+
throw new Error(`Vite server failed to start: ${err.stack}`);
|
|
1409
|
+
}
|
|
1410
|
+
if (!runArgs.args.baseUrl && runArgs.command === "run") {
|
|
1411
|
+
runArgs.args.baseUrl = this._config.baseUrl;
|
|
1412
|
+
}
|
|
1413
|
+
const worker = await super.run(runArgs);
|
|
1414
|
+
this.#communicator.register(server, worker);
|
|
1415
|
+
return worker;
|
|
1416
|
+
}
|
|
1417
|
+
/**
|
|
1418
|
+
* shutdown vite server
|
|
1419
|
+
*
|
|
1420
|
+
* @return {Promise} resolves when vite server has been shutdown
|
|
1421
|
+
*/
|
|
1422
|
+
async shutdown() {
|
|
1423
|
+
await super.shutdown();
|
|
1424
|
+
for (const server of this.#servers) {
|
|
1425
|
+
await server.close();
|
|
1426
|
+
}
|
|
1427
|
+
return this._generateCoverageReports();
|
|
1428
|
+
}
|
|
1429
|
+
async _generateCoverageReports() {
|
|
1430
|
+
const coverageMaps = this.#communicator.coverageMaps;
|
|
1431
|
+
if (!this.#coverageOptions.enabled || coverageMaps.length === 0) {
|
|
1432
|
+
return true;
|
|
1433
|
+
}
|
|
1434
|
+
const firstCoverageMapEntry = coverageMaps.shift();
|
|
1435
|
+
const coverageMap = libCoverage2.createCoverageMap(firstCoverageMapEntry);
|
|
1436
|
+
coverageMaps.forEach((cm) => coverageMap.merge(cm));
|
|
1437
|
+
const coverageIssues = [];
|
|
1438
|
+
try {
|
|
1439
|
+
const context = libReport.createContext({
|
|
1440
|
+
dir: this.#reportsDirectory,
|
|
1441
|
+
defaultSummarizer: "nested",
|
|
1442
|
+
coverageMap
|
|
1443
|
+
});
|
|
1444
|
+
const reporter = this.#coverageOptions.reporter ? Array.isArray(this.#coverageOptions.reporter) ? this.#coverageOptions.reporter : [this.#coverageOptions.reporter] : DEFAULT_COVERAGE_REPORTS;
|
|
1445
|
+
if (!reporter.includes(SUMMARY_REPORTER)) {
|
|
1446
|
+
reporter.push(SUMMARY_REPORTER);
|
|
1447
|
+
}
|
|
1448
|
+
const reportBases = reporter.map((r) => reports.create(r, {
|
|
1449
|
+
projectRoot: this.#config.rootDir,
|
|
1450
|
+
subdir: "html"
|
|
1451
|
+
}));
|
|
1452
|
+
reportBases.map((reportBase) => reportBase.execute(context));
|
|
1453
|
+
log8.info(`Successfully created coverage reports for ${reporter.join(", ")}`);
|
|
1454
|
+
const summaryFilePath = path9.join(this.#reportsDirectory, "coverage-summary.json");
|
|
1455
|
+
const summary = JSON.parse((await fs4.readFile(summaryFilePath)).toString());
|
|
1456
|
+
coverageIssues.push(
|
|
1457
|
+
...this.#coverageOptions.perFile ? Object.entries(summary).filter(([source]) => source !== "total").map(
|
|
1458
|
+
([source, summary2]) => getCoverageByFactor(this.#coverageOptions, summary2, source.replace(this.#config.rootDir, ""))
|
|
1459
|
+
).flat() : getCoverageByFactor(this.#coverageOptions, summary.total)
|
|
1460
|
+
);
|
|
1461
|
+
} catch (err) {
|
|
1462
|
+
console.error(`Failed to generate code coverage report: ${err.message}`);
|
|
1463
|
+
return false;
|
|
1464
|
+
}
|
|
1465
|
+
if (coverageIssues.length) {
|
|
1466
|
+
console.log(coverageIssues.join("\n"));
|
|
1467
|
+
return false;
|
|
1468
|
+
}
|
|
1469
|
+
return true;
|
|
1470
|
+
}
|
|
1471
|
+
};
|
|
1472
|
+
function mock(path10, factory) {
|
|
1473
|
+
}
|
|
1474
|
+
function unmock(moduleName) {
|
|
1475
|
+
}
|
|
1476
|
+
function mocked(item, options) {
|
|
1477
|
+
}
|
|
1478
|
+
export {
|
|
1479
|
+
BrowserRunner as default,
|
|
1480
|
+
mock,
|
|
1481
|
+
mocked,
|
|
1482
|
+
unmock
|
|
1483
|
+
};
|