velocious 1.0.591 → 1.0.593
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/README.md +9 -1
- package/build/configuration-types.js +1 -1
- package/build/database/drivers/base.js +53 -34
- package/build/database/drivers/mssql/index.js +32 -0
- package/build/database/drivers/mysql/index.js +17 -0
- package/build/database/drivers/pgsql/index.js +11 -0
- package/build/database/drivers/sqlite/base.js +12 -0
- package/build/database/drivers/sqlite/connection-sql-js.js +3 -2
- package/build/database/drivers/sqlite/index.js +7 -1
- package/build/database/drivers/sqlite/index.native.js +9 -3
- package/build/database/drivers/sqlite/index.web.js +6 -3
- package/build/environment-handlers/base.js +18 -0
- package/build/environment-handlers/node.js +32 -0
- package/build/src/configuration-types.d.ts +2 -2
- package/build/src/configuration-types.js +2 -2
- package/build/src/database/drivers/base.d.ts +11 -0
- package/build/src/database/drivers/base.d.ts.map +1 -1
- package/build/src/database/drivers/base.js +54 -37
- package/build/src/database/drivers/mssql/index.d.ts +7 -0
- package/build/src/database/drivers/mssql/index.d.ts.map +1 -1
- package/build/src/database/drivers/mssql/index.js +15 -1
- package/build/src/database/drivers/mysql/index.d.ts +7 -0
- package/build/src/database/drivers/mysql/index.d.ts.map +1 -1
- package/build/src/database/drivers/mysql/index.js +15 -1
- package/build/src/database/drivers/pgsql/index.d.ts +6 -0
- package/build/src/database/drivers/pgsql/index.d.ts.map +1 -1
- package/build/src/database/drivers/pgsql/index.js +10 -1
- package/build/src/database/drivers/sqlite/base.d.ts +7 -0
- package/build/src/database/drivers/sqlite/base.d.ts.map +1 -1
- package/build/src/database/drivers/sqlite/base.js +11 -1
- package/build/src/database/drivers/sqlite/connection-sql-js.d.ts +4 -1
- package/build/src/database/drivers/sqlite/connection-sql-js.d.ts.map +1 -1
- package/build/src/database/drivers/sqlite/connection-sql-js.js +4 -3
- package/build/src/database/drivers/sqlite/index.d.ts +2 -1
- package/build/src/database/drivers/sqlite/index.d.ts.map +1 -1
- package/build/src/database/drivers/sqlite/index.js +7 -2
- package/build/src/database/drivers/sqlite/index.native.d.ts +2 -1
- package/build/src/database/drivers/sqlite/index.native.d.ts.map +1 -1
- package/build/src/database/drivers/sqlite/index.native.js +9 -4
- package/build/src/database/drivers/sqlite/index.web.d.ts +2 -1
- package/build/src/database/drivers/sqlite/index.web.d.ts.map +1 -1
- package/build/src/database/drivers/sqlite/index.web.js +7 -4
- package/build/src/environment-handlers/base.d.ts +16 -0
- package/build/src/environment-handlers/base.d.ts.map +1 -1
- package/build/src/environment-handlers/base.js +17 -1
- package/build/src/environment-handlers/node.d.ts +20 -0
- package/build/src/environment-handlers/node.d.ts.map +1 -1
- package/build/src/environment-handlers/node.js +29 -1
- package/build/src/sync/conflict-strategy.d.ts.map +1 -1
- package/build/src/sync/conflict-strategy.js +7 -1
- package/build/src/sync/local-mutation-log.d.ts +10 -0
- package/build/src/sync/local-mutation-log.d.ts.map +1 -1
- package/build/src/sync/local-mutation-log.js +21 -1
- package/build/src/sync/sync-api-client-types.d.ts +15 -0
- package/build/src/sync/sync-api-client-types.d.ts.map +1 -1
- package/build/src/sync/sync-api-client-types.js +4 -1
- package/build/src/sync/sync-api-client.d.ts +91 -0
- package/build/src/sync/sync-api-client.d.ts.map +1 -1
- package/build/src/sync/sync-api-client.js +184 -1
- package/build/src/sync/sync-client-types.d.ts +57 -0
- package/build/src/sync/sync-client-types.d.ts.map +1 -1
- package/build/src/sync/sync-client-types.js +16 -1
- package/build/src/sync/sync-client.d.ts +56 -7
- package/build/src/sync/sync-client.d.ts.map +1 -1
- package/build/src/sync/sync-client.js +180 -15
- package/build/src/sync/sync-envelope-replay-service.d.ts +31 -2
- package/build/src/sync/sync-envelope-replay-service.d.ts.map +1 -1
- package/build/src/sync/sync-envelope-replay-service.js +98 -16
- package/build/src/sync/sync-model-change-feed-service.d.ts.map +1 -1
- package/build/src/sync/sync-model-change-feed-service.js +3 -4
- package/build/src/sync/sync-replay-persisted-data.d.ts +26 -0
- package/build/src/sync/sync-replay-persisted-data.d.ts.map +1 -0
- package/build/src/sync/sync-replay-persisted-data.js +43 -0
- package/build/src/testing/shared-transaction-broker.d.ts +14 -0
- package/build/src/testing/shared-transaction-broker.d.ts.map +1 -1
- package/build/src/testing/shared-transaction-broker.js +44 -7
- package/build/src/testing/shared-transaction-connection-coordinator.d.ts +5 -4
- package/build/src/testing/shared-transaction-connection-coordinator.d.ts.map +1 -1
- package/build/src/testing/shared-transaction-connection-coordinator.js +21 -10
- package/build/sync/conflict-strategy.js +3 -0
- package/build/sync/local-mutation-log.js +24 -0
- package/build/sync/sync-api-client-types.js +3 -0
- package/build/sync/sync-api-client.js +203 -0
- package/build/sync/sync-client-types.js +16 -0
- package/build/sync/sync-client.js +198 -14
- package/build/sync/sync-envelope-replay-service.js +109 -12
- package/build/sync/sync-model-change-feed-service.js +2 -4
- package/build/sync/sync-replay-persisted-data.js +50 -0
- package/build/testing/shared-transaction-broker.js +48 -7
- package/build/testing/shared-transaction-connection-coordinator.js +23 -9
- package/build/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/scripts/browser-test-session.js +489 -0
- package/scripts/prewarm-chromedriver.js +3 -22
- package/scripts/test-browser.js +47 -15
- package/src/configuration-types.js +1 -1
- package/src/database/drivers/base.js +53 -34
- package/src/database/drivers/mssql/index.js +32 -0
- package/src/database/drivers/mysql/index.js +17 -0
- package/src/database/drivers/pgsql/index.js +11 -0
- package/src/database/drivers/sqlite/base.js +12 -0
- package/src/database/drivers/sqlite/connection-sql-js.js +3 -2
- package/src/database/drivers/sqlite/index.js +7 -1
- package/src/database/drivers/sqlite/index.native.js +9 -3
- package/src/database/drivers/sqlite/index.web.js +6 -3
- package/src/environment-handlers/base.js +18 -0
- package/src/environment-handlers/node.js +32 -0
- package/src/sync/conflict-strategy.js +3 -0
- package/src/sync/local-mutation-log.js +24 -0
- package/src/sync/sync-api-client-types.js +3 -0
- package/src/sync/sync-api-client.js +203 -0
- package/src/sync/sync-client-types.js +16 -0
- package/src/sync/sync-client.js +198 -14
- package/src/sync/sync-envelope-replay-service.js +109 -12
- package/src/sync/sync-model-change-feed-service.js +2 -4
- package/src/sync/sync-replay-persisted-data.js +50 -0
- package/src/testing/shared-transaction-broker.js +48 -7
- package/src/testing/shared-transaction-connection-coordinator.js +23 -9
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
3
|
+
import { spawn } from "node:child_process"
|
|
4
|
+
import { execFile } from "node:child_process"
|
|
5
|
+
import fs from "node:fs/promises"
|
|
6
|
+
import { createRequire } from "node:module"
|
|
7
|
+
import path from "node:path"
|
|
8
|
+
import { promisify } from "node:util"
|
|
9
|
+
import timeout from "awaitery/build/timeout.js"
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {object} BrowserTestChromeRuntime
|
|
13
|
+
* @property {string} browserPath - Chrome executable path.
|
|
14
|
+
* @property {string} browserVersion - Chrome version.
|
|
15
|
+
* @property {string} driverPath - ChromeDriver executable path.
|
|
16
|
+
* @property {string} driverVersion - ChromeDriver version.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @typedef {object} BrowserTestSystemTest
|
|
21
|
+
* @property {() => Promise<void>} start - Starts SystemTest.
|
|
22
|
+
* @property {() => Promise<void>} stop - Stops SystemTest.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @typedef {object} BrowserTestChromeDriverService
|
|
27
|
+
* @property {() => Promise<string>} start - Starts ChromeDriver and returns its URL.
|
|
28
|
+
* @property {() => Promise<void>} stop - Stops ChromeDriver and its process group.
|
|
29
|
+
* @property {() => Promise<string>} diagnostics - Reads retained ChromeDriver diagnostics.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
const require = createRequire(import.meta.url)
|
|
33
|
+
const { binaryPaths } = require("selenium-webdriver/common/seleniumManager.js")
|
|
34
|
+
const { CancellationError, waitForServer } = require("selenium-webdriver/http/util.js")
|
|
35
|
+
const { findFreePort } = require("selenium-webdriver/net/portprober.js")
|
|
36
|
+
const execFileAsync = promisify(execFile)
|
|
37
|
+
const CHROMEDRIVER_START_TIMEOUT_MS = 30000
|
|
38
|
+
const PROCESS_STOP_TIMEOUT_MS = 10000
|
|
39
|
+
|
|
40
|
+
/** @returns {string} - Default runtime manifest path. */
|
|
41
|
+
function defaultManifestPath() {
|
|
42
|
+
return path.join(process.cwd(), "tmp", "browser-test-chrome-runtime.json")
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** @returns {string} - Default diagnostic directory. */
|
|
46
|
+
function defaultDiagnosticsDirectory() {
|
|
47
|
+
return path.join(process.cwd(), "tmp", "browser-test-chrome")
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @param {string} executablePath - Executable path.
|
|
52
|
+
* @returns {Promise<string>} - Version command output.
|
|
53
|
+
*/
|
|
54
|
+
async function readExecutableVersion(executablePath) {
|
|
55
|
+
const { stdout, stderr } = await execFileAsync(executablePath, ["--version"])
|
|
56
|
+
|
|
57
|
+
return `${stdout}${stderr}`.trim()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* @param {string} versionOutput - Version command output.
|
|
62
|
+
* @param {string} executableName - Executable description.
|
|
63
|
+
* @returns {string} - Parsed four-part version.
|
|
64
|
+
*/
|
|
65
|
+
function parseExecutableVersion(versionOutput, executableName) {
|
|
66
|
+
const match = versionOutput.match(/\b(\d+\.\d+\.\d+\.\d+)\b/)
|
|
67
|
+
|
|
68
|
+
if (!match) throw new Error(`Could not parse ${executableName} version from: ${versionOutput}`)
|
|
69
|
+
|
|
70
|
+
return match[1]
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* @param {string} executablePath - Executable path.
|
|
75
|
+
* @param {string} description - Executable description.
|
|
76
|
+
* @returns {Promise<void>} - Resolves when executable exists and is executable.
|
|
77
|
+
*/
|
|
78
|
+
async function assertExecutable(executablePath, description) {
|
|
79
|
+
if (!path.isAbsolute(executablePath)) {
|
|
80
|
+
throw new Error(`${description} path must be absolute: ${executablePath}`)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
await fs.access(executablePath, fs.constants.X_OK)
|
|
85
|
+
} catch (error) {
|
|
86
|
+
throw new Error(`${description} is not executable: ${executablePath}`, {cause: error})
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Resolves, validates, and persists the exact Chrome/ChromeDriver pair.
|
|
92
|
+
* @param {object} [args] - Prewarm options.
|
|
93
|
+
* @param {() => {browserPath?: string, driverPath?: string}} [args.binaryPathsResolver] - Selenium path resolver.
|
|
94
|
+
* @param {string} [args.manifestPath] - Runtime manifest path.
|
|
95
|
+
* @param {(executablePath: string) => Promise<string>} [args.versionReader] - Executable version reader.
|
|
96
|
+
* @returns {Promise<BrowserTestChromeRuntime>} - Prewarmed runtime.
|
|
97
|
+
*/
|
|
98
|
+
export async function prewarmBrowserTestChromeRuntime({
|
|
99
|
+
binaryPathsResolver = () => binaryPaths([
|
|
100
|
+
"--browser",
|
|
101
|
+
"chrome",
|
|
102
|
+
"--language-binding",
|
|
103
|
+
"javascript",
|
|
104
|
+
"--output",
|
|
105
|
+
"json",
|
|
106
|
+
"--avoid-browser-download"
|
|
107
|
+
]),
|
|
108
|
+
manifestPath = defaultManifestPath(),
|
|
109
|
+
versionReader = readExecutableVersion
|
|
110
|
+
} = {}) {
|
|
111
|
+
const { browserPath, driverPath } = binaryPathsResolver()
|
|
112
|
+
|
|
113
|
+
if (!browserPath) throw new Error("Selenium Manager did not resolve a Chrome executable")
|
|
114
|
+
if (!driverPath) throw new Error("Selenium Manager did not resolve a ChromeDriver executable")
|
|
115
|
+
|
|
116
|
+
await assertExecutable(browserPath, "Chrome")
|
|
117
|
+
await assertExecutable(driverPath, "ChromeDriver")
|
|
118
|
+
|
|
119
|
+
let browserVersionOutput
|
|
120
|
+
let driverVersionOutput
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
browserVersionOutput = await versionReader(browserPath)
|
|
124
|
+
} catch (error) {
|
|
125
|
+
throw new Error(`Chrome could not report its version at ${browserPath}`, {cause: error})
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
driverVersionOutput = await versionReader(driverPath)
|
|
130
|
+
} catch (error) {
|
|
131
|
+
throw new Error(`ChromeDriver could not report its version at ${driverPath}`, {cause: error})
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const browserVersion = parseExecutableVersion(browserVersionOutput, "Chrome")
|
|
135
|
+
const driverVersion = parseExecutableVersion(driverVersionOutput, "ChromeDriver")
|
|
136
|
+
|
|
137
|
+
if (browserVersion.split(".")[0] !== driverVersion.split(".")[0]) {
|
|
138
|
+
throw new Error(`Chrome ${browserVersion} is incompatible with ChromeDriver ${driverVersion}`)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const runtime = {browserPath, browserVersion, driverPath, driverVersion}
|
|
142
|
+
|
|
143
|
+
await fs.mkdir(path.dirname(manifestPath), {recursive: true})
|
|
144
|
+
await fs.writeFile(manifestPath, `${JSON.stringify(runtime, null, 2)}\n`, "utf8")
|
|
145
|
+
|
|
146
|
+
return runtime
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Loads the exact runtime persisted by prewarming.
|
|
151
|
+
* @param {object} [args] - Load options.
|
|
152
|
+
* @param {string} [args.manifestPath] - Runtime manifest path.
|
|
153
|
+
* @returns {Promise<BrowserTestChromeRuntime>} - Persisted runtime.
|
|
154
|
+
*/
|
|
155
|
+
export async function loadBrowserTestChromeRuntime({manifestPath = defaultManifestPath()} = {}) {
|
|
156
|
+
const parsed = JSON.parse(await fs.readFile(manifestPath, "utf8"))
|
|
157
|
+
const browserPath = parsed?.browserPath
|
|
158
|
+
const browserVersion = parsed?.browserVersion
|
|
159
|
+
const driverPath = parsed?.driverPath
|
|
160
|
+
const driverVersion = parsed?.driverVersion
|
|
161
|
+
|
|
162
|
+
if (typeof browserPath !== "string" || typeof browserVersion !== "string" || typeof driverPath !== "string" || typeof driverVersion !== "string") {
|
|
163
|
+
throw new Error(`Invalid browser test Chrome runtime manifest: ${manifestPath}`)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
await assertExecutable(browserPath, "Chrome")
|
|
167
|
+
await assertExecutable(driverPath, "ChromeDriver")
|
|
168
|
+
|
|
169
|
+
return {browserPath, browserVersion, driverPath, driverVersion}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Loads a prewarmed runtime, preparing it for direct local browser runs when absent.
|
|
174
|
+
* @returns {Promise<BrowserTestChromeRuntime>} - Browser runtime.
|
|
175
|
+
*/
|
|
176
|
+
export async function loadOrPrewarmBrowserTestChromeRuntime() {
|
|
177
|
+
try {
|
|
178
|
+
return await loadBrowserTestChromeRuntime()
|
|
179
|
+
} catch (error) {
|
|
180
|
+
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
|
|
181
|
+
return await prewarmBrowserTestChromeRuntime()
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
throw error
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Formats a child process exit.
|
|
190
|
+
* @param {{code: number | null, error?: Error, signal: NodeJS.Signals | null}} result - Exit result.
|
|
191
|
+
* @returns {string} - Exit description.
|
|
192
|
+
*/
|
|
193
|
+
function formatProcessExit(result) {
|
|
194
|
+
if (result.error) return result.error.message
|
|
195
|
+
if (result.signal) return `signal ${result.signal}`
|
|
196
|
+
|
|
197
|
+
return `exit code ${String(result.code)}`
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export class ManagedChromeDriverProcess {
|
|
201
|
+
/**
|
|
202
|
+
* @param {BrowserTestChromeRuntime} runtime - Chrome runtime.
|
|
203
|
+
* @param {string} [diagnosticsDirectory] - Diagnostic output directory.
|
|
204
|
+
*/
|
|
205
|
+
constructor(runtime, diagnosticsDirectory = defaultDiagnosticsDirectory()) {
|
|
206
|
+
this.runtime = runtime
|
|
207
|
+
this.diagnosticsDirectory = diagnosticsDirectory
|
|
208
|
+
this.driverLogPath = path.join(diagnosticsDirectory, "chromedriver.log")
|
|
209
|
+
this.processOutputPath = path.join(diagnosticsDirectory, "chromedriver-process.log")
|
|
210
|
+
/** @type {import("node:child_process").ChildProcess | undefined} */
|
|
211
|
+
this.child = undefined
|
|
212
|
+
/** @type {Promise<{code: number | null, error?: Error, signal: NodeJS.Signals | null}> | undefined} */
|
|
213
|
+
this.exitResult = undefined
|
|
214
|
+
/** @type {import("node:fs/promises").FileHandle | undefined} */
|
|
215
|
+
this.outputFile = undefined
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** @returns {Promise<string>} - ChromeDriver service URL. */
|
|
219
|
+
async start() {
|
|
220
|
+
await fs.mkdir(this.diagnosticsDirectory, {recursive: true})
|
|
221
|
+
await fs.rm(this.driverLogPath, {force: true})
|
|
222
|
+
this.outputFile = await fs.open(this.processOutputPath, "w")
|
|
223
|
+
|
|
224
|
+
const port = await findFreePort("127.0.0.1")
|
|
225
|
+
const child = spawn(this.runtime.driverPath, [
|
|
226
|
+
`--port=${port}`,
|
|
227
|
+
"--verbose",
|
|
228
|
+
`--log-path=${this.driverLogPath}`,
|
|
229
|
+
"--enable-chrome-logs"
|
|
230
|
+
], {
|
|
231
|
+
detached: process.platform !== "win32",
|
|
232
|
+
stdio: ["ignore", this.outputFile.fd, this.outputFile.fd]
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
this.child = child
|
|
236
|
+
this.exitResult = new Promise((resolve) => {
|
|
237
|
+
child.once("error", (error) => resolve({code: null, error, signal: null}))
|
|
238
|
+
child.once("exit", (code, signal) => resolve({code, signal}))
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
const serviceUrl = `http://127.0.0.1:${port}`
|
|
242
|
+
const earlyExit = this.exitResult.then((result) => {
|
|
243
|
+
throw new Error(`ChromeDriver terminated during startup with ${formatProcessExit(result)}`)
|
|
244
|
+
})
|
|
245
|
+
const exitCancellation = this.exitResult.then(() => undefined)
|
|
246
|
+
|
|
247
|
+
try {
|
|
248
|
+
await Promise.race([
|
|
249
|
+
waitForServer(serviceUrl, CHROMEDRIVER_START_TIMEOUT_MS, exitCancellation),
|
|
250
|
+
earlyExit
|
|
251
|
+
])
|
|
252
|
+
} catch (error) {
|
|
253
|
+
if (error instanceof CancellationError) await earlyExit
|
|
254
|
+
|
|
255
|
+
throw error
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return serviceUrl
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* @param {NodeJS.Signals} signal - Signal to deliver.
|
|
263
|
+
* @returns {void}
|
|
264
|
+
*/
|
|
265
|
+
signalProcessGroup(signal) {
|
|
266
|
+
const child = this.child
|
|
267
|
+
|
|
268
|
+
if (!child?.pid || child.exitCode !== null || child.signalCode !== null) return
|
|
269
|
+
|
|
270
|
+
try {
|
|
271
|
+
if (process.platform === "win32") {
|
|
272
|
+
child.kill(signal)
|
|
273
|
+
} else {
|
|
274
|
+
process.kill(-child.pid, signal)
|
|
275
|
+
}
|
|
276
|
+
} catch (error) {
|
|
277
|
+
if (error && typeof error === "object" && "code" in error && error.code === "ESRCH") return
|
|
278
|
+
|
|
279
|
+
throw error
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** @returns {Promise<void>} - Stops ChromeDriver and Chrome descendants. */
|
|
284
|
+
async stop() {
|
|
285
|
+
let stopError
|
|
286
|
+
|
|
287
|
+
try {
|
|
288
|
+
if (this.exitResult && this.child?.exitCode === null && this.child.signalCode === null) {
|
|
289
|
+
this.signalProcessGroup("SIGTERM")
|
|
290
|
+
|
|
291
|
+
try {
|
|
292
|
+
await timeout({
|
|
293
|
+
timeout: PROCESS_STOP_TIMEOUT_MS,
|
|
294
|
+
errorMessage: "Timed out waiting for ChromeDriver process group to stop"
|
|
295
|
+
}, async () => await this.exitResult)
|
|
296
|
+
} catch (error) {
|
|
297
|
+
this.signalProcessGroup("SIGKILL")
|
|
298
|
+
await this.exitResult
|
|
299
|
+
stopError = error instanceof Error ? error : new Error(String(error))
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
} catch (error) {
|
|
303
|
+
stopError = error instanceof Error ? error : new Error(String(error))
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
try {
|
|
307
|
+
await this.outputFile?.close()
|
|
308
|
+
} catch (error) {
|
|
309
|
+
const outputCloseError = error instanceof Error ? error : new Error(String(error))
|
|
310
|
+
stopError = stopError
|
|
311
|
+
? new AggregateError([stopError, outputCloseError], "ChromeDriver process and output cleanup failed")
|
|
312
|
+
: outputCloseError
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
this.outputFile = undefined
|
|
316
|
+
|
|
317
|
+
if (stopError) throw stopError
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** @returns {Promise<string>} - Retained ChromeDriver diagnostics. */
|
|
321
|
+
async diagnostics() {
|
|
322
|
+
const sections = []
|
|
323
|
+
|
|
324
|
+
for (const [label, filePath] of [
|
|
325
|
+
["ChromeDriver log", this.driverLogPath],
|
|
326
|
+
["ChromeDriver process output", this.processOutputPath]
|
|
327
|
+
]) {
|
|
328
|
+
try {
|
|
329
|
+
const content = await fs.readFile(filePath, "utf8")
|
|
330
|
+
sections.push(`${label} (${filePath}):\n${content || "(empty)"}`)
|
|
331
|
+
} catch (error) {
|
|
332
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
333
|
+
sections.push(`${label} (${filePath}) unavailable: ${message}`)
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return sections.join("\n")
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Captures Chrome and ChromeDriver processes for startup diagnostics.
|
|
343
|
+
* @param {string} phase - Snapshot phase.
|
|
344
|
+
* @returns {Promise<string>} - Process snapshot.
|
|
345
|
+
*/
|
|
346
|
+
async function browserProcessSnapshot(phase) {
|
|
347
|
+
if (process.platform === "win32") return `${phase}: browser process snapshots are unavailable on Windows`
|
|
348
|
+
|
|
349
|
+
try {
|
|
350
|
+
const { stdout } = await execFileAsync("ps", ["-eo", "pid,ppid,pgid,stat,etime,comm,args"])
|
|
351
|
+
const processLines = stdout
|
|
352
|
+
.split("\n")
|
|
353
|
+
.filter((line) => /\s(?:chrome|chromedriver|google-chrome)\s/.test(line))
|
|
354
|
+
|
|
355
|
+
return `${phase}:\n${processLines.length > 0 ? processLines.join("\n") : "(no Chrome/ChromeDriver processes)"}`
|
|
356
|
+
} catch (error) {
|
|
357
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
358
|
+
return `${phase}: process snapshot failed: ${message}`
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Owns ChromeDriver and SystemTest startup as one cleanup boundary.
|
|
364
|
+
*/
|
|
365
|
+
export class BrowserTestSession {
|
|
366
|
+
/**
|
|
367
|
+
* @param {object} args - Session options.
|
|
368
|
+
* @param {(runtime: BrowserTestChromeRuntime) => Promise<BrowserTestChromeDriverService>} [args.chromeDriverServiceFactory] - Driver service factory.
|
|
369
|
+
* @param {(phase: string) => Promise<string>} [args.processSnapshot] - Process snapshot callback.
|
|
370
|
+
* @param {BrowserTestChromeRuntime} args.runtime - Chrome runtime.
|
|
371
|
+
* @param {(args: {browserPath: string, remoteUrl: string}) => BrowserTestSystemTest} args.systemTestFactory - SystemTest factory.
|
|
372
|
+
*/
|
|
373
|
+
constructor({
|
|
374
|
+
chromeDriverServiceFactory = async (runtime) => new ManagedChromeDriverProcess(runtime),
|
|
375
|
+
processSnapshot = browserProcessSnapshot,
|
|
376
|
+
runtime,
|
|
377
|
+
systemTestFactory
|
|
378
|
+
}) {
|
|
379
|
+
this.chromeDriverServiceFactory = chromeDriverServiceFactory
|
|
380
|
+
this.processSnapshot = processSnapshot
|
|
381
|
+
this.runtime = runtime
|
|
382
|
+
this.systemTestFactory = systemTestFactory
|
|
383
|
+
/** @type {BrowserTestChromeDriverService | undefined} */
|
|
384
|
+
this.chromeDriverService = undefined
|
|
385
|
+
/** @type {BrowserTestSystemTest | undefined} */
|
|
386
|
+
this.systemTest = undefined
|
|
387
|
+
this.remoteUrlWasPresent = false
|
|
388
|
+
/** @type {string | undefined} */
|
|
389
|
+
this.previousRemoteUrl = undefined
|
|
390
|
+
/** @type {string | undefined} */
|
|
391
|
+
this.remoteUrl = undefined
|
|
392
|
+
this.startupPhase = "creating ChromeDriver service"
|
|
393
|
+
this.stopped = false
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/** @returns {Promise<BrowserTestSystemTest>} - Started SystemTest instance. */
|
|
397
|
+
async start() {
|
|
398
|
+
this.remoteUrlWasPresent = Object.hasOwn(process.env, "SELENIUM_REMOTE_URL")
|
|
399
|
+
this.previousRemoteUrl = process.env.SELENIUM_REMOTE_URL
|
|
400
|
+
|
|
401
|
+
try {
|
|
402
|
+
this.chromeDriverService = await this.chromeDriverServiceFactory(this.runtime)
|
|
403
|
+
this.startupPhase = "starting ChromeDriver service"
|
|
404
|
+
this.remoteUrl = await this.chromeDriverService.start()
|
|
405
|
+
process.env.SELENIUM_REMOTE_URL = this.remoteUrl
|
|
406
|
+
this.systemTest = this.systemTestFactory({browserPath: this.runtime.browserPath, remoteUrl: this.remoteUrl})
|
|
407
|
+
this.startupPhase = "creating Selenium WebDriver session"
|
|
408
|
+
await this.systemTest.start()
|
|
409
|
+
|
|
410
|
+
return this.systemTest
|
|
411
|
+
} catch (error) {
|
|
412
|
+
const startupError = error instanceof Error ? error : new Error(String(error))
|
|
413
|
+
const beforeCleanup = await this.processSnapshot("before cleanup")
|
|
414
|
+
const cleanupErrors = await this.stopResources({driverFirst: true})
|
|
415
|
+
const afterCleanup = await this.processSnapshot("after cleanup")
|
|
416
|
+
const driverDiagnostics = this.chromeDriverService
|
|
417
|
+
? await this.chromeDriverService.diagnostics()
|
|
418
|
+
: "ChromeDriver service was not created"
|
|
419
|
+
const message = [
|
|
420
|
+
`Browser test startup failed while ${this.startupPhase}: ${startupError.message}`,
|
|
421
|
+
`Chrome: ${this.runtime.browserVersion} (${this.runtime.browserPath})`,
|
|
422
|
+
`ChromeDriver: ${this.runtime.driverVersion} (${this.runtime.driverPath})`,
|
|
423
|
+
`ChromeDriver service: ${this.remoteUrl ?? "not started"}`,
|
|
424
|
+
beforeCleanup,
|
|
425
|
+
afterCleanup,
|
|
426
|
+
driverDiagnostics
|
|
427
|
+
].join("\n")
|
|
428
|
+
if (cleanupErrors.length > 0) {
|
|
429
|
+
throw new AggregateError([startupError, ...cleanupErrors], message, {cause: error})
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
throw new Error(message, {cause: error})
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* @param {object} args - Cleanup options.
|
|
438
|
+
* @param {boolean} args.driverFirst - Stop the driver before SystemTest during failed startup.
|
|
439
|
+
* @returns {Promise<Error[]>} - Cleanup errors.
|
|
440
|
+
*/
|
|
441
|
+
async stopResources({driverFirst}) {
|
|
442
|
+
if (this.stopped) return []
|
|
443
|
+
|
|
444
|
+
this.stopped = true
|
|
445
|
+
const cleanupErrors = []
|
|
446
|
+
const stopDriver = async () => {
|
|
447
|
+
try {
|
|
448
|
+
await this.chromeDriverService?.stop()
|
|
449
|
+
} catch (error) {
|
|
450
|
+
cleanupErrors.push(error instanceof Error ? error : new Error(String(error)))
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
const stopSystemTest = async () => {
|
|
454
|
+
try {
|
|
455
|
+
await this.systemTest?.stop()
|
|
456
|
+
} catch (error) {
|
|
457
|
+
cleanupErrors.push(error instanceof Error ? error : new Error(String(error)))
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (driverFirst) {
|
|
462
|
+
await stopDriver()
|
|
463
|
+
await stopSystemTest()
|
|
464
|
+
} else {
|
|
465
|
+
await stopSystemTest()
|
|
466
|
+
await stopDriver()
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (this.remoteUrlWasPresent) {
|
|
470
|
+
process.env.SELENIUM_REMOTE_URL = this.previousRemoteUrl
|
|
471
|
+
} else {
|
|
472
|
+
delete process.env.SELENIUM_REMOTE_URL
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
return cleanupErrors
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Stops SystemTest and the managed ChromeDriver for a browser test session.
|
|
481
|
+
* @param {BrowserTestSession} session - Browser test session.
|
|
482
|
+
* @returns {Promise<void>} - Resolves when all session resources have stopped.
|
|
483
|
+
*/
|
|
484
|
+
export async function stopBrowserTestSession(session) {
|
|
485
|
+
const cleanupErrors = await session.stopResources({driverFirst: false})
|
|
486
|
+
|
|
487
|
+
if (cleanupErrors.length === 1) throw cleanupErrors[0]
|
|
488
|
+
if (cleanupErrors.length > 1) throw new AggregateError(cleanupErrors, "Browser test session cleanup failed")
|
|
489
|
+
}
|
|
@@ -1,26 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
import { createRequire } from "node:module"
|
|
3
|
+
import { prewarmBrowserTestChromeRuntime } from "./browser-test-session.js"
|
|
5
4
|
|
|
6
|
-
const
|
|
7
|
-
const { binaryPaths } = require("selenium-webdriver/common/seleniumManager.js")
|
|
8
|
-
const { browserPath, driverPath } = binaryPaths([
|
|
9
|
-
"--browser",
|
|
10
|
-
"chrome",
|
|
11
|
-
"--language-binding",
|
|
12
|
-
"javascript",
|
|
13
|
-
"--output",
|
|
14
|
-
"json",
|
|
15
|
-
"--avoid-browser-download"
|
|
16
|
-
])
|
|
5
|
+
const runtime = await prewarmBrowserTestChromeRuntime()
|
|
17
6
|
|
|
18
|
-
|
|
19
|
-
throw new Error(`Selenium Manager did not find installed Chrome at ${browserPath || "<missing path>"}`)
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
if (!driverPath || !fs.existsSync(driverPath)) {
|
|
23
|
-
throw new Error(`Selenium Manager did not provision ChromeDriver at ${driverPath || "<missing path>"}`)
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
console.log(`Prewarmed ChromeDriver at ${driverPath} for Chrome at ${browserPath}`)
|
|
7
|
+
console.log(`Prewarmed ChromeDriver ${runtime.driverVersion} at ${runtime.driverPath} for Chrome ${runtime.browserVersion} at ${runtime.browserPath}`)
|
package/scripts/test-browser.js
CHANGED
|
@@ -20,6 +20,11 @@ import {normalizeExamplePatterns, parseFilters} from "../src/testing/test-filter
|
|
|
20
20
|
import fileExists from "../src/utils/file-exists.js"
|
|
21
21
|
import dummyDirectory from "../spec/dummy/dummy-directory.js"
|
|
22
22
|
import {withSourcePeerPackage} from "../src/environment-handlers/node/source-peer-package.js"
|
|
23
|
+
import {
|
|
24
|
+
BrowserTestSession,
|
|
25
|
+
loadOrPrewarmBrowserTestChromeRuntime,
|
|
26
|
+
stopBrowserTestSession
|
|
27
|
+
} from "./browser-test-session.js"
|
|
23
28
|
|
|
24
29
|
const rootDir = process.cwd()
|
|
25
30
|
const distDir = path.join(rootDir, "dist")
|
|
@@ -367,6 +372,8 @@ async function runBrowserTests() {
|
|
|
367
372
|
process.env.VELOCIOUS_BROWSER_BACKEND_PORT = String(browserBackendPort)
|
|
368
373
|
process.env.SYSTEM_TEST_HTTP_PORT = String(systemTestHttpPort)
|
|
369
374
|
|
|
375
|
+
const chromeRuntime = await loadOrPrewarmBrowserTestChromeRuntime()
|
|
376
|
+
|
|
370
377
|
await buildBrowserTestApp()
|
|
371
378
|
|
|
372
379
|
const {testFiles, lineFilters, includeTags, excludeTags, examplePatterns} = await resolveTests(processArgs)
|
|
@@ -381,21 +388,30 @@ async function runBrowserTests() {
|
|
|
381
388
|
lineFilters,
|
|
382
389
|
examplePatterns
|
|
383
390
|
})
|
|
384
|
-
/** @type {
|
|
385
|
-
let
|
|
391
|
+
/** @type {BrowserTestSession | undefined} */
|
|
392
|
+
let browserTestSession
|
|
386
393
|
/** @type {Application | undefined} */
|
|
387
394
|
let backendApplication
|
|
395
|
+
/** @type {Error | undefined} */
|
|
396
|
+
let runError
|
|
388
397
|
|
|
389
398
|
try {
|
|
390
399
|
const backendConfiguration = await loadBrowserBackendConfiguration()
|
|
391
400
|
globalThis.__velocious_browser_test_backend_configuration = backendConfiguration
|
|
392
401
|
backendApplication = await startBrowserBackendServer(backendConfiguration, browserBackendPort)
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
402
|
+
browserTestSession = new BrowserTestSession({
|
|
403
|
+
runtime: chromeRuntime,
|
|
404
|
+
systemTestFactory: ({browserPath}) => SystemTest.current({
|
|
405
|
+
debug: process.env.SYSTEM_TEST_DEBUG === "true",
|
|
406
|
+
driver: {
|
|
407
|
+
type: "selenium",
|
|
408
|
+
options: {chromeBinaryPath: browserPath}
|
|
409
|
+
},
|
|
410
|
+
httpHost: systemTestHttpHost,
|
|
411
|
+
httpPort: systemTestHttpPort
|
|
412
|
+
})
|
|
397
413
|
})
|
|
398
|
-
await
|
|
414
|
+
await browserTestSession.start()
|
|
399
415
|
|
|
400
416
|
await testRunner.prepare()
|
|
401
417
|
|
|
@@ -413,10 +429,7 @@ async function runBrowserTests() {
|
|
|
413
429
|
if ((hasTagFilters || hasLineFilters || hasExampleFilters) && executedTests === 0) {
|
|
414
430
|
console.error("\nNo tests matched the provided filters")
|
|
415
431
|
process.exitCode = 1
|
|
416
|
-
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
if (testRunner.isFailed()) {
|
|
432
|
+
} else if (testRunner.isFailed()) {
|
|
420
433
|
await testRunner.persistFailedTestConsoleOutputsToAssets()
|
|
421
434
|
const failedTests = testRunner.getFailedTestDetails()
|
|
422
435
|
|
|
@@ -443,15 +456,34 @@ async function runBrowserTests() {
|
|
|
443
456
|
} else {
|
|
444
457
|
console.log(`\nTest run succeeded with ${testRunner.getSuccessfulTests()} successful tests`)
|
|
445
458
|
}
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
|
|
459
|
+
} catch (error) {
|
|
460
|
+
runError = error instanceof Error ? error : new Error(String(error))
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const cleanupErrors = []
|
|
464
|
+
|
|
465
|
+
if (browserTestSession) {
|
|
466
|
+
try {
|
|
467
|
+
await stopBrowserTestSession(browserTestSession)
|
|
468
|
+
} catch (error) {
|
|
469
|
+
cleanupErrors.push(error instanceof Error ? error : new Error(String(error)))
|
|
449
470
|
}
|
|
471
|
+
}
|
|
450
472
|
|
|
451
|
-
|
|
473
|
+
if (backendApplication) {
|
|
474
|
+
try {
|
|
452
475
|
await backendApplication.stop()
|
|
476
|
+
} catch (error) {
|
|
477
|
+
cleanupErrors.push(error instanceof Error ? error : new Error(String(error)))
|
|
453
478
|
}
|
|
454
479
|
}
|
|
480
|
+
|
|
481
|
+
if (runError && cleanupErrors.length > 0) {
|
|
482
|
+
throw new AggregateError([runError, ...cleanupErrors], "Browser tests and resource cleanup failed", {cause: runError})
|
|
483
|
+
}
|
|
484
|
+
if (runError) throw runError
|
|
485
|
+
if (cleanupErrors.length === 1) throw cleanupErrors[0]
|
|
486
|
+
if (cleanupErrors.length > 1) throw new AggregateError(cleanupErrors, "Browser test resource cleanup failed")
|
|
455
487
|
}
|
|
456
488
|
|
|
457
489
|
async function main() {
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
* @property {() => ReturnType<typeof JSON.parse>} [getConnection] - Custom connection factory override.
|
|
73
73
|
* @property {string} [host] - Database host.
|
|
74
74
|
* @property {boolean} [migrations] - Whether migrations are enabled for this database.
|
|
75
|
-
* @property {boolean} [multipleStatements] - (MySQL) Opt in to multi-statement queries so
|
|
75
|
+
* @property {boolean} [multipleStatements] - (MySQL) Opt in to multi-statement queries so structure SQL loads and all-table cleanup can batch into one round-trip. Off by default; ordinary queries otherwise reject stacked statements.
|
|
76
76
|
* @property {number} [maxRowsPerInsert] - Maximum rows per `INSERT ... VALUES (...), (...), ...` statement generated by `Record.insertMultiple`. Defaults to 500.
|
|
77
77
|
* @property {number} [maxInsertSqlBytes] - Maximum serialized SQL size, in bytes, for a single `INSERT ... VALUES (...), (...), ...` statement. Defaults to 1 MiB (1048576).
|
|
78
78
|
* @property {number} [maxInClauseValues] - Maximum values in a single `IN (...)` cohort used by preloads, association counts, and queryData aggregates. Defaults to 999.
|