@vitest/browser 2.0.0-beta.9 → 2.0.1
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/context.d.ts +167 -14
- package/context.js +2 -0
- package/dist/client/.vite/manifest.json +9 -9
- package/dist/client/__vitest__/assets/index-CAY_OcEp.js +52 -0
- package/dist/client/__vitest__/assets/index-DEM1IsBG.css +1 -0
- package/dist/client/__vitest__/index.html +16 -11
- package/dist/client/__vitest_browser__/{rpc-DBukiZYG.js → client-dLyjuL0K.js} +156 -174
- package/dist/client/__vitest_browser__/orchestrator-x0A1t8rC.js +369 -0
- package/dist/client/__vitest_browser__/tester-BdcP5piS.js +12070 -0
- package/dist/client/esm-client-injector.js +21 -18
- package/dist/client/orchestrator.html +2 -2
- package/dist/client/{tester.html → tester/tester.html} +12 -3
- package/dist/context.js +206 -0
- package/dist/index.d.ts +128 -4
- package/dist/index.js +2074 -457
- package/dist/providers.js +16 -112
- package/dist/state.js +1 -0
- package/dist/webdriver-BdVqnfdE.js +249 -0
- package/jest-dom.d.ts +816 -0
- package/matchers.d.ts +22 -0
- package/package.json +20 -12
- package/providers/playwright.d.ts +60 -2
- package/providers/webdriverio.d.ts +5 -0
- package/dist/client/__vitest__/assets/index-BZPCs_Bg.js +0 -51
- package/dist/client/__vitest__/assets/index-DrQA2UkS.css +0 -1
- package/dist/client/__vitest_browser__/orchestrator-CTfJ7g35.js +0 -186
- package/dist/client/__vitest_browser__/tester-Bo1gw1oi.js +0 -696
package/dist/index.js
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
|
+
import { createDebugger, isFileServingAllowed as isFileServingAllowed$1, getFilePoolName, resolveApiServerConfig, resolveFsAllow, distDir, createServer } from 'vitest/node';
|
|
2
|
+
import fs, { existsSync, readdirSync, readFileSync, promises } from 'node:fs';
|
|
3
|
+
import { WebSocketServer } from 'ws';
|
|
4
|
+
import { isFileServingAllowed } from 'vite';
|
|
5
|
+
import { builtinModules, createRequire } from 'node:module';
|
|
6
|
+
import { readFile as readFile$1, mkdir } from 'node:fs/promises';
|
|
1
7
|
import { fileURLToPath } from 'node:url';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
8
|
+
import { createDefer, slash, toArray } from '@vitest/utils';
|
|
9
|
+
import { parseErrorStacktrace, parseStacktrace } from '@vitest/utils/source-map';
|
|
4
10
|
import sirv from 'sirv';
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import fs, { promises } from 'node:fs';
|
|
9
|
-
import { resolve as resolve$1, dirname as dirname$1 } from 'node:path';
|
|
11
|
+
import { defaultBrowserPort, coverageConfigDefaults } from 'vitest/config';
|
|
12
|
+
import { P as PlaywrightBrowserProvider, W as WebdriverBrowserProvider } from './webdriver-BdVqnfdE.js';
|
|
13
|
+
import { resolve as resolve$1, dirname as dirname$1, normalize as normalize$1 } from 'node:path';
|
|
10
14
|
import MagicString from 'magic-string';
|
|
11
15
|
import { esmWalker } from '@vitest/utils/ast';
|
|
16
|
+
import * as nodeos from 'node:os';
|
|
17
|
+
import crypto from 'node:crypto';
|
|
12
18
|
|
|
13
19
|
const _DRIVE_LETTER_START_RE = /^[A-Za-z]:\//;
|
|
14
20
|
function normalizeWindowsPath(input = "") {
|
|
@@ -21,6 +27,7 @@ function normalizeWindowsPath(input = "") {
|
|
|
21
27
|
const _UNC_REGEX = /^[/\\]{2}/;
|
|
22
28
|
const _IS_ABSOLUTE_RE = /^[/\\](?![/\\])|^[/\\]{2}(?!\.)|^[A-Za-z]:[/\\]/;
|
|
23
29
|
const _DRIVE_LETTER_RE = /^[A-Za-z]:$/;
|
|
30
|
+
const _ROOT_FOLDER_RE = /^\/([A-Za-z]:)?$/;
|
|
24
31
|
const normalize = function(path) {
|
|
25
32
|
if (path.length === 0) {
|
|
26
33
|
return ".";
|
|
@@ -155,52 +162,1527 @@ function normalizeString(path, allowAboveRoot) {
|
|
|
155
162
|
const isAbsolute = function(p) {
|
|
156
163
|
return _IS_ABSOLUTE_RE.test(p);
|
|
157
164
|
};
|
|
165
|
+
const _EXTNAME_RE = /.(\.[^./]+)$/;
|
|
166
|
+
const extname = function(p) {
|
|
167
|
+
const match = _EXTNAME_RE.exec(normalizeWindowsPath(p));
|
|
168
|
+
return match && match[1] || "";
|
|
169
|
+
};
|
|
170
|
+
const relative = function(from, to) {
|
|
171
|
+
const _from = resolve(from).replace(_ROOT_FOLDER_RE, "$1").split("/");
|
|
172
|
+
const _to = resolve(to).replace(_ROOT_FOLDER_RE, "$1").split("/");
|
|
173
|
+
if (_to[0][1] === ":" && _from[0][1] === ":" && _from[0] !== _to[0]) {
|
|
174
|
+
return _to.join("/");
|
|
175
|
+
}
|
|
176
|
+
const _fromCopy = [..._from];
|
|
177
|
+
for (const segment of _fromCopy) {
|
|
178
|
+
if (_to[0] !== segment) {
|
|
179
|
+
break;
|
|
180
|
+
}
|
|
181
|
+
_from.shift();
|
|
182
|
+
_to.shift();
|
|
183
|
+
}
|
|
184
|
+
return [..._from.map(() => ".."), ..._to].join("/");
|
|
185
|
+
};
|
|
158
186
|
const dirname = function(p) {
|
|
159
187
|
const segments = normalizeWindowsPath(p).replace(/\/$/, "").split("/").slice(0, -1);
|
|
160
188
|
if (segments.length === 1 && _DRIVE_LETTER_RE.test(segments[0])) {
|
|
161
189
|
segments[0] += "/";
|
|
162
190
|
}
|
|
163
|
-
return segments.join("/") || (isAbsolute(p) ? "/" : ".");
|
|
191
|
+
return segments.join("/") || (isAbsolute(p) ? "/" : ".");
|
|
192
|
+
};
|
|
193
|
+
const basename = function(p, extension) {
|
|
194
|
+
const lastSegment = normalizeWindowsPath(p).split("/").pop();
|
|
195
|
+
return extension && lastSegment.endsWith(extension) ? lastSegment.slice(0, -extension.length) : lastSegment;
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const DEFAULT_TIMEOUT = 6e4;
|
|
199
|
+
function defaultSerialize(i) {
|
|
200
|
+
return i;
|
|
201
|
+
}
|
|
202
|
+
const defaultDeserialize = defaultSerialize;
|
|
203
|
+
const { clearTimeout, setTimeout } = globalThis;
|
|
204
|
+
const random = Math.random.bind(Math);
|
|
205
|
+
function createBirpc(functions, options) {
|
|
206
|
+
const {
|
|
207
|
+
post,
|
|
208
|
+
on,
|
|
209
|
+
eventNames = [],
|
|
210
|
+
serialize = defaultSerialize,
|
|
211
|
+
deserialize = defaultDeserialize,
|
|
212
|
+
resolver,
|
|
213
|
+
timeout = DEFAULT_TIMEOUT
|
|
214
|
+
} = options;
|
|
215
|
+
const rpcPromiseMap = /* @__PURE__ */ new Map();
|
|
216
|
+
let _promise;
|
|
217
|
+
const rpc = new Proxy({}, {
|
|
218
|
+
get(_, method) {
|
|
219
|
+
if (method === "$functions")
|
|
220
|
+
return functions;
|
|
221
|
+
if (method === "then" && !eventNames.includes("then") && !("then" in functions))
|
|
222
|
+
return void 0;
|
|
223
|
+
const sendEvent = (...args) => {
|
|
224
|
+
post(serialize({ m: method, a: args, t: "q" }));
|
|
225
|
+
};
|
|
226
|
+
if (eventNames.includes(method)) {
|
|
227
|
+
sendEvent.asEvent = sendEvent;
|
|
228
|
+
return sendEvent;
|
|
229
|
+
}
|
|
230
|
+
const sendCall = async (...args) => {
|
|
231
|
+
await _promise;
|
|
232
|
+
return new Promise((resolve, reject) => {
|
|
233
|
+
const id = nanoid();
|
|
234
|
+
let timeoutId;
|
|
235
|
+
if (timeout >= 0) {
|
|
236
|
+
timeoutId = setTimeout(() => {
|
|
237
|
+
try {
|
|
238
|
+
options.onTimeoutError?.(method, args);
|
|
239
|
+
throw new Error(`[birpc] timeout on calling "${method}"`);
|
|
240
|
+
} catch (e) {
|
|
241
|
+
reject(e);
|
|
242
|
+
}
|
|
243
|
+
rpcPromiseMap.delete(id);
|
|
244
|
+
}, timeout);
|
|
245
|
+
if (typeof timeoutId === "object")
|
|
246
|
+
timeoutId = timeoutId.unref?.();
|
|
247
|
+
}
|
|
248
|
+
rpcPromiseMap.set(id, { resolve, reject, timeoutId });
|
|
249
|
+
post(serialize({ m: method, a: args, i: id, t: "q" }));
|
|
250
|
+
});
|
|
251
|
+
};
|
|
252
|
+
sendCall.asEvent = sendEvent;
|
|
253
|
+
return sendCall;
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
_promise = on(async (data, ...extra) => {
|
|
257
|
+
const msg = deserialize(data);
|
|
258
|
+
if (msg.t === "q") {
|
|
259
|
+
const { m: method, a: args } = msg;
|
|
260
|
+
let result, error;
|
|
261
|
+
const fn = resolver ? resolver(method, functions[method]) : functions[method];
|
|
262
|
+
if (!fn) {
|
|
263
|
+
error = new Error(`[birpc] function "${method}" not found`);
|
|
264
|
+
} else {
|
|
265
|
+
try {
|
|
266
|
+
result = await fn.apply(rpc, args);
|
|
267
|
+
} catch (e) {
|
|
268
|
+
error = e;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (msg.i) {
|
|
272
|
+
if (error && options.onError)
|
|
273
|
+
options.onError(error, method, args);
|
|
274
|
+
post(serialize({ t: "s", i: msg.i, r: result, e: error }), ...extra);
|
|
275
|
+
}
|
|
276
|
+
} else {
|
|
277
|
+
const { i: ack, r: result, e: error } = msg;
|
|
278
|
+
const promise = rpcPromiseMap.get(ack);
|
|
279
|
+
if (promise) {
|
|
280
|
+
clearTimeout(promise.timeoutId);
|
|
281
|
+
if (error)
|
|
282
|
+
promise.reject(error);
|
|
283
|
+
else
|
|
284
|
+
promise.resolve(result);
|
|
285
|
+
}
|
|
286
|
+
rpcPromiseMap.delete(ack);
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
return rpc;
|
|
290
|
+
}
|
|
291
|
+
const urlAlphabet = "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";
|
|
292
|
+
function nanoid(size = 21) {
|
|
293
|
+
let id = "";
|
|
294
|
+
let i = size;
|
|
295
|
+
while (i--)
|
|
296
|
+
id += urlAlphabet[random() * 64 | 0];
|
|
297
|
+
return id;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/// <reference types="../types/index.d.ts" />
|
|
301
|
+
|
|
302
|
+
// (c) 2020-present Andrea Giammarchi
|
|
303
|
+
|
|
304
|
+
const {parse: $parse, stringify: $stringify} = JSON;
|
|
305
|
+
const {keys} = Object;
|
|
306
|
+
|
|
307
|
+
const Primitive = String; // it could be Number
|
|
308
|
+
const primitive = 'string'; // it could be 'number'
|
|
309
|
+
|
|
310
|
+
const ignore = {};
|
|
311
|
+
const object = 'object';
|
|
312
|
+
|
|
313
|
+
const noop = (_, value) => value;
|
|
314
|
+
|
|
315
|
+
const primitives = value => (
|
|
316
|
+
value instanceof Primitive ? Primitive(value) : value
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
const Primitives = (_, value) => (
|
|
320
|
+
typeof value === primitive ? new Primitive(value) : value
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
const revive = (input, parsed, output, $) => {
|
|
324
|
+
const lazy = [];
|
|
325
|
+
for (let ke = keys(output), {length} = ke, y = 0; y < length; y++) {
|
|
326
|
+
const k = ke[y];
|
|
327
|
+
const value = output[k];
|
|
328
|
+
if (value instanceof Primitive) {
|
|
329
|
+
const tmp = input[value];
|
|
330
|
+
if (typeof tmp === object && !parsed.has(tmp)) {
|
|
331
|
+
parsed.add(tmp);
|
|
332
|
+
output[k] = ignore;
|
|
333
|
+
lazy.push({k, a: [input, parsed, tmp, $]});
|
|
334
|
+
}
|
|
335
|
+
else
|
|
336
|
+
output[k] = $.call(output, k, tmp);
|
|
337
|
+
}
|
|
338
|
+
else if (output[k] !== ignore)
|
|
339
|
+
output[k] = $.call(output, k, value);
|
|
340
|
+
}
|
|
341
|
+
for (let {length} = lazy, i = 0; i < length; i++) {
|
|
342
|
+
const {k, a} = lazy[i];
|
|
343
|
+
output[k] = $.call(output, k, revive.apply(null, a));
|
|
344
|
+
}
|
|
345
|
+
return output;
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
const set = (known, input, value) => {
|
|
349
|
+
const index = Primitive(input.push(value) - 1);
|
|
350
|
+
known.set(value, index);
|
|
351
|
+
return index;
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Converts a specialized flatted string into a JS value.
|
|
356
|
+
* @param {string} text
|
|
357
|
+
* @param {(this: any, key: string, value: any) => any} [reviver]
|
|
358
|
+
* @returns {any}
|
|
359
|
+
*/
|
|
360
|
+
const parse = (text, reviver) => {
|
|
361
|
+
const input = $parse(text, Primitives).map(primitives);
|
|
362
|
+
const value = input[0];
|
|
363
|
+
const $ = reviver || noop;
|
|
364
|
+
const tmp = typeof value === object && value ?
|
|
365
|
+
revive(input, new Set, value, $) :
|
|
366
|
+
value;
|
|
367
|
+
return $.call({'': tmp}, '', tmp);
|
|
368
|
+
};
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Converts a JS value into a specialized flatted string.
|
|
372
|
+
* @param {any} value
|
|
373
|
+
* @param {((this: any, key: string, value: any) => any) | (string | number)[] | null | undefined} [replacer]
|
|
374
|
+
* @param {string | number | undefined} [space]
|
|
375
|
+
* @returns {string}
|
|
376
|
+
*/
|
|
377
|
+
const stringify = (value, replacer, space) => {
|
|
378
|
+
const $ = replacer && typeof replacer === object ?
|
|
379
|
+
(k, v) => (k === '' || -1 < replacer.indexOf(k) ? v : void 0) :
|
|
380
|
+
(replacer || noop);
|
|
381
|
+
const known = new Map;
|
|
382
|
+
const input = [];
|
|
383
|
+
const output = [];
|
|
384
|
+
let i = +set(known, input, $.call({'': value}, '', value));
|
|
385
|
+
let firstRun = !i;
|
|
386
|
+
while (i < input.length) {
|
|
387
|
+
firstRun = true;
|
|
388
|
+
output[i] = $stringify(input[i++], replace, space);
|
|
389
|
+
}
|
|
390
|
+
return '[' + output.join(',') + ']';
|
|
391
|
+
function replace(key, value) {
|
|
392
|
+
if (firstRun) {
|
|
393
|
+
firstRun = !firstRun;
|
|
394
|
+
return value;
|
|
395
|
+
}
|
|
396
|
+
const after = $.call(this, key, value);
|
|
397
|
+
switch (typeof after) {
|
|
398
|
+
case object:
|
|
399
|
+
if (after === null) return after;
|
|
400
|
+
case primitive:
|
|
401
|
+
return known.get(after) || set(known, input, after);
|
|
402
|
+
}
|
|
403
|
+
return after;
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
|
|
407
|
+
async function resolveMock(project, rawId, importer, hasFactory) {
|
|
408
|
+
const { id, fsPath, external } = await resolveId(project, rawId, importer);
|
|
409
|
+
if (hasFactory) {
|
|
410
|
+
const needsInteropMap = viteDepsInteropMap(project.browser.vite.config);
|
|
411
|
+
const needsInterop = needsInteropMap?.get(fsPath) ?? false;
|
|
412
|
+
return { type: "factory", resolvedId: id, needsInterop };
|
|
413
|
+
}
|
|
414
|
+
const mockPath = resolveMockPath(project.config.root, fsPath, external);
|
|
415
|
+
return {
|
|
416
|
+
type: mockPath === null ? "automock" : "redirect",
|
|
417
|
+
mockPath,
|
|
418
|
+
resolvedId: id
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
async function resolveId(project, rawId, importer) {
|
|
422
|
+
const resolved = await project.browser.vite.pluginContainer.resolveId(
|
|
423
|
+
rawId,
|
|
424
|
+
importer,
|
|
425
|
+
{
|
|
426
|
+
ssr: false
|
|
427
|
+
}
|
|
428
|
+
);
|
|
429
|
+
return resolveModule(project, rawId, resolved);
|
|
430
|
+
}
|
|
431
|
+
async function resolveModule(project, rawId, resolved) {
|
|
432
|
+
const id = resolved?.id || rawId;
|
|
433
|
+
const external = !isAbsolute(id) || isModuleDirectory(project.config, id) ? rawId : null;
|
|
434
|
+
return {
|
|
435
|
+
id,
|
|
436
|
+
fsPath: cleanUrl(id),
|
|
437
|
+
external
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
function isModuleDirectory(config, path) {
|
|
441
|
+
const moduleDirectories = config.server.deps?.moduleDirectories || [
|
|
442
|
+
"/node_modules/"
|
|
443
|
+
];
|
|
444
|
+
return moduleDirectories.some((dir) => path.includes(dir));
|
|
445
|
+
}
|
|
446
|
+
function resolveMockPath(root, mockPath, external) {
|
|
447
|
+
const path = external || mockPath;
|
|
448
|
+
if (external || isNodeBuiltin(mockPath) || !existsSync(mockPath)) {
|
|
449
|
+
const mockDirname = dirname(path);
|
|
450
|
+
const mockFolder = join(
|
|
451
|
+
root,
|
|
452
|
+
"__mocks__",
|
|
453
|
+
mockDirname
|
|
454
|
+
);
|
|
455
|
+
if (!existsSync(mockFolder)) {
|
|
456
|
+
return null;
|
|
457
|
+
}
|
|
458
|
+
const files = readdirSync(mockFolder);
|
|
459
|
+
const baseOriginal = basename(path);
|
|
460
|
+
for (const file of files) {
|
|
461
|
+
const baseFile = basename(file, extname(file));
|
|
462
|
+
if (baseFile === baseOriginal) {
|
|
463
|
+
return resolve(mockFolder, file);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
return null;
|
|
467
|
+
}
|
|
468
|
+
const dir = dirname(path);
|
|
469
|
+
const baseId = basename(path);
|
|
470
|
+
const fullPath = resolve(dir, "__mocks__", baseId);
|
|
471
|
+
return existsSync(fullPath) ? fullPath : null;
|
|
472
|
+
}
|
|
473
|
+
const prefixedBuiltins = /* @__PURE__ */ new Set(["node:test"]);
|
|
474
|
+
const builtins = /* @__PURE__ */ new Set([
|
|
475
|
+
...builtinModules,
|
|
476
|
+
"assert/strict",
|
|
477
|
+
"diagnostics_channel",
|
|
478
|
+
"dns/promises",
|
|
479
|
+
"fs/promises",
|
|
480
|
+
"path/posix",
|
|
481
|
+
"path/win32",
|
|
482
|
+
"readline/promises",
|
|
483
|
+
"stream/consumers",
|
|
484
|
+
"stream/promises",
|
|
485
|
+
"stream/web",
|
|
486
|
+
"timers/promises",
|
|
487
|
+
"util/types",
|
|
488
|
+
"wasi"
|
|
489
|
+
]);
|
|
490
|
+
const NODE_BUILTIN_NAMESPACE = "node:";
|
|
491
|
+
function isNodeBuiltin(id) {
|
|
492
|
+
if (prefixedBuiltins.has(id)) {
|
|
493
|
+
return true;
|
|
494
|
+
}
|
|
495
|
+
return builtins.has(
|
|
496
|
+
id.startsWith(NODE_BUILTIN_NAMESPACE) ? id.slice(NODE_BUILTIN_NAMESPACE.length) : id
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
const postfixRE = /[?#].*$/;
|
|
500
|
+
function cleanUrl(url) {
|
|
501
|
+
return url.replace(postfixRE, "");
|
|
502
|
+
}
|
|
503
|
+
const metadata = /* @__PURE__ */ new WeakMap();
|
|
504
|
+
function viteDepsInteropMap(config) {
|
|
505
|
+
if (metadata.has(config)) {
|
|
506
|
+
return metadata.get(config);
|
|
507
|
+
}
|
|
508
|
+
const cacheDirPath = getDepsCacheDir(config);
|
|
509
|
+
const metadataPath = resolve(cacheDirPath, "_metadata.json");
|
|
510
|
+
if (!existsSync(metadataPath)) {
|
|
511
|
+
return null;
|
|
512
|
+
}
|
|
513
|
+
const { optimized } = JSON.parse(readFileSync(metadataPath, "utf-8"));
|
|
514
|
+
const needsInteropMap = /* @__PURE__ */ new Map();
|
|
515
|
+
for (const name in optimized) {
|
|
516
|
+
const dep = optimized[name];
|
|
517
|
+
const file = resolve(cacheDirPath, dep.file);
|
|
518
|
+
needsInteropMap.set(file, dep.needsInterop);
|
|
519
|
+
}
|
|
520
|
+
metadata.set(config, needsInteropMap);
|
|
521
|
+
return needsInteropMap;
|
|
522
|
+
}
|
|
523
|
+
function getDepsCacheDir(config) {
|
|
524
|
+
return resolve(config.cacheDir, "deps");
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
const debug$1 = createDebugger("vitest:browser:api");
|
|
528
|
+
const BROWSER_API_PATH = "/__vitest_browser_api__";
|
|
529
|
+
function setupBrowserRpc(server) {
|
|
530
|
+
const project = server.project;
|
|
531
|
+
const vite = server.vite;
|
|
532
|
+
const ctx = project.ctx;
|
|
533
|
+
const wss = new WebSocketServer({ noServer: true });
|
|
534
|
+
vite.httpServer?.on("upgrade", (request, socket, head) => {
|
|
535
|
+
if (!request.url) {
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
const { pathname, searchParams } = new URL(request.url, "http://localhost");
|
|
539
|
+
if (pathname !== BROWSER_API_PATH) {
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
const type = searchParams.get("type") ?? "tester";
|
|
543
|
+
const sessionId = searchParams.get("sessionId") ?? "0";
|
|
544
|
+
wss.handleUpgrade(request, socket, head, (ws) => {
|
|
545
|
+
wss.emit("connection", ws, request);
|
|
546
|
+
const rpc = setupClient(sessionId, ws);
|
|
547
|
+
const state = server.state;
|
|
548
|
+
const clients = type === "tester" ? state.testers : state.orchestrators;
|
|
549
|
+
clients.set(sessionId, rpc);
|
|
550
|
+
debug$1?.("[%s] Browser API connected to %s", sessionId, type);
|
|
551
|
+
ws.on("close", () => {
|
|
552
|
+
debug$1?.("[%s] Browser API disconnected from %s", sessionId, type);
|
|
553
|
+
clients.delete(sessionId);
|
|
554
|
+
server.state.removeCDPHandler(sessionId);
|
|
555
|
+
});
|
|
556
|
+
});
|
|
557
|
+
});
|
|
558
|
+
function checkFileAccess(path) {
|
|
559
|
+
if (!isFileServingAllowed(path, vite)) {
|
|
560
|
+
throw new Error(
|
|
561
|
+
`Access denied to "${path}". See Vite config documentation for "server.fs": https://vitejs.dev/config/server-options.html#server-fs-strict.`
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
function setupClient(sessionId, ws) {
|
|
566
|
+
const rpc = createBirpc(
|
|
567
|
+
{
|
|
568
|
+
async onUnhandledError(error, type) {
|
|
569
|
+
ctx.state.catchError(error, type);
|
|
570
|
+
},
|
|
571
|
+
async onCollected(files) {
|
|
572
|
+
ctx.state.collectFiles(files);
|
|
573
|
+
await ctx.report("onCollected", files);
|
|
574
|
+
},
|
|
575
|
+
async onTaskUpdate(packs) {
|
|
576
|
+
ctx.state.updateTasks(packs);
|
|
577
|
+
await ctx.report("onTaskUpdate", packs);
|
|
578
|
+
},
|
|
579
|
+
onAfterSuiteRun(meta) {
|
|
580
|
+
ctx.coverageProvider?.onAfterSuiteRun(meta);
|
|
581
|
+
},
|
|
582
|
+
sendLog(log) {
|
|
583
|
+
return ctx.report("onUserConsoleLog", log);
|
|
584
|
+
},
|
|
585
|
+
resolveSnapshotPath(testPath) {
|
|
586
|
+
return ctx.snapshot.resolvePath(testPath);
|
|
587
|
+
},
|
|
588
|
+
resolveSnapshotRawPath(testPath, rawPath) {
|
|
589
|
+
return ctx.snapshot.resolveRawPath(testPath, rawPath);
|
|
590
|
+
},
|
|
591
|
+
snapshotSaved(snapshot) {
|
|
592
|
+
ctx.snapshot.add(snapshot);
|
|
593
|
+
},
|
|
594
|
+
async readSnapshotFile(snapshotPath) {
|
|
595
|
+
checkFileAccess(snapshotPath);
|
|
596
|
+
if (!existsSync(snapshotPath)) {
|
|
597
|
+
return null;
|
|
598
|
+
}
|
|
599
|
+
return promises.readFile(snapshotPath, "utf-8");
|
|
600
|
+
},
|
|
601
|
+
async saveSnapshotFile(id, content) {
|
|
602
|
+
checkFileAccess(id);
|
|
603
|
+
await promises.mkdir(dirname(id), { recursive: true });
|
|
604
|
+
return promises.writeFile(id, content, "utf-8");
|
|
605
|
+
},
|
|
606
|
+
async removeSnapshotFile(id) {
|
|
607
|
+
checkFileAccess(id);
|
|
608
|
+
if (!existsSync(id)) {
|
|
609
|
+
throw new Error(`Snapshot file "${id}" does not exist.`);
|
|
610
|
+
}
|
|
611
|
+
return promises.unlink(id);
|
|
612
|
+
},
|
|
613
|
+
getBrowserFileSourceMap(id) {
|
|
614
|
+
const mod = server.vite.moduleGraph.getModuleById(id);
|
|
615
|
+
return mod?.transformResult?.map;
|
|
616
|
+
},
|
|
617
|
+
onCancel(reason) {
|
|
618
|
+
ctx.cancelCurrentRun(reason);
|
|
619
|
+
},
|
|
620
|
+
async resolveId(id, importer) {
|
|
621
|
+
const result = await project.server.pluginContainer.resolveId(
|
|
622
|
+
id,
|
|
623
|
+
importer,
|
|
624
|
+
{
|
|
625
|
+
ssr: false
|
|
626
|
+
}
|
|
627
|
+
);
|
|
628
|
+
return result;
|
|
629
|
+
},
|
|
630
|
+
debug(...args) {
|
|
631
|
+
ctx.logger.console.debug(...args);
|
|
632
|
+
},
|
|
633
|
+
getCountOfFailedTests() {
|
|
634
|
+
return ctx.state.getCountOfFailedTests();
|
|
635
|
+
},
|
|
636
|
+
async triggerCommand(contextId, command, testPath, payload) {
|
|
637
|
+
debug$1?.('[%s] Triggering command "%s"', contextId, command);
|
|
638
|
+
const provider = server.provider;
|
|
639
|
+
if (!provider) {
|
|
640
|
+
throw new Error("Commands are only available for browser tests.");
|
|
641
|
+
}
|
|
642
|
+
const commands = project.config.browser?.commands;
|
|
643
|
+
if (!commands || !commands[command]) {
|
|
644
|
+
throw new Error(`Unknown command "${command}".`);
|
|
645
|
+
}
|
|
646
|
+
if (provider.beforeCommand) {
|
|
647
|
+
await provider.beforeCommand(command, payload);
|
|
648
|
+
}
|
|
649
|
+
const context = Object.assign(
|
|
650
|
+
{
|
|
651
|
+
testPath,
|
|
652
|
+
project,
|
|
653
|
+
provider,
|
|
654
|
+
contextId
|
|
655
|
+
},
|
|
656
|
+
provider.getCommandsContext(contextId)
|
|
657
|
+
);
|
|
658
|
+
let result;
|
|
659
|
+
try {
|
|
660
|
+
result = await commands[command](context, ...payload);
|
|
661
|
+
} finally {
|
|
662
|
+
if (provider.afterCommand) {
|
|
663
|
+
await provider.afterCommand(command, payload);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
return result;
|
|
667
|
+
},
|
|
668
|
+
finishBrowserTests(contextId) {
|
|
669
|
+
debug$1?.("[%s] Finishing browser tests for context", contextId);
|
|
670
|
+
return server.state.getContext(contextId)?.resolve();
|
|
671
|
+
},
|
|
672
|
+
resolveMock(rawId, importer, hasFactory) {
|
|
673
|
+
return resolveMock(project, rawId, importer, hasFactory);
|
|
674
|
+
},
|
|
675
|
+
invalidate(ids) {
|
|
676
|
+
ids.forEach((id) => {
|
|
677
|
+
const moduleGraph = server.vite.moduleGraph;
|
|
678
|
+
const module = moduleGraph.getModuleById(id);
|
|
679
|
+
if (module) {
|
|
680
|
+
moduleGraph.invalidateModule(module, /* @__PURE__ */ new Set(), Date.now(), true);
|
|
681
|
+
}
|
|
682
|
+
});
|
|
683
|
+
},
|
|
684
|
+
// CDP
|
|
685
|
+
async sendCdpEvent(contextId, event, payload) {
|
|
686
|
+
const cdp = await server.ensureCDPHandler(contextId, sessionId);
|
|
687
|
+
return cdp.send(event, payload);
|
|
688
|
+
},
|
|
689
|
+
async trackCdpEvent(contextId, type, event, listenerId) {
|
|
690
|
+
const cdp = await server.ensureCDPHandler(contextId, sessionId);
|
|
691
|
+
cdp[type](event, listenerId);
|
|
692
|
+
}
|
|
693
|
+
},
|
|
694
|
+
{
|
|
695
|
+
post: (msg) => ws.send(msg),
|
|
696
|
+
on: (fn) => ws.on("message", fn),
|
|
697
|
+
eventNames: ["onCancel", "cdpEvent"],
|
|
698
|
+
serialize: (data) => stringify(data, stringifyReplace),
|
|
699
|
+
deserialize: parse,
|
|
700
|
+
onTimeoutError(functionName) {
|
|
701
|
+
throw new Error(`[vitest-api]: Timeout calling "${functionName}"`);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
);
|
|
705
|
+
ctx.onCancel((reason) => rpc.onCancel(reason));
|
|
706
|
+
return rpc;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
function cloneByOwnProperties(value) {
|
|
710
|
+
return Object.getOwnPropertyNames(value).reduce(
|
|
711
|
+
(clone, prop) => ({
|
|
712
|
+
...clone,
|
|
713
|
+
[prop]: value[prop]
|
|
714
|
+
}),
|
|
715
|
+
{}
|
|
716
|
+
);
|
|
717
|
+
}
|
|
718
|
+
function stringifyReplace(key, value) {
|
|
719
|
+
if (value instanceof Error) {
|
|
720
|
+
const cloned = cloneByOwnProperties(value);
|
|
721
|
+
return {
|
|
722
|
+
name: value.name,
|
|
723
|
+
message: value.message,
|
|
724
|
+
stack: value.stack,
|
|
725
|
+
...cloned
|
|
726
|
+
};
|
|
727
|
+
} else {
|
|
728
|
+
return value;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
class BrowserServerState {
|
|
733
|
+
orchestrators = /* @__PURE__ */ new Map();
|
|
734
|
+
testers = /* @__PURE__ */ new Map();
|
|
735
|
+
cdps = /* @__PURE__ */ new Map();
|
|
736
|
+
contexts = /* @__PURE__ */ new Map();
|
|
737
|
+
getContext(contextId) {
|
|
738
|
+
return this.contexts.get(contextId);
|
|
739
|
+
}
|
|
740
|
+
createAsyncContext(method, contextId, files) {
|
|
741
|
+
const defer = createDefer();
|
|
742
|
+
this.contexts.set(contextId, {
|
|
743
|
+
files,
|
|
744
|
+
method,
|
|
745
|
+
resolve: () => {
|
|
746
|
+
defer.resolve();
|
|
747
|
+
this.contexts.delete(contextId);
|
|
748
|
+
},
|
|
749
|
+
reject: defer.reject
|
|
750
|
+
});
|
|
751
|
+
return defer;
|
|
752
|
+
}
|
|
753
|
+
async removeCDPHandler(sessionId) {
|
|
754
|
+
this.cdps.delete(sessionId);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
function replacer(code, values) {
|
|
759
|
+
return code.replace(/\{\s*(\w+)\s*\}/g, (_, key) => values[key] ?? "");
|
|
760
|
+
}
|
|
761
|
+
const builtinProviders = ["webdriverio", "playwright", "preview"];
|
|
762
|
+
async function getBrowserProvider(options, project) {
|
|
763
|
+
if (options.provider == null || builtinProviders.includes(options.provider)) {
|
|
764
|
+
const providers = await import('./providers.js');
|
|
765
|
+
const provider = options.provider || "preview";
|
|
766
|
+
return providers[provider];
|
|
767
|
+
}
|
|
768
|
+
let customProviderModule;
|
|
769
|
+
try {
|
|
770
|
+
customProviderModule = await project.runner.executeId(
|
|
771
|
+
options.provider
|
|
772
|
+
);
|
|
773
|
+
} catch (error) {
|
|
774
|
+
throw new Error(
|
|
775
|
+
`Failed to load custom BrowserProvider from ${options.provider}`,
|
|
776
|
+
{ cause: error }
|
|
777
|
+
);
|
|
778
|
+
}
|
|
779
|
+
if (customProviderModule.default == null) {
|
|
780
|
+
throw new Error(
|
|
781
|
+
`Custom BrowserProvider loaded from ${options.provider} was not the default export`
|
|
782
|
+
);
|
|
783
|
+
}
|
|
784
|
+
return customProviderModule.default;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
class BrowserServerCDPHandler {
|
|
788
|
+
constructor(session, tester) {
|
|
789
|
+
this.session = session;
|
|
790
|
+
this.tester = tester;
|
|
791
|
+
}
|
|
792
|
+
listenerIds = {};
|
|
793
|
+
listeners = {};
|
|
794
|
+
send(method, params) {
|
|
795
|
+
return this.session.send(method, params);
|
|
796
|
+
}
|
|
797
|
+
on(event, id, once = false) {
|
|
798
|
+
if (!this.listenerIds[event]) {
|
|
799
|
+
this.listenerIds[event] = [];
|
|
800
|
+
}
|
|
801
|
+
this.listenerIds[event].push(id);
|
|
802
|
+
if (!this.listeners[event]) {
|
|
803
|
+
this.listeners[event] = (payload) => {
|
|
804
|
+
this.tester.cdpEvent(
|
|
805
|
+
event,
|
|
806
|
+
payload
|
|
807
|
+
);
|
|
808
|
+
if (once) {
|
|
809
|
+
this.off(event, id);
|
|
810
|
+
}
|
|
811
|
+
};
|
|
812
|
+
this.session.on(event, this.listeners[event]);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
off(event, id) {
|
|
816
|
+
if (!this.listenerIds[event]) {
|
|
817
|
+
this.listenerIds[event] = [];
|
|
818
|
+
}
|
|
819
|
+
this.listenerIds[event] = this.listenerIds[event].filter((l) => l !== id);
|
|
820
|
+
if (!this.listenerIds[event].length) {
|
|
821
|
+
this.session.off(event, this.listeners[event]);
|
|
822
|
+
delete this.listeners[event];
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
once(event, listener) {
|
|
826
|
+
this.on(event, listener, true);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
class BrowserServer {
|
|
831
|
+
constructor(project, base) {
|
|
832
|
+
this.project = project;
|
|
833
|
+
this.base = base;
|
|
834
|
+
this.stackTraceOptions = {
|
|
835
|
+
frameFilter: project.config.onStackTrace,
|
|
836
|
+
getSourceMap: (id) => {
|
|
837
|
+
const result = this.vite.moduleGraph.getModuleById(id)?.transformResult;
|
|
838
|
+
return result?.map;
|
|
839
|
+
},
|
|
840
|
+
getFileName: (id) => {
|
|
841
|
+
const mod = this.vite.moduleGraph.getModuleById(id);
|
|
842
|
+
if (mod?.file) {
|
|
843
|
+
return mod.file;
|
|
844
|
+
}
|
|
845
|
+
const modUrl = this.vite.moduleGraph.urlToModuleMap.get(id);
|
|
846
|
+
if (modUrl?.file) {
|
|
847
|
+
return modUrl.file;
|
|
848
|
+
}
|
|
849
|
+
return id;
|
|
850
|
+
}
|
|
851
|
+
};
|
|
852
|
+
this.state = new BrowserServerState();
|
|
853
|
+
const pkgRoot = resolve(fileURLToPath(import.meta.url), "../..");
|
|
854
|
+
const distRoot = resolve(pkgRoot, "dist");
|
|
855
|
+
this.prefixTesterUrl = `${base}__vitest_test__/__test__/`;
|
|
856
|
+
this.faviconUrl = `${base}__vitest__/favicon.svg`;
|
|
857
|
+
this.manifest = (async () => {
|
|
858
|
+
return JSON.parse(
|
|
859
|
+
await readFile$1(`${distRoot}/client/.vite/manifest.json`, "utf8")
|
|
860
|
+
);
|
|
861
|
+
})().then((manifest) => this.manifest = manifest);
|
|
862
|
+
this.testerHtml = readFile$1(
|
|
863
|
+
resolve(distRoot, "client/tester/tester.html"),
|
|
864
|
+
"utf8"
|
|
865
|
+
).then((html) => this.testerHtml = html);
|
|
866
|
+
this.orchestratorHtml = (project.config.browser.ui ? readFile$1(resolve(distRoot, "client/__vitest__/index.html"), "utf8") : readFile$1(resolve(distRoot, "client/orchestrator.html"), "utf8")).then((html) => this.orchestratorHtml = html);
|
|
867
|
+
this.injectorJs = readFile$1(
|
|
868
|
+
resolve(distRoot, "client/esm-client-injector.js"),
|
|
869
|
+
"utf8"
|
|
870
|
+
).then((js) => this.injectorJs = js);
|
|
871
|
+
this.stateJs = readFile$1(
|
|
872
|
+
resolve(distRoot, "state.js"),
|
|
873
|
+
"utf-8"
|
|
874
|
+
).then((js) => this.stateJs = js);
|
|
875
|
+
}
|
|
876
|
+
faviconUrl;
|
|
877
|
+
prefixTesterUrl;
|
|
878
|
+
orchestratorScripts;
|
|
879
|
+
testerScripts;
|
|
880
|
+
manifest;
|
|
881
|
+
testerHtml;
|
|
882
|
+
orchestratorHtml;
|
|
883
|
+
injectorJs;
|
|
884
|
+
stateJs;
|
|
885
|
+
state;
|
|
886
|
+
provider;
|
|
887
|
+
vite;
|
|
888
|
+
stackTraceOptions;
|
|
889
|
+
setServer(server) {
|
|
890
|
+
this.vite = server;
|
|
891
|
+
}
|
|
892
|
+
getSerializableConfig() {
|
|
893
|
+
const config = wrapConfig(this.project.getSerializableConfig());
|
|
894
|
+
config.env ??= {};
|
|
895
|
+
config.env.VITEST_BROWSER_DEBUG = process.env.VITEST_BROWSER_DEBUG || "";
|
|
896
|
+
return config;
|
|
897
|
+
}
|
|
898
|
+
resolveTesterUrl(pathname) {
|
|
899
|
+
const [contextId, testFile] = pathname.slice(this.prefixTesterUrl.length).split("/");
|
|
900
|
+
const decodedTestFile = decodeURIComponent(testFile);
|
|
901
|
+
return { contextId, testFile: decodedTestFile };
|
|
902
|
+
}
|
|
903
|
+
async formatScripts(scripts) {
|
|
904
|
+
if (!scripts?.length) {
|
|
905
|
+
return "";
|
|
906
|
+
}
|
|
907
|
+
const server = this.vite;
|
|
908
|
+
const promises = scripts.map(
|
|
909
|
+
async ({ content, src, async, id, type = "module" }, index) => {
|
|
910
|
+
const srcLink = (src ? (await server.pluginContainer.resolveId(src))?.id : void 0) || src;
|
|
911
|
+
const transformId = srcLink || join(server.config.root, `virtual__${id || `injected-${index}.js`}`);
|
|
912
|
+
await server.moduleGraph.ensureEntryFromUrl(transformId);
|
|
913
|
+
const contentProcessed = content && type === "module" ? (await server.pluginContainer.transform(content, transformId)).code : content;
|
|
914
|
+
return `<script type="${type}"${async ? " async" : ""}${srcLink ? ` src="${slash(`/@fs/${srcLink}`)}"` : ""}>${contentProcessed || ""}<\/script>`;
|
|
915
|
+
}
|
|
916
|
+
);
|
|
917
|
+
return (await Promise.all(promises)).join("\n");
|
|
918
|
+
}
|
|
919
|
+
async initBrowserProvider() {
|
|
920
|
+
if (this.provider) {
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
const Provider = await getBrowserProvider(this.project.config.browser, this.project);
|
|
924
|
+
this.provider = new Provider();
|
|
925
|
+
const browser = this.project.config.browser.name;
|
|
926
|
+
if (!browser) {
|
|
927
|
+
throw new Error(
|
|
928
|
+
`[${this.project.getName()}] Browser name is required. Please, set \`test.browser.name\` option manually.`
|
|
929
|
+
);
|
|
930
|
+
}
|
|
931
|
+
const supportedBrowsers = this.provider.getSupportedBrowsers();
|
|
932
|
+
if (supportedBrowsers.length && !supportedBrowsers.includes(browser)) {
|
|
933
|
+
throw new Error(
|
|
934
|
+
`[${this.project.getName()}] Browser "${browser}" is not supported by the browser provider "${this.provider.name}". Supported browsers: ${supportedBrowsers.join(", ")}.`
|
|
935
|
+
);
|
|
936
|
+
}
|
|
937
|
+
const providerOptions = this.project.config.browser.providerOptions;
|
|
938
|
+
await this.provider.initialize(this.project, {
|
|
939
|
+
browser,
|
|
940
|
+
options: providerOptions
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
parseErrorStacktrace(e, options = {}) {
|
|
944
|
+
return parseErrorStacktrace(e, {
|
|
945
|
+
...this.stackTraceOptions,
|
|
946
|
+
...options
|
|
947
|
+
});
|
|
948
|
+
}
|
|
949
|
+
parseStacktrace(trace, options = {}) {
|
|
950
|
+
return parseStacktrace(trace, {
|
|
951
|
+
...this.stackTraceOptions,
|
|
952
|
+
...options
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
cdpSessions = /* @__PURE__ */ new Map();
|
|
956
|
+
async ensureCDPHandler(contextId, sessionId) {
|
|
957
|
+
const cachedHandler = this.state.cdps.get(sessionId);
|
|
958
|
+
if (cachedHandler) {
|
|
959
|
+
return cachedHandler;
|
|
960
|
+
}
|
|
961
|
+
const provider = this.provider;
|
|
962
|
+
if (!provider.getCDPSession) {
|
|
963
|
+
throw new Error(`CDP is not supported by the provider "${provider.name}".`);
|
|
964
|
+
}
|
|
965
|
+
const promise = this.cdpSessions.get(sessionId) ?? await (async () => {
|
|
966
|
+
const promise2 = provider.getCDPSession(contextId).finally(() => {
|
|
967
|
+
this.cdpSessions.delete(sessionId);
|
|
968
|
+
});
|
|
969
|
+
this.cdpSessions.set(sessionId, promise2);
|
|
970
|
+
return promise2;
|
|
971
|
+
})();
|
|
972
|
+
const session = await promise;
|
|
973
|
+
const rpc = this.state.testers.get(sessionId);
|
|
974
|
+
if (!rpc) {
|
|
975
|
+
throw new Error(`Tester RPC "${sessionId}" was not established.`);
|
|
976
|
+
}
|
|
977
|
+
const handler = new BrowserServerCDPHandler(session, rpc);
|
|
978
|
+
this.state.cdps.set(
|
|
979
|
+
sessionId,
|
|
980
|
+
handler
|
|
981
|
+
);
|
|
982
|
+
return handler;
|
|
983
|
+
}
|
|
984
|
+
async close() {
|
|
985
|
+
await this.vite.close();
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
function wrapConfig(config) {
|
|
989
|
+
return {
|
|
990
|
+
...config,
|
|
991
|
+
// workaround RegExp serialization
|
|
992
|
+
testNamePattern: config.testNamePattern ? config.testNamePattern.toString() : void 0
|
|
993
|
+
};
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
const click = async (context, selector, options = {}) => {
|
|
997
|
+
const provider = context.provider;
|
|
998
|
+
if (provider instanceof PlaywrightBrowserProvider) {
|
|
999
|
+
const tester = context.iframe;
|
|
1000
|
+
await tester.locator(`css=${selector}`).click({
|
|
1001
|
+
timeout: 1e3,
|
|
1002
|
+
...options
|
|
1003
|
+
});
|
|
1004
|
+
} else if (provider instanceof WebdriverBrowserProvider) {
|
|
1005
|
+
const browser = context.browser;
|
|
1006
|
+
await browser.$(selector).click(options);
|
|
1007
|
+
} else {
|
|
1008
|
+
throw new TypeError(`Provider "${provider.name}" doesn't support click command`);
|
|
1009
|
+
}
|
|
1010
|
+
};
|
|
1011
|
+
const dblClick = async (context, selector, options = {}) => {
|
|
1012
|
+
const provider = context.provider;
|
|
1013
|
+
if (provider instanceof PlaywrightBrowserProvider) {
|
|
1014
|
+
const tester = context.iframe;
|
|
1015
|
+
await tester.locator(`css=${selector}`).dblclick(options);
|
|
1016
|
+
} else if (provider instanceof WebdriverBrowserProvider) {
|
|
1017
|
+
const browser = context.browser;
|
|
1018
|
+
await browser.$(selector).doubleClick();
|
|
1019
|
+
} else {
|
|
1020
|
+
throw new TypeError(`Provider "${provider.name}" doesn't support dblClick command`);
|
|
1021
|
+
}
|
|
1022
|
+
};
|
|
1023
|
+
const tripleClick = async (context, selector, options = {}) => {
|
|
1024
|
+
const provider = context.provider;
|
|
1025
|
+
if (provider instanceof PlaywrightBrowserProvider) {
|
|
1026
|
+
const tester = context.iframe;
|
|
1027
|
+
await tester.locator(`css=${selector}`).click({
|
|
1028
|
+
timeout: 1e3,
|
|
1029
|
+
...options,
|
|
1030
|
+
clickCount: 3
|
|
1031
|
+
});
|
|
1032
|
+
} else if (provider instanceof WebdriverBrowserProvider) {
|
|
1033
|
+
const browser = context.browser;
|
|
1034
|
+
await browser.action("pointer", { parameters: { pointerType: "mouse" } }).move({ origin: await browser.$(selector) }).down().up().pause(50).down().up().pause(50).down().up().pause(50).perform();
|
|
1035
|
+
} else {
|
|
1036
|
+
throw new TypeError(`Provider "${provider.name}" doesn't support tripleClick command`);
|
|
1037
|
+
}
|
|
1038
|
+
};
|
|
1039
|
+
|
|
1040
|
+
var clickableInputTypes;
|
|
1041
|
+
(function(clickableInputTypes) {
|
|
1042
|
+
clickableInputTypes["button"] = "button";
|
|
1043
|
+
clickableInputTypes["color"] = "color";
|
|
1044
|
+
clickableInputTypes["file"] = "file";
|
|
1045
|
+
clickableInputTypes["image"] = "image";
|
|
1046
|
+
clickableInputTypes["reset"] = "reset";
|
|
1047
|
+
clickableInputTypes["submit"] = "submit";
|
|
1048
|
+
clickableInputTypes["checkbox"] = "checkbox";
|
|
1049
|
+
clickableInputTypes["radio"] = "radio";
|
|
1050
|
+
})(clickableInputTypes || (clickableInputTypes = {}));
|
|
1051
|
+
|
|
1052
|
+
const ClipboardStubControl = Symbol('Manage ClipboardSub');
|
|
1053
|
+
function isClipboardStub(clipboard) {
|
|
1054
|
+
return !!(clipboard === null || clipboard === void 0 ? void 0 : clipboard[ClipboardStubControl]);
|
|
1055
|
+
}
|
|
1056
|
+
function resetClipboardStubOnView(window) {
|
|
1057
|
+
if (isClipboardStub(window.navigator.clipboard)) {
|
|
1058
|
+
window.navigator.clipboard[ClipboardStubControl].resetClipboardStub();
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
function detachClipboardStubFromView(window) {
|
|
1062
|
+
if (isClipboardStub(window.navigator.clipboard)) {
|
|
1063
|
+
window.navigator.clipboard[ClipboardStubControl].detachClipboardStub();
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
const g = globalThis;
|
|
1067
|
+
/* istanbul ignore else */ if (typeof g.afterEach === 'function') {
|
|
1068
|
+
g.afterEach(()=>resetClipboardStubOnView(globalThis.window));
|
|
1069
|
+
}
|
|
1070
|
+
/* istanbul ignore else */ if (typeof g.afterAll === 'function') {
|
|
1071
|
+
g.afterAll(()=>detachClipboardStubFromView(globalThis.window));
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
var editableInputTypes;
|
|
1075
|
+
(function(editableInputTypes) {
|
|
1076
|
+
editableInputTypes["text"] = "text";
|
|
1077
|
+
editableInputTypes["date"] = "date";
|
|
1078
|
+
editableInputTypes["datetime-local"] = "datetime-local";
|
|
1079
|
+
editableInputTypes["email"] = "email";
|
|
1080
|
+
editableInputTypes["month"] = "month";
|
|
1081
|
+
editableInputTypes["number"] = "number";
|
|
1082
|
+
editableInputTypes["password"] = "password";
|
|
1083
|
+
editableInputTypes["search"] = "search";
|
|
1084
|
+
editableInputTypes["tel"] = "tel";
|
|
1085
|
+
editableInputTypes["time"] = "time";
|
|
1086
|
+
editableInputTypes["url"] = "url";
|
|
1087
|
+
editableInputTypes["week"] = "week";
|
|
1088
|
+
})(editableInputTypes || (editableInputTypes = {}));
|
|
1089
|
+
|
|
1090
|
+
var maxLengthSupportedTypes;
|
|
1091
|
+
(function(maxLengthSupportedTypes) {
|
|
1092
|
+
maxLengthSupportedTypes["email"] = "email";
|
|
1093
|
+
maxLengthSupportedTypes["password"] = "password";
|
|
1094
|
+
maxLengthSupportedTypes["search"] = "search";
|
|
1095
|
+
maxLengthSupportedTypes["telephone"] = "telephone";
|
|
1096
|
+
maxLengthSupportedTypes["text"] = "text";
|
|
1097
|
+
maxLengthSupportedTypes["url"] = "url";
|
|
1098
|
+
})(maxLengthSupportedTypes || (maxLengthSupportedTypes = {}));
|
|
1099
|
+
|
|
1100
|
+
var bracketDict;
|
|
1101
|
+
(function(bracketDict) {
|
|
1102
|
+
bracketDict["{"] = "}";
|
|
1103
|
+
bracketDict["["] = "]";
|
|
1104
|
+
})(bracketDict || (bracketDict = {}));
|
|
1105
|
+
/**
|
|
1106
|
+
* Read the next key definition from user input
|
|
1107
|
+
*
|
|
1108
|
+
* Describe key per `{descriptor}` or `[descriptor]`.
|
|
1109
|
+
* Everything else will be interpreted as a single character as descriptor - e.g. `a`.
|
|
1110
|
+
* Brackets `{` and `[` can be escaped by doubling - e.g. `foo[[bar` translates to `foo[bar`.
|
|
1111
|
+
* A previously pressed key can be released per `{/descriptor}`.
|
|
1112
|
+
* Keeping the key pressed can be written as `{descriptor>}`.
|
|
1113
|
+
* When keeping the key pressed you can choose how long the key is pressed `{descriptor>3}`.
|
|
1114
|
+
* You can then release the key per `{descriptor>3/}` or keep it pressed and continue with the next key.
|
|
1115
|
+
*/ function readNextDescriptor(text, context) {
|
|
1116
|
+
let pos = 0;
|
|
1117
|
+
const startBracket = text[pos] in bracketDict ? text[pos] : '';
|
|
1118
|
+
pos += startBracket.length;
|
|
1119
|
+
const isEscapedChar = new RegExp(`^\\${startBracket}{2}`).test(text);
|
|
1120
|
+
const type = isEscapedChar ? '' : startBracket;
|
|
1121
|
+
return {
|
|
1122
|
+
type,
|
|
1123
|
+
...type === '' ? readPrintableChar(text, pos) : readTag(text, pos, type)
|
|
1124
|
+
};
|
|
1125
|
+
}
|
|
1126
|
+
function readPrintableChar(text, pos, context) {
|
|
1127
|
+
const descriptor = text[pos];
|
|
1128
|
+
assertDescriptor(descriptor, text, pos);
|
|
1129
|
+
pos += descriptor.length;
|
|
1130
|
+
return {
|
|
1131
|
+
consumedLength: pos,
|
|
1132
|
+
descriptor,
|
|
1133
|
+
releasePrevious: false,
|
|
1134
|
+
releaseSelf: true,
|
|
1135
|
+
repeat: 1
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
function readTag(text, pos, startBracket, context) {
|
|
1139
|
+
var _text_slice_match, _text_slice_match1;
|
|
1140
|
+
const releasePreviousModifier = text[pos] === '/' ? '/' : '';
|
|
1141
|
+
pos += releasePreviousModifier.length;
|
|
1142
|
+
const escapedDescriptor = startBracket === '{' && text[pos] === '\\';
|
|
1143
|
+
pos += Number(escapedDescriptor);
|
|
1144
|
+
const descriptor = escapedDescriptor ? text[pos] : (_text_slice_match = text.slice(pos).match(startBracket === '{' ? /^\w+|^[^}>/]/ : /^\w+/)) === null || _text_slice_match === void 0 ? void 0 : _text_slice_match[0];
|
|
1145
|
+
assertDescriptor(descriptor, text, pos);
|
|
1146
|
+
pos += descriptor.length;
|
|
1147
|
+
var _text_slice_match_;
|
|
1148
|
+
const repeatModifier = (_text_slice_match_ = (_text_slice_match1 = text.slice(pos).match(/^>\d+/)) === null || _text_slice_match1 === void 0 ? void 0 : _text_slice_match1[0]) !== null && _text_slice_match_ !== void 0 ? _text_slice_match_ : '';
|
|
1149
|
+
pos += repeatModifier.length;
|
|
1150
|
+
const releaseSelfModifier = text[pos] === '/' || !repeatModifier && text[pos] === '>' ? text[pos] : '';
|
|
1151
|
+
pos += releaseSelfModifier.length;
|
|
1152
|
+
const expectedEndBracket = bracketDict[startBracket];
|
|
1153
|
+
const endBracket = text[pos] === expectedEndBracket ? expectedEndBracket : '';
|
|
1154
|
+
if (!endBracket) {
|
|
1155
|
+
throw new Error(getErrorMessage([
|
|
1156
|
+
!repeatModifier && 'repeat modifier',
|
|
1157
|
+
!releaseSelfModifier && 'release modifier',
|
|
1158
|
+
`"${expectedEndBracket}"`
|
|
1159
|
+
].filter(Boolean).join(' or '), text[pos], text));
|
|
1160
|
+
}
|
|
1161
|
+
pos += endBracket.length;
|
|
1162
|
+
return {
|
|
1163
|
+
consumedLength: pos,
|
|
1164
|
+
descriptor,
|
|
1165
|
+
releasePrevious: !!releasePreviousModifier,
|
|
1166
|
+
repeat: repeatModifier ? Math.max(Number(repeatModifier.substr(1)), 1) : 1,
|
|
1167
|
+
releaseSelf: hasReleaseSelf(releaseSelfModifier, repeatModifier)
|
|
1168
|
+
};
|
|
1169
|
+
}
|
|
1170
|
+
function assertDescriptor(descriptor, text, pos, context) {
|
|
1171
|
+
if (!descriptor) {
|
|
1172
|
+
throw new Error(getErrorMessage('key descriptor', text[pos], text));
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
function hasReleaseSelf(releaseSelfModifier, repeatModifier) {
|
|
1176
|
+
if (releaseSelfModifier) {
|
|
1177
|
+
return releaseSelfModifier === '/';
|
|
1178
|
+
}
|
|
1179
|
+
if (repeatModifier) {
|
|
1180
|
+
return false;
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
function getErrorMessage(expected, found, text, context) {
|
|
1184
|
+
return `Expected ${expected} but found "${found !== null && found !== void 0 ? found : ''}" in "${text}"
|
|
1185
|
+
See ${`https://testing-library.com/docs/user-event/keyboard`}
|
|
1186
|
+
for more information about how userEvent parses your input.`;
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
var ApiLevel;
|
|
1190
|
+
(function(ApiLevel) {
|
|
1191
|
+
ApiLevel[ApiLevel["Trigger"] = 2] = "Trigger";
|
|
1192
|
+
ApiLevel[ApiLevel["Call"] = 1] = "Call";
|
|
1193
|
+
})(ApiLevel || (ApiLevel = {}));
|
|
1194
|
+
|
|
1195
|
+
var PointerEventsCheckLevel;
|
|
1196
|
+
(function(PointerEventsCheckLevel) {
|
|
1197
|
+
/**
|
|
1198
|
+
* Check pointer events on every user interaction that triggers a bunch of events.
|
|
1199
|
+
* E.g. once for releasing a mouse button even though this triggers `pointerup`, `mouseup`, `click`, etc...
|
|
1200
|
+
*/ PointerEventsCheckLevel[PointerEventsCheckLevel["EachTrigger"] = 4] = "EachTrigger";
|
|
1201
|
+
/** Check each target once per call to pointer (related) API */ PointerEventsCheckLevel[PointerEventsCheckLevel["EachApiCall"] = 2] = "EachApiCall";
|
|
1202
|
+
/** Check each event target once */ PointerEventsCheckLevel[PointerEventsCheckLevel["EachTarget"] = 1] = "EachTarget";
|
|
1203
|
+
/** No pointer events check */ PointerEventsCheckLevel[PointerEventsCheckLevel["Never"] = 0] = "Never";
|
|
1204
|
+
})(PointerEventsCheckLevel || (PointerEventsCheckLevel = {}));
|
|
1205
|
+
|
|
1206
|
+
/**
|
|
1207
|
+
* Parse key defintions per `keyboardMap`
|
|
1208
|
+
*
|
|
1209
|
+
* Keys can be referenced by `{key}` or `{special}` as well as physical locations per `[code]`.
|
|
1210
|
+
* Everything else will be interpreted as a typed character - e.g. `a`.
|
|
1211
|
+
* Brackets `{` and `[` can be escaped by doubling - e.g. `foo[[bar` translates to `foo[bar`.
|
|
1212
|
+
* Keeping the key pressed can be written as `{key>}`.
|
|
1213
|
+
* When keeping the key pressed you can choose how long (how many keydown and keypress) the key is pressed `{key>3}`.
|
|
1214
|
+
* You can then release the key per `{key>3/}` or keep it pressed and continue with the next key.
|
|
1215
|
+
*/ function parseKeyDef(keyboardMap, text) {
|
|
1216
|
+
const defs = [];
|
|
1217
|
+
do {
|
|
1218
|
+
const { type, descriptor, consumedLength, releasePrevious, releaseSelf = true, repeat } = readNextDescriptor(text);
|
|
1219
|
+
var _keyboardMap_find;
|
|
1220
|
+
const keyDef = (_keyboardMap_find = keyboardMap.find((def)=>{
|
|
1221
|
+
if (type === '[') {
|
|
1222
|
+
var _def_code;
|
|
1223
|
+
return ((_def_code = def.code) === null || _def_code === void 0 ? void 0 : _def_code.toLowerCase()) === descriptor.toLowerCase();
|
|
1224
|
+
} else if (type === '{') {
|
|
1225
|
+
var _def_key;
|
|
1226
|
+
return ((_def_key = def.key) === null || _def_key === void 0 ? void 0 : _def_key.toLowerCase()) === descriptor.toLowerCase();
|
|
1227
|
+
}
|
|
1228
|
+
return def.key === descriptor;
|
|
1229
|
+
})) !== null && _keyboardMap_find !== void 0 ? _keyboardMap_find : {
|
|
1230
|
+
key: 'Unknown',
|
|
1231
|
+
code: 'Unknown',
|
|
1232
|
+
[type === '[' ? 'code' : 'key']: descriptor
|
|
1233
|
+
};
|
|
1234
|
+
defs.push({
|
|
1235
|
+
keyDef,
|
|
1236
|
+
releasePrevious,
|
|
1237
|
+
releaseSelf,
|
|
1238
|
+
repeat
|
|
1239
|
+
});
|
|
1240
|
+
text = text.slice(consumedLength);
|
|
1241
|
+
}while (text)
|
|
1242
|
+
return defs;
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
var DOM_KEY_LOCATION;
|
|
1246
|
+
(function(DOM_KEY_LOCATION) {
|
|
1247
|
+
DOM_KEY_LOCATION[DOM_KEY_LOCATION["STANDARD"] = 0] = "STANDARD";
|
|
1248
|
+
DOM_KEY_LOCATION[DOM_KEY_LOCATION["LEFT"] = 1] = "LEFT";
|
|
1249
|
+
DOM_KEY_LOCATION[DOM_KEY_LOCATION["RIGHT"] = 2] = "RIGHT";
|
|
1250
|
+
DOM_KEY_LOCATION[DOM_KEY_LOCATION["NUMPAD"] = 3] = "NUMPAD";
|
|
1251
|
+
})(DOM_KEY_LOCATION || (DOM_KEY_LOCATION = {}));
|
|
1252
|
+
|
|
1253
|
+
/**
|
|
1254
|
+
* Mapping for a default US-104-QWERTY keyboard
|
|
1255
|
+
*/ const defaultKeyMap = [
|
|
1256
|
+
// alphanumeric keys
|
|
1257
|
+
...'0123456789'.split('').map((c)=>({
|
|
1258
|
+
code: `Digit${c}`,
|
|
1259
|
+
key: c
|
|
1260
|
+
})),
|
|
1261
|
+
...')!@#$%^&*('.split('').map((c, i)=>({
|
|
1262
|
+
code: `Digit${i}`,
|
|
1263
|
+
key: c,
|
|
1264
|
+
shiftKey: true
|
|
1265
|
+
})),
|
|
1266
|
+
...'abcdefghijklmnopqrstuvwxyz'.split('').map((c)=>({
|
|
1267
|
+
code: `Key${c.toUpperCase()}`,
|
|
1268
|
+
key: c
|
|
1269
|
+
})),
|
|
1270
|
+
...'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('').map((c)=>({
|
|
1271
|
+
code: `Key${c}`,
|
|
1272
|
+
key: c,
|
|
1273
|
+
shiftKey: true
|
|
1274
|
+
})),
|
|
1275
|
+
// alphanumeric block - functional
|
|
1276
|
+
{
|
|
1277
|
+
code: 'Space',
|
|
1278
|
+
key: ' '
|
|
1279
|
+
},
|
|
1280
|
+
{
|
|
1281
|
+
code: 'AltLeft',
|
|
1282
|
+
key: 'Alt',
|
|
1283
|
+
location: DOM_KEY_LOCATION.LEFT
|
|
1284
|
+
},
|
|
1285
|
+
{
|
|
1286
|
+
code: 'AltRight',
|
|
1287
|
+
key: 'Alt',
|
|
1288
|
+
location: DOM_KEY_LOCATION.RIGHT
|
|
1289
|
+
},
|
|
1290
|
+
{
|
|
1291
|
+
code: 'ShiftLeft',
|
|
1292
|
+
key: 'Shift',
|
|
1293
|
+
location: DOM_KEY_LOCATION.LEFT
|
|
1294
|
+
},
|
|
1295
|
+
{
|
|
1296
|
+
code: 'ShiftRight',
|
|
1297
|
+
key: 'Shift',
|
|
1298
|
+
location: DOM_KEY_LOCATION.RIGHT
|
|
1299
|
+
},
|
|
1300
|
+
{
|
|
1301
|
+
code: 'ControlLeft',
|
|
1302
|
+
key: 'Control',
|
|
1303
|
+
location: DOM_KEY_LOCATION.LEFT
|
|
1304
|
+
},
|
|
1305
|
+
{
|
|
1306
|
+
code: 'ControlRight',
|
|
1307
|
+
key: 'Control',
|
|
1308
|
+
location: DOM_KEY_LOCATION.RIGHT
|
|
1309
|
+
},
|
|
1310
|
+
{
|
|
1311
|
+
code: 'MetaLeft',
|
|
1312
|
+
key: 'Meta',
|
|
1313
|
+
location: DOM_KEY_LOCATION.LEFT
|
|
1314
|
+
},
|
|
1315
|
+
{
|
|
1316
|
+
code: 'MetaRight',
|
|
1317
|
+
key: 'Meta',
|
|
1318
|
+
location: DOM_KEY_LOCATION.RIGHT
|
|
1319
|
+
},
|
|
1320
|
+
{
|
|
1321
|
+
code: 'OSLeft',
|
|
1322
|
+
key: 'OS',
|
|
1323
|
+
location: DOM_KEY_LOCATION.LEFT
|
|
1324
|
+
},
|
|
1325
|
+
{
|
|
1326
|
+
code: 'OSRight',
|
|
1327
|
+
key: 'OS',
|
|
1328
|
+
location: DOM_KEY_LOCATION.RIGHT
|
|
1329
|
+
},
|
|
1330
|
+
{
|
|
1331
|
+
code: 'Tab',
|
|
1332
|
+
key: 'Tab'
|
|
1333
|
+
},
|
|
1334
|
+
{
|
|
1335
|
+
code: 'CapsLock',
|
|
1336
|
+
key: 'CapsLock'
|
|
1337
|
+
},
|
|
1338
|
+
{
|
|
1339
|
+
code: 'Backspace',
|
|
1340
|
+
key: 'Backspace'
|
|
1341
|
+
},
|
|
1342
|
+
{
|
|
1343
|
+
code: 'Enter',
|
|
1344
|
+
key: 'Enter'
|
|
1345
|
+
},
|
|
1346
|
+
// function
|
|
1347
|
+
{
|
|
1348
|
+
code: 'Escape',
|
|
1349
|
+
key: 'Escape'
|
|
1350
|
+
},
|
|
1351
|
+
// arrows
|
|
1352
|
+
{
|
|
1353
|
+
code: 'ArrowUp',
|
|
1354
|
+
key: 'ArrowUp'
|
|
1355
|
+
},
|
|
1356
|
+
{
|
|
1357
|
+
code: 'ArrowDown',
|
|
1358
|
+
key: 'ArrowDown'
|
|
1359
|
+
},
|
|
1360
|
+
{
|
|
1361
|
+
code: 'ArrowLeft',
|
|
1362
|
+
key: 'ArrowLeft'
|
|
1363
|
+
},
|
|
1364
|
+
{
|
|
1365
|
+
code: 'ArrowRight',
|
|
1366
|
+
key: 'ArrowRight'
|
|
1367
|
+
},
|
|
1368
|
+
// control pad
|
|
1369
|
+
{
|
|
1370
|
+
code: 'Home',
|
|
1371
|
+
key: 'Home'
|
|
1372
|
+
},
|
|
1373
|
+
{
|
|
1374
|
+
code: 'End',
|
|
1375
|
+
key: 'End'
|
|
1376
|
+
},
|
|
1377
|
+
{
|
|
1378
|
+
code: 'Delete',
|
|
1379
|
+
key: 'Delete'
|
|
1380
|
+
},
|
|
1381
|
+
{
|
|
1382
|
+
code: 'PageUp',
|
|
1383
|
+
key: 'PageUp'
|
|
1384
|
+
},
|
|
1385
|
+
{
|
|
1386
|
+
code: 'PageDown',
|
|
1387
|
+
key: 'PageDown'
|
|
1388
|
+
},
|
|
1389
|
+
// Special keys that are not part of a default US-layout but included for specific behavior
|
|
1390
|
+
{
|
|
1391
|
+
code: 'Fn',
|
|
1392
|
+
key: 'Fn'
|
|
1393
|
+
},
|
|
1394
|
+
{
|
|
1395
|
+
code: 'Symbol',
|
|
1396
|
+
key: 'Symbol'
|
|
1397
|
+
},
|
|
1398
|
+
{
|
|
1399
|
+
code: 'AltRight',
|
|
1400
|
+
key: 'AltGraph'
|
|
1401
|
+
}
|
|
1402
|
+
];
|
|
1403
|
+
|
|
1404
|
+
const keyboard = async (context, text) => {
|
|
1405
|
+
function focusIframe() {
|
|
1406
|
+
if (!document.activeElement || document.activeElement.ownerDocument !== document || document.activeElement === document.body) {
|
|
1407
|
+
window.focus();
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
if (context.provider instanceof PlaywrightBrowserProvider) {
|
|
1411
|
+
const frame = await context.frame();
|
|
1412
|
+
await frame.evaluate(focusIframe);
|
|
1413
|
+
} else if (context.provider instanceof WebdriverBrowserProvider) {
|
|
1414
|
+
await context.browser.execute(focusIframe);
|
|
1415
|
+
}
|
|
1416
|
+
await keyboardImplementation(
|
|
1417
|
+
context.provider,
|
|
1418
|
+
context.contextId,
|
|
1419
|
+
text,
|
|
1420
|
+
async () => {
|
|
1421
|
+
function selectAll() {
|
|
1422
|
+
const element = document.activeElement;
|
|
1423
|
+
if (element && element.select) {
|
|
1424
|
+
element.select();
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
if (context.provider instanceof PlaywrightBrowserProvider) {
|
|
1428
|
+
const frame = await context.frame();
|
|
1429
|
+
await frame.evaluate(selectAll);
|
|
1430
|
+
} else if (context.provider instanceof WebdriverBrowserProvider) {
|
|
1431
|
+
await context.browser.execute(selectAll);
|
|
1432
|
+
} else {
|
|
1433
|
+
throw new TypeError(`Provider "${context.provider.name}" does not support selecting all text`);
|
|
1434
|
+
}
|
|
1435
|
+
},
|
|
1436
|
+
false
|
|
1437
|
+
);
|
|
1438
|
+
};
|
|
1439
|
+
async function keyboardImplementation(provider, contextId, text, selectAll, skipRelease) {
|
|
1440
|
+
const pressed = /* @__PURE__ */ new Set();
|
|
1441
|
+
if (provider instanceof PlaywrightBrowserProvider) {
|
|
1442
|
+
const page = provider.getPage(contextId);
|
|
1443
|
+
const actions = parseKeyDef(defaultKeyMap, text);
|
|
1444
|
+
for (const { releasePrevious, releaseSelf, repeat, keyDef } of actions) {
|
|
1445
|
+
const key = keyDef.key;
|
|
1446
|
+
if (pressed.has(key)) {
|
|
1447
|
+
await page.keyboard.up(key);
|
|
1448
|
+
pressed.delete(key);
|
|
1449
|
+
}
|
|
1450
|
+
if (!releasePrevious) {
|
|
1451
|
+
if (key === "selectall") {
|
|
1452
|
+
await selectAll();
|
|
1453
|
+
continue;
|
|
1454
|
+
}
|
|
1455
|
+
for (let i = 1; i <= repeat; i++) {
|
|
1456
|
+
await page.keyboard.down(key);
|
|
1457
|
+
}
|
|
1458
|
+
if (releaseSelf) {
|
|
1459
|
+
await page.keyboard.up(key);
|
|
1460
|
+
} else {
|
|
1461
|
+
pressed.add(key);
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
if (!skipRelease && pressed.size) {
|
|
1466
|
+
for (const key of pressed) {
|
|
1467
|
+
await page.keyboard.up(key);
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
} else if (provider instanceof WebdriverBrowserProvider) {
|
|
1471
|
+
const { Key } = await import('webdriverio');
|
|
1472
|
+
const browser = provider.browser;
|
|
1473
|
+
const actions = parseKeyDef(defaultKeyMap, text);
|
|
1474
|
+
let keyboard2 = browser.action("key");
|
|
1475
|
+
for (const { releasePrevious, releaseSelf, repeat, keyDef } of actions) {
|
|
1476
|
+
let key = keyDef.key;
|
|
1477
|
+
const code = "location" in keyDef ? keyDef.key : keyDef.code;
|
|
1478
|
+
const special = Key[code];
|
|
1479
|
+
if (special) {
|
|
1480
|
+
key = special;
|
|
1481
|
+
}
|
|
1482
|
+
if (pressed.has(key)) {
|
|
1483
|
+
keyboard2.up(key);
|
|
1484
|
+
pressed.delete(key);
|
|
1485
|
+
}
|
|
1486
|
+
if (!releasePrevious) {
|
|
1487
|
+
if (key === "selectall") {
|
|
1488
|
+
await keyboard2.perform();
|
|
1489
|
+
keyboard2 = browser.action("key");
|
|
1490
|
+
await selectAll();
|
|
1491
|
+
continue;
|
|
1492
|
+
}
|
|
1493
|
+
for (let i = 1; i <= repeat; i++) {
|
|
1494
|
+
keyboard2.down(key);
|
|
1495
|
+
}
|
|
1496
|
+
if (releaseSelf) {
|
|
1497
|
+
keyboard2.up(key);
|
|
1498
|
+
} else {
|
|
1499
|
+
pressed.add(key);
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
await keyboard2.perform(skipRelease);
|
|
1504
|
+
}
|
|
1505
|
+
return {
|
|
1506
|
+
pressed
|
|
1507
|
+
};
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
const type = async (context, selector, text, options = {}) => {
|
|
1511
|
+
const { skipClick = false, skipAutoClose = false } = options;
|
|
1512
|
+
if (context.provider instanceof PlaywrightBrowserProvider) {
|
|
1513
|
+
const { iframe } = context;
|
|
1514
|
+
const element = iframe.locator(`css=${selector}`);
|
|
1515
|
+
if (!skipClick) {
|
|
1516
|
+
await element.focus();
|
|
1517
|
+
}
|
|
1518
|
+
await keyboardImplementation(
|
|
1519
|
+
context.provider,
|
|
1520
|
+
context.contextId,
|
|
1521
|
+
text,
|
|
1522
|
+
() => element.selectText(),
|
|
1523
|
+
skipAutoClose
|
|
1524
|
+
);
|
|
1525
|
+
} else if (context.provider instanceof WebdriverBrowserProvider) {
|
|
1526
|
+
const browser = context.browser;
|
|
1527
|
+
const element = browser.$(selector);
|
|
1528
|
+
if (!skipClick && !await element.isFocused()) {
|
|
1529
|
+
await element.click();
|
|
1530
|
+
}
|
|
1531
|
+
await keyboardImplementation(
|
|
1532
|
+
context.provider,
|
|
1533
|
+
context.contextId,
|
|
1534
|
+
text,
|
|
1535
|
+
() => browser.execute(() => {
|
|
1536
|
+
const element2 = document.activeElement;
|
|
1537
|
+
if (element2) {
|
|
1538
|
+
element2.select();
|
|
1539
|
+
}
|
|
1540
|
+
}),
|
|
1541
|
+
skipAutoClose
|
|
1542
|
+
);
|
|
1543
|
+
} else {
|
|
1544
|
+
throw new TypeError(`Provider "${context.provider.name}" does not support typing`);
|
|
1545
|
+
}
|
|
164
1546
|
};
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
1547
|
+
|
|
1548
|
+
const clear = async (context, selector) => {
|
|
1549
|
+
if (context.provider instanceof PlaywrightBrowserProvider) {
|
|
1550
|
+
const { iframe } = context;
|
|
1551
|
+
const element = iframe.locator(`css=${selector}`);
|
|
1552
|
+
await element.clear({
|
|
1553
|
+
timeout: 1e3
|
|
1554
|
+
});
|
|
1555
|
+
} else if (context.provider instanceof WebdriverBrowserProvider) {
|
|
1556
|
+
const browser = context.browser;
|
|
1557
|
+
const element = await browser.$(selector);
|
|
1558
|
+
await element.clearValue();
|
|
1559
|
+
} else {
|
|
1560
|
+
throw new TypeError(`Provider "${context.provider.name}" does not support clearing elements`);
|
|
1561
|
+
}
|
|
1562
|
+
};
|
|
1563
|
+
|
|
1564
|
+
const fill = async (context, selector, text, options = {}) => {
|
|
1565
|
+
if (context.provider instanceof PlaywrightBrowserProvider) {
|
|
1566
|
+
const { iframe } = context;
|
|
1567
|
+
const element = iframe.locator(`css=${selector}`);
|
|
1568
|
+
await element.fill(text, { timeout: 1e3, ...options });
|
|
1569
|
+
} else if (context.provider instanceof WebdriverBrowserProvider) {
|
|
1570
|
+
const browser = context.browser;
|
|
1571
|
+
await browser.$(selector).setValue(text);
|
|
1572
|
+
} else {
|
|
1573
|
+
throw new TypeError(`Provider "${context.provider.name}" does not support clearing elements`);
|
|
1574
|
+
}
|
|
1575
|
+
};
|
|
1576
|
+
|
|
1577
|
+
const selectOptions = async (context, selector, userValues, options = {}) => {
|
|
1578
|
+
if (context.provider instanceof PlaywrightBrowserProvider) {
|
|
1579
|
+
const value = userValues;
|
|
1580
|
+
const { iframe } = context;
|
|
1581
|
+
const selectElement = iframe.locator(`css=${selector}`);
|
|
1582
|
+
const values = await Promise.all(value.map(async (v) => {
|
|
1583
|
+
if (typeof v === "string") {
|
|
1584
|
+
return v;
|
|
1585
|
+
}
|
|
1586
|
+
const elementHandler = await iframe.locator(`css=${v.element}`).elementHandle();
|
|
1587
|
+
if (!elementHandler) {
|
|
1588
|
+
throw new Error(`Element not found: ${v.element}`);
|
|
1589
|
+
}
|
|
1590
|
+
return elementHandler;
|
|
1591
|
+
}));
|
|
1592
|
+
await selectElement.selectOption(values, {
|
|
1593
|
+
timeout: 1e3,
|
|
1594
|
+
...options
|
|
1595
|
+
});
|
|
1596
|
+
} else if (context.provider instanceof WebdriverBrowserProvider) {
|
|
1597
|
+
const values = userValues;
|
|
1598
|
+
if (!values.length) {
|
|
1599
|
+
return;
|
|
1600
|
+
}
|
|
1601
|
+
const browser = context.browser;
|
|
1602
|
+
if (values.length === 1 && "index" in values[0]) {
|
|
1603
|
+
const selectElement = browser.$(selector);
|
|
1604
|
+
await selectElement.selectByIndex(values[0].index);
|
|
1605
|
+
} else {
|
|
1606
|
+
throw new Error(`Provider "webdriverio" doesn't support selecting multiple values at once`);
|
|
1607
|
+
}
|
|
1608
|
+
} else {
|
|
1609
|
+
throw new TypeError(`Provider "${context.provider.name}" doesn't support selectOptions command`);
|
|
1610
|
+
}
|
|
168
1611
|
};
|
|
169
1612
|
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
1613
|
+
const tab = async (context, options = {}) => {
|
|
1614
|
+
const provider = context.provider;
|
|
1615
|
+
if (provider instanceof PlaywrightBrowserProvider) {
|
|
1616
|
+
const page = context.page;
|
|
1617
|
+
await page.keyboard.press(options.shift === true ? "Shift+Tab" : "Tab");
|
|
174
1618
|
return;
|
|
175
1619
|
}
|
|
176
|
-
if (provider
|
|
177
|
-
const
|
|
178
|
-
const
|
|
179
|
-
await
|
|
180
|
-
const xpath = `//${element}`;
|
|
181
|
-
await (await page.$(xpath)).click(options);
|
|
1620
|
+
if (provider instanceof WebdriverBrowserProvider) {
|
|
1621
|
+
const { Key } = await import('webdriverio');
|
|
1622
|
+
const browser = context.browser;
|
|
1623
|
+
await browser.keys(options.shift === true ? [Key.Shift, Key.Tab] : [Key.Tab]);
|
|
182
1624
|
return;
|
|
183
1625
|
}
|
|
184
|
-
throw new Error(`Provider "${provider.name}" doesn't support
|
|
1626
|
+
throw new Error(`Provider "${provider.name}" doesn't support tab command`);
|
|
1627
|
+
};
|
|
1628
|
+
|
|
1629
|
+
const dragAndDrop = async (context, source, target, options) => {
|
|
1630
|
+
if (context.provider instanceof PlaywrightBrowserProvider) {
|
|
1631
|
+
const frame = await context.frame();
|
|
1632
|
+
await frame.dragAndDrop(
|
|
1633
|
+
`css=${source}`,
|
|
1634
|
+
`css=${target}`,
|
|
1635
|
+
{
|
|
1636
|
+
timeout: 1e3,
|
|
1637
|
+
...options
|
|
1638
|
+
}
|
|
1639
|
+
);
|
|
1640
|
+
} else if (context.provider instanceof WebdriverBrowserProvider) {
|
|
1641
|
+
const $source = context.browser.$(source);
|
|
1642
|
+
const $target = context.browser.$(target);
|
|
1643
|
+
const duration = options?.duration ?? 10;
|
|
1644
|
+
await context.browser.action("pointer").move({ duration: 0, origin: $source, x: 0, y: 0 }).down({ button: 0 }).move({ duration: 0, origin: "pointer", x: 0, y: 0 }).pause(duration).move({ duration: 0, origin: $target, x: 0, y: 0 }).move({ duration: 0, origin: "pointer", x: 1, y: 0 }).move({ duration: 0, origin: "pointer", x: -1, y: 0 }).up({ button: 0 }).perform();
|
|
1645
|
+
} else {
|
|
1646
|
+
throw new TypeError(`Provider "${context.provider.name}" does not support dragging elements`);
|
|
1647
|
+
}
|
|
1648
|
+
};
|
|
1649
|
+
|
|
1650
|
+
const hover = async (context, selector, options = {}) => {
|
|
1651
|
+
if (context.provider instanceof PlaywrightBrowserProvider) {
|
|
1652
|
+
await context.iframe.locator(`css=${selector}`).hover({
|
|
1653
|
+
timeout: 1e3,
|
|
1654
|
+
...options
|
|
1655
|
+
});
|
|
1656
|
+
} else if (context.provider instanceof WebdriverBrowserProvider) {
|
|
1657
|
+
const browser = context.browser;
|
|
1658
|
+
await browser.$(selector).moveTo(options);
|
|
1659
|
+
} else {
|
|
1660
|
+
throw new TypeError(`Provider "${context.provider.name}" does not support hover`);
|
|
1661
|
+
}
|
|
185
1662
|
};
|
|
186
1663
|
|
|
187
1664
|
function assertFileAccess(path, project) {
|
|
188
|
-
if (!isFileServingAllowed(path, project.server) && !isFileServingAllowed(path, project.ctx.server))
|
|
189
|
-
throw new Error(
|
|
1665
|
+
if (!isFileServingAllowed$1(path, project.server) && !isFileServingAllowed$1(path, project.ctx.server)) {
|
|
1666
|
+
throw new Error(
|
|
1667
|
+
`Access denied to "${path}". See Vite config documentation for "server.fs": https://vitejs.dev/config/server-options.html#server-fs-strict.`
|
|
1668
|
+
);
|
|
1669
|
+
}
|
|
190
1670
|
}
|
|
191
1671
|
const readFile = async ({ project, testPath = process.cwd() }, path, options = {}) => {
|
|
192
1672
|
const filepath = resolve$1(dirname$1(testPath), path);
|
|
193
1673
|
assertFileAccess(filepath, project);
|
|
194
|
-
if (typeof options === "object" && !options.encoding)
|
|
1674
|
+
if (typeof options === "object" && !options.encoding) {
|
|
195
1675
|
options.encoding = "utf-8";
|
|
1676
|
+
}
|
|
196
1677
|
return promises.readFile(filepath, options);
|
|
197
1678
|
};
|
|
198
1679
|
const writeFile = async ({ project, testPath = process.cwd() }, path, data, options) => {
|
|
199
1680
|
const filepath = resolve$1(dirname$1(testPath), path);
|
|
200
1681
|
assertFileAccess(filepath, project);
|
|
201
1682
|
const dir = dirname$1(filepath);
|
|
202
|
-
if (!fs.existsSync(dir))
|
|
1683
|
+
if (!fs.existsSync(dir)) {
|
|
203
1684
|
await promises.mkdir(dir, { recursive: true });
|
|
1685
|
+
}
|
|
204
1686
|
await promises.writeFile(filepath, data, options);
|
|
205
1687
|
};
|
|
206
1688
|
const removeFile = async ({ project, testPath = process.cwd() }, path) => {
|
|
@@ -209,354 +1691,179 @@ const removeFile = async ({ project, testPath = process.cwd() }, path) => {
|
|
|
209
1691
|
await promises.rm(filepath);
|
|
210
1692
|
};
|
|
211
1693
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
function isSendKeysPayload(payload) {
|
|
216
|
-
const validOptions = ["type", "press", "down", "up"];
|
|
217
|
-
if (!isObject(payload))
|
|
218
|
-
throw new Error("You must provide a `SendKeysPayload` object");
|
|
219
|
-
const numberOfValidOptions = Object.keys(payload).filter(
|
|
220
|
-
(key) => validOptions.includes(key)
|
|
221
|
-
).length;
|
|
222
|
-
const unknownOptions = Object.keys(payload).filter((key) => !validOptions.includes(key));
|
|
223
|
-
if (numberOfValidOptions > 1) {
|
|
224
|
-
throw new Error(
|
|
225
|
-
`You must provide ONLY one of the following properties to pass to the browser runner: ${validOptions.join(
|
|
226
|
-
", "
|
|
227
|
-
)}.`
|
|
228
|
-
);
|
|
1694
|
+
const screenshot = async (context, name, options = {}) => {
|
|
1695
|
+
if (!context.testPath) {
|
|
1696
|
+
throw new Error(`Cannot take a screenshot without a test path`);
|
|
229
1697
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
1698
|
+
const path = resolveScreenshotPath(
|
|
1699
|
+
context.testPath,
|
|
1700
|
+
name,
|
|
1701
|
+
context.project.config
|
|
1702
|
+
);
|
|
1703
|
+
const savePath = normalize$1(path);
|
|
1704
|
+
await mkdir(dirname(path), { recursive: true });
|
|
1705
|
+
if (context.provider instanceof PlaywrightBrowserProvider) {
|
|
1706
|
+
if (options.element) {
|
|
1707
|
+
const { element: css, ...config } = options;
|
|
1708
|
+
const element = context.iframe.locator(`css=${css}`);
|
|
1709
|
+
const buffer2 = await element.screenshot({
|
|
1710
|
+
timeout: 1e3,
|
|
1711
|
+
...config,
|
|
1712
|
+
path: savePath
|
|
1713
|
+
});
|
|
1714
|
+
return returnResult(options, path, buffer2);
|
|
1715
|
+
}
|
|
1716
|
+
const buffer = await context.iframe.locator("body").screenshot({
|
|
1717
|
+
...options,
|
|
1718
|
+
path: savePath
|
|
1719
|
+
});
|
|
1720
|
+
return returnResult(options, path, buffer);
|
|
236
1721
|
}
|
|
237
|
-
if (
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
function isDownPayload(payload) {
|
|
248
|
-
return "down" in payload;
|
|
249
|
-
}
|
|
250
|
-
function isUpPayload(payload) {
|
|
251
|
-
return "up" in payload;
|
|
252
|
-
}
|
|
253
|
-
const sendKeys = async ({ provider }, payload) => {
|
|
254
|
-
if (!isSendKeysPayload(payload) || !payload)
|
|
255
|
-
throw new Error("You must provide a `SendKeysPayload` object");
|
|
256
|
-
if (provider.name === "playwright") {
|
|
257
|
-
const page = provider.page;
|
|
258
|
-
if (isTypePayload(payload))
|
|
259
|
-
await page.keyboard.type(payload.type);
|
|
260
|
-
else if (isPressPayload(payload))
|
|
261
|
-
await page.keyboard.press(payload.press);
|
|
262
|
-
else if (isDownPayload(payload))
|
|
263
|
-
await page.keyboard.down(payload.down);
|
|
264
|
-
else if (isUpPayload(payload))
|
|
265
|
-
await page.keyboard.up(payload.up);
|
|
266
|
-
} else if (provider.name === "webdriverio") {
|
|
267
|
-
const browser = provider.browser;
|
|
268
|
-
if (isTypePayload(payload))
|
|
269
|
-
await browser.keys(payload.type.split(""));
|
|
270
|
-
else if (isPressPayload(payload))
|
|
271
|
-
await browser.keys([payload.press]);
|
|
272
|
-
else
|
|
273
|
-
throw new Error('Only "press" and "type" are supported by webdriverio.');
|
|
274
|
-
} else {
|
|
275
|
-
throw new Error(`"sendKeys" is not supported for ${provider.name} browser provider.`);
|
|
1722
|
+
if (context.provider instanceof WebdriverBrowserProvider) {
|
|
1723
|
+
const page = context.provider.browser;
|
|
1724
|
+
if (!options.element) {
|
|
1725
|
+
const body = await page.$("body");
|
|
1726
|
+
const buffer2 = await body.saveScreenshot(savePath);
|
|
1727
|
+
return returnResult(options, path, buffer2);
|
|
1728
|
+
}
|
|
1729
|
+
const element = await page.$(`${options.element}`);
|
|
1730
|
+
const buffer = await element.saveScreenshot(savePath);
|
|
1731
|
+
return returnResult(options, path, buffer);
|
|
276
1732
|
}
|
|
1733
|
+
throw new Error(
|
|
1734
|
+
`Provider "${context.provider.name}" does not support screenshots`
|
|
1735
|
+
);
|
|
277
1736
|
};
|
|
1737
|
+
function resolveScreenshotPath(testPath, name, config) {
|
|
1738
|
+
const dir = dirname(testPath);
|
|
1739
|
+
const base = basename(testPath);
|
|
1740
|
+
if (config.browser.screenshotDirectory) {
|
|
1741
|
+
return resolve(
|
|
1742
|
+
config.browser.screenshotDirectory,
|
|
1743
|
+
relative(config.root, dir),
|
|
1744
|
+
base,
|
|
1745
|
+
name
|
|
1746
|
+
);
|
|
1747
|
+
}
|
|
1748
|
+
return resolve(dir, "__screenshots__", base, name);
|
|
1749
|
+
}
|
|
1750
|
+
function returnResult(options, path, buffer) {
|
|
1751
|
+
if (options.base64) {
|
|
1752
|
+
return { path, base64: buffer.toString("base64") };
|
|
1753
|
+
}
|
|
1754
|
+
return path;
|
|
1755
|
+
}
|
|
278
1756
|
|
|
279
1757
|
var builtinCommands = {
|
|
280
1758
|
readFile,
|
|
281
1759
|
removeFile,
|
|
282
1760
|
writeFile,
|
|
283
|
-
|
|
284
|
-
|
|
1761
|
+
__vitest_click: click,
|
|
1762
|
+
__vitest_dblClick: dblClick,
|
|
1763
|
+
__vitest_tripleClick: tripleClick,
|
|
1764
|
+
__vitest_screenshot: screenshot,
|
|
1765
|
+
__vitest_type: type,
|
|
1766
|
+
__vitest_clear: clear,
|
|
1767
|
+
__vitest_fill: fill,
|
|
1768
|
+
__vitest_tab: tab,
|
|
1769
|
+
__vitest_keyboard: keyboard,
|
|
1770
|
+
__vitest_selectOptions: selectOptions,
|
|
1771
|
+
__vitest_dragAndDrop: dragAndDrop,
|
|
1772
|
+
__vitest_hover: hover
|
|
285
1773
|
};
|
|
286
1774
|
|
|
287
1775
|
const VIRTUAL_ID_CONTEXT = "\0@vitest/browser/context";
|
|
288
1776
|
const ID_CONTEXT = "@vitest/browser/context";
|
|
289
1777
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
290
|
-
function BrowserContext(
|
|
1778
|
+
function BrowserContext(server) {
|
|
1779
|
+
const project = server.project;
|
|
291
1780
|
project.config.browser.commands ??= {};
|
|
292
|
-
for (const [name, command] of Object.entries(builtinCommands))
|
|
1781
|
+
for (const [name, command] of Object.entries(builtinCommands)) {
|
|
293
1782
|
project.config.browser.commands[name] ??= command;
|
|
1783
|
+
}
|
|
294
1784
|
for (const command in project.config.browser.commands) {
|
|
295
|
-
if (!/^[a-z_$][\w$]*$/i.test(command))
|
|
296
|
-
throw new Error(
|
|
1785
|
+
if (!/^[a-z_$][\w$]*$/i.test(command)) {
|
|
1786
|
+
throw new Error(
|
|
1787
|
+
`Invalid command name "${command}". Only alphanumeric characters, $ and _ are allowed.`
|
|
1788
|
+
);
|
|
1789
|
+
}
|
|
297
1790
|
}
|
|
298
1791
|
return {
|
|
299
1792
|
name: "vitest:browser:virtual-module:context",
|
|
300
1793
|
enforce: "pre",
|
|
301
1794
|
resolveId(id) {
|
|
302
|
-
if (id === ID_CONTEXT)
|
|
1795
|
+
if (id === ID_CONTEXT) {
|
|
303
1796
|
return VIRTUAL_ID_CONTEXT;
|
|
1797
|
+
}
|
|
304
1798
|
},
|
|
305
1799
|
load(id) {
|
|
306
|
-
if (id === VIRTUAL_ID_CONTEXT)
|
|
307
|
-
return generateContextFile.call(this,
|
|
1800
|
+
if (id === VIRTUAL_ID_CONTEXT) {
|
|
1801
|
+
return generateContextFile.call(this, server);
|
|
1802
|
+
}
|
|
308
1803
|
}
|
|
309
1804
|
};
|
|
310
1805
|
}
|
|
311
|
-
async function generateContextFile(
|
|
312
|
-
const commands = Object.keys(project.config.browser.commands ?? {});
|
|
1806
|
+
async function generateContextFile(server) {
|
|
1807
|
+
const commands = Object.keys(server.project.config.browser.commands ?? {});
|
|
313
1808
|
const filepathCode = "__vitest_worker__.filepath || __vitest_worker__.current?.file?.filepath || undefined";
|
|
314
|
-
const provider =
|
|
315
|
-
const commandsCode = commands.map((command) => {
|
|
316
|
-
return ` ["${command}"]: (...args) => rpc().triggerCommand("${command}", filepath(), args),`;
|
|
1809
|
+
const provider = server.provider;
|
|
1810
|
+
const commandsCode = commands.filter((command) => !command.startsWith("__vitest")).map((command) => {
|
|
1811
|
+
return ` ["${command}"]: (...args) => rpc().triggerCommand(contextId, "${command}", filepath(), args),`;
|
|
317
1812
|
}).join("\n");
|
|
318
|
-
const userEventNonProviderImport = await getUserEventImport(
|
|
1813
|
+
const userEventNonProviderImport = await getUserEventImport(
|
|
1814
|
+
provider,
|
|
1815
|
+
this.resolve.bind(this)
|
|
1816
|
+
);
|
|
1817
|
+
const distContextPath = slash(`/@fs/${resolve(__dirname, "context.js")}`);
|
|
319
1818
|
return `
|
|
1819
|
+
import { page, userEvent as __userEvent_CDP__, cdp } from '${distContextPath}'
|
|
320
1820
|
${userEventNonProviderImport}
|
|
321
1821
|
const filepath = () => ${filepathCode}
|
|
322
1822
|
const rpc = () => __vitest_worker__.rpc
|
|
323
|
-
const
|
|
1823
|
+
const contextId = __vitest_browser_runner__.contextId
|
|
324
1824
|
|
|
325
1825
|
export const server = {
|
|
326
1826
|
platform: ${JSON.stringify(process.platform)},
|
|
327
1827
|
version: ${JSON.stringify(process.version)},
|
|
328
1828
|
provider: ${JSON.stringify(provider.name)},
|
|
329
|
-
browser: ${JSON.stringify(project.config.browser.name)},
|
|
1829
|
+
browser: ${JSON.stringify(server.project.config.browser.name)},
|
|
330
1830
|
commands: {
|
|
331
1831
|
${commandsCode}
|
|
332
1832
|
}
|
|
333
1833
|
}
|
|
334
1834
|
export const commands = server.commands
|
|
335
|
-
export const
|
|
336
|
-
|
|
337
|
-
return __vitest_browser_runner__.config
|
|
338
|
-
},
|
|
339
|
-
viewport(width, height) {
|
|
340
|
-
const id = __vitest_browser_runner__.iframeId
|
|
341
|
-
channel.postMessage({ type: 'viewport', width, height, id })
|
|
342
|
-
return new Promise((resolve, reject) => {
|
|
343
|
-
channel.addEventListener('message', function handler(e) {
|
|
344
|
-
if (e.data.type === 'viewport:done' && e.data.id === id) {
|
|
345
|
-
channel.removeEventListener('message', handler)
|
|
346
|
-
resolve()
|
|
347
|
-
}
|
|
348
|
-
if (e.data.type === 'viewport:fail' && e.data.id === id) {
|
|
349
|
-
channel.removeEventListener('message', handler)
|
|
350
|
-
reject(new Error(e.data.error))
|
|
351
|
-
}
|
|
352
|
-
})
|
|
353
|
-
})
|
|
354
|
-
},
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
export const userEvent = ${getUserEventScript(project)}
|
|
358
|
-
|
|
359
|
-
function convertElementToXPath(element) {
|
|
360
|
-
if (!element || !(element instanceof Element)) {
|
|
361
|
-
// TODO: better error message
|
|
362
|
-
throw new Error('Expected element to be an instance of Element')
|
|
363
|
-
}
|
|
364
|
-
return getPathTo(element)
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
function getPathTo(element) {
|
|
368
|
-
if (element.id !== '')
|
|
369
|
-
return \`id("\${element.id}")\`
|
|
370
|
-
|
|
371
|
-
if (!element.parentNode || element === document.documentElement)
|
|
372
|
-
return element.tagName
|
|
373
|
-
|
|
374
|
-
let ix = 0
|
|
375
|
-
const siblings = element.parentNode.childNodes
|
|
376
|
-
for (let i = 0; i < siblings.length; i++) {
|
|
377
|
-
const sibling = siblings[i]
|
|
378
|
-
if (sibling === element)
|
|
379
|
-
return \`\${getPathTo(element.parentNode)}/\${element.tagName}[\${ix + 1}]\`
|
|
380
|
-
if (sibling.nodeType === 1 && sibling.tagName === element.tagName)
|
|
381
|
-
ix++
|
|
382
|
-
}
|
|
383
|
-
}
|
|
1835
|
+
export const userEvent = ${getUserEvent(provider)}
|
|
1836
|
+
export { page, cdp }
|
|
384
1837
|
`;
|
|
385
1838
|
}
|
|
386
|
-
|
|
387
|
-
if (provider.name !== "preview")
|
|
388
|
-
return "";
|
|
389
|
-
|
|
390
|
-
if (!resolved)
|
|
391
|
-
throw new Error(`Failed to resolve user-event package from ${__dirname}`);
|
|
392
|
-
return `import { userEvent as __vitest_user_event__ } from '${slash(`/@fs/${resolved.id}`)}'`;
|
|
393
|
-
}
|
|
394
|
-
function getUserEventScript(project) {
|
|
395
|
-
if (project.browserProvider?.name === "preview")
|
|
396
|
-
return `__vitest_user_event__`;
|
|
1839
|
+
function getUserEvent(provider) {
|
|
1840
|
+
if (provider.name !== "preview") {
|
|
1841
|
+
return "__userEvent_CDP__";
|
|
1842
|
+
}
|
|
397
1843
|
return `{
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
1844
|
+
...__vitest_user_event__,
|
|
1845
|
+
fill: async (element, text) => {
|
|
1846
|
+
await __vitest_user_event__.clear(element)
|
|
1847
|
+
await __vitest_user_event__.type(element, text)
|
|
401
1848
|
},
|
|
1849
|
+
dragAndDrop: async () => {
|
|
1850
|
+
throw new Error('Provider "preview" does not support dragging elements')
|
|
1851
|
+
}
|
|
402
1852
|
}`;
|
|
403
1853
|
}
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
const m = new MagicString(code);
|
|
408
|
-
const allSpecifiers = [];
|
|
409
|
-
let importIndex = 0;
|
|
410
|
-
for (const _node of ast.body) {
|
|
411
|
-
if (_node.type === "ExportAllDeclaration") {
|
|
412
|
-
throw new Error(
|
|
413
|
-
`automocking files with \`export *\` is not supported in browser mode because it cannot be statically analysed`
|
|
414
|
-
);
|
|
415
|
-
}
|
|
416
|
-
if (_node.type === "ExportNamedDeclaration") {
|
|
417
|
-
let traversePattern2 = function(expression) {
|
|
418
|
-
if (expression.type === "Identifier") {
|
|
419
|
-
allSpecifiers.push({ name: expression.name });
|
|
420
|
-
} else if (expression.type === "ArrayPattern") {
|
|
421
|
-
expression.elements.forEach((element) => {
|
|
422
|
-
if (!element)
|
|
423
|
-
return;
|
|
424
|
-
traversePattern2(element);
|
|
425
|
-
});
|
|
426
|
-
} else if (expression.type === "ObjectPattern") {
|
|
427
|
-
expression.properties.forEach((property) => {
|
|
428
|
-
if (property.type === "RestElement")
|
|
429
|
-
traversePattern2(property);
|
|
430
|
-
else if (property.type === "Property")
|
|
431
|
-
traversePattern2(property.value);
|
|
432
|
-
else
|
|
433
|
-
;
|
|
434
|
-
});
|
|
435
|
-
} else if (expression.type === "RestElement") {
|
|
436
|
-
traversePattern2(expression.argument);
|
|
437
|
-
} else if (expression.type === "AssignmentPattern") {
|
|
438
|
-
throw new Error(`AssignmentPattern is not supported. Please open a new bug report.`);
|
|
439
|
-
} else if (expression.type === "MemberExpression") {
|
|
440
|
-
throw new Error(`MemberExpression is not supported. Please open a new bug report.`);
|
|
441
|
-
} else ;
|
|
442
|
-
};
|
|
443
|
-
const node = _node;
|
|
444
|
-
const declaration = node.declaration;
|
|
445
|
-
if (declaration) {
|
|
446
|
-
if (declaration.type === "FunctionDeclaration") {
|
|
447
|
-
allSpecifiers.push({ name: declaration.id.name });
|
|
448
|
-
} else if (declaration.type === "VariableDeclaration") {
|
|
449
|
-
declaration.declarations.forEach((declaration2) => {
|
|
450
|
-
traversePattern2(declaration2.id);
|
|
451
|
-
});
|
|
452
|
-
} else if (declaration.type === "ClassDeclaration") {
|
|
453
|
-
allSpecifiers.push({ name: declaration.id.name });
|
|
454
|
-
} else ;
|
|
455
|
-
m.remove(node.start, declaration.start);
|
|
456
|
-
}
|
|
457
|
-
const specifiers = node.specifiers || [];
|
|
458
|
-
const source = node.source;
|
|
459
|
-
if (!source && specifiers.length) {
|
|
460
|
-
specifiers.forEach((specifier) => {
|
|
461
|
-
const exported = specifier.exported;
|
|
462
|
-
allSpecifiers.push({
|
|
463
|
-
alias: exported.type === "Literal" ? exported.raw : exported.name,
|
|
464
|
-
name: specifier.local.name
|
|
465
|
-
});
|
|
466
|
-
});
|
|
467
|
-
m.remove(node.start, node.end);
|
|
468
|
-
} else if (source && specifiers.length) {
|
|
469
|
-
const importNames = [];
|
|
470
|
-
specifiers.forEach((specifier) => {
|
|
471
|
-
const importedName = `__vitest_imported_${importIndex++}__`;
|
|
472
|
-
const exported = specifier.exported;
|
|
473
|
-
importNames.push([specifier.local.name, importedName]);
|
|
474
|
-
allSpecifiers.push({
|
|
475
|
-
name: importedName,
|
|
476
|
-
alias: exported.type === "Literal" ? exported.raw : exported.name
|
|
477
|
-
});
|
|
478
|
-
});
|
|
479
|
-
const importString = `import { ${importNames.map(([name, alias]) => `${name} as ${alias}`).join(", ")} } from '${source.value}'`;
|
|
480
|
-
m.overwrite(node.start, node.end, importString);
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
if (_node.type === "ExportDefaultDeclaration") {
|
|
484
|
-
const node = _node;
|
|
485
|
-
const declaration = node.declaration;
|
|
486
|
-
allSpecifiers.push({ name: "__vitest_default", alias: "default" });
|
|
487
|
-
m.overwrite(node.start, declaration.start, `const __vitest_default = `);
|
|
488
|
-
}
|
|
1854
|
+
async function getUserEventImport(provider, resolve2) {
|
|
1855
|
+
if (provider.name !== "preview") {
|
|
1856
|
+
return "";
|
|
489
1857
|
}
|
|
490
|
-
const
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
`;
|
|
497
|
-
const assigning = allSpecifiers.map(({ name }, index) => {
|
|
498
|
-
return `const __vitest_mocked_${index}__ = __vitest_mocked_module__["${name}"]`;
|
|
499
|
-
}).join("\n");
|
|
500
|
-
const redeclarations = allSpecifiers.map(({ name, alias }, index) => {
|
|
501
|
-
return ` __vitest_mocked_${index}__ as ${alias || name},`;
|
|
502
|
-
}).join("\n");
|
|
503
|
-
const specifiersExports = `
|
|
504
|
-
export {
|
|
505
|
-
${redeclarations}
|
|
506
|
-
}
|
|
507
|
-
`;
|
|
508
|
-
m.append(moduleObject + assigning + specifiersExports);
|
|
509
|
-
return m;
|
|
1858
|
+
const resolved = await resolve2("@testing-library/user-event", __dirname);
|
|
1859
|
+
if (!resolved) {
|
|
1860
|
+
throw new Error(`Failed to resolve user-event package from ${__dirname}`);
|
|
1861
|
+
}
|
|
1862
|
+
return `import { userEvent as __vitest_user_event__ } from '${slash(
|
|
1863
|
+
`/@fs/${resolved.id}`
|
|
1864
|
+
)}'`;
|
|
510
1865
|
}
|
|
511
1866
|
|
|
512
|
-
var BrowserMocker = (project) => {
|
|
513
|
-
return [
|
|
514
|
-
{
|
|
515
|
-
name: "vitest:browser:mocker",
|
|
516
|
-
enforce: "pre",
|
|
517
|
-
async load(id) {
|
|
518
|
-
const data = project.browserMocker.mocks.get(id);
|
|
519
|
-
if (!data)
|
|
520
|
-
return;
|
|
521
|
-
const { mock, sessionId } = data;
|
|
522
|
-
if (mock === void 0) {
|
|
523
|
-
const rpc = project.browserRpc.testers.get(sessionId);
|
|
524
|
-
if (!rpc)
|
|
525
|
-
throw new Error(`WebSocket rpc was destroyed for session ${sessionId}`);
|
|
526
|
-
const exports = await rpc.startMocking(id);
|
|
527
|
-
const module = `const module = __vitest_mocker__.get('${id}');`;
|
|
528
|
-
const keys = exports.map((name) => {
|
|
529
|
-
if (name === "default")
|
|
530
|
-
return `export default module['default'];`;
|
|
531
|
-
return `export const ${name} = module['${name}'];`;
|
|
532
|
-
}).join("\n");
|
|
533
|
-
return `${module}
|
|
534
|
-
${keys}`;
|
|
535
|
-
}
|
|
536
|
-
if (mock === null)
|
|
537
|
-
return;
|
|
538
|
-
return readFile$1(mock, "utf-8");
|
|
539
|
-
}
|
|
540
|
-
},
|
|
541
|
-
{
|
|
542
|
-
name: "vitest:browser:automocker",
|
|
543
|
-
enforce: "post",
|
|
544
|
-
transform(code, id) {
|
|
545
|
-
const data = project.browserMocker.mocks.get(id);
|
|
546
|
-
if (!data)
|
|
547
|
-
return;
|
|
548
|
-
if (data.mock === null) {
|
|
549
|
-
const m = automockModule(code, this.parse);
|
|
550
|
-
return {
|
|
551
|
-
code: m.toString(),
|
|
552
|
-
map: m.generateMap({ hires: "boundary", source: id })
|
|
553
|
-
};
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
];
|
|
558
|
-
};
|
|
559
|
-
|
|
560
1867
|
function injectDynamicImport(code, id, parse) {
|
|
561
1868
|
const s = new MagicString(code);
|
|
562
1869
|
let ast;
|
|
@@ -572,8 +1879,14 @@ ${err.message}`);
|
|
|
572
1879
|
onImportMeta() {
|
|
573
1880
|
},
|
|
574
1881
|
onDynamicImport(node) {
|
|
575
|
-
const
|
|
576
|
-
|
|
1882
|
+
const replaceString = "__vitest_browser_runner__.wrapModule(() => import(";
|
|
1883
|
+
const importSubstring = code.substring(node.start, node.end);
|
|
1884
|
+
const hasIgnore = importSubstring.includes("/* @vite-ignore */");
|
|
1885
|
+
s.overwrite(
|
|
1886
|
+
node.start,
|
|
1887
|
+
node.source.start,
|
|
1888
|
+
replaceString + (hasIgnore ? "/* @vite-ignore */ " : "")
|
|
1889
|
+
);
|
|
577
1890
|
s.overwrite(node.end - 1, node.end, "))");
|
|
578
1891
|
}
|
|
579
1892
|
});
|
|
@@ -590,137 +1903,189 @@ var DynamicImport = () => {
|
|
|
590
1903
|
name: "vitest:browser:esm-injector",
|
|
591
1904
|
enforce: "post",
|
|
592
1905
|
transform(source, id) {
|
|
593
|
-
if (!regexDynamicImport.test(source))
|
|
1906
|
+
if (!regexDynamicImport.test(source)) {
|
|
594
1907
|
return;
|
|
1908
|
+
}
|
|
595
1909
|
return injectDynamicImport(source, id, this.parse);
|
|
596
1910
|
}
|
|
597
1911
|
};
|
|
598
1912
|
};
|
|
599
1913
|
|
|
600
|
-
|
|
1914
|
+
async function resolveOrchestrator(server, url, res) {
|
|
1915
|
+
const project = server.project;
|
|
1916
|
+
let contextId = url.searchParams.get("contextId");
|
|
1917
|
+
if (!contextId) {
|
|
1918
|
+
const contexts = [...server.state.orchestrators.keys()];
|
|
1919
|
+
contextId = contexts[contexts.length - 1] ?? "none";
|
|
1920
|
+
}
|
|
1921
|
+
const files = server.state.getContext(contextId)?.files ?? [];
|
|
1922
|
+
const config = server.getSerializableConfig();
|
|
1923
|
+
const injectorJs = typeof server.injectorJs === "string" ? server.injectorJs : await server.injectorJs;
|
|
1924
|
+
const injector = replacer(injectorJs, {
|
|
1925
|
+
__VITEST_PROVIDER__: JSON.stringify(server.provider.name),
|
|
1926
|
+
__VITEST_CONFIG__: JSON.stringify(config),
|
|
1927
|
+
__VITEST_VITE_CONFIG__: JSON.stringify({
|
|
1928
|
+
root: server.vite.config.root
|
|
1929
|
+
}),
|
|
1930
|
+
__VITEST_FILES__: JSON.stringify(files),
|
|
1931
|
+
__VITEST_TYPE__: '"orchestrator"',
|
|
1932
|
+
__VITEST_CONTEXT_ID__: JSON.stringify(contextId),
|
|
1933
|
+
__VITEST_PROVIDED_CONTEXT__: "{}"
|
|
1934
|
+
});
|
|
1935
|
+
res.removeHeader("Content-Security-Policy");
|
|
1936
|
+
if (!server.orchestratorScripts) {
|
|
1937
|
+
server.orchestratorScripts = await server.formatScripts(
|
|
1938
|
+
project.config.browser.orchestratorScripts
|
|
1939
|
+
);
|
|
1940
|
+
}
|
|
1941
|
+
let baseHtml = typeof server.orchestratorHtml === "string" ? server.orchestratorHtml : await server.orchestratorHtml;
|
|
1942
|
+
if (project.config.browser.ui) {
|
|
1943
|
+
const manifestContent = server.manifest instanceof Promise ? await server.manifest : server.manifest;
|
|
1944
|
+
const jsEntry = manifestContent["orchestrator.html"].file;
|
|
1945
|
+
const base = server.vite.config.base || "/";
|
|
1946
|
+
baseHtml = baseHtml.replaceAll("./assets/", `${base}__vitest__/assets/`).replace(
|
|
1947
|
+
"<!-- !LOAD_METADATA! -->",
|
|
1948
|
+
[
|
|
1949
|
+
"<script>{__VITEST_INJECTOR__}<\/script>",
|
|
1950
|
+
"{__VITEST_SCRIPTS__}",
|
|
1951
|
+
`<script type="module" crossorigin src="${base}${jsEntry}"><\/script>`
|
|
1952
|
+
].join("\n")
|
|
1953
|
+
);
|
|
1954
|
+
}
|
|
1955
|
+
return replacer(baseHtml, {
|
|
1956
|
+
__VITEST_FAVICON__: server.faviconUrl,
|
|
1957
|
+
__VITEST_TITLE__: "Vitest Browser Runner",
|
|
1958
|
+
__VITEST_SCRIPTS__: server.orchestratorScripts,
|
|
1959
|
+
__VITEST_INJECTOR__: injector,
|
|
1960
|
+
__VITEST_CONTEXT_ID__: JSON.stringify(contextId)
|
|
1961
|
+
});
|
|
1962
|
+
}
|
|
1963
|
+
|
|
1964
|
+
async function resolveTester(server, url, res) {
|
|
1965
|
+
const csp = res.getHeader("Content-Security-Policy");
|
|
1966
|
+
if (typeof csp === "string") {
|
|
1967
|
+
res.setHeader(
|
|
1968
|
+
"Content-Security-Policy",
|
|
1969
|
+
csp.replace(/frame-ancestors [^;]+/, "frame-ancestors *")
|
|
1970
|
+
);
|
|
1971
|
+
}
|
|
1972
|
+
const { contextId, testFile } = server.resolveTesterUrl(url.pathname);
|
|
1973
|
+
const project = server.project;
|
|
1974
|
+
const state = server.state;
|
|
1975
|
+
const testFiles = await project.globTestFiles();
|
|
1976
|
+
const tests = testFile === "__vitest_all__" || !testFiles.includes(testFile) ? "__vitest_browser_runner__.files" : JSON.stringify([testFile]);
|
|
1977
|
+
const iframeId = JSON.stringify(testFile);
|
|
1978
|
+
const context = state.getContext(contextId);
|
|
1979
|
+
const files = context?.files ?? [];
|
|
1980
|
+
const method = context?.method ?? "run";
|
|
1981
|
+
const injectorJs = typeof server.injectorJs === "string" ? server.injectorJs : await server.injectorJs;
|
|
1982
|
+
const config = server.getSerializableConfig();
|
|
1983
|
+
const injector = replacer(injectorJs, {
|
|
1984
|
+
__VITEST_PROVIDER__: JSON.stringify(server.provider.name),
|
|
1985
|
+
__VITEST_CONFIG__: JSON.stringify(config),
|
|
1986
|
+
__VITEST_FILES__: JSON.stringify(files),
|
|
1987
|
+
__VITEST_VITE_CONFIG__: JSON.stringify({
|
|
1988
|
+
root: server.vite.config.root
|
|
1989
|
+
}),
|
|
1990
|
+
__VITEST_TYPE__: '"tester"',
|
|
1991
|
+
__VITEST_CONTEXT_ID__: JSON.stringify(contextId),
|
|
1992
|
+
__VITEST_PROVIDED_CONTEXT__: JSON.stringify(stringify(project.getProvidedContext()))
|
|
1993
|
+
});
|
|
1994
|
+
if (!server.testerScripts) {
|
|
1995
|
+
const testerScripts = await server.formatScripts(
|
|
1996
|
+
project.config.browser.testerScripts
|
|
1997
|
+
);
|
|
1998
|
+
const clientScript = `<script type="module" src="${config.base || "/"}@vite/client"><\/script>`;
|
|
1999
|
+
const stateJs = typeof server.stateJs === "string" ? server.stateJs : await server.stateJs;
|
|
2000
|
+
const stateScript = `<script type="module">${stateJs}<\/script>`;
|
|
2001
|
+
server.testerScripts = `${stateScript}${clientScript}${testerScripts}`;
|
|
2002
|
+
}
|
|
2003
|
+
const testerHtml = typeof server.testerHtml === "string" ? server.testerHtml : await server.testerHtml;
|
|
2004
|
+
return replacer(testerHtml, {
|
|
2005
|
+
__VITEST_FAVICON__: server.faviconUrl,
|
|
2006
|
+
__VITEST_TITLE__: "Vitest Browser Tester",
|
|
2007
|
+
__VITEST_SCRIPTS__: server.testerScripts,
|
|
2008
|
+
__VITEST_INJECTOR__: injector,
|
|
2009
|
+
__VITEST_APPEND__: `<script type="module">
|
|
2010
|
+
__vitest_browser_runner__.runningFiles = ${tests}
|
|
2011
|
+
__vitest_browser_runner__.iframeId = ${iframeId}
|
|
2012
|
+
__vitest_browser_runner__.${method === "run" ? "runTests" : "collectTests"}(__vitest_browser_runner__.runningFiles)
|
|
2013
|
+
<\/script>`
|
|
2014
|
+
});
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
var BrowserPlugin = (browserServer, base = "/") => {
|
|
601
2018
|
const pkgRoot = resolve(fileURLToPath(import.meta.url), "../..");
|
|
602
2019
|
const distRoot = resolve(pkgRoot, "dist");
|
|
2020
|
+
const project = browserServer.project;
|
|
2021
|
+
let loupePath;
|
|
603
2022
|
return [
|
|
604
|
-
...BrowserMocker(project),
|
|
605
2023
|
{
|
|
606
2024
|
enforce: "pre",
|
|
607
2025
|
name: "vitest:browser",
|
|
608
|
-
async config(viteConfig) {
|
|
609
|
-
// Enables using ignore hint for coverage providers with @preserve keyword
|
|
610
|
-
if (viteConfig.esbuild !== false) {
|
|
611
|
-
viteConfig.esbuild ||= {};
|
|
612
|
-
viteConfig.esbuild.legalComments = "inline";
|
|
613
|
-
}
|
|
614
|
-
},
|
|
615
2026
|
async configureServer(server) {
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
const injectorJs = readFile$1(resolve(distRoot, "client/esm-client-injector.js"), "utf8");
|
|
619
|
-
const manifest = (async () => {
|
|
620
|
-
return JSON.parse(await readFile$1(`${distRoot}/client/.vite/manifest.json`, "utf8"));
|
|
621
|
-
})();
|
|
622
|
-
const favicon = `${base}favicon.svg`;
|
|
623
|
-
const testerPrefix = `${base}__vitest_test__/__test__/`;
|
|
624
|
-
server.middlewares.use((_req, res, next) => {
|
|
2027
|
+
browserServer.setServer(server);
|
|
2028
|
+
server.middlewares.use(function vitestHeaders(_req, res, next) {
|
|
625
2029
|
const headers = server.config.server.headers;
|
|
626
2030
|
if (headers) {
|
|
627
|
-
for (const name in headers)
|
|
2031
|
+
for (const name in headers) {
|
|
628
2032
|
res.setHeader(name, headers[name]);
|
|
2033
|
+
}
|
|
629
2034
|
}
|
|
630
2035
|
next();
|
|
631
2036
|
});
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
server.middlewares.use(async (req, res, next) => {
|
|
635
|
-
if (!req.url)
|
|
2037
|
+
server.middlewares.use(async function vitestBrowserMode(req, res, next) {
|
|
2038
|
+
if (!req.url) {
|
|
636
2039
|
return next();
|
|
2040
|
+
}
|
|
637
2041
|
const url = new URL(req.url, "http://localhost");
|
|
638
|
-
if (!url.pathname.startsWith(
|
|
2042
|
+
if (!url.pathname.startsWith(browserServer.prefixTesterUrl) && url.pathname !== base) {
|
|
639
2043
|
return next();
|
|
640
|
-
|
|
2044
|
+
}
|
|
2045
|
+
res.setHeader(
|
|
2046
|
+
"Cache-Control",
|
|
2047
|
+
"no-cache, max-age=0, must-revalidate"
|
|
2048
|
+
);
|
|
641
2049
|
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
642
|
-
const files = project.browserState?.files ?? [];
|
|
643
|
-
const config = wrapConfig(project.getSerializableConfig());
|
|
644
|
-
config.env ??= {};
|
|
645
|
-
config.env.VITEST_BROWSER_DEBUG = process.env.VITEST_BROWSER_DEBUG || "";
|
|
646
|
-
const injector = replacer(await injectorJs, {
|
|
647
|
-
__VITEST_CONFIG__: JSON.stringify(config),
|
|
648
|
-
__VITEST_FILES__: JSON.stringify(files),
|
|
649
|
-
__VITEST_TYPE__: url.pathname === base ? '"orchestrator"' : '"tester"'
|
|
650
|
-
});
|
|
651
2050
|
res.removeHeader("X-Frame-Options");
|
|
652
2051
|
if (url.pathname === base) {
|
|
653
|
-
res
|
|
654
|
-
if (!orchestratorScripts)
|
|
655
|
-
orchestratorScripts = await formatScripts(project.config.browser.orchestratorScripts, server);
|
|
656
|
-
let baseHtml = await orchestratorHtml;
|
|
657
|
-
if (project.config.browser.ui) {
|
|
658
|
-
const manifestContent = await manifest;
|
|
659
|
-
const jsEntry = manifestContent["orchestrator.html"].file;
|
|
660
|
-
baseHtml = baseHtml.replaceAll("./assets/", `${base}__vitest__/assets/`).replace(
|
|
661
|
-
"<!-- !LOAD_METADATA! -->",
|
|
662
|
-
[
|
|
663
|
-
"<script>{__VITEST_INJECTOR__}<\/script>",
|
|
664
|
-
"{__VITEST_SCRIPTS__}",
|
|
665
|
-
`<script type="module" crossorigin src="${jsEntry}"><\/script>`
|
|
666
|
-
].join("\n")
|
|
667
|
-
);
|
|
668
|
-
}
|
|
669
|
-
const html2 = replacer(baseHtml, {
|
|
670
|
-
__VITEST_FAVICON__: favicon,
|
|
671
|
-
__VITEST_TITLE__: "Vitest Browser Runner",
|
|
672
|
-
__VITEST_SCRIPTS__: orchestratorScripts,
|
|
673
|
-
__VITEST_INJECTOR__: injector
|
|
674
|
-
});
|
|
2052
|
+
const html2 = await resolveOrchestrator(browserServer, url, res);
|
|
675
2053
|
res.write(html2, "utf-8");
|
|
676
2054
|
res.end();
|
|
677
2055
|
return;
|
|
678
2056
|
}
|
|
679
|
-
const
|
|
680
|
-
if (typeof csp === "string") {
|
|
681
|
-
res.setHeader("Content-Security-Policy", csp.replace(/frame-ancestors [^;]+/, "frame-ancestors *"));
|
|
682
|
-
}
|
|
683
|
-
const decodedTestFile = decodeURIComponent(url.pathname.slice(testerPrefix.length));
|
|
684
|
-
const testFiles = await project.globTestFiles();
|
|
685
|
-
const tests = decodedTestFile === "__vitest_all__" || !testFiles.includes(decodedTestFile) ? "__vitest_browser_runner__.files" : JSON.stringify([decodedTestFile]);
|
|
686
|
-
const iframeId = JSON.stringify(decodedTestFile);
|
|
687
|
-
if (!testerScripts)
|
|
688
|
-
testerScripts = await formatScripts(project.config.browser.testerScripts, server);
|
|
689
|
-
const html = replacer(await testerHtml, {
|
|
690
|
-
__VITEST_FAVICON__: favicon,
|
|
691
|
-
__VITEST_TITLE__: "Vitest Browser Tester",
|
|
692
|
-
__VITEST_SCRIPTS__: testerScripts,
|
|
693
|
-
__VITEST_INJECTOR__: injector,
|
|
694
|
-
__VITEST_APPEND__: (
|
|
695
|
-
// TODO: have only a single global variable to not pollute the global scope
|
|
696
|
-
`<script type="module">
|
|
697
|
-
__vitest_browser_runner__.runningFiles = ${tests}
|
|
698
|
-
__vitest_browser_runner__.iframeId = ${iframeId}
|
|
699
|
-
__vitest_browser_runner__.runTests(__vitest_browser_runner__.runningFiles)
|
|
700
|
-
<\/script>`
|
|
701
|
-
)
|
|
702
|
-
});
|
|
2057
|
+
const html = await resolveTester(browserServer, url, res);
|
|
703
2058
|
res.write(html, "utf-8");
|
|
704
2059
|
res.end();
|
|
705
2060
|
});
|
|
706
2061
|
server.middlewares.use(
|
|
707
|
-
base
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
2062
|
+
`${base}favicon.svg`,
|
|
2063
|
+
(_, res) => {
|
|
2064
|
+
const content = readFileSync(resolve(distRoot, "client/favicon.svg"));
|
|
2065
|
+
res.write(content, "utf-8");
|
|
2066
|
+
res.end();
|
|
2067
|
+
}
|
|
712
2068
|
);
|
|
713
2069
|
const coverageFolder = resolveCoverageFolder(project);
|
|
714
2070
|
const coveragePath = coverageFolder ? coverageFolder[1] : void 0;
|
|
715
|
-
if (coveragePath && base === coveragePath)
|
|
716
|
-
throw new Error(
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
2071
|
+
if (coveragePath && base === coveragePath) {
|
|
2072
|
+
throw new Error(
|
|
2073
|
+
`The ui base path and the coverage path cannot be the same: ${base}, change coverage.reportsDirectory`
|
|
2074
|
+
);
|
|
2075
|
+
}
|
|
2076
|
+
coverageFolder && server.middlewares.use(
|
|
2077
|
+
coveragePath,
|
|
2078
|
+
sirv(coverageFolder[0], {
|
|
2079
|
+
single: true,
|
|
2080
|
+
dev: true,
|
|
2081
|
+
setHeaders: (res) => {
|
|
2082
|
+
res.setHeader(
|
|
2083
|
+
"Cache-Control",
|
|
2084
|
+
"public,max-age=0,must-revalidate"
|
|
2085
|
+
);
|
|
2086
|
+
}
|
|
2087
|
+
})
|
|
2088
|
+
);
|
|
724
2089
|
}
|
|
725
2090
|
},
|
|
726
2091
|
{
|
|
@@ -728,27 +2093,59 @@ var index = (project, base = "/") => {
|
|
|
728
2093
|
enforce: "pre",
|
|
729
2094
|
async config() {
|
|
730
2095
|
const allTestFiles = await project.globTestFiles();
|
|
731
|
-
const browserTestFiles = allTestFiles.filter(
|
|
2096
|
+
const browserTestFiles = allTestFiles.filter(
|
|
2097
|
+
(file) => getFilePoolName(project, file) === "browser"
|
|
2098
|
+
);
|
|
732
2099
|
const setupFiles = toArray(project.config.setupFiles);
|
|
733
|
-
const
|
|
2100
|
+
const define = {};
|
|
2101
|
+
for (const env in project.config.env || {}) {
|
|
2102
|
+
const stringValue = JSON.stringify(project.config.env[env]);
|
|
2103
|
+
define[`process.env.${env}`] = stringValue;
|
|
2104
|
+
define[`import.meta.env.${env}`] = stringValue;
|
|
2105
|
+
}
|
|
2106
|
+
const entries = [
|
|
2107
|
+
...browserTestFiles,
|
|
2108
|
+
...setupFiles,
|
|
734
2109
|
resolve(distDir, "index.js"),
|
|
735
2110
|
resolve(distDir, "browser.js"),
|
|
736
2111
|
resolve(distDir, "runners.js"),
|
|
737
|
-
resolve(distDir, "utils.js")
|
|
2112
|
+
resolve(distDir, "utils.js"),
|
|
2113
|
+
...project.config.snapshotSerializers || []
|
|
738
2114
|
];
|
|
2115
|
+
if (project.config.diff) {
|
|
2116
|
+
entries.push(project.config.diff);
|
|
2117
|
+
}
|
|
2118
|
+
if (project.ctx.coverageProvider) {
|
|
2119
|
+
const coverage = project.ctx.config.coverage;
|
|
2120
|
+
const provider = coverage.provider;
|
|
2121
|
+
if (provider === "v8") {
|
|
2122
|
+
const path = tryResolve("@vitest/coverage-v8", [project.ctx.config.root]);
|
|
2123
|
+
if (path) {
|
|
2124
|
+
entries.push(path);
|
|
2125
|
+
}
|
|
2126
|
+
} else if (provider === "istanbul") {
|
|
2127
|
+
const path = tryResolve("@vitest/coverage-istanbul", [project.ctx.config.root]);
|
|
2128
|
+
if (path) {
|
|
2129
|
+
entries.push(path);
|
|
2130
|
+
}
|
|
2131
|
+
} else if (provider === "custom" && coverage.customProviderModule) {
|
|
2132
|
+
entries.push(coverage.customProviderModule);
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
739
2135
|
return {
|
|
2136
|
+
define,
|
|
2137
|
+
resolve: {
|
|
2138
|
+
dedupe: ["vitest"]
|
|
2139
|
+
},
|
|
740
2140
|
optimizeDeps: {
|
|
741
|
-
entries
|
|
742
|
-
...browserTestFiles,
|
|
743
|
-
...setupFiles,
|
|
744
|
-
...vitestPaths
|
|
745
|
-
],
|
|
2141
|
+
entries,
|
|
746
2142
|
exclude: [
|
|
747
2143
|
"vitest",
|
|
748
2144
|
"vitest/utils",
|
|
749
2145
|
"vitest/browser",
|
|
750
2146
|
"vitest/runners",
|
|
751
2147
|
"@vitest/utils",
|
|
2148
|
+
"@vitest/utils/source-map",
|
|
752
2149
|
"@vitest/runner",
|
|
753
2150
|
"@vitest/spy",
|
|
754
2151
|
"@vitest/utils/error",
|
|
@@ -757,7 +2154,9 @@ var index = (project, base = "/") => {
|
|
|
757
2154
|
"std-env",
|
|
758
2155
|
"tinybench",
|
|
759
2156
|
"tinyspy",
|
|
760
|
-
"pathe"
|
|
2157
|
+
"pathe",
|
|
2158
|
+
"msw",
|
|
2159
|
+
"msw/browser"
|
|
761
2160
|
],
|
|
762
2161
|
include: [
|
|
763
2162
|
"vitest > @vitest/utils > pretty-format",
|
|
@@ -768,8 +2167,9 @@ var index = (project, base = "/") => {
|
|
|
768
2167
|
"vitest > pretty-format > ansi-regex",
|
|
769
2168
|
"vitest > chai",
|
|
770
2169
|
"vitest > chai > loupe",
|
|
771
|
-
"vitest > @vitest/runner >
|
|
2170
|
+
"vitest > @vitest/runner > pretty-format",
|
|
772
2171
|
"vitest > @vitest/utils > diff-sequences",
|
|
2172
|
+
"vitest > @vitest/utils > loupe",
|
|
773
2173
|
"@vitest/browser > @testing-library/user-event",
|
|
774
2174
|
"@vitest/browser > @testing-library/dom"
|
|
775
2175
|
]
|
|
@@ -777,32 +2177,122 @@ var index = (project, base = "/") => {
|
|
|
777
2177
|
};
|
|
778
2178
|
},
|
|
779
2179
|
async resolveId(id) {
|
|
780
|
-
if (!/\?browserv=\w+$/.test(id))
|
|
2180
|
+
if (!/\?browserv=\w+$/.test(id)) {
|
|
781
2181
|
return;
|
|
2182
|
+
}
|
|
782
2183
|
let useId = id.slice(0, id.lastIndexOf("?"));
|
|
783
|
-
if (useId.startsWith("/@fs/"))
|
|
2184
|
+
if (useId.startsWith("/@fs/")) {
|
|
784
2185
|
useId = useId.slice(5);
|
|
785
|
-
|
|
2186
|
+
}
|
|
2187
|
+
if (/^\w:/.test(useId)) {
|
|
786
2188
|
useId = useId.replace(/\\/g, "/");
|
|
2189
|
+
}
|
|
787
2190
|
return useId;
|
|
788
2191
|
}
|
|
789
2192
|
},
|
|
790
|
-
|
|
2193
|
+
{
|
|
2194
|
+
name: "vitest:browser:resolve-virtual",
|
|
2195
|
+
async resolveId(rawId) {
|
|
2196
|
+
if (rawId.startsWith("/__virtual_vitest__")) {
|
|
2197
|
+
const url = new URL(rawId, "http://localhost");
|
|
2198
|
+
if (!url.searchParams.has("id")) {
|
|
2199
|
+
return;
|
|
2200
|
+
}
|
|
2201
|
+
const id = decodeURIComponent(url.searchParams.get("id"));
|
|
2202
|
+
const resolved = await this.resolve(id, distRoot, {
|
|
2203
|
+
skipSelf: true
|
|
2204
|
+
});
|
|
2205
|
+
return resolved;
|
|
2206
|
+
}
|
|
2207
|
+
if (rawId === "/__vitest_msw__") {
|
|
2208
|
+
return this.resolve("msw/mockServiceWorker.js", distRoot, {
|
|
2209
|
+
skipSelf: true
|
|
2210
|
+
});
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
},
|
|
2214
|
+
{
|
|
2215
|
+
name: "vitest:browser:assets",
|
|
2216
|
+
resolveId(id) {
|
|
2217
|
+
if (id.startsWith("/__vitest_browser__/") || id.startsWith("/__vitest__/")) {
|
|
2218
|
+
return resolve(distRoot, "client", id.slice(1));
|
|
2219
|
+
}
|
|
2220
|
+
},
|
|
2221
|
+
configResolved(config) {
|
|
2222
|
+
loupePath = resolve(config.cacheDir, "deps/loupe.js");
|
|
2223
|
+
},
|
|
2224
|
+
transform(code, id) {
|
|
2225
|
+
if (id.startsWith(loupePath)) {
|
|
2226
|
+
const utilRequire = "nodeUtil = require_util();";
|
|
2227
|
+
return code.replace(utilRequire, " ".repeat(utilRequire.length));
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
2230
|
+
},
|
|
2231
|
+
BrowserContext(browserServer),
|
|
791
2232
|
DynamicImport(),
|
|
2233
|
+
{
|
|
2234
|
+
name: "vitest:browser:config",
|
|
2235
|
+
enforce: "post",
|
|
2236
|
+
async config(viteConfig) {
|
|
2237
|
+
// Enables using ignore hint for coverage providers with @preserve keyword
|
|
2238
|
+
viteConfig.esbuild ||= {};
|
|
2239
|
+
viteConfig.esbuild.legalComments = "inline";
|
|
2240
|
+
const server = resolveApiServerConfig(
|
|
2241
|
+
viteConfig.test?.browser || {},
|
|
2242
|
+
defaultBrowserPort
|
|
2243
|
+
) || {
|
|
2244
|
+
port: defaultBrowserPort
|
|
2245
|
+
};
|
|
2246
|
+
server.middlewareMode = false;
|
|
2247
|
+
viteConfig.server = {
|
|
2248
|
+
...viteConfig.server,
|
|
2249
|
+
...server,
|
|
2250
|
+
open: false
|
|
2251
|
+
};
|
|
2252
|
+
viteConfig.server.fs ??= {};
|
|
2253
|
+
viteConfig.server.fs.allow = viteConfig.server.fs.allow || [];
|
|
2254
|
+
viteConfig.server.fs.allow.push(
|
|
2255
|
+
...resolveFsAllow(
|
|
2256
|
+
project.ctx.config.root,
|
|
2257
|
+
project.ctx.server.config.configFile
|
|
2258
|
+
)
|
|
2259
|
+
);
|
|
2260
|
+
return {
|
|
2261
|
+
resolve: {
|
|
2262
|
+
alias: viteConfig.test?.alias
|
|
2263
|
+
}
|
|
2264
|
+
};
|
|
2265
|
+
}
|
|
2266
|
+
},
|
|
792
2267
|
// TODO: remove this when @testing-library/vue supports ESM
|
|
793
2268
|
{
|
|
794
|
-
name: "vitest:browser:support-
|
|
2269
|
+
name: "vitest:browser:support-testing-library",
|
|
795
2270
|
config() {
|
|
796
2271
|
return {
|
|
2272
|
+
define: {
|
|
2273
|
+
// testing-library/preact
|
|
2274
|
+
"process.env.PTL_SKIP_AUTO_CLEANUP": !!process.env.PTL_SKIP_AUTO_CLEANUP,
|
|
2275
|
+
// testing-library/react
|
|
2276
|
+
"process.env.RTL_SKIP_AUTO_CLEANUP": !!process.env.RTL_SKIP_AUTO_CLEANUP,
|
|
2277
|
+
"process.env?.RTL_SKIP_AUTO_CLEANUP": !!process.env.RTL_SKIP_AUTO_CLEANUP,
|
|
2278
|
+
// testing-library/svelte, testing-library/solid
|
|
2279
|
+
"process.env.STL_SKIP_AUTO_CLEANUP": !!process.env.STL_SKIP_AUTO_CLEANUP,
|
|
2280
|
+
// testing-library/vue
|
|
2281
|
+
"process.env.VTL_SKIP_AUTO_CLEANUP": !!process.env.VTL_SKIP_AUTO_CLEANUP,
|
|
2282
|
+
// dom.debug()
|
|
2283
|
+
"process.env.DEBUG_PRINT_LIMIT": process.env.DEBUG_PRINT_LIMIT || 7e3
|
|
2284
|
+
},
|
|
797
2285
|
optimizeDeps: {
|
|
798
2286
|
esbuildOptions: {
|
|
799
2287
|
plugins: [
|
|
800
2288
|
{
|
|
801
2289
|
name: "test-utils-rewrite",
|
|
802
2290
|
setup(build) {
|
|
803
|
-
const _require = createRequire(import.meta.url);
|
|
804
2291
|
build.onResolve({ filter: /@vue\/test-utils/ }, (args) => {
|
|
805
|
-
const
|
|
2292
|
+
const _require2 = getRequire();
|
|
2293
|
+
const resolved = _require2.resolve(args.path, {
|
|
2294
|
+
paths: [args.importer]
|
|
2295
|
+
});
|
|
806
2296
|
return { path: resolved };
|
|
807
2297
|
});
|
|
808
2298
|
}
|
|
@@ -815,45 +2305,172 @@ var index = (project, base = "/") => {
|
|
|
815
2305
|
}
|
|
816
2306
|
];
|
|
817
2307
|
};
|
|
2308
|
+
function tryResolve(path, paths) {
|
|
2309
|
+
try {
|
|
2310
|
+
const _require2 = getRequire();
|
|
2311
|
+
return _require2.resolve(path, { paths });
|
|
2312
|
+
} catch {
|
|
2313
|
+
return void 0;
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
2316
|
+
let _require;
|
|
2317
|
+
function getRequire() {
|
|
2318
|
+
if (!_require) {
|
|
2319
|
+
_require = createRequire(import.meta.url);
|
|
2320
|
+
}
|
|
2321
|
+
return _require;
|
|
2322
|
+
}
|
|
818
2323
|
function resolveCoverageFolder(project) {
|
|
819
2324
|
const options = project.ctx.config;
|
|
820
|
-
const htmlReporter = options.coverage?.enabled ? options.coverage.reporter.find((reporter) => {
|
|
821
|
-
if (typeof reporter === "string")
|
|
2325
|
+
const htmlReporter = options.coverage?.enabled ? toArray(options.coverage.reporter).find((reporter) => {
|
|
2326
|
+
if (typeof reporter === "string") {
|
|
822
2327
|
return reporter === "html";
|
|
2328
|
+
}
|
|
823
2329
|
return reporter[0] === "html";
|
|
824
2330
|
}) : void 0;
|
|
825
|
-
if (!htmlReporter)
|
|
2331
|
+
if (!htmlReporter) {
|
|
826
2332
|
return void 0;
|
|
2333
|
+
}
|
|
827
2334
|
const root = resolve(
|
|
828
2335
|
options.root || process.cwd(),
|
|
829
2336
|
options.coverage.reportsDirectory || coverageConfigDefaults.reportsDirectory
|
|
830
2337
|
);
|
|
831
2338
|
const subdir = Array.isArray(htmlReporter) && htmlReporter.length > 1 && "subdir" in htmlReporter[1] ? htmlReporter[1].subdir : void 0;
|
|
832
|
-
if (!subdir || typeof subdir !== "string")
|
|
2339
|
+
if (!subdir || typeof subdir !== "string") {
|
|
833
2340
|
return [root, `/${basename(root)}/`];
|
|
2341
|
+
}
|
|
834
2342
|
return [resolve(root, subdir), `/${basename(root)}/${subdir}/`];
|
|
835
2343
|
}
|
|
836
|
-
|
|
2344
|
+
|
|
2345
|
+
const debug = createDebugger("vitest:browser:pool");
|
|
2346
|
+
function createBrowserPool(ctx) {
|
|
2347
|
+
const providers = /* @__PURE__ */ new Set();
|
|
2348
|
+
const waitForTests = async (method, contextId, project, files) => {
|
|
2349
|
+
const context = project.browser.state.createAsyncContext(method, contextId, files);
|
|
2350
|
+
return await context;
|
|
2351
|
+
};
|
|
2352
|
+
const executeTests = async (method, project, files) => {
|
|
2353
|
+
ctx.state.clearFiles(project, files);
|
|
2354
|
+
const browser = project.browser;
|
|
2355
|
+
const threadsCount = getThreadsCount(project);
|
|
2356
|
+
const provider = browser.provider;
|
|
2357
|
+
providers.add(provider);
|
|
2358
|
+
const resolvedUrls = browser.vite.resolvedUrls;
|
|
2359
|
+
const origin = resolvedUrls?.local[0] ?? resolvedUrls?.network[0];
|
|
2360
|
+
if (!origin) {
|
|
2361
|
+
throw new Error(
|
|
2362
|
+
`Can't find browser origin URL for project "${project.getName()}" when running tests for files "${files.join('", "')}"`
|
|
2363
|
+
);
|
|
2364
|
+
}
|
|
2365
|
+
const filesPerThread = Math.ceil(files.length / threadsCount);
|
|
2366
|
+
const chunks = [];
|
|
2367
|
+
for (let i = 0; i < files.length; i += filesPerThread) {
|
|
2368
|
+
const chunk = files.slice(i, i + filesPerThread);
|
|
2369
|
+
chunks.push(chunk);
|
|
2370
|
+
}
|
|
2371
|
+
debug?.(
|
|
2372
|
+
`[%s] Running %s tests in %s chunks (%s threads)`,
|
|
2373
|
+
project.getName() || "core",
|
|
2374
|
+
files.length,
|
|
2375
|
+
chunks.length,
|
|
2376
|
+
threadsCount
|
|
2377
|
+
);
|
|
2378
|
+
const orchestrators = [...browser.state.orchestrators.entries()];
|
|
2379
|
+
const promises = [];
|
|
2380
|
+
chunks.forEach((files2, index) => {
|
|
2381
|
+
if (orchestrators[index]) {
|
|
2382
|
+
const [contextId, orchestrator] = orchestrators[index];
|
|
2383
|
+
debug?.(
|
|
2384
|
+
"Reusing orchestrator (context %s) for files: %s",
|
|
2385
|
+
contextId,
|
|
2386
|
+
[...files2.map((f) => relative(project.config.root, f))].join(", ")
|
|
2387
|
+
);
|
|
2388
|
+
const promise = waitForTests(method, contextId, project, files2);
|
|
2389
|
+
promises.push(promise);
|
|
2390
|
+
orchestrator.createTesters(files2);
|
|
2391
|
+
} else {
|
|
2392
|
+
const contextId = crypto.randomUUID();
|
|
2393
|
+
const waitPromise = waitForTests(method, contextId, project, files2);
|
|
2394
|
+
debug?.(
|
|
2395
|
+
"Opening a new context %s for files: %s",
|
|
2396
|
+
contextId,
|
|
2397
|
+
[...files2.map((f) => relative(project.config.root, f))].join(", ")
|
|
2398
|
+
);
|
|
2399
|
+
const url = new URL("/", origin);
|
|
2400
|
+
url.searchParams.set("contextId", contextId);
|
|
2401
|
+
const page = provider.openPage(contextId, url.toString()).then(() => waitPromise);
|
|
2402
|
+
promises.push(page);
|
|
2403
|
+
}
|
|
2404
|
+
});
|
|
2405
|
+
await Promise.all(promises);
|
|
2406
|
+
};
|
|
2407
|
+
const runWorkspaceTests = async (method, specs) => {
|
|
2408
|
+
const groupedFiles = /* @__PURE__ */ new Map();
|
|
2409
|
+
for (const [project, file] of specs) {
|
|
2410
|
+
const files = groupedFiles.get(project) || [];
|
|
2411
|
+
files.push(file);
|
|
2412
|
+
groupedFiles.set(project, files);
|
|
2413
|
+
}
|
|
2414
|
+
let isCancelled = false;
|
|
2415
|
+
ctx.onCancel(() => {
|
|
2416
|
+
isCancelled = true;
|
|
2417
|
+
});
|
|
2418
|
+
for (const [project, files] of groupedFiles.entries()) {
|
|
2419
|
+
if (isCancelled) {
|
|
2420
|
+
break;
|
|
2421
|
+
}
|
|
2422
|
+
await executeTests(method, project, files);
|
|
2423
|
+
}
|
|
2424
|
+
};
|
|
2425
|
+
const numCpus = typeof nodeos.availableParallelism === "function" ? nodeos.availableParallelism() : nodeos.cpus().length;
|
|
2426
|
+
function getThreadsCount(project) {
|
|
2427
|
+
const config = project.config.browser;
|
|
2428
|
+
if (!config.headless || !project.browser.provider.supportsParallelism) {
|
|
2429
|
+
return 1;
|
|
2430
|
+
}
|
|
2431
|
+
if (!config.fileParallelism) {
|
|
2432
|
+
return 1;
|
|
2433
|
+
}
|
|
2434
|
+
return ctx.config.watch ? Math.max(Math.floor(numCpus / 2), 1) : Math.max(numCpus - 1, 1);
|
|
2435
|
+
}
|
|
837
2436
|
return {
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
2437
|
+
name: "browser",
|
|
2438
|
+
async close() {
|
|
2439
|
+
await Promise.all([...providers].map((provider) => provider.close()));
|
|
2440
|
+
providers.clear();
|
|
2441
|
+
},
|
|
2442
|
+
runTests: (files) => runWorkspaceTests("run", files),
|
|
2443
|
+
collectTests: (files) => runWorkspaceTests("collect", files)
|
|
841
2444
|
};
|
|
842
2445
|
}
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
const
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
2446
|
+
|
|
2447
|
+
async function createBrowserServer(project, configFile, prePlugins = [], postPlugins = []) {
|
|
2448
|
+
const server = new BrowserServer(project, "/");
|
|
2449
|
+
const root = project.config.root;
|
|
2450
|
+
await project.ctx.packageInstaller.ensureInstalled("@vitest/browser", root);
|
|
2451
|
+
const configPath = typeof configFile === "string" ? configFile : false;
|
|
2452
|
+
const vite = await createServer({
|
|
2453
|
+
...project.options,
|
|
2454
|
+
// spread project config inlined in root workspace config
|
|
2455
|
+
base: "/",
|
|
2456
|
+
logLevel: process.env.VITEST_BROWSER_DEBUG ?? "info",
|
|
2457
|
+
mode: project.config.mode,
|
|
2458
|
+
configFile: configPath,
|
|
2459
|
+
// watch is handled by Vitest
|
|
2460
|
+
server: {
|
|
2461
|
+
hmr: false,
|
|
2462
|
+
watch: null
|
|
2463
|
+
},
|
|
2464
|
+
plugins: [
|
|
2465
|
+
...prePlugins,
|
|
2466
|
+
...project.options?.plugins || [],
|
|
2467
|
+
BrowserPlugin(server),
|
|
2468
|
+
...postPlugins
|
|
2469
|
+
]
|
|
855
2470
|
});
|
|
856
|
-
|
|
2471
|
+
await vite.listen();
|
|
2472
|
+
setupBrowserRpc(server);
|
|
2473
|
+
return server;
|
|
857
2474
|
}
|
|
858
2475
|
|
|
859
|
-
export {
|
|
2476
|
+
export { createBrowserPool, createBrowserServer };
|