@codeyam-editor/codeyam-editor 0.1.7 → 0.1.9
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/cloud.js +477 -41
- package/npm/editor-dev.js +24 -2
- package/npm/editor.js +15 -5
- package/npm/fleet-add-failure.js +406 -25
- package/npm/fleet-branch.js +124 -1
- package/npm/fleet-build-data.js +12 -1
- package/npm/fleet-cleanup.js +84 -3
- package/npm/fleet-datastore-health.js +107 -0
- package/npm/fleet-memory.js +29 -4
- package/npm/fleet-queue.js +141 -0
- package/npm/fleet-revision.js +108 -0
- package/npm/fleet-run-setup.js +66 -0
- package/npm/fleet-session-activity.js +162 -0
- package/npm/fleet-vm-access.js +162 -0
- package/npm/fleet-vm-readiness.js +57 -16
- package/npm/ingress-invoke.js +46 -2
- package/npm/launch-config.js +169 -2
- package/npm/scenario-check.js +708 -35
- package/npm/scenario-handlers.js +18 -0
- package/npm/scenario-interactivity.js +477 -23
- package/npm/scenario-issues.js +72 -0
- package/npm/scenario-mocks.js +245 -39
- package/npm/scenario-playwright.js +137 -26
- package/npm/utils.js +262 -22
- package/package.json +5 -5
- package/ui/dist/assets/{ContainerInspector-Cu1tSN-o.js → ContainerInspector-C7I9wctz.js} +1 -1
- package/ui/dist/assets/EditorShell-9aYMeOdZ.js +330 -0
- package/ui/dist/assets/EditorShell-CE8iG_EM.css +1 -0
- package/ui/dist/assets/{ProjectLauncherScreen-Dmm6Z0mv.js → ProjectLauncherScreen-E0NYYiwy.js} +1 -1
- package/ui/dist/assets/ScenarioDataPanel-CLwkrex0.js +18 -0
- package/ui/dist/assets/{ScenarioDataPanelFullPage-BqMxWVKW.js → ScenarioDataPanelFullPage-DT0TZC4M.js} +1 -1
- package/ui/dist/assets/ScenarioRenderer-Bsarhcrl.js +21 -0
- package/ui/dist/assets/index-BRoAenc1.css +1 -0
- package/ui/dist/assets/index-iMR5siiG.js +27 -0
- package/ui/dist/assets/useServerIdentity-CVuHTzjg.js +1 -0
- package/ui/dist/index.html +2 -2
- package/ui/dist/preview-subdir-demo/index.html +62 -0
- package/ui/dist/preview-subdir-demo/sibling.html +48 -0
- package/ui/dist/assets/EditorShell--V2ExYQ6.js +0 -260
- package/ui/dist/assets/EditorShell-DP5UY20G.css +0 -1
- package/ui/dist/assets/ScenarioDataPanel-DjprZq2L.js +0 -9
- package/ui/dist/assets/ScenarioRenderer-DqLVVMYS.js +0 -21
- package/ui/dist/assets/index-CjxWhkSy.css +0 -1
- package/ui/dist/assets/index-VuyqBbGJ.js +0 -27
- package/ui/dist/assets/useServerIdentity-BdXy8TQv.js +0 -1
package/npm/scenario-issues.js
CHANGED
|
@@ -7,6 +7,19 @@ function createIssue(kind, message, extra = {}) {
|
|
|
7
7
|
};
|
|
8
8
|
if (extra.matchedPattern != null) issue.matchedPattern = extra.matchedPattern;
|
|
9
9
|
if (extra.contextSnippet != null) issue.contextSnippet = extra.contextSnippet;
|
|
10
|
+
// The hydration gate's cross-origin ATTRIBUTION, carried as data rather than
|
|
11
|
+
// only as prose. A consumer that must act on the verdict — escalating the
|
|
12
|
+
// preview off the subpath when the counterpart origin hydrates — would
|
|
13
|
+
// otherwise have to regex the English message, which breaks the moment the
|
|
14
|
+
// wording is improved. Same conditional shape as the two fields above so an
|
|
15
|
+
// issue that carries no attribution serializes exactly as it did before.
|
|
16
|
+
if (extra.crossOrigin != null) issue.crossOrigin = extra.crossOrigin;
|
|
17
|
+
if (extra.counterpartUrl != null) issue.counterpartUrl = extra.counterpartUrl;
|
|
18
|
+
// The dev-server-down verdict, carried as data for the same reason as the
|
|
19
|
+
// attribution fields above: a consumer that must distinguish "the app served
|
|
20
|
+
// an error" from "the app was not serving at all" should branch on a field,
|
|
21
|
+
// not on the prose of the message.
|
|
22
|
+
if (extra.devServerDown != null) issue.devServerDown = extra.devServerDown;
|
|
10
23
|
return issue;
|
|
11
24
|
}
|
|
12
25
|
|
|
@@ -17,6 +30,42 @@ function pushIssue(issues, issue) {
|
|
|
17
30
|
}
|
|
18
31
|
}
|
|
19
32
|
|
|
33
|
+
// Derive which origin a capture actually loaded from, and whether that differs
|
|
34
|
+
// from the origin the VIEWER's browser is served.
|
|
35
|
+
//
|
|
36
|
+
// The two can legitimately differ: the capture browser runs where the app is,
|
|
37
|
+
// the viewer may not, and aiming them independently is what makes a capture
|
|
38
|
+
// work on a cloud VM at all. A capture that renders perfectly from the app's
|
|
39
|
+
// own origin while the user's pane sits on `/__codeyam_preview` is a USEFUL
|
|
40
|
+
// result — but only if the difference travels with it instead of having to be
|
|
41
|
+
// inferred from a URL the reader must parse themselves.
|
|
42
|
+
//
|
|
43
|
+
// Both unknowns resolve toward "no split", never toward a guess. A caller that
|
|
44
|
+
// supplies no `viewerOrigin` (every pre-existing call site) and a `url` with no
|
|
45
|
+
// parseable origin both yield `false`, so no existing consumer sees a changed
|
|
46
|
+
// verdict and an unclassifiable capture is never reported as diverging.
|
|
47
|
+
//
|
|
48
|
+
// Deliberately not reusing `isCrossOriginRequest` from scenario-check.js: that
|
|
49
|
+
// module imports THIS one, so depending on it would invert the dependency, and
|
|
50
|
+
// its fail-safe answers a different question (never strip markers) than this
|
|
51
|
+
// one does (never claim a split).
|
|
52
|
+
function deriveOriginSplit(url, viewerOrigin) {
|
|
53
|
+
let captureOrigin = null;
|
|
54
|
+
try {
|
|
55
|
+
captureOrigin = new URL(url).origin;
|
|
56
|
+
} catch (_) {
|
|
57
|
+
/* a malformed or relative capture URL has no origin to report */
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
captureOrigin,
|
|
61
|
+
viewerOrigin: viewerOrigin ?? null,
|
|
62
|
+
captureOriginDiffersFromViewer:
|
|
63
|
+
captureOrigin != null &&
|
|
64
|
+
viewerOrigin != null &&
|
|
65
|
+
captureOrigin !== viewerOrigin,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
20
69
|
function buildResult({
|
|
21
70
|
loaded,
|
|
22
71
|
hasContent,
|
|
@@ -24,7 +73,15 @@ function buildResult({
|
|
|
24
73
|
outputPath,
|
|
25
74
|
url,
|
|
26
75
|
unmockedRoutes = [],
|
|
76
|
+
mockUsage = { used: [], unused: [] },
|
|
77
|
+
externalRequests = [],
|
|
78
|
+
viewerOrigin = null,
|
|
27
79
|
}) {
|
|
80
|
+
const { captureOrigin, captureOriginDiffersFromViewer } = deriveOriginSplit(
|
|
81
|
+
url,
|
|
82
|
+
viewerOrigin,
|
|
83
|
+
);
|
|
84
|
+
|
|
28
85
|
return {
|
|
29
86
|
ok: loaded && hasContent && issues.length === 0,
|
|
30
87
|
loaded,
|
|
@@ -37,6 +94,20 @@ function buildResult({
|
|
|
37
94
|
// the actionable route list the failure message and `stub-unmocked-routes`
|
|
38
95
|
// consume. Defaults to `[]` so callers that omit it are unchanged.
|
|
39
96
|
unmockedRoutes,
|
|
97
|
+
// Diagnostic-only, and deliberately NOT part of `ok`: an unused mock is not
|
|
98
|
+
// automatically an error (a scenario may legitimately declare a mock for a
|
|
99
|
+
// request the page makes only on interaction). Reporting it turns a silent
|
|
100
|
+
// inertness into a visible fact; failing on it would break working scenarios
|
|
101
|
+
// for a heuristic.
|
|
102
|
+
mockUsage,
|
|
103
|
+
// Requests grouped by origin, split mocked/unmocked and same/cross-origin.
|
|
104
|
+
// The line that ends a "why is this page blank" misdiagnosis.
|
|
105
|
+
externalRequests,
|
|
106
|
+
// The two hops, named. See `deriveOriginSplit` for why they can differ and
|
|
107
|
+
// why reporting the difference is not optional.
|
|
108
|
+
captureOrigin,
|
|
109
|
+
viewerOrigin,
|
|
110
|
+
captureOriginDiffersFromViewer,
|
|
40
111
|
};
|
|
41
112
|
}
|
|
42
113
|
|
|
@@ -44,4 +115,5 @@ module.exports = {
|
|
|
44
115
|
createIssue,
|
|
45
116
|
pushIssue,
|
|
46
117
|
buildResult,
|
|
118
|
+
deriveOriginSplit,
|
|
47
119
|
};
|
package/npm/scenario-mocks.js
CHANGED
|
@@ -1,22 +1,46 @@
|
|
|
1
|
-
// Route matcher
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
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.
|
|
1
|
+
// Route matcher for the capture harness — one of THREE implementations of a
|
|
2
|
+
// single semantic. The authority is the Rust engine
|
|
3
|
+
// (crates/mock-engine/src/route_parser.rs), where the contract is written down;
|
|
4
|
+
// this file and the injected live-preview shim
|
|
5
|
+
// (crates/proxy-http/src/fetch_patch.js) are ports of it.
|
|
11
6
|
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
7
|
+
// This is NOT a verbatim copy of the shim, and describing it as one is what let
|
|
8
|
+
// the drift hide: the two JS matchers agreed with EACH OTHER while both
|
|
9
|
+
// disagreed with the server (they prefix-matched full-URL patterns and returned
|
|
10
|
+
// before applying any query spec, where the server anchored). All three are now
|
|
11
|
+
// pinned against one shared fixture table,
|
|
12
|
+
// crates/mock-engine/fixtures/route-match-fixtures.json, asserted with its
|
|
13
|
+
// expected verdicts by both `scenario-mocks.test.js` and the Rust tests — so a
|
|
14
|
+
// change to one implementation the others do not follow is a test failure, and
|
|
15
|
+
// two matchers agreeing on a wrong answer no longer passes.
|
|
16
|
+
//
|
|
17
|
+
// The semantic, in short (see route_parser.rs for the full contract): `:name`
|
|
18
|
+
// and `*` each match one non-empty path segment; a TRAILING `**` matches the
|
|
19
|
+
// remaining path including `/` and including an empty remainder; a `?k=v` /
|
|
20
|
+
// `?k=*` suffix constrains the query as a subset match. A full-URL pattern
|
|
21
|
+
// (`http://…`) with no wildcard matches by PREFIX — the compatibility rule
|
|
22
|
+
// every shipped external mock relies on — while a wildcarded one matches by the
|
|
23
|
+
// wildcard rules.
|
|
16
24
|
function matchUrl(pattern, url) {
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
|
|
25
|
+
// Split the query spec off FIRST — for full-URL and path-only patterns
|
|
26
|
+
// alike. Returning early for full URLs (as this did) meant a spec like
|
|
27
|
+
// `?k=*` on an external mock was silently ignored.
|
|
28
|
+
const pqi = pattern.indexOf("?");
|
|
29
|
+
const patPath = pqi >= 0 ? pattern.slice(0, pqi) : pattern;
|
|
30
|
+
const patSpec = pqi >= 0 ? pattern.slice(pqi + 1) : "";
|
|
31
|
+
|
|
32
|
+
// Full-URL patterns (external APIs) match against the whole URL, with its
|
|
33
|
+
// own query stripped before the compare.
|
|
34
|
+
if (patPath.startsWith("http://") || patPath.startsWith("https://")) {
|
|
35
|
+
const [rawPath, rawQuery] = splitUrlQuery(url);
|
|
36
|
+
// Compatibility rule: a wildcard-free full-URL pattern prefix-matches,
|
|
37
|
+
// which is what every external mock relies on today. A wildcarded one
|
|
38
|
+
// matches by the wildcard rules, with no prefix fallback.
|
|
39
|
+
const urlMatched = hasWildcard(patPath)
|
|
40
|
+
? matchPath(patPath, rawPath)
|
|
41
|
+
: rawPath.indexOf(patPath) === 0;
|
|
42
|
+
if (!urlMatched) return false;
|
|
43
|
+
return matchQuerySpec(patSpec, rawQuery);
|
|
20
44
|
}
|
|
21
45
|
|
|
22
46
|
let reqPath = url;
|
|
@@ -26,35 +50,71 @@ function matchUrl(pattern, url) {
|
|
|
26
50
|
reqPath = u.pathname;
|
|
27
51
|
reqQuery = u.search.replace(/^\?/, "");
|
|
28
52
|
} catch {
|
|
29
|
-
|
|
30
|
-
if (qi >= 0) {
|
|
31
|
-
reqPath = url.slice(0, qi);
|
|
32
|
-
reqQuery = url.slice(qi + 1);
|
|
33
|
-
}
|
|
53
|
+
[reqPath, reqQuery] = splitUrlQuery(url);
|
|
34
54
|
}
|
|
35
55
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
56
|
+
if (!matchPath(patPath, reqPath)) return false;
|
|
57
|
+
return matchQuerySpec(patSpec, reqQuery);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Split a raw URL into [everything-before-the-query, query]. The `#fragment`
|
|
61
|
+
// is dropped from both halves, mirroring the server's strip_url_query.
|
|
62
|
+
function splitUrlQuery(url) {
|
|
63
|
+
const qi = url.indexOf("?");
|
|
64
|
+
const hi = url.indexOf("#");
|
|
65
|
+
const cut = qi < 0 ? hi : hi < 0 ? qi : Math.min(qi, hi);
|
|
66
|
+
const head = cut < 0 ? url : url.slice(0, cut);
|
|
67
|
+
if (qi < 0) return [head, ""];
|
|
68
|
+
const query = url.slice(qi + 1);
|
|
69
|
+
const fi = query.indexOf("#");
|
|
70
|
+
return [head, fi >= 0 ? query.slice(0, fi) : query];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// True when a segment is a valid `:name` param. A `:`-prefixed segment with
|
|
74
|
+
// an empty or punctuated name is matched literally, same as the server.
|
|
75
|
+
function isParamSegment(seg) {
|
|
76
|
+
return /^:[\p{L}\p{N}_]+$/u.test(seg);
|
|
39
77
|
}
|
|
40
78
|
|
|
41
|
-
//
|
|
42
|
-
|
|
43
|
-
|
|
79
|
+
// True when a segment is any wildcard: `:name`, `*`, or `**`.
|
|
80
|
+
function isWildcardSegment(seg) {
|
|
81
|
+
return seg === "*" || seg === "**" || isParamSegment(seg);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// True when the pattern contains any wildcard segment.
|
|
85
|
+
function hasWildcard(pattern) {
|
|
86
|
+
return pattern.split("/").some(isWildcardSegment);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Segment-wise path match. `:name` and `*` each match any single non-empty
|
|
90
|
+
// segment; a TRAILING `**` matches the remaining path including `/` and
|
|
91
|
+
// including an empty remainder (`/api/**` covers `/api`); every other segment
|
|
92
|
+
// must be equal. No implicit prefix match, same as the server.
|
|
44
93
|
function matchPath(pattern, path) {
|
|
45
94
|
const pp = pattern.split("/");
|
|
46
95
|
const rp = path.split("/");
|
|
96
|
+
if (pp[pp.length - 1] === "**") {
|
|
97
|
+
const head = pp.length - 1;
|
|
98
|
+
if (rp.length < head) return false;
|
|
99
|
+
for (let i = 0; i < head; i++) {
|
|
100
|
+
if (!matchSegment(pp[i], rp[i])) return false;
|
|
101
|
+
}
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
47
104
|
if (pp.length !== rp.length) return false;
|
|
48
105
|
for (let i = 0; i < pp.length; i++) {
|
|
49
|
-
if (pp[i]
|
|
50
|
-
if (rp[i] === "") return false;
|
|
51
|
-
continue;
|
|
52
|
-
}
|
|
53
|
-
if (pp[i] !== rp[i]) return false;
|
|
106
|
+
if (!matchSegment(pp[i], rp[i])) return false;
|
|
54
107
|
}
|
|
55
108
|
return true;
|
|
56
109
|
}
|
|
57
110
|
|
|
111
|
+
// One segment. `:name`, `*`, and a non-final `**` each match any single
|
|
112
|
+
// non-empty segment; anything else must be equal.
|
|
113
|
+
function matchSegment(pat, seg) {
|
|
114
|
+
if (isWildcardSegment(pat)) return seg !== "";
|
|
115
|
+
return pat === seg;
|
|
116
|
+
}
|
|
117
|
+
|
|
58
118
|
// Query-spec match. An empty spec matches any query (a path-only route still
|
|
59
119
|
// matches a request that carries query params). Otherwise every `k=v` pair in
|
|
60
120
|
// the spec must be present in the request query; `k=*` matches any value.
|
|
@@ -82,7 +142,11 @@ function splitMockKey(key) {
|
|
|
82
142
|
return { method: key.slice(0, spaceIdx), route: key.slice(spaceIdx + 1) };
|
|
83
143
|
}
|
|
84
144
|
|
|
85
|
-
|
|
145
|
+
// The matching mock together with the KEY that declared it, or null. The key is
|
|
146
|
+
// what the usage tally counts — reporting "which declared mock fired" needs the
|
|
147
|
+
// declaration's identity, not just its body, since two keys can carry
|
|
148
|
+
// structurally identical mocks.
|
|
149
|
+
function findHttpMockEntry(httpMocks, request) {
|
|
86
150
|
const method = request.method().toUpperCase();
|
|
87
151
|
const url = request.url();
|
|
88
152
|
for (const [key, mock] of Object.entries(httpMocks)) {
|
|
@@ -90,11 +154,16 @@ function findHttpMock(httpMocks, request) {
|
|
|
90
154
|
if (!parsed) continue;
|
|
91
155
|
// Method stays an exact check; only the route is matched by pattern.
|
|
92
156
|
if (parsed.method.toUpperCase() !== method) continue;
|
|
93
|
-
if (matchUrl(parsed.route, url)) return mock;
|
|
157
|
+
if (matchUrl(parsed.route, url)) return { key, mock };
|
|
94
158
|
}
|
|
95
159
|
return null;
|
|
96
160
|
}
|
|
97
161
|
|
|
162
|
+
function findHttpMock(httpMocks, request) {
|
|
163
|
+
const entry = findHttpMockEntry(httpMocks, request);
|
|
164
|
+
return entry ? entry.mock : null;
|
|
165
|
+
}
|
|
166
|
+
|
|
98
167
|
// The set of path/URL route patterns declared by the mock keys. A key is
|
|
99
168
|
// `"<METHOD> <route>"` (e.g. `"GET /api/plans"`); we strip the method so the
|
|
100
169
|
// route matcher can decide whether a request *could* match any mock without
|
|
@@ -121,8 +190,134 @@ function requestTargetsMock(targets, url) {
|
|
|
121
190
|
return false;
|
|
122
191
|
}
|
|
123
192
|
|
|
124
|
-
|
|
125
|
-
|
|
193
|
+
// The origin half of a URL, or the raw string when it will not parse. Grouping
|
|
194
|
+
// by origin is what turns twenty tile URLs into one readable line.
|
|
195
|
+
function originOf(url) {
|
|
196
|
+
try {
|
|
197
|
+
return new URL(url).origin;
|
|
198
|
+
} catch {
|
|
199
|
+
return url;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Which declared mock keys actually served a response.
|
|
204
|
+
//
|
|
205
|
+
// This exists because a declared mock whose key never matches any request is
|
|
206
|
+
// silently inert: the capture still reports success, and the frame that lands
|
|
207
|
+
// is the healthy state under a name promising a degraded one. The declared keys
|
|
208
|
+
// were always known; nothing ever compared them against what fired.
|
|
209
|
+
function createMockUsageTally(httpMocks) {
|
|
210
|
+
const declared = Object.keys(httpMocks || {});
|
|
211
|
+
const usedKeys = new Set();
|
|
212
|
+
return {
|
|
213
|
+
recordFulfilled(key) {
|
|
214
|
+
usedKeys.add(key);
|
|
215
|
+
},
|
|
216
|
+
// Declared order is preserved in both lists so the reported set reads the
|
|
217
|
+
// same way the scenario's mock block does.
|
|
218
|
+
get used() {
|
|
219
|
+
return declared.filter((key) => usedKeys.has(key));
|
|
220
|
+
},
|
|
221
|
+
get unused() {
|
|
222
|
+
return declared.filter((key) => !usedKeys.has(key));
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Every request the page made, collapsed to one row per `(origin, mocked)` pair.
|
|
228
|
+
// A full URL list is noise (twenty tile URLs); origin + count + a representative
|
|
229
|
+
// URL is the line that ends a "why is this page blank" misdiagnosis.
|
|
230
|
+
function createRequestInventory(appOrigin) {
|
|
231
|
+
// Keyed by `origin|mocked` rather than origin alone: an origin serving both a
|
|
232
|
+
// mocked API call and unmocked sub-resources would otherwise collapse into one
|
|
233
|
+
// row whose `mocked` flag is a coin toss.
|
|
234
|
+
const groups = new Map();
|
|
235
|
+
|
|
236
|
+
function groupFor(url, mocked) {
|
|
237
|
+
const origin = originOf(url);
|
|
238
|
+
const groupKey = `${origin}|${mocked ? "mocked" : "unmocked"}`;
|
|
239
|
+
let row = groups.get(groupKey);
|
|
240
|
+
if (!row) {
|
|
241
|
+
row = {
|
|
242
|
+
origin,
|
|
243
|
+
count: 0,
|
|
244
|
+
sampleUrl: url,
|
|
245
|
+
mocked: !!mocked,
|
|
246
|
+
failed: false,
|
|
247
|
+
// `false` when the capture URL did not parse — the caller then has no
|
|
248
|
+
// origin to compare against, and treating everything as cross-origin
|
|
249
|
+
// over-reports rather than hiding a request.
|
|
250
|
+
sameOrigin: !!appOrigin && origin === appOrigin,
|
|
251
|
+
};
|
|
252
|
+
groups.set(groupKey, row);
|
|
253
|
+
}
|
|
254
|
+
return row;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
recordRequest(url, mocked) {
|
|
259
|
+
groupFor(url, mocked).count += 1;
|
|
260
|
+
},
|
|
261
|
+
recordFailure(url, mocked) {
|
|
262
|
+
groupFor(url, mocked).failed = true;
|
|
263
|
+
},
|
|
264
|
+
// Busiest origin first — the one worth explaining leads the report.
|
|
265
|
+
get groups() {
|
|
266
|
+
return [...groups.values()].sort(
|
|
267
|
+
(a, b) => b.count - a.count || a.origin.localeCompare(b.origin),
|
|
268
|
+
);
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Composes the two independent halves above into the single handle the capture
|
|
274
|
+
// harness threads through.
|
|
275
|
+
//
|
|
276
|
+
// Purely observational. Nothing here changes which requests are intercepted:
|
|
277
|
+
// `attachHttpMocks` scopes `page.route` to declared targets so unmocked requests
|
|
278
|
+
// load natively, and that scoping is load-bearing (a blanket catch-all route
|
|
279
|
+
// predicate breaks Vite ESM module loading and blanks the SPA).
|
|
280
|
+
function createMockObserver(httpMocks, appOrigin) {
|
|
281
|
+
const usage = createMockUsageTally(httpMocks);
|
|
282
|
+
const inventory = createRequestInventory(appOrigin);
|
|
283
|
+
return {
|
|
284
|
+
recordFulfilled: (key) => usage.recordFulfilled(key),
|
|
285
|
+
recordRequest: (url, mocked) => inventory.recordRequest(url, mocked),
|
|
286
|
+
recordFailure: (url, mocked) => inventory.recordFailure(url, mocked),
|
|
287
|
+
get used() {
|
|
288
|
+
return usage.used;
|
|
289
|
+
},
|
|
290
|
+
get unused() {
|
|
291
|
+
return usage.unused;
|
|
292
|
+
},
|
|
293
|
+
get externalRequests() {
|
|
294
|
+
return inventory.groups;
|
|
295
|
+
},
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Passive request inventory. `page.on` observes; it never routes, so attaching
|
|
300
|
+
// this cannot change what the page loads.
|
|
301
|
+
function attachRequestInventory(page, observer, httpMocks) {
|
|
302
|
+
const targets = mockedTargets(httpMocks || {});
|
|
303
|
+
page.on("request", (request) => {
|
|
304
|
+
const url = request.url();
|
|
305
|
+
observer.recordRequest(url, requestTargetsMock(targets, url));
|
|
306
|
+
});
|
|
307
|
+
page.on("requestfailed", (request) => {
|
|
308
|
+
const url = request.url();
|
|
309
|
+
observer.recordFailure(url, requestTargetsMock(targets, url));
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Returns the capture's mock observer (see `createMockObserver`). Always returns
|
|
314
|
+
// one, including for a scenario that declares no mocks at all — that is exactly
|
|
315
|
+
// the case where an unexplained third-party fetch is hardest to account for.
|
|
316
|
+
async function attachHttpMocks(page, httpMocks, options = {}) {
|
|
317
|
+
const observer = createMockObserver(httpMocks, options.appOrigin);
|
|
318
|
+
attachRequestInventory(page, observer, httpMocks);
|
|
319
|
+
|
|
320
|
+
if (!httpMocks || Object.keys(httpMocks).length === 0) return observer;
|
|
126
321
|
|
|
127
322
|
// Intercept ONLY requests whose path matches a declared mock — not every
|
|
128
323
|
// request. A blanket `page.route("**/*")` intercepts the dev server's ESM
|
|
@@ -134,11 +329,15 @@ async function attachHttpMocks(page, httpMocks) {
|
|
|
134
329
|
await page.route(
|
|
135
330
|
(url) => requestTargetsMock(targets, url),
|
|
136
331
|
async (route) => {
|
|
137
|
-
const
|
|
138
|
-
if (!
|
|
332
|
+
const entry = findHttpMockEntry(httpMocks, route.request());
|
|
333
|
+
if (!entry) {
|
|
139
334
|
await route.continue();
|
|
140
335
|
return;
|
|
141
336
|
}
|
|
337
|
+
const { key, mock } = entry;
|
|
338
|
+
// Counted at the fulfil branch, not at match time: a key is "used" only
|
|
339
|
+
// when it actually served a response.
|
|
340
|
+
observer.recordFulfilled(key);
|
|
142
341
|
|
|
143
342
|
const headers = { ...(mock.headers || {}) };
|
|
144
343
|
let body;
|
|
@@ -175,6 +374,8 @@ async function attachHttpMocks(page, httpMocks) {
|
|
|
175
374
|
body: "[]",
|
|
176
375
|
});
|
|
177
376
|
});
|
|
377
|
+
|
|
378
|
+
return observer;
|
|
178
379
|
}
|
|
179
380
|
|
|
180
381
|
|
|
@@ -222,8 +423,13 @@ module.exports = {
|
|
|
222
423
|
matchPath,
|
|
223
424
|
matchQuerySpec,
|
|
224
425
|
findHttpMock,
|
|
426
|
+
findHttpMockEntry,
|
|
225
427
|
mockedTargets,
|
|
226
428
|
requestTargetsMock,
|
|
429
|
+
originOf,
|
|
430
|
+
createMockUsageTally,
|
|
431
|
+
createRequestInventory,
|
|
432
|
+
createMockObserver,
|
|
227
433
|
attachHttpMocks,
|
|
228
434
|
isDeclaredErrorMock,
|
|
229
435
|
unmockedRouteFrom,
|