@codeyam-editor/codeyam-editor 0.1.5 → 0.1.7
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/cli.js +5 -1
- package/npm/cloud.js +2079 -242
- package/npm/codesign-plan.js +68 -0
- package/npm/compose-conflict.js +59 -0
- package/npm/empty-project-dir.js +92 -7
- package/npm/fleet-add-failure.js +361 -0
- package/npm/fleet-asset-version.js +37 -0
- package/npm/fleet-auth-health.js +143 -5
- package/npm/fleet-build-data.js +60 -5
- package/npm/fleet-cleanup.js +243 -9
- package/npm/fleet-destroy-safety.js +230 -13
- package/npm/fleet-early-roster.js +87 -0
- package/npm/fleet-effect-poll.js +110 -0
- package/npm/fleet-env.js +86 -0
- package/npm/fleet-ghcr-secret.js +129 -0
- package/npm/fleet-git-maintenance.js +142 -0
- package/npm/fleet-host-op-limiter.js +68 -0
- package/npm/fleet-iap-token.js +165 -0
- package/npm/fleet-job-badge.js +28 -12
- package/npm/fleet-live-status.js +40 -8
- package/npm/fleet-memory.js +289 -9
- package/npm/fleet-plans.js +98 -4
- package/npm/fleet-pool.js +75 -16
- package/npm/fleet-prewarm.js +87 -11
- package/npm/fleet-probe-transport.js +155 -0
- package/npm/fleet-project-drift.js +58 -0
- package/npm/fleet-public-url.js +234 -0
- package/npm/fleet-queue.js +58 -1
- package/npm/fleet-reachability-debounce.js +69 -0
- package/npm/fleet-reachability.js +80 -0
- package/npm/fleet-reset.js +32 -0
- package/npm/fleet-revision.js +104 -0
- package/npm/fleet-running-plan.js +115 -0
- package/npm/fleet-session-token-cache.js +65 -0
- package/npm/fleet-setup-steps.js +19 -12
- package/npm/fleet-status-refs.js +96 -0
- package/npm/fleet-strand.js +39 -8
- package/npm/fleet-stuck-status.js +13 -7
- package/npm/fleet-vm-access.js +170 -0
- package/npm/fleet-vm-probe-target.js +282 -0
- package/npm/fleet-vm-readiness.js +136 -3
- package/npm/google-ingress.js +15 -0
- package/npm/ingress-invoke.js +478 -0
- package/npm/launch-config.js +302 -10
- package/npm/scenario-check.js +318 -12
- package/npm/scenario-handlers.js +85 -27
- package/npm/scenario-interactivity.js +145 -6
- package/npm/scenario-issues.js +13 -1
- package/npm/scenario-mocks.js +129 -26
- package/npm/scenario-playwright.js +330 -22
- package/npm/script-drift.js +72 -1
- package/npm/utils.js +37 -3
- package/package.json +5 -5
- package/ui/dist/assets/{ContainerInspector-D7VZkkaj.js → ContainerInspector-Cu1tSN-o.js} +1 -1
- package/ui/dist/assets/EditorShell--V2ExYQ6.js +260 -0
- package/ui/dist/assets/EditorShell-DP5UY20G.css +1 -0
- package/ui/dist/assets/{Markdown-BoLHHFXH.js → Markdown-Chgdf3PW.js} +1 -1
- package/ui/dist/assets/ProjectLauncherScreen-Dmm6Z0mv.js +1 -0
- package/ui/dist/assets/{ScenarioDataPanel-DYP7pi_n.css → ScenarioDataPanel-C1LukYI6.css} +1 -1
- package/ui/dist/assets/ScenarioDataPanel-DjprZq2L.js +9 -0
- package/ui/dist/assets/{ScenarioDataPanelFullPage-CBLTGC4-.js → ScenarioDataPanelFullPage-BqMxWVKW.js} +1 -1
- package/ui/dist/assets/ScenarioRenderer-DqLVVMYS.js +21 -0
- package/ui/dist/assets/{StepsSimulator-BFldWyoX.js → StepsSimulator-CymGgfbu.js} +1 -1
- package/ui/dist/assets/index-CjxWhkSy.css +1 -0
- package/ui/dist/assets/{index-QsOiOG9x.js → index-VuyqBbGJ.js} +4 -4
- package/ui/dist/assets/{markdown-v0F0UEt3.js → markdown-C9tbsVHX.js} +1 -1
- package/ui/dist/assets/react-nrLBr15I.js +28 -0
- package/ui/dist/assets/{useEvents-DHkLJZR4.js → useEvents-S3Qk1Y_d.js} +1 -1
- package/ui/dist/assets/useServerIdentity-BdXy8TQv.js +1 -0
- package/ui/dist/index.html +4 -4
- package/ui/dist/open-source/codeyam-counter.png +0 -0
- package/ui/dist/open-source/el-carot.png +0 -0
- package/ui/dist/open-source/tabcommand.png +0 -0
- package/ui/dist/assets/EditorShell-DHhIaotd.css +0 -1
- package/ui/dist/assets/EditorShell-r-796N_W.js +0 -191
- package/ui/dist/assets/ProjectLauncherScreen-Ck-XWkQN.js +0 -1
- package/ui/dist/assets/ScenarioDataPanel-B9iDSUpA.js +0 -5
- package/ui/dist/assets/ScenarioRenderer-BhLfT-nX.js +0 -21
- package/ui/dist/assets/index-CIXZkwfB.css +0 -1
- package/ui/dist/assets/react-CSS0HapR.js +0 -28
|
@@ -19,13 +19,36 @@ const { createIssue } = require("./scenario-issues");
|
|
|
19
19
|
// reliable in-page attachment signal are ALSO a conservative pass: we never
|
|
20
20
|
// flag a page we cannot prove is dead, so a healthy Svelte / Solid / vanilla
|
|
21
21
|
// page is never a false negative. A new framework detector is a new entry in
|
|
22
|
-
// `detectors` below plus a `KNOWN_FRAMEWORKS` mapping — not an edit here
|
|
22
|
+
// `detectors` below plus a `KNOWN_FRAMEWORKS` mapping — not an edit here; a
|
|
23
|
+
// meta-framework that hydrates an already-detected runtime is a new row in
|
|
24
|
+
// `META_FRAMEWORK_ALIASES`.
|
|
23
25
|
|
|
24
26
|
// Frameworks we have an in-page attachment detector for. Inference only
|
|
25
27
|
// resolves to one of these; an unrecognised framework yields `null`, which
|
|
26
28
|
// the caller treats as "cannot determine" (conservative pass).
|
|
27
29
|
const KNOWN_FRAMEWORKS = ["react", "vue"];
|
|
28
30
|
|
|
31
|
+
// Meta-frameworks mapped to the underlying runtime whose hydration detector
|
|
32
|
+
// applies. These do not carry their runtime's name in their stack identity: a
|
|
33
|
+
// Next.js app is `id: "nextjs-prisma-sqlite"`, `name: "Next.js + Prisma +
|
|
34
|
+
// SQLite"` — nothing contains the literal "react". Without this map the
|
|
35
|
+
// substring scan below returns `null`, the capture skips the hydration gate,
|
|
36
|
+
// and a genuinely un-hydrated page is reported as an inert control rather than
|
|
37
|
+
// as hydration never having run.
|
|
38
|
+
//
|
|
39
|
+
// Saying "Next hydrates React" is a fact about the runtime, not a framework
|
|
40
|
+
// assumption, so this stays consistent with the data-driven design above: a new
|
|
41
|
+
// meta-framework is a new row here, not a new branch in the capture flow.
|
|
42
|
+
//
|
|
43
|
+
// Matched with word boundaries rather than bare substrings so an unrelated name
|
|
44
|
+
// like "NextGen CLI" or "Remixer" cannot masquerade as a meta-framework.
|
|
45
|
+
const META_FRAMEWORK_ALIASES = [
|
|
46
|
+
{ pattern: /\bnext(\.js|js)?\b/, framework: "react" },
|
|
47
|
+
{ pattern: /\bremix\b/, framework: "react" },
|
|
48
|
+
{ pattern: /\bgatsby\b/, framework: "react" },
|
|
49
|
+
{ pattern: /\bnuxt(\.js|js)?\b/, framework: "vue" },
|
|
50
|
+
];
|
|
51
|
+
|
|
29
52
|
// Read `.codeyam/stack.json` relative to the capture script's cwd (the project
|
|
30
53
|
// dir — `scenario_check.rs` sets `.current_dir(project_dir)`). Never throws: a
|
|
31
54
|
// missing or malformed file yields `null` so the probe degrades to a no-op
|
|
@@ -51,6 +74,9 @@ function inferFramework(stack) {
|
|
|
51
74
|
.filter((s) => typeof s === "string")
|
|
52
75
|
.join(" ")
|
|
53
76
|
.toLowerCase();
|
|
77
|
+
for (const { pattern, framework } of META_FRAMEWORK_ALIASES) {
|
|
78
|
+
if (pattern.test(haystack)) return framework;
|
|
79
|
+
}
|
|
54
80
|
for (const fw of KNOWN_FRAMEWORKS) {
|
|
55
81
|
if (haystack.includes(fw)) return fw;
|
|
56
82
|
}
|
|
@@ -214,24 +240,136 @@ function interpretHydration({
|
|
|
214
240
|
// project's stack.json unless the caller injects a `stack`), short-circuit when
|
|
215
241
|
// no client runtime is expected, collect the in-page state, and interpret it.
|
|
216
242
|
// Never throws — a probe failure must not break an otherwise-good capture.
|
|
217
|
-
|
|
243
|
+
//
|
|
244
|
+
// Returns `{ hydrated, issue }`:
|
|
245
|
+
// hydrated — `true` (runtime demonstrably attached), `false` (PROVEN dead:
|
|
246
|
+
// framework-owned controls rendered but nothing attached), or `null`
|
|
247
|
+
// ("cannot determine" — no client runtime expected, no control to probe, no
|
|
248
|
+
// detector for the framework, or the probe threw).
|
|
249
|
+
// issue — the `hydration` issue to surface, or `null` to pass.
|
|
250
|
+
//
|
|
251
|
+
// `hydrated` is deliberately three-valued rather than a bare boolean: the
|
|
252
|
+
// capture flow branches a page to `interactionEffect: "unhydrated"` ONLY on a
|
|
253
|
+
// proven `false`. A `null` must never be read as "dead" — that would turn every
|
|
254
|
+
// unknown-framework page into a false hydration failure.
|
|
255
|
+
async function probeHydrationState(frame, { url, stack } = {}) {
|
|
218
256
|
const descriptor = stack !== undefined ? stack : readStackJson();
|
|
219
257
|
const { expectInteractive, framework } =
|
|
220
258
|
resolveInteractivityExpectation(descriptor);
|
|
221
|
-
if (!expectInteractive) return null;
|
|
259
|
+
if (!expectInteractive) return { hydrated: null, issue: null };
|
|
222
260
|
let state;
|
|
223
261
|
try {
|
|
224
262
|
state = await collectHydrationState(frame, { framework });
|
|
225
263
|
} catch (_) {
|
|
226
|
-
return null;
|
|
264
|
+
return { hydrated: null, issue: null };
|
|
227
265
|
}
|
|
228
|
-
|
|
266
|
+
const issue = interpretHydration({
|
|
229
267
|
expectInteractive: true,
|
|
230
268
|
controlCount: state.controlCount,
|
|
231
269
|
frameworkAttached: state.frameworkAttached,
|
|
232
270
|
framework,
|
|
233
271
|
url,
|
|
234
272
|
});
|
|
273
|
+
// `frameworkAttached` is already the three-valued signal `hydrated` needs —
|
|
274
|
+
// pass it through rather than re-deriving it from the presence of an issue,
|
|
275
|
+
// which would conflate "no issue" (a pass) with "hydrated" (a positive).
|
|
276
|
+
return { hydrated: state.frameworkAttached, issue };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Sleep for `ms` — a promisified `setTimeout`, so the poll loop yields the
|
|
280
|
+
// event loop between ticks instead of spinning.
|
|
281
|
+
function sleep(ms) {
|
|
282
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Bounded WAIT for hydration — the polling generalization of
|
|
286
|
+
// `probeHydrationState`. Where the probe reads the attachment state once,
|
|
287
|
+
// this re-polls `collectHydrationState` until the framework demonstrably
|
|
288
|
+
// attaches, the state becomes unjudgeable, or a wall-clock cap elapses. It is
|
|
289
|
+
// the seam the capture flow calls so that every screenshot and every driven
|
|
290
|
+
// interaction happens AFTER the client runtime has attached, not on inert SSR
|
|
291
|
+
// markup (the bug where a fill/click landed before React hydrated, the handler
|
|
292
|
+
// never fired, yet the run reported success).
|
|
293
|
+
//
|
|
294
|
+
// Stack-agnostic by construction: it resolves the same expectation as the
|
|
295
|
+
// probe, so a non-interactive stack (backend/static/CLI, or an explicit
|
|
296
|
+
// `capture.interactivity === false`) returns immediately with `hydrated: null`
|
|
297
|
+
// and never waits. An unknown framework (no detector) reads
|
|
298
|
+
// `frameworkAttached === null` on the first poll and also passes instantly — we
|
|
299
|
+
// never block on a page we cannot judge.
|
|
300
|
+
//
|
|
301
|
+
// Never fails closed: a timeout yields `hydrated: false` and lets the existing
|
|
302
|
+
// `interpretHydration` path decide whether to surface the loud `hydration`
|
|
303
|
+
// issue; a thrown probe (detached frame mid-nav) is caught and treated as a
|
|
304
|
+
// pass. The wait can never hang a capture (hard cap) and never turns a page we
|
|
305
|
+
// cannot prove dead into a failure.
|
|
306
|
+
//
|
|
307
|
+
// Honest limitation: the poll can only judge a page that has already rendered
|
|
308
|
+
// framework-owned controls. `waitForStablePage` runs first (its `rootUnpainted`
|
|
309
|
+
// guard waits for the SPA root to paint), so by the time this runs, content is
|
|
310
|
+
// present for the SSR case this guards; a pure-CSR shell that renders zero
|
|
311
|
+
// controls reads `null` and passes instantly, unchanged from today.
|
|
312
|
+
//
|
|
313
|
+
// Returns `{ hydrated, issue, timedOut, waitedMs }` where `hydrated` is the
|
|
314
|
+
// same three-valued signal `probeHydrationState` returns.
|
|
315
|
+
async function waitForHydration(
|
|
316
|
+
frame,
|
|
317
|
+
{ url, stack, framework, timeoutMs = 10000, pollIntervalMs = 150 } = {},
|
|
318
|
+
) {
|
|
319
|
+
let resolvedFramework;
|
|
320
|
+
let expectInteractive;
|
|
321
|
+
if (typeof framework === "string" && framework.length > 0) {
|
|
322
|
+
expectInteractive = true;
|
|
323
|
+
resolvedFramework = framework.toLowerCase();
|
|
324
|
+
} else {
|
|
325
|
+
const descriptor = stack !== undefined ? stack : readStackJson();
|
|
326
|
+
const resolution = resolveInteractivityExpectation(descriptor);
|
|
327
|
+
expectInteractive = resolution.expectInteractive;
|
|
328
|
+
resolvedFramework = resolution.framework;
|
|
329
|
+
}
|
|
330
|
+
if (!expectInteractive) {
|
|
331
|
+
return { hydrated: null, issue: null, timedOut: false, waitedMs: 0 };
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const start = Date.now();
|
|
335
|
+
let state = { controlCount: 0, frameworkAttached: null };
|
|
336
|
+
let timedOut = false;
|
|
337
|
+
for (;;) {
|
|
338
|
+
try {
|
|
339
|
+
state = await collectHydrationState(frame, {
|
|
340
|
+
framework: resolvedFramework,
|
|
341
|
+
});
|
|
342
|
+
} catch (_) {
|
|
343
|
+
// Detached frame mid-navigation (or any probe throw): treat as "cannot
|
|
344
|
+
// determine" and pass — a probe failure must never fail a capture.
|
|
345
|
+
state = { controlCount: 0, frameworkAttached: null };
|
|
346
|
+
break;
|
|
347
|
+
}
|
|
348
|
+
// `true` (attached) and `null` (cannot judge) are both terminal — the
|
|
349
|
+
// conservative "can't prove dead" signal the probe already trusts. Only a
|
|
350
|
+
// proven `false` keeps us waiting.
|
|
351
|
+
if (state.frameworkAttached !== false) break;
|
|
352
|
+
// Stop before a sleep that would overrun the cap, so the wait stays bounded.
|
|
353
|
+
if (Date.now() - start + pollIntervalMs >= timeoutMs) {
|
|
354
|
+
timedOut = true;
|
|
355
|
+
break;
|
|
356
|
+
}
|
|
357
|
+
await sleep(pollIntervalMs);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const issue = interpretHydration({
|
|
361
|
+
expectInteractive: true,
|
|
362
|
+
controlCount: state.controlCount,
|
|
363
|
+
frameworkAttached: state.frameworkAttached,
|
|
364
|
+
framework: resolvedFramework,
|
|
365
|
+
url,
|
|
366
|
+
});
|
|
367
|
+
return {
|
|
368
|
+
hydrated: state.frameworkAttached,
|
|
369
|
+
issue,
|
|
370
|
+
timedOut,
|
|
371
|
+
waitedMs: Date.now() - start,
|
|
372
|
+
};
|
|
235
373
|
}
|
|
236
374
|
|
|
237
375
|
module.exports = {
|
|
@@ -241,5 +379,6 @@ module.exports = {
|
|
|
241
379
|
resolveInteractivityExpectation,
|
|
242
380
|
collectHydrationState,
|
|
243
381
|
interpretHydration,
|
|
244
|
-
|
|
382
|
+
probeHydrationState,
|
|
383
|
+
waitForHydration,
|
|
245
384
|
};
|
package/npm/scenario-issues.js
CHANGED
|
@@ -17,7 +17,14 @@ function pushIssue(issues, issue) {
|
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
function buildResult({
|
|
20
|
+
function buildResult({
|
|
21
|
+
loaded,
|
|
22
|
+
hasContent,
|
|
23
|
+
issues,
|
|
24
|
+
outputPath,
|
|
25
|
+
url,
|
|
26
|
+
unmockedRoutes = [],
|
|
27
|
+
}) {
|
|
21
28
|
return {
|
|
22
29
|
ok: loaded && hasContent && issues.length === 0,
|
|
23
30
|
loaded,
|
|
@@ -25,6 +32,11 @@ function buildResult({ loaded, hasContent, issues, outputPath, url }) {
|
|
|
25
32
|
url,
|
|
26
33
|
outputPath: outputPath ?? null,
|
|
27
34
|
issues,
|
|
35
|
+
// Diagnostic-only: same-origin 4xx routes with no scenario mock. Does NOT
|
|
36
|
+
// affect `ok` — the paired console error already fails the capture; this is
|
|
37
|
+
// the actionable route list the failure message and `stub-unmocked-routes`
|
|
38
|
+
// consume. Defaults to `[]` so callers that omit it are unchanged.
|
|
39
|
+
unmockedRoutes,
|
|
28
40
|
};
|
|
29
41
|
}
|
|
30
42
|
|
package/npm/scenario-mocks.js
CHANGED
|
@@ -1,45 +1,124 @@
|
|
|
1
|
-
|
|
1
|
+
// Route matcher shared, semantically, with the injected live-preview shim
|
|
2
|
+
// (crates/proxy-http/src/fetch_patch.js::matchUrl / matchPath / matchQuerySpec).
|
|
3
|
+
// This is a VERBATIM port of that shim's matcher — the two interception paths
|
|
4
|
+
// MUST agree on which requests a mock route covers. When they drifted (the
|
|
5
|
+
// shim gained `:param` + query-spec parity but this capture-harness matcher was
|
|
6
|
+
// left doing exact string matching), a parameterized or query-spec'd mock
|
|
7
|
+
// rendered in the Live Preview but escaped Playwright's `page.route` during
|
|
8
|
+
// capture and rendered an empty list. `scenario-mocks.test.js` pins the two to
|
|
9
|
+
// identical verdicts on a shared fixture table so a future divergence is a
|
|
10
|
+
// test failure, not another lost build session.
|
|
11
|
+
//
|
|
12
|
+
// `:param` path segments match any single non-empty segment; a `?k=v` / `?k=*`
|
|
13
|
+
// query-spec suffix constrains the query as a subset match (`k=*` matches any
|
|
14
|
+
// value; an empty spec matches any query). Full-URL patterns (`http://…`) match
|
|
15
|
+
// by prefix on the raw URL. Mirrors crates/mock-engine's server matcher.
|
|
16
|
+
function matchUrl(pattern, url) {
|
|
17
|
+
// Full-URL patterns (external APIs) match by prefix on the raw URL.
|
|
18
|
+
if (pattern.startsWith("http://") || pattern.startsWith("https://")) {
|
|
19
|
+
return url.indexOf(pattern) === 0;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let reqPath = url;
|
|
23
|
+
let reqQuery = "";
|
|
2
24
|
try {
|
|
3
|
-
const
|
|
4
|
-
|
|
25
|
+
const u = new URL(url);
|
|
26
|
+
reqPath = u.pathname;
|
|
27
|
+
reqQuery = u.search.replace(/^\?/, "");
|
|
5
28
|
} catch {
|
|
6
|
-
|
|
29
|
+
const qi = url.indexOf("?");
|
|
30
|
+
if (qi >= 0) {
|
|
31
|
+
reqPath = url.slice(0, qi);
|
|
32
|
+
reqQuery = url.slice(qi + 1);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const patParts = pattern.split("?");
|
|
37
|
+
if (!matchPath(patParts[0], reqPath)) return false;
|
|
38
|
+
return matchQuerySpec(patParts.length > 1 ? patParts[1] : "", reqQuery);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Segment-wise path match with `:param` wildcards. A `:name` segment matches
|
|
42
|
+
// any single non-empty segment; every other segment must be equal, and the
|
|
43
|
+
// segment counts must match — no implicit prefix match, same as the server.
|
|
44
|
+
function matchPath(pattern, path) {
|
|
45
|
+
const pp = pattern.split("/");
|
|
46
|
+
const rp = path.split("/");
|
|
47
|
+
if (pp.length !== rp.length) return false;
|
|
48
|
+
for (let i = 0; i < pp.length; i++) {
|
|
49
|
+
if (pp[i].charAt(0) === ":") {
|
|
50
|
+
if (rp[i] === "") return false;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (pp[i] !== rp[i]) return false;
|
|
7
54
|
}
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Query-spec match. An empty spec matches any query (a path-only route still
|
|
59
|
+
// matches a request that carries query params). Otherwise every `k=v` pair in
|
|
60
|
+
// the spec must be present in the request query; `k=*` matches any value.
|
|
61
|
+
function matchQuerySpec(spec, query) {
|
|
62
|
+
if (!spec) return true;
|
|
63
|
+
const want = new URLSearchParams(spec);
|
|
64
|
+
const have = new URLSearchParams(query);
|
|
65
|
+
let ok = true;
|
|
66
|
+
want.forEach((v, k) => {
|
|
67
|
+
const got = have.get(k);
|
|
68
|
+
if (got === null) {
|
|
69
|
+
ok = false;
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (v !== "*" && got !== v) ok = false;
|
|
73
|
+
});
|
|
74
|
+
return ok;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Split a mock key (`"<METHOD> <route>"`, e.g. `"GET /api/plans"`) into its
|
|
78
|
+
// method and route pattern. Returns null for a malformed key with no separator.
|
|
79
|
+
function splitMockKey(key) {
|
|
80
|
+
const spaceIdx = key.indexOf(" ");
|
|
81
|
+
if (spaceIdx === -1) return null;
|
|
82
|
+
return { method: key.slice(0, spaceIdx), route: key.slice(spaceIdx + 1) };
|
|
8
83
|
}
|
|
9
84
|
|
|
10
85
|
function findHttpMock(httpMocks, request) {
|
|
11
86
|
const method = request.method().toUpperCase();
|
|
12
|
-
const
|
|
13
|
-
for (const
|
|
14
|
-
const
|
|
15
|
-
if (
|
|
16
|
-
|
|
17
|
-
|
|
87
|
+
const url = request.url();
|
|
88
|
+
for (const [key, mock] of Object.entries(httpMocks)) {
|
|
89
|
+
const parsed = splitMockKey(key);
|
|
90
|
+
if (!parsed) continue;
|
|
91
|
+
// Method stays an exact check; only the route is matched by pattern.
|
|
92
|
+
if (parsed.method.toUpperCase() !== method) continue;
|
|
93
|
+
if (matchUrl(parsed.route, url)) return mock;
|
|
18
94
|
}
|
|
19
95
|
return null;
|
|
20
96
|
}
|
|
21
97
|
|
|
22
|
-
// The set of path/URL
|
|
23
|
-
// `"<METHOD> <
|
|
98
|
+
// The set of path/URL route patterns declared by the mock keys. A key is
|
|
99
|
+
// `"<METHOD> <route>"` (e.g. `"GET /api/plans"`); we strip the method so the
|
|
24
100
|
// route matcher can decide whether a request *could* match any mock without
|
|
25
101
|
// knowing the method (the handler re-checks method via findHttpMock).
|
|
26
102
|
function mockedTargets(httpMocks) {
|
|
27
103
|
const targets = new Set();
|
|
28
104
|
for (const key of Object.keys(httpMocks)) {
|
|
29
|
-
const
|
|
30
|
-
if (
|
|
31
|
-
targets.add(
|
|
105
|
+
const parsed = splitMockKey(key);
|
|
106
|
+
if (!parsed) continue;
|
|
107
|
+
targets.add(parsed.route);
|
|
32
108
|
}
|
|
33
109
|
return targets;
|
|
34
110
|
}
|
|
35
111
|
|
|
36
|
-
// True when a request URL matches one of the declared mock
|
|
37
|
-
//
|
|
112
|
+
// True when a request URL matches one of the declared mock route patterns under
|
|
113
|
+
// the shared route matcher (`:param` + query-spec parity), not exact string
|
|
114
|
+
// equality. Accepts either a string or a WHATWG URL (Playwright's URL-matcher
|
|
115
|
+
// passes a URL).
|
|
38
116
|
function requestTargetsMock(targets, url) {
|
|
39
117
|
const href = typeof url === "string" ? url : url.href;
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
118
|
+
for (const target of targets) {
|
|
119
|
+
if (matchUrl(target, href)) return true;
|
|
120
|
+
}
|
|
121
|
+
return false;
|
|
43
122
|
}
|
|
44
123
|
|
|
45
124
|
async function attachHttpMocks(page, httpMocks) {
|
|
@@ -106,22 +185,46 @@ async function attachHttpMocks(page, httpMocks) {
|
|
|
106
185
|
// produces. Console errors carry no HTTP method, so any method's mock on a
|
|
107
186
|
// matching target counts.
|
|
108
187
|
function isDeclaredErrorMock(httpMocks, url) {
|
|
109
|
-
const candidates = normalizeMockCandidates(url);
|
|
110
188
|
for (const [key, mock] of Object.entries(httpMocks || {})) {
|
|
111
|
-
const
|
|
112
|
-
if (
|
|
113
|
-
|
|
114
|
-
|
|
189
|
+
const parsed = splitMockKey(key);
|
|
190
|
+
if (!parsed) continue;
|
|
191
|
+
// Method-agnostic on purpose: console errors carry no HTTP method, so any
|
|
192
|
+
// method's error mock on a matching route counts. Route matching uses the
|
|
193
|
+
// shared matcher so a `:param`/query-spec error mock is recognised too.
|
|
194
|
+
if (!matchUrl(parsed.route, url)) continue;
|
|
115
195
|
if ((mock.status || 200) >= 400) return true;
|
|
116
196
|
}
|
|
117
197
|
return false;
|
|
118
198
|
}
|
|
119
199
|
|
|
200
|
+
// Given a same-origin response's method/url/status and the scenario's declared
|
|
201
|
+
// mocks, return the `{method, path, status}` to record as an unmocked route, or
|
|
202
|
+
// `null` if it should be ignored: a sub-4xx response, or one already covered by
|
|
203
|
+
// a declared error mock (status >= 400, whose 4xx is the mock's intended
|
|
204
|
+
// behavior). The caller applies the same-origin and `expectedConsoleErrors`
|
|
205
|
+
// gates before calling. `path` is the URL pathname — the route half of the
|
|
206
|
+
// `"<METHOD> <path>"` stub-mock key; a malformed URL falls back to the raw
|
|
207
|
+
// string so the key is still well-formed.
|
|
208
|
+
function unmockedRouteFrom(method, url, status, httpMocks) {
|
|
209
|
+
if (status < 400) return null;
|
|
210
|
+
if (isDeclaredErrorMock(httpMocks, url)) return null;
|
|
211
|
+
let path = url;
|
|
212
|
+
try {
|
|
213
|
+
path = new URL(url).pathname;
|
|
214
|
+
} catch {
|
|
215
|
+
/* malformed URL — fall back to the raw string for the stub key */
|
|
216
|
+
}
|
|
217
|
+
return { method: String(method).toUpperCase(), path, status };
|
|
218
|
+
}
|
|
219
|
+
|
|
120
220
|
module.exports = {
|
|
121
|
-
|
|
221
|
+
matchUrl,
|
|
222
|
+
matchPath,
|
|
223
|
+
matchQuerySpec,
|
|
122
224
|
findHttpMock,
|
|
123
225
|
mockedTargets,
|
|
124
226
|
requestTargetsMock,
|
|
125
227
|
attachHttpMocks,
|
|
126
228
|
isDeclaredErrorMock,
|
|
229
|
+
unmockedRouteFrom,
|
|
127
230
|
};
|