doc-detective 4.10.0 → 4.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/common/src/schemas/schemas.json +2254 -28
- package/dist/common/src/types/generated/record_v3.d.ts +25 -2
- package/dist/common/src/types/generated/record_v3.d.ts.map +1 -1
- package/dist/common/src/types/generated/step_v3.d.ts +25 -2
- package/dist/common/src/types/generated/step_v3.d.ts.map +1 -1
- package/dist/common/src/types/generated/test_v3.d.ts +50 -4
- package/dist/common/src/types/generated/test_v3.d.ts.map +1 -1
- package/dist/core/tests/ffmpegRecorder.d.ts +62 -0
- package/dist/core/tests/ffmpegRecorder.d.ts.map +1 -0
- package/dist/core/tests/ffmpegRecorder.js +385 -0
- package/dist/core/tests/ffmpegRecorder.js.map +1 -0
- package/dist/core/tests/startRecording.d.ts.map +1 -1
- package/dist/core/tests/startRecording.js +144 -16
- package/dist/core/tests/startRecording.js.map +1 -1
- package/dist/core/tests/stopRecording.d.ts.map +1 -1
- package/dist/core/tests/stopRecording.js +158 -60
- package/dist/core/tests/stopRecording.js.map +1 -1
- package/dist/core/tests.d.ts.map +1 -1
- package/dist/core/tests.js +111 -13
- package/dist/core/tests.js.map +1 -1
- package/dist/hints/context.d.ts.map +1 -1
- package/dist/hints/context.js +3 -0
- package/dist/hints/context.js.map +1 -1
- package/dist/hints/hints.d.ts.map +1 -1
- package/dist/hints/hints.js +17 -0
- package/dist/hints/hints.js.map +1 -1
- package/dist/hints/types.d.ts +7 -0
- package/dist/hints/types.d.ts.map +1 -1
- package/dist/index.cjs +2911 -185
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stopRecording.d.ts","sourceRoot":"","sources":["../../../src/core/tests/stopRecording.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"stopRecording.d.ts","sourceRoot":"","sources":["../../../src/core/tests/stopRecording.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,aAAa,EAAE,CAAC;AAEzB,iBAAe,aAAa,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE;IAAE,MAAM,EAAE,GAAG,CAAC;IAAC,IAAI,EAAE,GAAG,CAAC;IAAC,MAAM,EAAE,GAAG,CAAA;CAAE,gBAwJ7F"}
|
|
@@ -1,41 +1,24 @@
|
|
|
1
1
|
import { validate } from "../../common/src/validate.js";
|
|
2
2
|
import { log } from "../utils.js";
|
|
3
|
-
import {
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import fs from "node:fs";
|
|
6
|
-
import {
|
|
7
|
-
// Resolve the ffmpeg binary path lazily — @ffmpeg-installer/ffmpeg is a
|
|
8
|
-
// heavy runtime dep that should only be loaded when a stopRecording step
|
|
9
|
-
// actually runs. The ctx is threaded through so a user-overridden
|
|
10
|
-
// cacheDir is honored here just as it is by the JIT pre-flight installer.
|
|
11
|
-
async function getFfmpegPath(ctx = {}) {
|
|
12
|
-
const mod = await loadHeavyDep("@ffmpeg-installer/ffmpeg", { ctx });
|
|
13
|
-
// The package's CJS entry exports an object with a .path field; in ESM
|
|
14
|
-
// dynamic import we get { default: { path }, path? } shape depending on
|
|
15
|
-
// bundler. Try both, then guard before handing it to execFile so a
|
|
16
|
-
// malformed install fails with an actionable message instead of a
|
|
17
|
-
// confusing "argument must be of type string" deep in node's exec.
|
|
18
|
-
const candidate = mod && (mod.path ?? mod.default?.path);
|
|
19
|
-
if (typeof candidate !== "string" || candidate.length === 0) {
|
|
20
|
-
throw new Error("ffmpeg binary path is missing or malformed in the installed @ffmpeg-installer/ffmpeg package. Try `doc-detective install runtime --force` to reinstall.");
|
|
21
|
-
}
|
|
22
|
-
return candidate;
|
|
23
|
-
}
|
|
6
|
+
import { getFfmpegPath } from "./ffmpegRecorder.js";
|
|
24
7
|
export { stopRecording };
|
|
25
8
|
async function stopRecording({ config, step, driver }) {
|
|
26
9
|
let result = {
|
|
27
10
|
status: "PASS",
|
|
28
11
|
description: "Stopped recording.",
|
|
29
12
|
};
|
|
30
|
-
// Validate step payload
|
|
13
|
+
// Validate step payload. (The stopRecord step carries no fields we read
|
|
14
|
+
// here — the recording state lives on driver.state — so we only assert
|
|
15
|
+
// validity and don't keep the coerced object.)
|
|
31
16
|
const isValidStep = validate({ schemaKey: "step_v3", object: step });
|
|
32
17
|
if (!isValidStep.valid) {
|
|
33
18
|
result.status = "FAIL";
|
|
34
19
|
result.description = `Invalid step definition: ${isValidStep.errors}`;
|
|
35
20
|
return result;
|
|
36
21
|
}
|
|
37
|
-
// Accept coerced and defaulted values
|
|
38
|
-
step = isValidStep.object;
|
|
39
22
|
// Skip if recording is not started. Recording state is per-context (it
|
|
40
23
|
// lives on driver.state), so concurrent contexts can't see each other's
|
|
41
24
|
// recordings.
|
|
@@ -47,7 +30,7 @@ async function stopRecording({ config, step, driver }) {
|
|
|
47
30
|
}
|
|
48
31
|
try {
|
|
49
32
|
if (recording.type === "MediaRecorder") {
|
|
50
|
-
//
|
|
33
|
+
// Browser engine.
|
|
51
34
|
// Switch to recording tab
|
|
52
35
|
await driver.switchToWindow(recording.tab);
|
|
53
36
|
// Check that recorder was properly initialized
|
|
@@ -71,13 +54,12 @@ async function stopRecording({ config, step, driver }) {
|
|
|
71
54
|
await driver.execute(() => {
|
|
72
55
|
window.recorder.stop();
|
|
73
56
|
});
|
|
74
|
-
// Wait for
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
if (!fs.existsSync(recording.downloadPath)) {
|
|
57
|
+
// Wait for the download to appear AND finish writing. Chrome streams the
|
|
58
|
+
// blob to disk (and may use a .crdownload temp first), so existence
|
|
59
|
+
// alone isn't enough — transcoding a still-growing file makes ffmpeg
|
|
60
|
+
// fail. Wait for the size to hold steady across two reads.
|
|
61
|
+
const downloaded = await waitForStableFile(recording.downloadPath, 60);
|
|
62
|
+
if (!downloaded) {
|
|
81
63
|
result.status = "FAIL";
|
|
82
64
|
result.description = "Recording download timed out.";
|
|
83
65
|
// Clear the state so the auto-stop in runContext doesn't re-invoke
|
|
@@ -92,43 +74,68 @@ async function stopRecording({ config, step, driver }) {
|
|
|
92
74
|
if (remainingHandles.length > 0) {
|
|
93
75
|
await driver.switchToWindow(remainingHandles[0]);
|
|
94
76
|
}
|
|
95
|
-
// Convert the
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
77
|
+
// Convert the downloaded .webm into the target format/location.
|
|
78
|
+
await transcode({
|
|
79
|
+
config,
|
|
80
|
+
sourcePath: recording.downloadPath,
|
|
81
|
+
targetPath: recording.targetPath,
|
|
82
|
+
deleteSource: true,
|
|
83
|
+
});
|
|
84
|
+
driver.state.recording = null;
|
|
85
|
+
}
|
|
86
|
+
else if (recording.type === "ffmpeg") {
|
|
87
|
+
// ffmpeg engine. Stop the capture gracefully (write "q" to stdin so the
|
|
88
|
+
// container is finalized), then transcode the temp .mkv into the target
|
|
89
|
+
// format, cropping to the requested window/viewport if one was resolved.
|
|
90
|
+
const proc = recording.process;
|
|
91
|
+
try {
|
|
92
|
+
proc.stdin?.write("q");
|
|
93
|
+
proc.stdin?.end?.();
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
/* fall through to kill */
|
|
102
97
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
}
|
|
115
|
-
catch { /* ignore */ }
|
|
116
|
-
}
|
|
117
|
-
log(config, "debug", endMessage);
|
|
98
|
+
await new Promise((resolve) => {
|
|
99
|
+
// Already exited (e.g. ffmpeg reacted to "q" before we got here) —
|
|
100
|
+
// don't wait on a "close" that will never fire again.
|
|
101
|
+
if (proc.exitCode !== null || proc.signalCode !== null) {
|
|
102
|
+
resolve();
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
let settled = false;
|
|
106
|
+
const finish = () => {
|
|
107
|
+
if (!settled) {
|
|
108
|
+
settled = true;
|
|
118
109
|
resolve();
|
|
119
110
|
}
|
|
120
|
-
|
|
121
|
-
|
|
111
|
+
};
|
|
112
|
+
// Normal path: ffmpeg exits after "q"; close clears the kill timer and
|
|
113
|
+
// we transcode the fully-flushed .mkv.
|
|
114
|
+
const killTimer = setTimeout(() => {
|
|
115
|
+
try {
|
|
116
|
+
proc.kill("SIGKILL");
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
/* ignore */
|
|
122
120
|
}
|
|
123
|
-
|
|
124
|
-
.
|
|
121
|
+
// Resolve on the post-kill close, or after a short grace if it never
|
|
122
|
+
// arrives. The .mkv survives a hard kill.
|
|
123
|
+
setTimeout(finish, 2000);
|
|
124
|
+
}, 15000);
|
|
125
|
+
proc.once("close", () => {
|
|
126
|
+
clearTimeout(killTimer);
|
|
127
|
+
finish();
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
await transcode({
|
|
131
|
+
config,
|
|
132
|
+
sourcePath: recording.tempPath,
|
|
133
|
+
targetPath: recording.targetPath,
|
|
134
|
+
deleteSource: true,
|
|
135
|
+
crop: recording.crop,
|
|
125
136
|
});
|
|
126
137
|
driver.state.recording = null;
|
|
127
138
|
}
|
|
128
|
-
else {
|
|
129
|
-
// FFMPEG
|
|
130
|
-
// recording.stdin.write("q");
|
|
131
|
-
}
|
|
132
139
|
}
|
|
133
140
|
catch (error) {
|
|
134
141
|
// Couldn't stop recording
|
|
@@ -142,4 +149,95 @@ async function stopRecording({ config, step, driver }) {
|
|
|
142
149
|
// PASS
|
|
143
150
|
return result;
|
|
144
151
|
}
|
|
152
|
+
// Transcode a recording into the requested target format/location with
|
|
153
|
+
// ffmpeg, applying an optional crop and the gif scale filter. Deletes the
|
|
154
|
+
// source on success when requested (and when it isn't the target itself).
|
|
155
|
+
async function transcode({ config, sourcePath, targetPath, deleteSource, crop, }) {
|
|
156
|
+
// Drop audio (-an): doc recordings are visual, and the browser engine's
|
|
157
|
+
// captured opus track fails to mux into mp4 ("Too many packets buffered for
|
|
158
|
+
// output stream"). Silent video is the intended, reliable output.
|
|
159
|
+
const ffmpegArgs = ["-y", "-i", `${sourcePath}`, "-an", "-pix_fmt", "yuv420p"];
|
|
160
|
+
const filters = [];
|
|
161
|
+
if (crop) {
|
|
162
|
+
// Clamp the crop to the captured frame using ffmpeg expressions (iw/ih),
|
|
163
|
+
// so a window/viewport rectangle larger than the captured display can't
|
|
164
|
+
// make the crop filter fail with "Invalid too big size". Commas inside
|
|
165
|
+
// min()/max() are escaped (\,) so they aren't read as filter separators.
|
|
166
|
+
const cw = `min(iw\\,${crop.w})`;
|
|
167
|
+
const ch = `min(ih\\,${crop.h})`;
|
|
168
|
+
const cx = `max(0\\,min(${crop.x}\\,iw-${cw}))`;
|
|
169
|
+
const cy = `max(0\\,min(${crop.y}\\,ih-${ch}))`;
|
|
170
|
+
filters.push(`crop=w=${cw}:h=${ch}:x=${cx}:y=${cy}`);
|
|
171
|
+
}
|
|
172
|
+
if (path.extname(targetPath) === ".gif") {
|
|
173
|
+
filters.push("scale=iw:-1:flags=lanczos");
|
|
174
|
+
}
|
|
175
|
+
if (filters.length > 0) {
|
|
176
|
+
ffmpegArgs.push("-vf", filters.join(","));
|
|
177
|
+
}
|
|
178
|
+
ffmpegArgs.push(`${targetPath}`);
|
|
179
|
+
const ffmpegPath = await getFfmpegPath({ cacheDir: config?.cacheDir });
|
|
180
|
+
await new Promise((resolve, reject) => {
|
|
181
|
+
// spawn (not execFile): a long/noisy ffmpeg transcode can emit megabytes
|
|
182
|
+
// of progress on stderr, which would overflow execFile's internal buffer
|
|
183
|
+
// (ERR_CHILD_PROCESS_STDIO_MAXBUFFER). We stream stderr into a bounded tail.
|
|
184
|
+
const child = spawn(ffmpegPath, ffmpegArgs);
|
|
185
|
+
let stderr = "";
|
|
186
|
+
child.stderr?.on("data", (d) => {
|
|
187
|
+
stderr = (stderr + d.toString()).slice(-2000);
|
|
188
|
+
});
|
|
189
|
+
child
|
|
190
|
+
.on("close", (code) => {
|
|
191
|
+
if (code === 0) {
|
|
192
|
+
if (deleteSource && sourcePath !== targetPath) {
|
|
193
|
+
try {
|
|
194
|
+
fs.unlinkSync(sourcePath);
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
/* ignore */
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
log(config, "debug", `Finished processing file: ${targetPath}`);
|
|
201
|
+
resolve();
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
reject(new Error(`ffmpeg exited with code ${code}: ${stderr.slice(-600)}`));
|
|
205
|
+
}
|
|
206
|
+
})
|
|
207
|
+
.on("error", reject);
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
// Wait for a file to exist and stop growing (size unchanged across two reads
|
|
211
|
+
// ~500ms apart), up to `maxSeconds`. Returns true once stable, false on
|
|
212
|
+
// timeout. Guards against transcoding a download that's still being written.
|
|
213
|
+
async function waitForStableFile(filePath, maxSeconds) {
|
|
214
|
+
let lastSize = -1;
|
|
215
|
+
let stableReads = 0;
|
|
216
|
+
const deadline = maxSeconds * 2; // two checks per second
|
|
217
|
+
for (let i = 0; i < deadline; i++) {
|
|
218
|
+
let size = -1;
|
|
219
|
+
try {
|
|
220
|
+
size = fs.statSync(filePath).size;
|
|
221
|
+
}
|
|
222
|
+
catch {
|
|
223
|
+
size = -1;
|
|
224
|
+
}
|
|
225
|
+
// Require a non-empty, steady size: Chrome may pre-create the .webm
|
|
226
|
+
// before writing data, and transcoding an empty file fails.
|
|
227
|
+
if (size > 0 && size === lastSize) {
|
|
228
|
+
stableReads++;
|
|
229
|
+
if (stableReads >= 1)
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
stableReads = 0;
|
|
234
|
+
}
|
|
235
|
+
lastSize = size;
|
|
236
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
237
|
+
}
|
|
238
|
+
// Timed out without the size ever holding steady — the file is missing or
|
|
239
|
+
// still being written. Report not-stable so the caller fails cleanly rather
|
|
240
|
+
// than transcoding a partial download.
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
145
243
|
//# sourceMappingURL=stopRecording.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stopRecording.js","sourceRoot":"","sources":["../../../src/core/tests/stopRecording.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,8BAA8B,CAAC;AACxD,OAAO,EAAE,GAAG,EAAE,MAAM,aAAa,CAAC;AAClC,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"stopRecording.js","sourceRoot":"","sources":["../../../src/core/tests/stopRecording.ts"],"names":[],"mappings":"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"}
|
package/dist/core/tests.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tests.d.ts","sourceRoot":"","sources":["../../src/core/tests.ts"],"names":[],"mappings":"AAQA,OAAO,EAGL,KAAK,gBAAgB,EACtB,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"tests.d.ts","sourceRoot":"","sources":["../../src/core/tests.ts"],"names":[],"mappings":"AAQA,OAAO,EAGL,KAAK,gBAAgB,EACtB,MAAM,wBAAwB,CAAC;AA6DhC,OAAO,EACL,QAAQ,EACR,SAAS,EACT,SAAS,EACT,qBAAqB,EACrB,6BAA6B,EAC7B,cAAc,EACd,cAAc,EACd,mBAAmB,EACnB,qBAAqB,EACrB,iBAAiB,EACjB,kBAAkB,EAClB,qBAAqB,GACtB,CAAC;AAuBF;;;;;;;;GAQG;AACH,iBAAS,cAAc,CAAC,OAAO,EAAE,GAAG,GAAG,MAAM,CAI5C;AAED;;;;;;GAMG;AACH,iBAAS,cAAc,CAAC,IAAI,EAAE,IAAI,GAAG,QAAQ,GAAG,SAAS,GAAG,SAAS,GAAG,MAAM,CAE7E;AAGD,iBAAS,qBAAqB,CAAC,EAAE,aAAa,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE;IAAE,aAAa,EAAE,GAAG,CAAC;IAAC,IAAI,EAAE,GAAG,CAAC;IAAC,OAAO,EAAE,GAAG,CAAA;CAAE,GAAG,GAAG,CA2HrH;AAiBD,iBAAS,kBAAkB,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE;IAAE,OAAO,EAAE,GAAG,CAAC;IAAC,IAAI,EAAE,GAAG,EAAE,CAAC;IAAC,QAAQ,EAAE,GAAG,CAAA;CAAE,WAwBpG;AAED,iBAAS,iBAAiB,CAAC,EAAE,aAAa,EAAE,EAAE;IAAE,aAAa,EAAE,GAAG,CAAA;CAAE,OAUnE;AA8CD,iBAAe,SAAS,CAAC,EAAE,aAAa,EAAE,MAAM,EAAE,MAAW,EAAE,EAAE;IAAE,aAAa,EAAE,GAAG,CAAC;IAAC,MAAM,EAAE,GAAG,CAAC;IAAC,MAAM,CAAC,EAAE,GAAG,CAAA;CAAE,GAAG,OAAO,CAAC,GAAG,CAAC,CAwIhI;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,iBAAe,QAAQ,CAAC,EAAE,aAAa,EAAE,EAAE;IAAE,aAAa,EAAE,GAAG,CAAA;CAAE,gBA6ZhE;AAED;;;;;;;;;GASG;AACH,iBAAS,mBAAmB,CAC1B,IAAI,EAAE,GAAG,EAAE,EACX,aAAa,EAAE,GAAG,GACjB,KAAK,CAAC;IAAE,OAAO,EAAE,GAAG,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC,CAyBxC;AA8ID,iBAAS,qBAAqB,CAAC,EAC7B,MAAM,EACN,IAAI,EACJ,IAAI,GACL,EAAE;IACD,MAAM,EAAE,GAAG,CAAC;IACZ,IAAI,EAAE,GAAG,CAAC;IACV,IAAI,EAAE,GAAG,CAAC;CACX,GAAG,OAAO,CAIV;AAikBD,iBAAe,OAAO,CAAC,EACrB,MAAW,EACX,OAAY,EACZ,IAAI,EACJ,MAAM,EACN,UAAe,EACf,OAAY,GACb,EAAE;IACD,MAAM,CAAC,EAAE,GAAG,CAAC;IACb,OAAO,CAAC,EAAE,GAAG,CAAC;IACd,IAAI,EAAE,GAAG,CAAC;IACV,MAAM,EAAE,GAAG,CAAC;IACZ,UAAU,CAAC,EAAE,GAAG,CAAC;IACjB,OAAO,CAAC,EAAE,GAAG,CAAC;CACf,GAAG,OAAO,CAAC,GAAG,CAAC,CAuGf;AA4KD;;;;;;;;;;;GAWG;AACH,iBAAe,qBAAqB,CAClC,MAAM,EAAE,GAAG,EACX,IAAI,EAAE;IACJ,MAAM,EAAE,CAAC,MAAM,EAAE,GAAG,KAAK,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;IACxC,SAAS,EAAE,CAAC,MAAM,EAAE,GAAG,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1C,UAAU,EAAE,CAAC,MAAM,EAAE,GAAG,KAAK,IAAI,CAAC;IAClC,GAAG,CAAC,EAAE,CAAC,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CACzD,GACA,OAAO,CAAC,GAAG,EAAE,CAAC,CAmChB;AAED;;;;;;;;;;;GAWG;AACH,iBAAe,6BAA6B,CAAC,EAC3C,WAAW,EACX,MAAM,EACN,eAAe,EACf,IAAI,GACL,EAAE;IACD,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,MAAM,EAAE,GAAG,CAAC;IACZ,eAAe,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,GAAG,QAAQ,GAAG,gBAAgB,CAAC,CAAC;IACxE,IAAI,EAAE;QACJ,aAAa,EAAE,CAAC,KAAK,EAAE,gBAAgB,EAAE,OAAO,EAAE,GAAG,KAAK,OAAO,CAAC,GAAG,CAAC,CAAC;QACvE,GAAG,CAAC,EAAE,CAAC,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;KACzD,CAAC;CACH,GAAG,OAAO,CAAC,WAAW,GAAG,QAAQ,GAAG,gBAAgB,CAAC,CAsCrD;AAED,iBAAe,SAAS,CAAC,OAAO,GAAE,GAAQ;;;;;GAoHzC"}
|
package/dist/core/tests.js
CHANGED
|
@@ -20,6 +20,7 @@ import { wait } from "./tests/wait.js";
|
|
|
20
20
|
import { saveScreenshot } from "./tests/saveScreenshot.js";
|
|
21
21
|
import { startRecording } from "./tests/startRecording.js";
|
|
22
22
|
import { stopRecording } from "./tests/stopRecording.js";
|
|
23
|
+
import { browserCaptureTitle, browserDownloadDir, coerceRecordContextBrowser, jobIsFfmpegRecording, computeEffectiveConcurrency, checkSystemBinary, xvfbDisplay, startXvfb, XVFB_SCREEN_SIZE, } from "./tests/ffmpegRecorder.js";
|
|
23
24
|
import { loadVariables } from "./tests/loadVariables.js";
|
|
24
25
|
import { saveCookie } from "./tests/saveCookie.js";
|
|
25
26
|
import { loadCookie } from "./tests/loadCookie.js";
|
|
@@ -152,7 +153,12 @@ function getDriverCapabilities({ runnerDetails, name, options }) {
|
|
|
152
153
|
break;
|
|
153
154
|
// Set args
|
|
154
155
|
args.push(`--enable-chrome-browser-cloud-management`);
|
|
155
|
-
|
|
156
|
+
// Auto-select the getDisplayMedia capture source by window title. A
|
|
157
|
+
// per-context title (set on document.title in startRecording) makes
|
|
158
|
+
// concurrent Chrome recordings safe: each browser process auto-selects
|
|
159
|
+
// only its own window. Falls back to the shared default for callers
|
|
160
|
+
// (warm-up, non-record contexts) that don't supply one.
|
|
161
|
+
args.push(`--auto-select-desktop-capture-source=${options.captureSourceTitle || "RECORD_ME"}`);
|
|
156
162
|
if (options.headless)
|
|
157
163
|
args.push("--headless", "--disable-gpu");
|
|
158
164
|
if (process.platform === "linux") {
|
|
@@ -176,7 +182,9 @@ function getDriverCapabilities({ runnerDetails, name, options }) {
|
|
|
176
182
|
// Reference: https://chromedriver.chromium.org/capabilities#h.p_ID_102
|
|
177
183
|
args,
|
|
178
184
|
prefs: {
|
|
179
|
-
|
|
185
|
+
// Per-context download dir keeps concurrent recordings from
|
|
186
|
+
// colliding on the same .webm filename in a shared temp dir.
|
|
187
|
+
"download.default_directory": options.downloadDir || os.tmpdir(),
|
|
180
188
|
"download.prompt_for_download": false,
|
|
181
189
|
"download.directory_upgrade": true,
|
|
182
190
|
},
|
|
@@ -496,8 +504,8 @@ async function runSpecs({ resolvedTests }) {
|
|
|
496
504
|
// Resolve concurrency up front (defensive re-resolve: API callers can hand
|
|
497
505
|
// runSpecs a config that never went through core setConfig, leaving
|
|
498
506
|
// concurrentRunners as `true`). Drives both the worker pool and how many
|
|
499
|
-
// Appium servers to start.
|
|
500
|
-
|
|
507
|
+
// Appium servers to start. Mutable: recording constraints may cap it below.
|
|
508
|
+
let limit = resolveConcurrentRunners(config);
|
|
501
509
|
// Phase 1: pre-build the report skeleton and a flat list of context jobs
|
|
502
510
|
// across all specs and tests. Slots are pre-assigned so report order always
|
|
503
511
|
// matches input order, no matter what order concurrent contexts finish in.
|
|
@@ -550,15 +558,45 @@ async function runSpecs({ resolvedTests }) {
|
|
|
550
558
|
usedContextIds.add(id);
|
|
551
559
|
context.contextId = id;
|
|
552
560
|
}
|
|
561
|
+
// Auto-resolution: when a record step has no explicit engine and the
|
|
562
|
+
// user never chose a browser, prefer the concurrency-safe browser
|
|
563
|
+
// engine by coercing to headed Chrome (when available). Done here,
|
|
564
|
+
// before the concurrency calc below, so each job's engine is settled.
|
|
565
|
+
// Non-record contexts keep runContext's normal browser defaulting.
|
|
566
|
+
const coercedBrowser = coerceRecordContextBrowser({
|
|
567
|
+
context,
|
|
568
|
+
availableApps: runnerDetails.availableApps,
|
|
569
|
+
});
|
|
570
|
+
if (coercedBrowser)
|
|
571
|
+
context.browser = coercedBrowser;
|
|
553
572
|
jobs.push({ spec, test, context, contexts: testReport.contexts, slot });
|
|
554
573
|
});
|
|
555
574
|
}
|
|
556
575
|
}
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
576
|
+
// Recording concurrency. The browser (Chrome getDisplayMedia) engine is
|
|
577
|
+
// concurrency-safe via per-context capture titles, but the ffmpeg engine
|
|
578
|
+
// grabs the whole physical display and must own it — so concurrent ffmpeg
|
|
579
|
+
// recordings are only safe on Linux with per-runner Xvfb displays. Probe
|
|
580
|
+
// Xvfb only when it could matter, then let computeEffectiveConcurrency
|
|
581
|
+
// decide the effective limit.
|
|
582
|
+
// Only ffmpeg-engine recordings need Xvfb; a browser-engine-only run
|
|
583
|
+
// shouldn't pay for an `Xvfb -help` spawn. Contexts are already coerced
|
|
584
|
+
// above, so resolveRecordPlan reflects the engine that will actually run.
|
|
585
|
+
const anyFfmpegRecording = jobs.some(jobIsFfmpegRecording);
|
|
586
|
+
let xvfbAvailable = false;
|
|
587
|
+
if (anyFfmpegRecording && process.platform === "linux") {
|
|
588
|
+
xvfbAvailable = await checkSystemBinary("Xvfb");
|
|
589
|
+
}
|
|
590
|
+
const concurrency = computeEffectiveConcurrency({
|
|
591
|
+
requestedLimit: limit,
|
|
592
|
+
jobs,
|
|
593
|
+
platform: process.platform,
|
|
594
|
+
xvfbAvailable,
|
|
595
|
+
});
|
|
596
|
+
limit = concurrency.limit;
|
|
597
|
+
if (concurrency.forcedSerial) {
|
|
598
|
+
log(config, "warning", "Recording with the ffmpeg engine needs exclusive use of the display, so this run is executing serially (concurrentRunners=1). To record concurrently, use the Chrome browser engine (record: { engine: \"browser\" }) or, on Linux, install Xvfb.");
|
|
599
|
+
report.recordingForcedSerial = true;
|
|
562
600
|
}
|
|
563
601
|
// Start one Appium server per concurrent runner that will actually use a
|
|
564
602
|
// driver (capped at the number of driver contexts). Each server owns a
|
|
@@ -568,6 +606,12 @@ async function runSpecs({ resolvedTests }) {
|
|
|
568
606
|
const driverJobCount = jobs.filter((job) => isDriverRequired({ test: job.context })).length;
|
|
569
607
|
let appiumServers = [];
|
|
570
608
|
let appiumPool;
|
|
609
|
+
// Per-server virtual displays (Linux Xvfb) for concurrent ffmpeg recording,
|
|
610
|
+
// and the port→display map so a context that acquires a server records the
|
|
611
|
+
// same display its browser renders on.
|
|
612
|
+
const xvfbProcesses = [];
|
|
613
|
+
const useXvfbDisplays = concurrency.xvfbContexts.length > 0;
|
|
614
|
+
let portToDisplay;
|
|
571
615
|
if (driverJobCount > 0) {
|
|
572
616
|
setAppiumHome({ cacheDir: config?.cacheDir });
|
|
573
617
|
// Resolve appium's actual JS entrypoint via `require.resolve` (shim
|
|
@@ -593,7 +637,13 @@ async function runSpecs({ resolvedTests }) {
|
|
|
593
637
|
// come up, tearing down any already started so they don't leak.
|
|
594
638
|
try {
|
|
595
639
|
for (let i = 0; i < serverCount; i++) {
|
|
596
|
-
|
|
640
|
+
let display;
|
|
641
|
+
if (useXvfbDisplays) {
|
|
642
|
+
display = xvfbDisplay(i);
|
|
643
|
+
xvfbProcesses.push(await startXvfb(display));
|
|
644
|
+
log(config, "debug", `Started Xvfb on ${display} for recording.`);
|
|
645
|
+
}
|
|
646
|
+
appiumServers.push(await startAppiumServer(appiumEntry, config, display));
|
|
597
647
|
}
|
|
598
648
|
}
|
|
599
649
|
catch (error) {
|
|
@@ -605,9 +655,22 @@ async function runSpecs({ resolvedTests }) {
|
|
|
605
655
|
// best-effort
|
|
606
656
|
}
|
|
607
657
|
}
|
|
658
|
+
for (const xvfb of xvfbProcesses) {
|
|
659
|
+
try {
|
|
660
|
+
xvfb.kill();
|
|
661
|
+
}
|
|
662
|
+
catch {
|
|
663
|
+
// best-effort
|
|
664
|
+
}
|
|
665
|
+
}
|
|
608
666
|
throw error;
|
|
609
667
|
}
|
|
610
668
|
appiumPool = createAppiumPool(appiumServers.map((s) => s.port));
|
|
669
|
+
if (useXvfbDisplays) {
|
|
670
|
+
portToDisplay = new Map(appiumServers
|
|
671
|
+
.filter((s) => s.display)
|
|
672
|
+
.map((s) => [s.port, s.display]));
|
|
673
|
+
}
|
|
611
674
|
}
|
|
612
675
|
// Everything that uses the Appium servers runs inside this try so the
|
|
613
676
|
// shutdown in `finally` always reaches them — otherwise a throw in
|
|
@@ -645,6 +708,7 @@ async function runSpecs({ resolvedTests }) {
|
|
|
645
708
|
context: job.context,
|
|
646
709
|
runnerDetails,
|
|
647
710
|
appiumPool,
|
|
711
|
+
portToDisplay,
|
|
648
712
|
metaValues,
|
|
649
713
|
installAttempts,
|
|
650
714
|
warmUpResults,
|
|
@@ -700,6 +764,15 @@ async function runSpecs({ resolvedTests }) {
|
|
|
700
764
|
// Process may already be terminated
|
|
701
765
|
}
|
|
702
766
|
}
|
|
767
|
+
// Tear down any Xvfb virtual displays started for recording.
|
|
768
|
+
for (const xvfb of xvfbProcesses) {
|
|
769
|
+
try {
|
|
770
|
+
xvfb.kill();
|
|
771
|
+
}
|
|
772
|
+
catch {
|
|
773
|
+
// Process may already be terminated
|
|
774
|
+
}
|
|
775
|
+
}
|
|
703
776
|
}
|
|
704
777
|
// Upload changed files back to source integrations (best-effort)
|
|
705
778
|
// This automatically syncs any changed screenshots back to their source CMS
|
|
@@ -944,7 +1017,7 @@ async function captureAutoScreenshot({ config, driver, spec, test, context, step
|
|
|
944
1017
|
* report or summary counters — the caller owns aggregation, which keeps this
|
|
945
1018
|
* function safe to run concurrently with sibling contexts.
|
|
946
1019
|
*/
|
|
947
|
-
async function runContext({ config, spec, test, context, runnerDetails, appiumPool, metaValues, installAttempts, warmUpResults, logPrefix = "", }) {
|
|
1020
|
+
async function runContext({ config, spec, test, context, runnerDetails, appiumPool, portToDisplay, metaValues, installAttempts, warmUpResults, logPrefix = "", }) {
|
|
948
1021
|
const platform = runnerDetails.environment.platform;
|
|
949
1022
|
// `let`, not `const`: an on-demand browser install below re-detects available
|
|
950
1023
|
// apps and reassigns this snapshot.
|
|
@@ -1082,8 +1155,26 @@ async function runContext({ config, spec, test, context, runnerDetails, appiumPo
|
|
|
1082
1155
|
// Check out a server for this context's lifetime — released in the
|
|
1083
1156
|
// finally so the next queued context can reuse it.
|
|
1084
1157
|
appiumPort = await appiumPool.acquire();
|
|
1158
|
+
// If this server runs on a dedicated Xvfb display, record it on the
|
|
1159
|
+
// context so the ffmpeg recorder captures the same display the browser
|
|
1160
|
+
// renders on.
|
|
1161
|
+
if (portToDisplay) {
|
|
1162
|
+
const display = portToDisplay.get(appiumPort);
|
|
1163
|
+
if (display) {
|
|
1164
|
+
context.__display = display;
|
|
1165
|
+
// The Xvfb displays are created at a known fixed size; record it so
|
|
1166
|
+
// x11grab captures the full display (its default grabs only 640x480).
|
|
1167
|
+
context.__displaySize = XVFB_SCREEN_SIZE;
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1085
1170
|
// Define driver capabilities
|
|
1086
1171
|
// TODO: Support custom apps
|
|
1172
|
+
// Per-context recording identifiers so concurrent Chrome recordings
|
|
1173
|
+
// auto-select their own window and download to their own dir.
|
|
1174
|
+
const recordOptions = {
|
|
1175
|
+
captureSourceTitle: browserCaptureTitle(context.contextId),
|
|
1176
|
+
downloadDir: browserDownloadDir(context.contextId),
|
|
1177
|
+
};
|
|
1087
1178
|
let caps = getDriverCapabilities({
|
|
1088
1179
|
runnerDetails: runnerDetails,
|
|
1089
1180
|
name: context.browser.name,
|
|
@@ -1091,6 +1182,7 @@ async function runContext({ config, spec, test, context, runnerDetails, appiumPo
|
|
|
1091
1182
|
width: context.browser?.window?.width || 1200,
|
|
1092
1183
|
height: context.browser?.window?.height || 800,
|
|
1093
1184
|
headless: context.browser?.headless !== false,
|
|
1185
|
+
...recordOptions,
|
|
1094
1186
|
},
|
|
1095
1187
|
});
|
|
1096
1188
|
clog("debug", "CAPABILITIES:");
|
|
@@ -1111,6 +1203,7 @@ async function runContext({ config, spec, test, context, runnerDetails, appiumPo
|
|
|
1111
1203
|
width: context.browser?.window?.width || 1200,
|
|
1112
1204
|
height: context.browser?.window?.height || 800,
|
|
1113
1205
|
headless: context.browser?.headless !== false,
|
|
1206
|
+
...recordOptions,
|
|
1114
1207
|
},
|
|
1115
1208
|
});
|
|
1116
1209
|
driver = await driverStart(caps, appiumPort, 4, { cacheDir: config?.cacheDir });
|
|
@@ -1414,12 +1507,17 @@ async function runStep({ config = {}, context = {}, step, driver, metaValues = {
|
|
|
1414
1507
|
// Start one Appium server on a free port and resolve once it answers /status.
|
|
1415
1508
|
// Each concurrent runner gets its own server (own port) so parallel contexts
|
|
1416
1509
|
// never create sessions on the same Appium instance.
|
|
1417
|
-
async function startAppiumServer(appiumEntry, config) {
|
|
1510
|
+
async function startAppiumServer(appiumEntry, config, display) {
|
|
1418
1511
|
const port = await findFreePort();
|
|
1419
1512
|
log(config, "debug", `Starting Appium on port ${port}`);
|
|
1513
|
+
// When a virtual display is supplied (Linux Xvfb recording), launch the
|
|
1514
|
+
// server with DISPLAY set so the browser it spawns (via chromedriver)
|
|
1515
|
+
// renders on that display — which is what ffmpeg x11grab then captures.
|
|
1516
|
+
const env = display ? { ...process.env, DISPLAY: display } : process.env;
|
|
1420
1517
|
const proc = spawn(process.execPath, [appiumEntry, "-a", "127.0.0.1", "-p", String(port)], {
|
|
1421
1518
|
windowsHide: true,
|
|
1422
1519
|
cwd: path.join(__dirname, "../.."),
|
|
1520
|
+
env,
|
|
1423
1521
|
});
|
|
1424
1522
|
proc.on("error", (err) => {
|
|
1425
1523
|
log(config, "warning", `Appium process error: ${err?.stack ?? err?.message ?? String(err)}`);
|
|
@@ -1443,7 +1541,7 @@ async function startAppiumServer(appiumEntry, config) {
|
|
|
1443
1541
|
throw error;
|
|
1444
1542
|
}
|
|
1445
1543
|
log(config, "debug", `Appium is ready on port ${port}.`);
|
|
1446
|
-
return { port, process: proc };
|
|
1544
|
+
return { port, process: proc, display };
|
|
1447
1545
|
}
|
|
1448
1546
|
// Delay execution until Appium server is available.
|
|
1449
1547
|
async function appiumIsReady(port, timeoutMs = 120000) {
|