@jsenv/test 1.5.2 → 1.6.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/dist/jsenv_test.js +1229 -981
- package/package.json +1 -1
- package/src/execution/execute.js +5 -1
- package/src/execution/execute_steps.js +8 -22
- package/src/execution/execute_test_plan.js +29 -3
- package/src/execution/web_server/start_using_command.js +132 -0
- package/src/execution/web_server/start_using_module_url.js +79 -0
- package/src/execution/web_server/worker_importing_module_starting_web_server.mjs +5 -0
- package/src/execution/web_server_param.js +53 -32
- package/src/helpers/teardown.js +15 -0
- package/src/runtime_browsers/using_playwright.js +6 -9
package/package.json
CHANGED
package/src/execution/execute.js
CHANGED
|
@@ -14,6 +14,7 @@ import { Abort, raceProcessTeardownEvents } from "@jsenv/abort";
|
|
|
14
14
|
import { assertAndNormalizeDirectoryUrl } from "@jsenv/filesystem";
|
|
15
15
|
import { createLogger } from "@jsenv/log";
|
|
16
16
|
|
|
17
|
+
import { createTeardown } from "../helpers/teardown.js";
|
|
17
18
|
import { assertAndNormalizeWebServer } from "./web_server_param.js";
|
|
18
19
|
import { run } from "./run.js";
|
|
19
20
|
|
|
@@ -44,6 +45,7 @@ export const execute = async ({
|
|
|
44
45
|
rootDirectoryUrl,
|
|
45
46
|
"rootDirectoryUrl",
|
|
46
47
|
);
|
|
48
|
+
const teardown = createTeardown();
|
|
47
49
|
const executeOperation = Abort.startOperation();
|
|
48
50
|
executeOperation.addAbortSignal(signal);
|
|
49
51
|
if (handleSIGINT) {
|
|
@@ -58,7 +60,7 @@ export const execute = async ({
|
|
|
58
60
|
}
|
|
59
61
|
|
|
60
62
|
if (runtime.type === "browser") {
|
|
61
|
-
await assertAndNormalizeWebServer(webServer);
|
|
63
|
+
await assertAndNormalizeWebServer(webServer, { signal, teardown, logger });
|
|
62
64
|
}
|
|
63
65
|
|
|
64
66
|
let resultTransformer = (result) => result;
|
|
@@ -67,6 +69,7 @@ export const execute = async ({
|
|
|
67
69
|
webServer,
|
|
68
70
|
fileRelativeUrl,
|
|
69
71
|
importMap,
|
|
72
|
+
teardown,
|
|
70
73
|
...runtimeParams,
|
|
71
74
|
};
|
|
72
75
|
|
|
@@ -106,6 +109,7 @@ export const execute = async ({
|
|
|
106
109
|
}
|
|
107
110
|
return result;
|
|
108
111
|
} finally {
|
|
112
|
+
await teardown.trigger();
|
|
109
113
|
await executeOperation.end();
|
|
110
114
|
}
|
|
111
115
|
};
|
|
@@ -4,7 +4,7 @@ import { takeCoverage } from "node:v8";
|
|
|
4
4
|
import stripAnsi from "strip-ansi";
|
|
5
5
|
import { urlToFileSystemPath } from "@jsenv/urls";
|
|
6
6
|
import { createLog, startSpinner } from "@jsenv/log";
|
|
7
|
-
import { Abort
|
|
7
|
+
import { Abort } from "@jsenv/abort";
|
|
8
8
|
import { ensureEmptyDirectory, writeFileSync } from "@jsenv/filesystem";
|
|
9
9
|
|
|
10
10
|
import { reportToCoverage } from "../coverage/report_to_coverage.js";
|
|
@@ -16,7 +16,7 @@ export const executeSteps = async (
|
|
|
16
16
|
executionSteps,
|
|
17
17
|
{
|
|
18
18
|
signal,
|
|
19
|
-
|
|
19
|
+
teardown,
|
|
20
20
|
logger,
|
|
21
21
|
logRefresh,
|
|
22
22
|
logRuntime,
|
|
@@ -56,23 +56,9 @@ export const executeSteps = async (
|
|
|
56
56
|
const executePlanReturnValue = {};
|
|
57
57
|
const report = {};
|
|
58
58
|
const callbacks = [];
|
|
59
|
-
const stopAfterAllSignal = { notify: () => {} };
|
|
60
59
|
|
|
61
60
|
const multipleExecutionsOperation = Abort.startOperation();
|
|
62
61
|
multipleExecutionsOperation.addAbortSignal(signal);
|
|
63
|
-
if (handleSIGINT) {
|
|
64
|
-
multipleExecutionsOperation.addAbortSource((abort) => {
|
|
65
|
-
return raceProcessTeardownEvents(
|
|
66
|
-
{
|
|
67
|
-
SIGINT: true,
|
|
68
|
-
},
|
|
69
|
-
() => {
|
|
70
|
-
logger.debug(`SIGINT abort`);
|
|
71
|
-
abort();
|
|
72
|
-
},
|
|
73
|
-
);
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
62
|
const failFastAbortController = new AbortController();
|
|
77
63
|
if (failFast) {
|
|
78
64
|
multipleExecutionsOperation.addAbortSignal(failFastAbortController.signal);
|
|
@@ -125,7 +111,8 @@ export const executeSteps = async (
|
|
|
125
111
|
coverageConfig,
|
|
126
112
|
coverageMethodForBrowsers,
|
|
127
113
|
coverageMethodForNodeJs,
|
|
128
|
-
|
|
114
|
+
isTestPlan: true,
|
|
115
|
+
teardown,
|
|
129
116
|
};
|
|
130
117
|
|
|
131
118
|
if (logMergeForCompletedExecutions && !process.stdout.isTTY) {
|
|
@@ -323,8 +310,8 @@ export const executeSteps = async (
|
|
|
323
310
|
},
|
|
324
311
|
});
|
|
325
312
|
if (!keepRunning) {
|
|
326
|
-
logger.debug("
|
|
327
|
-
await
|
|
313
|
+
logger.debug("trigger test plan teardown");
|
|
314
|
+
await teardown.trigger();
|
|
328
315
|
}
|
|
329
316
|
|
|
330
317
|
counters.cancelled = counters.total - counters.done;
|
|
@@ -346,10 +333,9 @@ export const executeSteps = async (
|
|
|
346
333
|
executePlanReturnValue.aborted = multipleExecutionsOperation.signal.aborted;
|
|
347
334
|
executePlanReturnValue.planSummary = summary;
|
|
348
335
|
executePlanReturnValue.planReport = report;
|
|
349
|
-
|
|
350
|
-
await previous;
|
|
336
|
+
for (const callback of callbacks) {
|
|
351
337
|
await callback();
|
|
352
|
-
}
|
|
338
|
+
}
|
|
353
339
|
return executePlanReturnValue;
|
|
354
340
|
} finally {
|
|
355
341
|
await multipleExecutionsOperation.end();
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
|
+
import { Abort, raceProcessTeardownEvents } from "@jsenv/abort";
|
|
2
3
|
import { URL_META } from "@jsenv/url-meta";
|
|
3
4
|
import { urlToFileSystemPath, urlToRelativeUrl } from "@jsenv/urls";
|
|
4
5
|
import {
|
|
@@ -8,6 +9,7 @@ import {
|
|
|
8
9
|
} from "@jsenv/filesystem";
|
|
9
10
|
import { createLogger, createDetailedMessage } from "@jsenv/log";
|
|
10
11
|
|
|
12
|
+
import { createTeardown } from "../helpers/teardown.js";
|
|
11
13
|
import { generateCoverageJsonFile } from "../coverage/coverage_reporter_json_file.js";
|
|
12
14
|
import { generateCoverageHtmlDirectory } from "../coverage/coverage_reporter_html_directory.js";
|
|
13
15
|
import { generateCoverageTextLog } from "../coverage/coverage_reporter_text_log.js";
|
|
@@ -103,6 +105,25 @@ export const executeTestPlan = async ({
|
|
|
103
105
|
coverageReportHtmlDirectoryUrl,
|
|
104
106
|
...rest
|
|
105
107
|
}) => {
|
|
108
|
+
const teardown = createTeardown();
|
|
109
|
+
|
|
110
|
+
const operation = Abort.startOperation();
|
|
111
|
+
operation.addAbortSignal(signal);
|
|
112
|
+
if (handleSIGINT) {
|
|
113
|
+
operation.addAbortSource((abort) => {
|
|
114
|
+
return raceProcessTeardownEvents(
|
|
115
|
+
{
|
|
116
|
+
SIGINT: true,
|
|
117
|
+
},
|
|
118
|
+
() => {
|
|
119
|
+
logger.debug(`SIGINT abort`);
|
|
120
|
+
abort();
|
|
121
|
+
},
|
|
122
|
+
);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
let logger;
|
|
106
127
|
let someNeedsServer = false;
|
|
107
128
|
let someHasCoverageV8 = false;
|
|
108
129
|
let someNodeRuntime = false;
|
|
@@ -126,6 +147,8 @@ export const executeTestPlan = async ({
|
|
|
126
147
|
throw new Error(`testPlan must be an object, got ${testPlan}`);
|
|
127
148
|
}
|
|
128
149
|
|
|
150
|
+
logger = createLogger({ logLevel });
|
|
151
|
+
|
|
129
152
|
Object.keys(testPlan).forEach((filePattern) => {
|
|
130
153
|
const filePlan = testPlan[filePattern];
|
|
131
154
|
if (!filePlan) return;
|
|
@@ -148,7 +171,11 @@ export const executeTestPlan = async ({
|
|
|
148
171
|
});
|
|
149
172
|
|
|
150
173
|
if (someNeedsServer) {
|
|
151
|
-
await assertAndNormalizeWebServer(webServer
|
|
174
|
+
await assertAndNormalizeWebServer(webServer, {
|
|
175
|
+
signal: operation.signal,
|
|
176
|
+
teardown,
|
|
177
|
+
logger,
|
|
178
|
+
});
|
|
152
179
|
}
|
|
153
180
|
|
|
154
181
|
if (coverageEnabled) {
|
|
@@ -233,7 +260,6 @@ export const executeTestPlan = async ({
|
|
|
233
260
|
}
|
|
234
261
|
}
|
|
235
262
|
|
|
236
|
-
const logger = createLogger({ logLevel });
|
|
237
263
|
logger.debug(
|
|
238
264
|
createDetailedMessage(`Prepare executing plan`, {
|
|
239
265
|
runtimes: JSON.stringify(runtimes, null, " "),
|
|
@@ -288,7 +314,7 @@ export const executeTestPlan = async ({
|
|
|
288
314
|
|
|
289
315
|
const result = await executeSteps(executionSteps, {
|
|
290
316
|
signal,
|
|
291
|
-
|
|
317
|
+
teardown,
|
|
292
318
|
logger,
|
|
293
319
|
logRefresh,
|
|
294
320
|
logSummary,
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
+
import { Abort } from "@jsenv/abort";
|
|
3
|
+
|
|
4
|
+
import { pingServer } from "../../helpers/ping_server.js";
|
|
5
|
+
|
|
6
|
+
export const startServerUsingCommand = async (
|
|
7
|
+
webServer,
|
|
8
|
+
{ signal, allocatedMs, logger, teardown },
|
|
9
|
+
) => {
|
|
10
|
+
const spawnedProcess = spawn(webServer.command, [], {
|
|
11
|
+
// On non-windows platforms, `detached: true` makes child process a leader of a new
|
|
12
|
+
// process group, making it possible to kill child process tree with `.kill(-pid)` command.
|
|
13
|
+
// @see https://nodejs.org/api/child_process.html#child_process_options_detached
|
|
14
|
+
detached: process.platform !== "win32",
|
|
15
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
16
|
+
shell: true,
|
|
17
|
+
cwd: webServer.cwd,
|
|
18
|
+
});
|
|
19
|
+
if (!spawnedProcess.pid) {
|
|
20
|
+
await new Promise((resolve, reject) => {
|
|
21
|
+
spawnedProcess.once("error", (error) => {
|
|
22
|
+
reject(new Error(`Failed to launch: ${error}`));
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let errorReceived = false;
|
|
28
|
+
const errorPromise = new Promise((resolve, reject) => {
|
|
29
|
+
spawnedProcess.on("error", (e) => {
|
|
30
|
+
errorReceived = true;
|
|
31
|
+
reject(e);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// const stdout = readline.createInterface({ input: spawnedProcess.stdout });
|
|
36
|
+
// stdout.on("line", () => {
|
|
37
|
+
// logger.debug(`[pid=${spawnedProcess.pid}][out] ${data}`);
|
|
38
|
+
// });
|
|
39
|
+
// const stderr = readline.createInterface({ input: spawnedProcess.stderr });
|
|
40
|
+
// stderr.on("line", (data) => {
|
|
41
|
+
// logger.debug(`[pid=${spawnedProcess.pid}][err] ${data}`);
|
|
42
|
+
// });
|
|
43
|
+
let processClosed = false;
|
|
44
|
+
const closedPromise = new Promise((resolve) => {
|
|
45
|
+
spawnedProcess.once("exit", (exitCode, signal) => {
|
|
46
|
+
logger.info(
|
|
47
|
+
`[pid=${spawnedProcess.pid}] <process did exit: exitCode=${exitCode}, signal=${signal}>`,
|
|
48
|
+
);
|
|
49
|
+
processClosed = true;
|
|
50
|
+
resolve();
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
const killProcess = async () => {
|
|
54
|
+
logger.info(`[pid=${spawnedProcess.pid}] <kill>`);
|
|
55
|
+
if (!spawnedProcess.pid || spawnedProcess.killed || processClosed) {
|
|
56
|
+
logger.info(
|
|
57
|
+
`[pid=${spawnedProcess.pid}] <skipped force kill spawnedProcess.killed=${spawnedProcess.killed} processClosed=${processClosed}>`,
|
|
58
|
+
);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
logger.info(`[pid=${spawnedProcess.pid}] <will force kill>`);
|
|
62
|
+
// Force kill the browser.
|
|
63
|
+
try {
|
|
64
|
+
if (process.platform === "win32") {
|
|
65
|
+
const taskkillProcess = spawnSync(
|
|
66
|
+
`taskkill /pid ${spawnedProcess.pid} /T /F`,
|
|
67
|
+
{ shell: true },
|
|
68
|
+
);
|
|
69
|
+
const [stdout, stderr] = [
|
|
70
|
+
taskkillProcess.stdout.toString(),
|
|
71
|
+
taskkillProcess.stderr.toString(),
|
|
72
|
+
];
|
|
73
|
+
if (stdout)
|
|
74
|
+
logger.info(`[pid=${spawnedProcess.pid}] taskkill stdout: ${stdout}`);
|
|
75
|
+
if (stderr)
|
|
76
|
+
logger.info(`[pid=${spawnedProcess.pid}] taskkill stderr: ${stderr}`);
|
|
77
|
+
} else {
|
|
78
|
+
process.kill(-spawnedProcess.pid, "SIGKILL");
|
|
79
|
+
}
|
|
80
|
+
} catch (e) {
|
|
81
|
+
logger.info(
|
|
82
|
+
`[pid=${spawnedProcess.pid}] exception while trying to kill process: ${e}`,
|
|
83
|
+
);
|
|
84
|
+
// the process might have already stopped
|
|
85
|
+
}
|
|
86
|
+
await closedPromise;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const startOperation = Abort.startOperation();
|
|
90
|
+
startOperation.addAbortSignal(signal);
|
|
91
|
+
const timeoutAbortSource = startOperation.timeout(allocatedMs);
|
|
92
|
+
startOperation.addAbortCallback(killProcess);
|
|
93
|
+
teardown.addCallback(killProcess);
|
|
94
|
+
|
|
95
|
+
const startedPromise = (async () => {
|
|
96
|
+
const logScale = [100, 250, 500];
|
|
97
|
+
// eslint-disable-next-line no-constant-condition
|
|
98
|
+
while (true) {
|
|
99
|
+
if (errorReceived) {
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
const connected = await pingServer(webServer.origin);
|
|
103
|
+
if (connected) {
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
startOperation.throwIfAborted();
|
|
107
|
+
const delay = logScale.shift() || 1000;
|
|
108
|
+
logger.debug(`Waiting ${delay}ms`);
|
|
109
|
+
await new Promise((x) => setTimeout(x, delay));
|
|
110
|
+
}
|
|
111
|
+
})();
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
await Promise.race([errorPromise, startedPromise]);
|
|
115
|
+
} catch (e) {
|
|
116
|
+
if (Abort.isAbortError(e)) {
|
|
117
|
+
if (timeoutAbortSource.signal.aborted) {
|
|
118
|
+
// aborted by timeout
|
|
119
|
+
throw new Error(
|
|
120
|
+
`"${webServer.command}" command did not start a server in less than ${allocatedMs}ms (webServer.command)`,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
if (signal.aborted) {
|
|
124
|
+
// aborted from outside
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
throw e;
|
|
129
|
+
} finally {
|
|
130
|
+
await startOperation.end();
|
|
131
|
+
}
|
|
132
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { Worker } from "node:worker_threads";
|
|
3
|
+
import { Abort } from "@jsenv/abort";
|
|
4
|
+
|
|
5
|
+
import { pingServer } from "../../helpers/ping_server.js";
|
|
6
|
+
|
|
7
|
+
export const startServerUsingModuleUrl = async (
|
|
8
|
+
webServer,
|
|
9
|
+
{ signal, teardown, logger, allocatedMs },
|
|
10
|
+
) => {
|
|
11
|
+
if (!existsSync(new URL(webServer.moduleUrl))) {
|
|
12
|
+
throw new Error(
|
|
13
|
+
`webServer.moduleUrl does not lead to a file at "${webServer.moduleUrl}"`,
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
const worker = new Worker(webServer.moduleUrl, {
|
|
17
|
+
env: {
|
|
18
|
+
// IMPORTED_BY_TEST_PLAN: "1",
|
|
19
|
+
},
|
|
20
|
+
stdin: true,
|
|
21
|
+
stdout: true,
|
|
22
|
+
});
|
|
23
|
+
let errorReceived = false;
|
|
24
|
+
const errorPromise = new Promise((resolve, reject) => {
|
|
25
|
+
worker.on("error", (e) => {
|
|
26
|
+
errorReceived = true;
|
|
27
|
+
reject(e);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const killWorker = async () => {
|
|
32
|
+
await worker.terminate();
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const startOperation = Abort.startOperation();
|
|
36
|
+
startOperation.addAbortSignal(signal);
|
|
37
|
+
const timeoutAbortSource = startOperation.timeout(allocatedMs);
|
|
38
|
+
startOperation.addAbortCallback(killWorker);
|
|
39
|
+
teardown.addCallback(killWorker);
|
|
40
|
+
|
|
41
|
+
const startedPromise = (async () => {
|
|
42
|
+
const logScale = [100, 250, 500];
|
|
43
|
+
// eslint-disable-next-line no-constant-condition
|
|
44
|
+
while (true) {
|
|
45
|
+
if (errorReceived) {
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
const connected = await pingServer(webServer.origin);
|
|
49
|
+
if (connected) {
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
startOperation.throwIfAborted();
|
|
54
|
+
const delay = logScale.shift() || 1000;
|
|
55
|
+
logger.debug(`Waiting ${delay}ms`);
|
|
56
|
+
await new Promise((x) => setTimeout(x, delay));
|
|
57
|
+
}
|
|
58
|
+
})();
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
await Promise.race([errorPromise, startedPromise]);
|
|
62
|
+
} catch (e) {
|
|
63
|
+
if (Abort.isAbortError(e)) {
|
|
64
|
+
if (timeoutAbortSource.signal.aborted) {
|
|
65
|
+
// aborted by timeout
|
|
66
|
+
throw new Error(
|
|
67
|
+
`"${webServer.moduleUrl}" did not start a server in less than ${allocatedMs}ms (webServer.moduleUrl)`,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
if (signal.aborted) {
|
|
71
|
+
// aborted from outside
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
throw e;
|
|
76
|
+
} finally {
|
|
77
|
+
await startOperation.end();
|
|
78
|
+
}
|
|
79
|
+
};
|
|
@@ -1,53 +1,43 @@
|
|
|
1
|
-
import { fileURLToPath } from "node:url";
|
|
2
1
|
import { assertAndNormalizeDirectoryUrl } from "@jsenv/filesystem";
|
|
3
2
|
|
|
3
|
+
import { startServerUsingModuleUrl } from "./web_server/start_using_module_url.js";
|
|
4
|
+
import { startServerUsingCommand } from "./web_server/start_using_command.js";
|
|
4
5
|
import { pingServer } from "../helpers/ping_server.js";
|
|
5
6
|
import { basicFetch } from "../helpers/basic_fetch.js";
|
|
6
7
|
|
|
7
|
-
export const assertAndNormalizeWebServer = async (
|
|
8
|
+
export const assertAndNormalizeWebServer = async (
|
|
9
|
+
webServer,
|
|
10
|
+
{ signal, logger, teardown },
|
|
11
|
+
) => {
|
|
8
12
|
if (!webServer) {
|
|
9
13
|
throw new TypeError(
|
|
10
14
|
`webServer is required when running tests on browser(s)`,
|
|
11
15
|
);
|
|
12
16
|
}
|
|
13
17
|
const unexpectedParamNames = Object.keys(webServer).filter((key) => {
|
|
14
|
-
return ![
|
|
18
|
+
return ![
|
|
19
|
+
"origin",
|
|
20
|
+
"moduleUrl",
|
|
21
|
+
"command",
|
|
22
|
+
"cwd",
|
|
23
|
+
"rootDirectoryUrl",
|
|
24
|
+
].includes(key);
|
|
15
25
|
});
|
|
16
26
|
if (unexpectedParamNames.length > 0) {
|
|
17
27
|
throw new TypeError(
|
|
18
28
|
`${unexpectedParamNames.join(",")}: there is no such param to webServer`,
|
|
19
29
|
);
|
|
20
30
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
throw new TypeError(
|
|
26
|
-
`webServer.moduleUrl is required as there is no server listening "${webServer.origin}"`,
|
|
27
|
-
);
|
|
28
|
-
}
|
|
29
|
-
try {
|
|
30
|
-
process.env.IMPORTED_BY_TEST_PLAN = "1";
|
|
31
|
-
await import(webServer.moduleUrl);
|
|
32
|
-
delete process.env.IMPORTED_BY_TEST_PLAN;
|
|
33
|
-
} catch (e) {
|
|
34
|
-
if (
|
|
35
|
-
e.code === "ERR_MODULE_NOT_FOUND" &&
|
|
36
|
-
e.message.includes(fileURLToPath(webServer.moduleUrl))
|
|
37
|
-
) {
|
|
38
|
-
throw new Error(
|
|
39
|
-
`webServer.moduleUrl does not lead to a file at "${webServer.moduleUrl}"`,
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
|
-
throw e;
|
|
43
|
-
}
|
|
44
|
-
aServerIsListening = await pingServer(webServer.origin);
|
|
45
|
-
if (!aServerIsListening) {
|
|
46
|
-
throw new Error(
|
|
47
|
-
`webServer.moduleUrl did not start a server listening at "${webServer.origin}", check file at "${webServer.moduleUrl}"`,
|
|
48
|
-
);
|
|
49
|
-
}
|
|
31
|
+
if (typeof webServer.origin !== "string") {
|
|
32
|
+
throw new TypeError(
|
|
33
|
+
`webServer.origin must be a string, got ${webServer.origin}`,
|
|
34
|
+
);
|
|
50
35
|
}
|
|
36
|
+
await ensureWebServerIsStarted(webServer, {
|
|
37
|
+
signal,
|
|
38
|
+
teardown,
|
|
39
|
+
logger,
|
|
40
|
+
});
|
|
51
41
|
const { headers } = await basicFetch(webServer.origin, {
|
|
52
42
|
method: "GET",
|
|
53
43
|
rejectUnauthorized: false,
|
|
@@ -76,3 +66,34 @@ export const assertAndNormalizeWebServer = async (webServer) => {
|
|
|
76
66
|
);
|
|
77
67
|
}
|
|
78
68
|
};
|
|
69
|
+
|
|
70
|
+
export const ensureWebServerIsStarted = async (
|
|
71
|
+
webServer,
|
|
72
|
+
{ signal, teardown, logger, allocatedMs = 5_000 },
|
|
73
|
+
) => {
|
|
74
|
+
const aServerIsListening = await pingServer(webServer.origin);
|
|
75
|
+
if (aServerIsListening) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (webServer.moduleUrl) {
|
|
79
|
+
await startServerUsingModuleUrl(webServer, {
|
|
80
|
+
signal,
|
|
81
|
+
allocatedMs,
|
|
82
|
+
teardown,
|
|
83
|
+
logger,
|
|
84
|
+
});
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (webServer.command) {
|
|
88
|
+
await startServerUsingCommand(webServer, {
|
|
89
|
+
signal,
|
|
90
|
+
allocatedMs,
|
|
91
|
+
teardown,
|
|
92
|
+
logger,
|
|
93
|
+
});
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
throw new TypeError(
|
|
97
|
+
`webServer.moduleUrl or webServer.command is required as there is no server listening "${webServer.origin}"`,
|
|
98
|
+
);
|
|
99
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export const createTeardown = () => {
|
|
2
|
+
const teardownCallbackSet = new Set();
|
|
3
|
+
return {
|
|
4
|
+
addCallback: (callback) => {
|
|
5
|
+
teardownCallbackSet.add(callback);
|
|
6
|
+
},
|
|
7
|
+
trigger: async () => {
|
|
8
|
+
await Promise.all(
|
|
9
|
+
Array.from(teardownCallbackSet.values()).map(async (callback) => {
|
|
10
|
+
await callback();
|
|
11
|
+
}),
|
|
12
|
+
);
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
};
|
|
@@ -52,7 +52,8 @@ export const createRuntimeUsingPlaywright = ({
|
|
|
52
52
|
coverageMethodForBrowsers,
|
|
53
53
|
coverageFileUrl,
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
teardown,
|
|
56
|
+
isTestPlan,
|
|
56
57
|
stopSignal,
|
|
57
58
|
keepRunning,
|
|
58
59
|
onConsole,
|
|
@@ -71,7 +72,7 @@ export const createRuntimeUsingPlaywright = ({
|
|
|
71
72
|
await cleanupCallbackList.notify({ reason });
|
|
72
73
|
});
|
|
73
74
|
|
|
74
|
-
const isBrowserDedicatedToExecution = isolatedTab || !
|
|
75
|
+
const isBrowserDedicatedToExecution = isolatedTab || !isTestPlan;
|
|
75
76
|
let browserAndContextPromise = isBrowserDedicatedToExecution
|
|
76
77
|
? null
|
|
77
78
|
: browserPromiseCache.get(label);
|
|
@@ -320,15 +321,11 @@ export const createRuntimeUsingPlaywright = ({
|
|
|
320
321
|
cleanupCallbackList.add(() => {
|
|
321
322
|
browser.removeListener("disconnected", disconnectedCallback);
|
|
322
323
|
});
|
|
323
|
-
|
|
324
|
-
stopAfterAllSignal.notify = async () => {
|
|
325
|
-
await notifyPrevious();
|
|
324
|
+
teardown.addCallback(async () => {
|
|
326
325
|
browser.removeListener("disconnected", disconnectedCallback);
|
|
327
|
-
logger.debug(
|
|
328
|
-
`stopAfterAllSignal notified -> closing ${browserName}`,
|
|
329
|
-
);
|
|
326
|
+
logger.debug(`testPlan teardown -> closing ${browserName}`);
|
|
330
327
|
await closeBrowser();
|
|
331
|
-
};
|
|
328
|
+
});
|
|
332
329
|
}
|
|
333
330
|
},
|
|
334
331
|
response: async (cb) => {
|