@jskit-ai/jskit-catalog 0.2.34 → 0.2.36
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/catalog/packages.json +477 -384
- package/package.json +1 -1
- package/scripts/browser-test-servers.mjs +45 -9
- package/scripts/browser-tests.mjs +16 -3
- package/scripts/data-access.mjs +564 -0
- package/scripts/jskit.mjs +13 -2
- package/scripts/project-packages.mjs +65 -3
- package/scripts/verify-packages.mjs +23 -0
package/package.json
CHANGED
|
@@ -4,10 +4,15 @@
|
|
|
4
4
|
// Each worker's server environment is also left, readable only by this user, where the worker's own process takes it
|
|
5
5
|
// (browser-tests.mjs). With the app's `signIn` module, each server is then signed in once, and its storage state left
|
|
6
6
|
// for its worker. Each server's output goes to test-results/browser-test-servers/worker-<n>.log; a server that stops
|
|
7
|
-
// ends the run.
|
|
7
|
+
// ends the run. A port that already answers is refused before anything is built or started: a server left there by an
|
|
8
|
+
// earlier run would answer for this run's. Stopped by Playwright (SIGTERM), by Ctrl-C, or by the end of the stdin pipe
|
|
9
|
+
// Playwright gives its webServer (a Playwright that ended without stopping it), the launcher stops its servers and
|
|
10
|
+
// waits for them, within Playwright's grace, before it exits.
|
|
8
11
|
import { spawn } from "node:child_process";
|
|
9
|
-
import {
|
|
12
|
+
import { once } from "node:events";
|
|
13
|
+
import { createWriteStream, fstatSync, mkdirSync, writeFileSync } from "node:fs";
|
|
10
14
|
import { createRequire } from "node:module";
|
|
15
|
+
import net from "node:net";
|
|
11
16
|
import path from "node:path";
|
|
12
17
|
import process from "node:process";
|
|
13
18
|
import { pathToFileURL } from "node:url";
|
|
@@ -16,6 +21,8 @@ import { buildFrontend } from "./browser-test-build.mjs";
|
|
|
16
21
|
const SETTINGS_VARIABLE = "JSKIT_BROWSER_TESTS";
|
|
17
22
|
const LOG_DIRECTORY = path.join("test-results", "browser-test-servers");
|
|
18
23
|
const READY_TIMEOUT_MS = 60_000;
|
|
24
|
+
// Within the 10 s gracefulShutdown browser-tests.mjs gives Playwright's webServer; a server still running then is killed.
|
|
25
|
+
const STOP_WAIT_MS = 9_000;
|
|
19
26
|
|
|
20
27
|
const seconds = (since) => `${((performance.now() - since) / 1000).toFixed(1)}s`;
|
|
21
28
|
const words = (command) => String(command).trim().split(/\s+/u).filter(Boolean);
|
|
@@ -51,6 +58,21 @@ async function signInWorkers(appRoot, signIn, baseUrls, environments, signedInSt
|
|
|
51
58
|
console.log(`[browser-tests] ${baseUrls.length} worker(s) signed in in ${seconds(started)}.`);
|
|
52
59
|
}
|
|
53
60
|
|
|
61
|
+
const answers = (host, port) => new Promise((resolve) => {
|
|
62
|
+
const socket = net.connect(port, host);
|
|
63
|
+
socket.once("connect", () => { socket.destroy(); resolve(true); });
|
|
64
|
+
socket.once("error", () => resolve(false));
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
async function refuseBusyPorts(host, ports) {
|
|
68
|
+
const taken = await Promise.all(ports.map((port) => answers(host, port)));
|
|
69
|
+
const busy = ports.filter((_, index) => taken[index]);
|
|
70
|
+
if (busy.length > 0) {
|
|
71
|
+
throw new Error(`Port ${busy.join(", ")} on ${host} already answers: a server left there would answer for this run's. ` +
|
|
72
|
+
"Stop it, or set PLAYWRIGHT_PORT to free ports.");
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
54
76
|
async function waitUntilReady(url, server, name) {
|
|
55
77
|
const started = performance.now();
|
|
56
78
|
const deadline = Date.now() + READY_TIMEOUT_MS;
|
|
@@ -72,12 +94,27 @@ async function waitUntilReady(url, server, name) {
|
|
|
72
94
|
|
|
73
95
|
const servers = [];
|
|
74
96
|
let stopping = false;
|
|
97
|
+
const running = (server) => server.exitCode === null && server.signalCode === null;
|
|
75
98
|
function stopServers() {
|
|
76
99
|
stopping = true;
|
|
77
|
-
for (const server of servers) server.kill("SIGTERM");
|
|
100
|
+
for (const server of servers) if (running(server)) server.kill("SIGTERM");
|
|
101
|
+
}
|
|
102
|
+
async function stopAndExit(code) {
|
|
103
|
+
if (stopping) return;
|
|
104
|
+
stopServers();
|
|
105
|
+
const stopped = Promise.all(servers.filter(running).map((server) => once(server, "exit")));
|
|
106
|
+
await Promise.race([stopped, new Promise((resolve) => setTimeout(resolve, STOP_WAIT_MS))]);
|
|
107
|
+
for (const server of servers) if (running(server)) server.kill("SIGKILL");
|
|
108
|
+
process.exit(code);
|
|
109
|
+
}
|
|
110
|
+
process.once("SIGINT", () => stopAndExit(130));
|
|
111
|
+
process.once("SIGTERM", () => stopAndExit(143));
|
|
112
|
+
// Only a pipe ends when Playwright does; a launcher started without one (stdin ignored, /dev/null) would end at once.
|
|
113
|
+
const input = fstatSync(0);
|
|
114
|
+
if (input.isFIFO() || input.isSocket()) {
|
|
115
|
+
process.stdin.once("end", () => stopAndExit(1));
|
|
116
|
+
process.stdin.resume();
|
|
78
117
|
}
|
|
79
|
-
process.once("SIGINT", () => { stopServers(); process.exit(130); });
|
|
80
|
-
process.once("SIGTERM", () => { stopServers(); process.exit(143); });
|
|
81
118
|
process.once("exit", stopServers);
|
|
82
119
|
|
|
83
120
|
const started = performance.now();
|
|
@@ -90,6 +127,7 @@ const baseEnvironment = { ...process.env };
|
|
|
90
127
|
delete baseEnvironment[SETTINGS_VARIABLE];
|
|
91
128
|
const baseUrls = Array.from({ length: workers }, (_, index) => `http://${host}:${basePort + index}`);
|
|
92
129
|
try {
|
|
130
|
+
await refuseBusyPorts(host, baseUrls.map((_, index) => basePort + index));
|
|
93
131
|
const [, workerEnvironments] = await Promise.all([
|
|
94
132
|
buildFrontend({ appRoot, build }),
|
|
95
133
|
prepareWorkers(appRoot, prepare, workers, baseUrls)
|
|
@@ -117,8 +155,7 @@ try {
|
|
|
117
155
|
child.once("exit", (code, signal) => {
|
|
118
156
|
if (stopping) return;
|
|
119
157
|
console.error(`[browser-tests] ${name} stopped (${signal || `exit ${code}`}); log: ${log}`);
|
|
120
|
-
|
|
121
|
-
process.exit(1);
|
|
158
|
+
stopAndExit(1);
|
|
122
159
|
});
|
|
123
160
|
servers.push(child);
|
|
124
161
|
return waitUntilReady(new URL(health, baseUrl), child, name);
|
|
@@ -127,6 +164,5 @@ try {
|
|
|
127
164
|
console.log(`jskit browser tests: ${workers} server(s) ready in ${seconds(started)}; logs in ${LOG_DIRECTORY}.`);
|
|
128
165
|
} catch (error) {
|
|
129
166
|
console.error(`[browser-tests] ${error.message}`);
|
|
130
|
-
|
|
131
|
-
process.exit(1);
|
|
167
|
+
await stopAndExit(1);
|
|
132
168
|
}
|
|
@@ -21,6 +21,11 @@ const DEFAULT_PORT = 4173;
|
|
|
21
21
|
// A whole run's default bound: a suite that would take hours stops instead, and says so. A suite measured to need
|
|
22
22
|
// longer passes its own globalTimeout, with the measurement beside it.
|
|
23
23
|
const GLOBAL_TIMEOUT_MS = 20 * 60_000;
|
|
24
|
+
// A click, fill or navigation waiting on something that never comes (a stale locator) fails in seconds, with its own
|
|
25
|
+
// error, instead of holding the test until its 60 s timeout. Over 652 passing app tests the slowest action or page
|
|
26
|
+
// load took 9.0 s, and expect already waits 10 s.
|
|
27
|
+
const ACTION_TIMEOUT_MS = 15_000;
|
|
28
|
+
const NAVIGATION_TIMEOUT_MS = 30_000;
|
|
24
29
|
// Building the frontend when its sources changed, preparing the databases and starting every server.
|
|
25
30
|
const SERVERS_TIMEOUT_MS = 180_000;
|
|
26
31
|
// Where the servers' launcher leaves each worker's server environment, for that worker's own process.
|
|
@@ -90,6 +95,8 @@ function preparedWorkerEnvironment(file, index) {
|
|
|
90
95
|
* @param {Record<string, string>} [options.env] Environment every server gets, such as its test-only drivers.
|
|
91
96
|
* @param {string} [options.health] The path that answers 200 once a server is ready.
|
|
92
97
|
* @param {number} [options.globalTimeout] The whole run's bound in milliseconds (20 minutes); never 0.
|
|
98
|
+
* @param {number} [options.actionTimeout] One click, fill or other action's bound in milliseconds (15 s); never 0.
|
|
99
|
+
* @param {number} [options.navigationTimeout] One navigation's bound in milliseconds (30 s); never 0.
|
|
93
100
|
* @param {Record<string, string | undefined>} [options.environment] Read, and given a worker's prepared environment,
|
|
94
101
|
* instead of process.env (tests).
|
|
95
102
|
* @param {string} [options.cwd] The directory Playwright runs in (tests).
|
|
@@ -104,12 +111,19 @@ function browserTestConfig({
|
|
|
104
111
|
env = {},
|
|
105
112
|
health = "/api/health",
|
|
106
113
|
globalTimeout = GLOBAL_TIMEOUT_MS,
|
|
114
|
+
actionTimeout = ACTION_TIMEOUT_MS,
|
|
115
|
+
navigationTimeout = NAVIGATION_TIMEOUT_MS,
|
|
107
116
|
environment = process.env,
|
|
108
117
|
cwd = process.cwd()
|
|
109
118
|
} = {}) {
|
|
110
119
|
if (!Number.isInteger(globalTimeout) || globalTimeout < 1) {
|
|
111
120
|
throw new Error("browserTestConfig() needs a globalTimeout of at least 1 ms; a run is always bounded.");
|
|
112
121
|
}
|
|
122
|
+
for (const [name, value] of Object.entries({ actionTimeout, navigationTimeout })) {
|
|
123
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
124
|
+
throw new Error(`browserTestConfig() needs an ${name} of at least 1 ms; a stale locator fails in seconds, not minutes.`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
113
127
|
const managed = managedBaseUrl(environment);
|
|
114
128
|
const workers = browserTestWorkers({ env: environment });
|
|
115
129
|
const basePort = positiveInteger(environment.PLAYWRIGHT_PORT) || port;
|
|
@@ -145,9 +159,8 @@ function browserTestConfig({
|
|
|
145
159
|
use: {
|
|
146
160
|
baseURL: managed || `http://${HOST}:${basePort + workerIndex}`,
|
|
147
161
|
headless: true,
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
navigationTimeout: 30_000,
|
|
162
|
+
actionTimeout,
|
|
163
|
+
navigationTimeout,
|
|
151
164
|
// A failed test keeps its trace to read; no screencast is recorded while tests run.
|
|
152
165
|
trace: { mode: "retain-on-failure", screenshots: false, snapshots: true, sources: true },
|
|
153
166
|
...(storageState ? { storageState } : {})
|