@codeyam-editor/codeyam-editor 0.1.15 → 0.1.16
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/npm/app-port-drift.js +23 -0
- package/npm/fleet-add-failure-evidence.js +309 -0
- package/npm/fleet-add-failure.js +51 -311
- package/npm/fleet-build-data.js +47 -20
- package/npm/fleet-preview-health.js +156 -0
- package/npm/fleet-revision.js +50 -0
- package/npm/fleet-sticky-fields.js +124 -0
- package/npm/launch-config.js +43 -142
- package/npm/scenario-check.js +68 -383
- package/npm/scenario-issues.js +47 -0
- package/npm/scenario-launch.js +349 -0
- package/npm/scenario-playwright.js +42 -321
- package/npm/scenario-targeting.js +338 -0
- package/npm/vm-forwards.js +158 -0
- package/package.json +5 -5
- package/ui/dist/assets/{ContainerInspector-Ceop11Fv.js → ContainerInspector-Cig2PDjT.js} +1 -1
- package/ui/dist/assets/{EditorShell-DjI5cEF_.js → EditorShell-DOuUJNSm.js} +76 -74
- package/ui/dist/assets/{ScenarioDataPanel-D-nWT7xD.js → ScenarioDataPanel-BRUYhl1o.js} +10 -10
- package/ui/dist/assets/{ScenarioDataPanelFullPage-ZNFZcTQU.js → ScenarioDataPanelFullPage-DjwRM8HJ.js} +1 -1
- package/ui/dist/assets/ScenarioRenderer-CcbccxCW.js +26 -0
- package/ui/dist/assets/{index-ClrdYzlw.js → index-Bkrhkuf5.js} +6 -6
- package/ui/dist/assets/index-Dw4EB88N.css +1 -0
- package/ui/dist/index.html +2 -2
- package/ui/dist/assets/ScenarioRenderer-BW6QLGFW.js +0 -26
- package/ui/dist/assets/index-B3SMxYUB.css +0 -1
package/npm/scenario-check.js
CHANGED
|
@@ -11,29 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
const fs = require("fs");
|
|
13
13
|
const path = require("path");
|
|
14
|
-
const {
|
|
15
|
-
const { chromium } = require("playwright");
|
|
16
|
-
|
|
17
|
-
// Substring Playwright has emitted across every 1.x release when the
|
|
18
|
-
// browser cache is empty. Match the substring (not the full string)
|
|
19
|
-
// because Playwright includes the offending path inline.
|
|
20
|
-
const PLAYWRIGHT_MISSING_BROWSER_PATTERN = "Executable doesn't exist";
|
|
21
|
-
const PLAYWRIGHT_INSTALL_COMMAND = "npx playwright install chromium";
|
|
22
|
-
|
|
23
|
-
// Substrings Playwright/Chromium emit when the browser process dies on a
|
|
24
|
-
// transient launch crash. On the cloud VM the base image has no dbus session,
|
|
25
|
-
// so Chromium intermittently aborts at launch with `Received signal 11`
|
|
26
|
-
// (SIGSEGV) — an environmental fault, not a scenario fault. Match either form
|
|
27
|
-
// because the wording varies across Playwright releases and the OS signal
|
|
28
|
-
// reporter.
|
|
29
|
-
const PLAYWRIGHT_LAUNCH_CRASH_PATTERNS = ["SIGSEGV", "Received signal 11"];
|
|
30
|
-
// A transient SIGSEGV launch crash is environmental, so a bounded retry clears
|
|
31
|
-
// it. Keep it small — a genuinely broken environment must still surface loudly
|
|
32
|
-
// rather than spin.
|
|
33
|
-
const CAPTURE_LAUNCH_SIGSEGV_RETRIES = 2;
|
|
34
|
-
// Short backoff between SIGSEGV retries so the transient fault has a moment to
|
|
35
|
-
// clear; small enough not to eat the per-slug recapture budget.
|
|
36
|
-
const CAPTURE_LAUNCH_SIGSEGV_BACKOFF_MS = 250;
|
|
14
|
+
const { launchChromiumWithSelfHeal } = require("./scenario-launch");
|
|
37
15
|
|
|
38
16
|
// The sentinel the reverse proxy stamps on its dev-server-down placeholder —
|
|
39
17
|
// the other half of this contract is
|
|
@@ -44,306 +22,6 @@ const CAPTURE_LAUNCH_SIGSEGV_BACKOFF_MS = 250;
|
|
|
44
22
|
// the moment `render_placeholder` is reworded.
|
|
45
23
|
const DEV_SERVER_DOWN_HEADER = "x-codeyam-dev-server-down";
|
|
46
24
|
|
|
47
|
-
// Pin the headless capture browser's `localhost` resolution to the IPv4
|
|
48
|
-
// loopback the editor's listeners bind. The browser-facing preview origin is
|
|
49
|
-
// now `localhost` (secure-context apps refuse a bare IP — see
|
|
50
|
-
// `BROWSER_FACING_HOST`), but on a dual-stack host `localhost` can resolve to
|
|
51
|
-
// `::1` first, which nothing answers on — stalling the capture or paying a
|
|
52
|
-
// happy-eyeballs fallback delay. Forcing the map removes that intermittency
|
|
53
|
-
// deterministically for capture. The operator's OWN preview browser is covered
|
|
54
|
-
// separately by the editor binding `::1` alongside `127.0.0.1` (see start.rs).
|
|
55
|
-
const CAPTURE_HOST_RESOLVER_RULES = "MAP localhost 127.0.0.1";
|
|
56
|
-
const CAPTURE_LAUNCH_ARGS = [
|
|
57
|
-
`--host-resolver-rules=${CAPTURE_HOST_RESOLVER_RULES}`,
|
|
58
|
-
// Cloud-VM-no-dbus / containerized-launch hardening. Each flag below is
|
|
59
|
-
// inert on a healthy desktop, so the laptop capture path is unchanged; on
|
|
60
|
-
// the dbus-less cloud base image they stop Chromium from SIGSEGV-ing at
|
|
61
|
-
// launch (signal 11). See plan chromium-capture-dbus-crash-hardening.
|
|
62
|
-
// No setuid sandbox in the container (the cloud VM runs capture without the
|
|
63
|
-
// kernel namespaces the sandbox needs).
|
|
64
|
-
"--no-sandbox",
|
|
65
|
-
// /dev/shm is tiny in many containers; route shared memory to /tmp so the
|
|
66
|
-
// renderer doesn't crash allocating it.
|
|
67
|
-
"--disable-dev-shm-usage",
|
|
68
|
-
// No GPU on the headless cloud VM — avoid the GL init path that aborts.
|
|
69
|
-
"--disable-gpu",
|
|
70
|
-
// Software WebGL2, asked for EXPLICITLY rather than inherited. Playwright
|
|
71
|
-
// adds this itself on Linux, which is why `--disable-gpu` left canvas apps
|
|
72
|
-
// with working WebGL2 there — and why it did not on macOS, where Playwright
|
|
73
|
-
// does not add it: `getContext("webgl2")` returned null and the WebGL2
|
|
74
|
-
// capture test failed on every mac dev machine while passing in CI. That is
|
|
75
|
-
// the host-capability dependency CLAUDE.md forbids, so the flag is now
|
|
76
|
-
// stated here for every platform. A duplicate on Linux is harmless
|
|
77
|
-
// (last-wins, same value). WebGPU is still unavailable either way.
|
|
78
|
-
"--enable-unsafe-swiftshader",
|
|
79
|
-
];
|
|
80
|
-
// Features the capture browser disables on top of Playwright's own list.
|
|
81
|
-
// DBus: stub out the dbus integration Chromium reaches for at launch; the cloud
|
|
82
|
-
// base image has no dbus session bus, which is the proximate cause of the
|
|
83
|
-
// signal-11 abort.
|
|
84
|
-
//
|
|
85
|
-
// These are NOT a second `--disable-features=` switch. Chromium keeps only the
|
|
86
|
-
// LAST value of a repeated switch (verified empirically: `--disable-features=
|
|
87
|
-
// WebHID` hides `navigator.hid`, and a later `--disable-features=DBus` brings it
|
|
88
|
-
// back), so appending our own switch silently re-enabled every feature
|
|
89
|
-
// Playwright disables. `captureLaunchOptions` merges them into one value.
|
|
90
|
-
const CAPTURE_DISABLED_FEATURES = ["DBus"];
|
|
91
|
-
const DISABLE_FEATURES_PREFIX = "--disable-features=";
|
|
92
|
-
|
|
93
|
-
// Playwright's own default `--disable-features=<list>` switch, exactly as it
|
|
94
|
-
// will pass it, or null when it cannot be read. Read from playwright-core's
|
|
95
|
-
// switch table (not part of its public exports, so it is required by file path
|
|
96
|
-
// and any failure degrades to null rather than throwing).
|
|
97
|
-
function playwrightDefaultDisableFeaturesArg() {
|
|
98
|
-
try {
|
|
99
|
-
const playwrightDir = path.dirname(require.resolve("playwright/package.json"));
|
|
100
|
-
const coreDir = path.dirname(
|
|
101
|
-
require.resolve("playwright-core/package.json", { paths: [playwrightDir] }),
|
|
102
|
-
);
|
|
103
|
-
const { chromiumSwitches } = require(
|
|
104
|
-
path.join(coreDir, "lib", "server", "chromium", "chromiumSwitches.js"),
|
|
105
|
-
);
|
|
106
|
-
const found = chromiumSwitches(false).find(
|
|
107
|
-
(arg) => typeof arg === "string" && arg.startsWith(DISABLE_FEATURES_PREFIX),
|
|
108
|
-
);
|
|
109
|
-
return found || null;
|
|
110
|
-
} catch (_) {
|
|
111
|
-
return null;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
// One `--disable-features=` value carrying the default list plus `extra`,
|
|
116
|
-
// de-duplicated and order-preserving.
|
|
117
|
-
function mergeDisableFeaturesArg(defaultArg, extra) {
|
|
118
|
-
const base =
|
|
119
|
-
typeof defaultArg === "string" && defaultArg.startsWith(DISABLE_FEATURES_PREFIX)
|
|
120
|
-
? defaultArg.slice(DISABLE_FEATURES_PREFIX.length).split(",")
|
|
121
|
-
: [];
|
|
122
|
-
const merged = [];
|
|
123
|
-
for (const feature of base.concat(extra)) {
|
|
124
|
-
if (feature && !merged.includes(feature)) merged.push(feature);
|
|
125
|
-
}
|
|
126
|
-
return DISABLE_FEATURES_PREFIX + merged.join(",");
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// The `chromium.launch` args for capture. Playwright's default
|
|
130
|
-
// `--disable-features` switch is suppressed via `ignoreDefaultArgs` and
|
|
131
|
-
// re-supplied merged with ours. Should the suppression ever miss (its match is
|
|
132
|
-
// exact-string), our merged switch is still the later one and a superset, so
|
|
133
|
-
// the last-wins rule still lands every feature.
|
|
134
|
-
function captureLaunchOptions({
|
|
135
|
-
defaultDisableFeaturesArg = playwrightDefaultDisableFeaturesArg(),
|
|
136
|
-
} = {}) {
|
|
137
|
-
const args = [
|
|
138
|
-
...CAPTURE_LAUNCH_ARGS,
|
|
139
|
-
mergeDisableFeaturesArg(defaultDisableFeaturesArg, CAPTURE_DISABLED_FEATURES),
|
|
140
|
-
];
|
|
141
|
-
return defaultDisableFeaturesArg
|
|
142
|
-
? { args, ignoreDefaultArgs: [defaultDisableFeaturesArg] }
|
|
143
|
-
: { args };
|
|
144
|
-
}
|
|
145
|
-
// Point Chromium's dbus client at a dead address so it never blocks on (or
|
|
146
|
-
// crashes against) a missing session bus. Spread over the real env in the
|
|
147
|
-
// default launcher so the laptop path keeps its normal environment.
|
|
148
|
-
const CAPTURE_LAUNCH_DBUS_ENV = { DBUS_SESSION_BUS_ADDRESS: "/dev/null" };
|
|
149
|
-
|
|
150
|
-
// Does this launch error look like a transient SIGSEGV browser crash?
|
|
151
|
-
// (environmental — e.g. the cloud VM's missing dbus session bus — not a
|
|
152
|
-
// scenario fault, so it is safe to retry).
|
|
153
|
-
function isTransientLaunchCrash(error) {
|
|
154
|
-
return (
|
|
155
|
-
error &&
|
|
156
|
-
typeof error.message === "string" &&
|
|
157
|
-
PLAYWRIGHT_LAUNCH_CRASH_PATTERNS.some((pattern) =>
|
|
158
|
-
error.message.includes(pattern),
|
|
159
|
-
)
|
|
160
|
-
);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// Stable first token of the error a missing-system-library launch failure is
|
|
164
|
-
// rethrown as. The Rust side keys on it (`scenario_check.rs`
|
|
165
|
-
// `CAPTURE_BROWSER_DEPS_MISSING_MARKER`) to turn the failure into a blocked
|
|
166
|
-
// precondition, so the two spellings must stay greppable from each other.
|
|
167
|
-
const CAPTURE_BROWSER_DEPS_MISSING_MARKER = "CODEYAM_CAPTURE_BROWSER_DEPS_MISSING";
|
|
168
|
-
// The two shapes a missing shared library takes in a launch error: the dynamic
|
|
169
|
-
// loader's own line (Chromium starts and dies), and Playwright's host
|
|
170
|
-
// validation box listing every missing library before it even spawns.
|
|
171
|
-
const MISSING_SHARED_LIBRARY_PATTERNS = [
|
|
172
|
-
"error while loading shared libraries",
|
|
173
|
-
"Host system is missing dependencies",
|
|
174
|
-
];
|
|
175
|
-
const SHARED_LIBRARY_NAME_PATTERN = /\blib[\w+-]+(?:\.[\w+-]+)*?\.so(?:\.\d+)*/g;
|
|
176
|
-
const PLAYWRIGHT_INSTALL_DEPS_COMMAND = "npx playwright install-deps chromium";
|
|
177
|
-
// Nix package for the libraries Chromium most often lacks on Replit, keyed by
|
|
178
|
-
// the stem before `.so`. A library outside this map still gets a remedy — it is
|
|
179
|
-
// just named without a package suggestion.
|
|
180
|
-
const REPLIT_NIX_PACKAGE_FOR_LIBRARY = {
|
|
181
|
-
"libglib-2.0": "pkgs.glib",
|
|
182
|
-
"libgobject-2.0": "pkgs.glib",
|
|
183
|
-
"libgio-2.0": "pkgs.glib",
|
|
184
|
-
libnss3: "pkgs.nss",
|
|
185
|
-
libnssutil3: "pkgs.nss",
|
|
186
|
-
libsmime3: "pkgs.nss",
|
|
187
|
-
libnspr4: "pkgs.nspr",
|
|
188
|
-
"libatk-1.0": "pkgs.atk",
|
|
189
|
-
"libatk-bridge-2.0": "pkgs.at-spi2-atk",
|
|
190
|
-
"libatspi": "pkgs.at-spi2-core",
|
|
191
|
-
libcups: "pkgs.cups",
|
|
192
|
-
"libdbus-1": "pkgs.dbus",
|
|
193
|
-
libdrm: "pkgs.libdrm",
|
|
194
|
-
libgbm: "pkgs.mesa",
|
|
195
|
-
libxkbcommon: "pkgs.libxkbcommon",
|
|
196
|
-
libasound: "pkgs.alsa-lib",
|
|
197
|
-
"libpango-1.0": "pkgs.pango",
|
|
198
|
-
libcairo: "pkgs.cairo",
|
|
199
|
-
libexpat: "pkgs.expat",
|
|
200
|
-
libX11: "pkgs.xorg.libX11",
|
|
201
|
-
libXcomposite: "pkgs.xorg.libXcomposite",
|
|
202
|
-
libXdamage: "pkgs.xorg.libXdamage",
|
|
203
|
-
libXext: "pkgs.xorg.libXext",
|
|
204
|
-
libXfixes: "pkgs.xorg.libXfixes",
|
|
205
|
-
libXrandr: "pkgs.xorg.libXrandr",
|
|
206
|
-
libxcb: "pkgs.xorg.libxcb",
|
|
207
|
-
};
|
|
208
|
-
|
|
209
|
-
// Does this launch error say the host is missing a shared library Chromium
|
|
210
|
-
// needs? Not retryable and not fixed by downloading the browser again.
|
|
211
|
-
function isMissingSystemLibrary(error) {
|
|
212
|
-
return (
|
|
213
|
-
!!error &&
|
|
214
|
-
typeof error.message === "string" &&
|
|
215
|
-
MISSING_SHARED_LIBRARY_PATTERNS.some((pattern) =>
|
|
216
|
-
error.message.includes(pattern),
|
|
217
|
-
)
|
|
218
|
-
);
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
// Every distinct `lib*.so*` name a missing-library launch error mentions, in
|
|
222
|
-
// first-seen order. Covers both the loader line and Playwright's list.
|
|
223
|
-
function missingLibraryNames(error) {
|
|
224
|
-
const message = (error && error.message) || "";
|
|
225
|
-
return [...new Set(message.match(SHARED_LIBRARY_NAME_PATTERN) || [])];
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// The fix for the platform the capture is running on. Replit is detected by the
|
|
229
|
-
// same env signals `preview_host_provider/replit.rs` uses; there system
|
|
230
|
-
// packages come from replit.nix. Any other Linux host gets Playwright's own
|
|
231
|
-
// dependency installer; anything else gets the library names and a hint.
|
|
232
|
-
function missingLibraryRemedy(libraries, { env = process.env, platform = process.platform } = {}) {
|
|
233
|
-
const named = libraries.length > 0 ? libraries.join(", ") : "the missing libraries";
|
|
234
|
-
if (env.REPL_ID || env.REPLIT_DEV_DOMAIN) {
|
|
235
|
-
const packages = [
|
|
236
|
-
...new Set(
|
|
237
|
-
libraries
|
|
238
|
-
.map((lib) => REPLIT_NIX_PACKAGE_FOR_LIBRARY[lib.split(".so")[0]])
|
|
239
|
-
.filter(Boolean),
|
|
240
|
-
),
|
|
241
|
-
];
|
|
242
|
-
const suggestion =
|
|
243
|
-
packages.length > 0 ? ` (${packages.join(", ")})` : "";
|
|
244
|
-
return `add the Nix package(s) providing ${named}${suggestion} to the \`deps\` list in replit.nix, then restart the Repl and re-run the capture`;
|
|
245
|
-
}
|
|
246
|
-
if (platform === "linux") {
|
|
247
|
-
return `install Chromium's system libraries with \`${PLAYWRIGHT_INSTALL_DEPS_COMMAND}\` (may need sudo), then re-run the capture`;
|
|
248
|
-
}
|
|
249
|
-
return `install the system package(s) that provide ${named} for this OS, then re-run the capture`;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
// The rethrown error for a missing-library launch failure: marker line naming
|
|
253
|
-
// the libraries, a `remedy:` line, then the original Playwright message as
|
|
254
|
-
// diagnostic. Line-oriented so the Rust side can lift each part out.
|
|
255
|
-
function missingSystemLibraryError(error, { env, platform } = {}) {
|
|
256
|
-
const libraries = missingLibraryNames(error);
|
|
257
|
-
const remedy = missingLibraryRemedy(libraries, { env, platform });
|
|
258
|
-
const wrapped = new Error(
|
|
259
|
-
`${CAPTURE_BROWSER_DEPS_MISSING_MARKER} ${libraries.join(",")}\n` +
|
|
260
|
-
`remedy: ${remedy}\n` +
|
|
261
|
-
error.message,
|
|
262
|
-
);
|
|
263
|
-
wrapped.cause = error;
|
|
264
|
-
return wrapped;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
// Self-heal around `chromium.launch()`. Two recoverable classes:
|
|
268
|
-
//
|
|
269
|
-
// 1. "missing browser" — run `npx playwright install chromium` synchronously
|
|
270
|
-
// (with `stdio: "inherit"` so the user sees progress) and retry once.
|
|
271
|
-
// 2. transient SIGSEGV launch crash — retry the launch up to
|
|
272
|
-
// `CAPTURE_LAUNCH_SIGSEGV_RETRIES` times with a short backoff, since the
|
|
273
|
-
// crash is environmental (dbus-less cloud VM) and clears on a retry.
|
|
274
|
-
//
|
|
275
|
-
// A third class is diagnosed but deliberately NOT healed: a host missing a
|
|
276
|
-
// system library Chromium links against. Installing system packages changes
|
|
277
|
-
// the user's environment (replit.nix, apt, often root), so it is rethrown —
|
|
278
|
-
// with no retry and no browser install — as `missingSystemLibraryError`,
|
|
279
|
-
// naming the library and the platform's remedy.
|
|
280
|
-
//
|
|
281
|
-
// For any unrecognized error, or after exhausting retries, rethrow the
|
|
282
|
-
// ORIGINAL Playwright error so the existing `Scenario check failed: <stderr>`
|
|
283
|
-
// path keeps showing the actionable message — looping would hide a real ops
|
|
284
|
-
// failure under a slow timeout.
|
|
285
|
-
async function launchChromiumWithSelfHeal({
|
|
286
|
-
launch = () =>
|
|
287
|
-
chromium.launch({
|
|
288
|
-
...captureLaunchOptions(),
|
|
289
|
-
env: { ...process.env, ...CAPTURE_LAUNCH_DBUS_ENV },
|
|
290
|
-
}),
|
|
291
|
-
install = () => execSync(PLAYWRIGHT_INSTALL_COMMAND, { stdio: "inherit" }),
|
|
292
|
-
stderr = process.stderr,
|
|
293
|
-
sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
294
|
-
env = process.env,
|
|
295
|
-
platform = process.platform,
|
|
296
|
-
} = {}) {
|
|
297
|
-
try {
|
|
298
|
-
return await launch();
|
|
299
|
-
} catch (error) {
|
|
300
|
-
if (isMissingSystemLibrary(error)) {
|
|
301
|
-
throw missingSystemLibraryError(error, { env, platform });
|
|
302
|
-
}
|
|
303
|
-
const isMissingBrowser =
|
|
304
|
-
error &&
|
|
305
|
-
typeof error.message === "string" &&
|
|
306
|
-
error.message.includes(PLAYWRIGHT_MISSING_BROWSER_PATTERN);
|
|
307
|
-
const isTransientCrash = isTransientLaunchCrash(error);
|
|
308
|
-
if (!isMissingBrowser && !isTransientCrash) throw error;
|
|
309
|
-
|
|
310
|
-
if (isTransientCrash) {
|
|
311
|
-
for (let attempt = 1; attempt <= CAPTURE_LAUNCH_SIGSEGV_RETRIES; attempt++) {
|
|
312
|
-
stderr.write(
|
|
313
|
-
`Chromium crashed at launch (transient, likely a dbus-less environment) — retry ${attempt}/${CAPTURE_LAUNCH_SIGSEGV_RETRIES}.\n`,
|
|
314
|
-
);
|
|
315
|
-
await sleep(CAPTURE_LAUNCH_SIGSEGV_BACKOFF_MS);
|
|
316
|
-
try {
|
|
317
|
-
return await launch();
|
|
318
|
-
} catch (retryError) {
|
|
319
|
-
if (!isTransientLaunchCrash(retryError)) throw retryError;
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
throw error;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
stderr.write(
|
|
326
|
-
"Playwright's Chromium browser is missing — installing it now (one-time ~150 MB download). Subsequent runs will be instant.\n",
|
|
327
|
-
);
|
|
328
|
-
try {
|
|
329
|
-
install();
|
|
330
|
-
} catch (_installError) {
|
|
331
|
-
throw error;
|
|
332
|
-
}
|
|
333
|
-
try {
|
|
334
|
-
return await launch();
|
|
335
|
-
} catch (retryError) {
|
|
336
|
-
// A fresh download onto a host without Chromium's system libraries
|
|
337
|
-
// (the common first-capture shape on Replit) fails HERE, and the
|
|
338
|
-
// original "missing browser" error would now be a lie.
|
|
339
|
-
if (isMissingSystemLibrary(retryError)) {
|
|
340
|
-
throw missingSystemLibraryError(retryError, { env, platform });
|
|
341
|
-
}
|
|
342
|
-
throw error;
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
|
|
347
25
|
const {
|
|
348
26
|
findScenarioError,
|
|
349
27
|
SCENARIO_ERROR_MARKER,
|
|
@@ -378,6 +56,8 @@ const {
|
|
|
378
56
|
createIssue,
|
|
379
57
|
pushIssue,
|
|
380
58
|
buildResult,
|
|
59
|
+
RELOADED_AFTER_READY,
|
|
60
|
+
retryOnLiveReload,
|
|
381
61
|
} = require("./scenario-issues");
|
|
382
62
|
|
|
383
63
|
const {
|
|
@@ -394,6 +74,7 @@ const {
|
|
|
394
74
|
resolveHarnessOrigin,
|
|
395
75
|
waitForStablePage,
|
|
396
76
|
waitForReadySignal,
|
|
77
|
+
watchNavigationAfterReady,
|
|
397
78
|
createNetworkTracker,
|
|
398
79
|
waitForNetworkQuiet,
|
|
399
80
|
collectContentState,
|
|
@@ -402,7 +83,6 @@ const {
|
|
|
402
83
|
forceFinalVisualState,
|
|
403
84
|
centerCaptureWrapper,
|
|
404
85
|
isCanvasInteraction,
|
|
405
|
-
devicePixelRatioOf,
|
|
406
86
|
resolveInteractionTarget,
|
|
407
87
|
performInteraction,
|
|
408
88
|
pixelSnapshot,
|
|
@@ -412,6 +92,7 @@ const {
|
|
|
412
92
|
performInteractionSequence,
|
|
413
93
|
logCaptureTiming,
|
|
414
94
|
} = require("./scenario-playwright");
|
|
95
|
+
const { devicePixelRatioOf } = require("./scenario-targeting");
|
|
415
96
|
|
|
416
97
|
// Name of the cookie the editor's control-API accepts the session token in —
|
|
417
98
|
// must match `SESSION_COOKIE` in crates/control-api/src/origin_guard.rs. On a
|
|
@@ -2046,6 +1727,48 @@ async function closeBrowserBounded(browser, boundMs = 5000) {
|
|
|
2046
1727
|
return outcome;
|
|
2047
1728
|
}
|
|
2048
1729
|
|
|
1730
|
+
// A flow's `navigate` step: load `stepPath` (resolved relative to the flow's
|
|
1731
|
+
// initial url) with the same loader strategy as the initial load, re-settle,
|
|
1732
|
+
// and re-wait for hydration. Returns the new content frame and its hydration
|
|
1733
|
+
// verdict, which later steps target and classify against.
|
|
1734
|
+
async function navigateFlowStep(page, stepPath, ctx) {
|
|
1735
|
+
const { url, navigation, preflight, iframeBackground, harnessOrigin } = ctx;
|
|
1736
|
+
const target = new URL(stepPath, url).href;
|
|
1737
|
+
const loadResult =
|
|
1738
|
+
navigation === "topLevel"
|
|
1739
|
+
? await loadScenarioTopLevel(page, target, { preflight })
|
|
1740
|
+
: await loadScenarioInIframe(page, target, {
|
|
1741
|
+
background: iframeBackground,
|
|
1742
|
+
preflight,
|
|
1743
|
+
harnessOrigin,
|
|
1744
|
+
});
|
|
1745
|
+
const frame = loadResult.frame;
|
|
1746
|
+
await waitForStablePage(page, frame, ctx.settleMs, ctx.loadingMarkers);
|
|
1747
|
+
// A navigate loads a fresh route that must re-hydrate before the next
|
|
1748
|
+
// fill/click, or the interaction lands on inert SSR markup (the same
|
|
1749
|
+
// bug the top-level wait fixes, one route deeper). Bounded and
|
|
1750
|
+
// stack-gated exactly like the top-level wait.
|
|
1751
|
+
const navHydration = await waitForHydration(frame, {
|
|
1752
|
+
// `target` is only where we aimed; the frame's own URL is where we
|
|
1753
|
+
// landed, so a redirect during the step is followed. Falls back to
|
|
1754
|
+
// `target` when a detached frame cannot answer.
|
|
1755
|
+
url: safeFrameUrl(frame) || target,
|
|
1756
|
+
timeoutMs: ctx.hydrationTimeoutMs,
|
|
1757
|
+
probeCounterpart: ctx.probeCounterpart,
|
|
1758
|
+
});
|
|
1759
|
+
// The mount that breaks a top-level load breaks it here too, and
|
|
1760
|
+
// before this a route that died one navigation deep got no
|
|
1761
|
+
// attribution at all. Report it so the editor can escalate the
|
|
1762
|
+
// preview off the subpath for the rest of the session.
|
|
1763
|
+
//
|
|
1764
|
+
// The returned issue is deliberately NOT pushed: this wait exists to
|
|
1765
|
+
// gate the next interaction, and adding an issue here would give a
|
|
1766
|
+
// flow that passes today a new way to fail. Computing a verdict and
|
|
1767
|
+
// not raising it reads as a bug otherwise — it is the point.
|
|
1768
|
+
await escalateSubpathHydration(navHydration);
|
|
1769
|
+
return { frame, hydrated: navHydration.hydrated };
|
|
1770
|
+
}
|
|
1771
|
+
|
|
2049
1772
|
// Drive an ordered list of flow steps against ONE already-loaded browser
|
|
2050
1773
|
// session so a scripted multi-step demo (`editor preview-flow`) is captured as
|
|
2051
1774
|
// the real round-trip — click state and client transients persist across
|
|
@@ -2066,15 +1789,9 @@ async function closeBrowserBounded(browser, boundMs = 5000) {
|
|
|
2066
1789
|
// result URL reflect the last navigated route.
|
|
2067
1790
|
async function runFlowSteps(page, initialFrame, steps, ctx) {
|
|
2068
1791
|
const {
|
|
2069
|
-
url,
|
|
2070
1792
|
loadingMarkers,
|
|
2071
|
-
navigation,
|
|
2072
|
-
iframeBackground,
|
|
2073
|
-
preflight,
|
|
2074
|
-
harnessOrigin,
|
|
2075
1793
|
hydrationTimeoutMs = 10000,
|
|
2076
1794
|
settleMs,
|
|
2077
|
-
probeCounterpart,
|
|
2078
1795
|
stepEffects,
|
|
2079
1796
|
downloads,
|
|
2080
1797
|
downloadDir,
|
|
@@ -2111,43 +1828,13 @@ async function runFlowSteps(page, initialFrame, steps, ctx) {
|
|
|
2111
1828
|
logCaptureTiming("flow-step", { step: n, action: step.action, outcome: "started" });
|
|
2112
1829
|
try {
|
|
2113
1830
|
switch (step.action) {
|
|
2114
|
-
case "navigate":
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
background: iframeBackground,
|
|
2121
|
-
preflight,
|
|
2122
|
-
harnessOrigin,
|
|
2123
|
-
});
|
|
2124
|
-
frame = loadResult.frame;
|
|
2125
|
-
await waitForStablePage(page, frame, navigateSettleMs, loadingMarkers);
|
|
2126
|
-
// A navigate loads a fresh route that must re-hydrate before the next
|
|
2127
|
-
// fill/click, or the interaction lands on inert SSR markup (the same
|
|
2128
|
-
// bug the top-level wait fixes, one route deeper). Bounded and
|
|
2129
|
-
// stack-gated exactly like the top-level wait.
|
|
2130
|
-
const navHydration = await waitForHydration(frame, {
|
|
2131
|
-
// `target` is only where we aimed; the frame's own URL is where we
|
|
2132
|
-
// landed, so a redirect during the step is followed. Falls back to
|
|
2133
|
-
// `target` when a detached frame cannot answer.
|
|
2134
|
-
url: safeFrameUrl(frame) || target,
|
|
2135
|
-
timeoutMs: hydrationTimeoutMs,
|
|
2136
|
-
probeCounterpart,
|
|
2137
|
-
});
|
|
2138
|
-
// The mount that breaks a top-level load breaks it here too, and
|
|
2139
|
-
// before this a route that died one navigation deep got no
|
|
2140
|
-
// attribution at all. Report it so the editor can escalate the
|
|
2141
|
-
// preview off the subpath for the rest of the session.
|
|
2142
|
-
//
|
|
2143
|
-
// The returned issue is deliberately NOT pushed: this wait exists to
|
|
2144
|
-
// gate the next interaction, and adding an issue here would give a
|
|
2145
|
-
// flow that passes today a new way to fail. Computing a verdict and
|
|
2146
|
-
// not raising it reads as a bug otherwise — it is the point.
|
|
2147
|
-
await escalateSubpathHydration(navHydration);
|
|
2148
|
-
hydrated = navHydration.hydrated;
|
|
1831
|
+
case "navigate":
|
|
1832
|
+
({ frame, hydrated } = await navigateFlowStep(page, step.path, {
|
|
1833
|
+
...ctx,
|
|
1834
|
+
settleMs: navigateSettleMs,
|
|
1835
|
+
hydrationTimeoutMs,
|
|
1836
|
+
}));
|
|
2149
1837
|
break;
|
|
2150
|
-
}
|
|
2151
1838
|
case "click":
|
|
2152
1839
|
case "fill":
|
|
2153
1840
|
case "press":
|
|
@@ -2244,6 +1931,14 @@ async function runFlowSteps(page, initialFrame, steps, ctx) {
|
|
|
2244
1931
|
return frame;
|
|
2245
1932
|
}
|
|
2246
1933
|
|
|
1934
|
+
// One capture, retried once if a dev server's live reload interrupted it (see
|
|
1935
|
+
// `retryOnLiveReload`).
|
|
1936
|
+
async function runScenarioCheck(config, options = {}) {
|
|
1937
|
+
return retryOnLiveReload(() => runScenarioCheckOnce(config, options), {
|
|
1938
|
+
onRetry: (firstError) => logCaptureTiming("live-reload-retry", { firstError }),
|
|
1939
|
+
});
|
|
1940
|
+
}
|
|
1941
|
+
|
|
2247
1942
|
// `preflight` is injectable (defaulting to the real app-port reachability
|
|
2248
1943
|
// check) so unit tests that mock the browser can stay network-free.
|
|
2249
1944
|
// `harnessOrigin` is likewise injectable: it defaults to resolving the editor's
|
|
@@ -2251,7 +1946,7 @@ async function runFlowSteps(page, initialFrame, steps, ctx) {
|
|
|
2251
1946
|
// explicit value (e.g. `null` to force the legacy `setContent` harness) so the
|
|
2252
1947
|
// iframe-load path never silently depends on a server-state file in cwd.
|
|
2253
1948
|
// Resolved ONCE here and threaded into every iframe load below.
|
|
2254
|
-
async function
|
|
1949
|
+
async function runScenarioCheckOnce(
|
|
2255
1950
|
config,
|
|
2256
1951
|
{ preflight = assertAppPortReachable, harnessOrigin } = {},
|
|
2257
1952
|
) {
|
|
@@ -2603,6 +2298,9 @@ async function runScenarioCheck(
|
|
|
2603
2298
|
if (!readyOutcome.ready) {
|
|
2604
2299
|
pushIssue(issues, readySignalTimeoutIssue(readiness, page.url() || url));
|
|
2605
2300
|
}
|
|
2301
|
+
// Checked before the screenshot below; a declared interaction may navigate
|
|
2302
|
+
// on purpose, so it is only acted on for a static capture.
|
|
2303
|
+
const reloadWatch = watchNavigationAfterReady(page, frame);
|
|
2606
2304
|
// Held here — after stability, before the content/hydration assertions and
|
|
2607
2305
|
// the screenshot — so every downstream check sees the same frame the
|
|
2608
2306
|
// scenario asked for rather than an earlier one.
|
|
@@ -2898,6 +2596,9 @@ async function runScenarioCheck(
|
|
|
2898
2596
|
!!config.interaction ||
|
|
2899
2597
|
(Array.isArray(config.interactions) && config.interactions.length > 0) ||
|
|
2900
2598
|
(Array.isArray(config.steps) && config.steps.length > 0);
|
|
2599
|
+
if (reloadWatch.navigated() && !hasDeclaredInteractiveState) {
|
|
2600
|
+
throw new Error(RELOADED_AFTER_READY);
|
|
2601
|
+
}
|
|
2901
2602
|
if (outputPath && loaded && !hasDeclaredInteractiveState) {
|
|
2902
2603
|
await forceFinalVisualState(frame).catch(() => {});
|
|
2903
2604
|
}
|
|
@@ -3126,17 +2827,6 @@ module.exports = {
|
|
|
3126
2827
|
DEV_SERVER_DOWN_HEADER,
|
|
3127
2828
|
stripMarkerHeaders,
|
|
3128
2829
|
main,
|
|
3129
|
-
launchChromiumWithSelfHeal,
|
|
3130
|
-
isTransientLaunchCrash,
|
|
3131
|
-
isMissingSystemLibrary,
|
|
3132
|
-
missingLibraryNames,
|
|
3133
|
-
missingLibraryRemedy,
|
|
3134
|
-
CAPTURE_BROWSER_DEPS_MISSING_MARKER,
|
|
3135
|
-
CAPTURE_LAUNCH_ARGS,
|
|
3136
|
-
CAPTURE_DISABLED_FEATURES,
|
|
3137
|
-
captureLaunchOptions,
|
|
3138
|
-
mergeDisableFeaturesArg,
|
|
3139
|
-
playwrightDefaultDisableFeaturesArg,
|
|
3140
2830
|
DEFAULT_READY_TIMEOUT_MS,
|
|
3141
2831
|
resolveReadySignal,
|
|
3142
2832
|
readStackReadySignal,
|
|
@@ -3149,11 +2839,6 @@ module.exports = {
|
|
|
3149
2839
|
readySignalTimeoutIssue,
|
|
3150
2840
|
canvasBlankIssue,
|
|
3151
2841
|
decodeScreenshotPng,
|
|
3152
|
-
CAPTURE_HOST_RESOLVER_RULES,
|
|
3153
|
-
CAPTURE_LAUNCH_SIGSEGV_RETRIES,
|
|
3154
|
-
PLAYWRIGHT_INSTALL_COMMAND,
|
|
3155
|
-
PLAYWRIGHT_MISSING_BROWSER_PATTERN,
|
|
3156
|
-
PLAYWRIGHT_LAUNCH_CRASH_PATTERNS,
|
|
3157
2842
|
};
|
|
3158
2843
|
|
|
3159
2844
|
if (require.main === module) {
|
package/npm/scenario-issues.js
CHANGED
|
@@ -111,9 +111,56 @@ function buildResult({
|
|
|
111
111
|
};
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
// The message a capture fails with when the scenario frame navigated after its
|
|
115
|
+
// ready signal arrived — a dev server's live reload swapping the page under the
|
|
116
|
+
// shot.
|
|
117
|
+
const RELOADED_AFTER_READY =
|
|
118
|
+
"The scenario page navigated after it signalled ready (a dev-server live reload)";
|
|
119
|
+
|
|
120
|
+
// Failure messages that mean the page was reloaded mid-capture rather than that
|
|
121
|
+
// the app is broken: Playwright's destroyed-context and detached-frame errors,
|
|
122
|
+
// and the marker above. Any dev server with live reload (Vite full reload,
|
|
123
|
+
// trunk, Flutter) produces them, so the match names no stack.
|
|
124
|
+
const LIVE_RELOAD_FAILURE_PATTERNS = [
|
|
125
|
+
/Execution context was destroyed/i,
|
|
126
|
+
/Frame was detached/i,
|
|
127
|
+
/navigated after it signalled ready/,
|
|
128
|
+
];
|
|
129
|
+
|
|
130
|
+
// The first issue of a failed capture whose message names a mid-capture
|
|
131
|
+
// reload, or null. A capture that passed, or failed for any other reason, is
|
|
132
|
+
// never a reload candidate.
|
|
133
|
+
function liveReloadIssue(result) {
|
|
134
|
+
if (!result || result.ok) return null;
|
|
135
|
+
return (
|
|
136
|
+
(result.issues || []).find((issue) =>
|
|
137
|
+
LIVE_RELOAD_FAILURE_PATTERNS.some((re) => re.test(String(issue.message || ""))),
|
|
138
|
+
) || null
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Run a capture `attempt`, and run it ONCE more when a dev server's live reload
|
|
143
|
+
// interrupted it: the reload is a temporary condition, and the second attempt
|
|
144
|
+
// waits for the ready signal on the reloaded page from scratch. The retry is
|
|
145
|
+
// recorded on the returned result (`reloadRetry`) so it is visible rather than
|
|
146
|
+
// silently absorbed, and a second failure is returned as-is, with its own
|
|
147
|
+
// reason. `onRetry` is told the first failure before the retry starts.
|
|
148
|
+
async function retryOnLiveReload(attempt, { onRetry = () => {} } = {}) {
|
|
149
|
+
const first = await attempt();
|
|
150
|
+
const reload = liveReloadIssue(first);
|
|
151
|
+
if (!reload) return first;
|
|
152
|
+
onRetry(reload.message);
|
|
153
|
+
const second = await attempt();
|
|
154
|
+
second.reloadRetry = { retried: true, firstError: reload.message };
|
|
155
|
+
return second;
|
|
156
|
+
}
|
|
157
|
+
|
|
114
158
|
module.exports = {
|
|
115
159
|
createIssue,
|
|
116
160
|
pushIssue,
|
|
117
161
|
buildResult,
|
|
118
162
|
deriveOriginSplit,
|
|
163
|
+
RELOADED_AFTER_READY,
|
|
164
|
+
liveReloadIssue,
|
|
165
|
+
retryOnLiveReload,
|
|
119
166
|
};
|