@codeyam-editor/codeyam-editor 0.1.8 → 0.1.10
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 +377 -21
- package/npm/editor-dev.js +24 -2
- package/npm/editor.js +15 -5
- package/npm/fleet-add-failure.js +355 -21
- package/npm/fleet-auth-account.js +61 -0
- package/npm/fleet-auth.js +69 -1
- 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-public-url.js +81 -12
- package/npm/fleet-reconcile.js +46 -11
- 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-readiness.js +57 -16
- package/npm/group-bounded-spawn.js +111 -0
- package/npm/launch-config.js +216 -23
- package/npm/orphan-ssh-reaper.js +81 -0
- package/npm/scenario-check.js +697 -34
- package/npm/scenario-interactivity.js +477 -23
- package/npm/scenario-issues.js +61 -0
- package/npm/scenario-playwright.js +174 -20
- package/npm/utils.js +262 -22
- package/package.json +5 -5
- package/ui/dist/assets/{ContainerInspector-DSVZvEkB.js → ContainerInspector-DiN91foX.js} +1 -1
- package/ui/dist/assets/EditorShell-BEIdHnHd.css +1 -0
- package/ui/dist/assets/EditorShell-CPV5vhjo.js +330 -0
- package/ui/dist/assets/ScenarioDataPanel-DHAHyraq.js +18 -0
- package/ui/dist/assets/{ScenarioDataPanelFullPage-Cyk6efiI.js → ScenarioDataPanelFullPage-CHIWip3n.js} +1 -1
- package/ui/dist/assets/ScenarioRenderer-D9a91iwS.js +21 -0
- package/ui/dist/assets/index--cE0hUJw.css +1 -0
- package/ui/dist/assets/{index-BPLKgwon.js → index-sW8wcg7v.js} +9 -9
- 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-DnXdHrar.js +0 -256
- package/ui/dist/assets/EditorShell-FvbXyqqD.css +0 -1
- package/ui/dist/assets/ScenarioDataPanel-6qJE-0lA.js +0 -15
- package/ui/dist/assets/ScenarioRenderer-Uyt54vHI.js +0 -21
- package/ui/dist/assets/index-DKpgJLo3.css +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,
|
|
@@ -26,7 +75,13 @@ function buildResult({
|
|
|
26
75
|
unmockedRoutes = [],
|
|
27
76
|
mockUsage = { used: [], unused: [] },
|
|
28
77
|
externalRequests = [],
|
|
78
|
+
viewerOrigin = null,
|
|
29
79
|
}) {
|
|
80
|
+
const { captureOrigin, captureOriginDiffersFromViewer } = deriveOriginSplit(
|
|
81
|
+
url,
|
|
82
|
+
viewerOrigin,
|
|
83
|
+
);
|
|
84
|
+
|
|
30
85
|
return {
|
|
31
86
|
ok: loaded && hasContent && issues.length === 0,
|
|
32
87
|
loaded,
|
|
@@ -48,6 +103,11 @@ function buildResult({
|
|
|
48
103
|
// Requests grouped by origin, split mocked/unmocked and same/cross-origin.
|
|
49
104
|
// The line that ends a "why is this page blank" misdiagnosis.
|
|
50
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,
|
|
51
111
|
};
|
|
52
112
|
}
|
|
53
113
|
|
|
@@ -55,4 +115,5 @@ module.exports = {
|
|
|
55
115
|
createIssue,
|
|
56
116
|
pushIssue,
|
|
57
117
|
buildResult,
|
|
118
|
+
deriveOriginSplit,
|
|
58
119
|
};
|
|
@@ -345,9 +345,18 @@ async function collectVisibleTextLength(target) {
|
|
|
345
345
|
// style id) and best-effort. Returns true when the style is present after the
|
|
346
346
|
// call.
|
|
347
347
|
//
|
|
348
|
-
// The reveal is
|
|
349
|
-
//
|
|
350
|
-
//
|
|
348
|
+
// The reveal is conditional on ANIMATION EVIDENCE, not on transparency alone:
|
|
349
|
+
// an element is revealed only when, before the reset lands, an animation or a
|
|
350
|
+
// transition was actually holding it back. An element resting at `opacity: 0`
|
|
351
|
+
// with neither is hidden BY INTENT — a closed dropdown, a hover-only action, an
|
|
352
|
+
// empty toast slot, an off-screen drawer — and is left exactly as the app
|
|
353
|
+
// rendered it. Elements carrying an explicit hidden-intent signal
|
|
354
|
+
// (`aria-hidden="true"`, `inert`, or `pointer-events: none`), on themselves or
|
|
355
|
+
// on an ancestor, are skipped even when a transition is declared — that is the
|
|
356
|
+
// resting shape of a closed popover which animates on open.
|
|
357
|
+
//
|
|
358
|
+
// It is never a blanket `opacity: 1 !important` / `transform: none !important`
|
|
359
|
+
// over `*`. A blanket rule cannot tell an entrance animation's `opacity: 0` from
|
|
351
360
|
// DELIBERATE, resting state — a disabled control's dim, a muted row, a
|
|
352
361
|
// collapsed chevron's rotation — so it silently flattened every one of them out
|
|
353
362
|
// of every screenshot the capture pipeline produced. Two scenarios differing
|
|
@@ -385,14 +394,104 @@ async function forceFinalVisualState(target) {
|
|
|
385
394
|
"}";
|
|
386
395
|
const head = document.head || document.documentElement;
|
|
387
396
|
if (!head || typeof head.appendChild !== "function") return false;
|
|
397
|
+
|
|
398
|
+
// Which elements is an animation or a transition ACTUALLY holding back?
|
|
399
|
+
// Snapshot that BEFORE the stylesheet lands, because injecting it destroys
|
|
400
|
+
// the evidence: `animation: none` / `transition: none` clears
|
|
401
|
+
// `animationName`, empties the running-animation list, and zeroes every
|
|
402
|
+
// duration. Only elements in this set are eligible for the reveal below.
|
|
403
|
+
const isAnimationHeld = (el) => {
|
|
404
|
+
let animations = null;
|
|
405
|
+
if (typeof el.getAnimations === "function") {
|
|
406
|
+
try {
|
|
407
|
+
animations = el.getAnimations();
|
|
408
|
+
} catch (_) {
|
|
409
|
+
animations = null;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
if (animations && animations.length > 0) return true;
|
|
413
|
+
// No `getAnimations` (older engines) falls through to the computed-style
|
|
414
|
+
// evidence below — never to "reveal every invisible element".
|
|
415
|
+
let computed = null;
|
|
416
|
+
try {
|
|
417
|
+
computed = getComputedStyle(el);
|
|
418
|
+
} catch (_) {
|
|
419
|
+
return false;
|
|
420
|
+
}
|
|
421
|
+
if (!computed) return false;
|
|
422
|
+
const animationName = computed.animationName;
|
|
423
|
+
if (
|
|
424
|
+
typeof animationName === "string" &&
|
|
425
|
+
animationName !== "" &&
|
|
426
|
+
animationName !== "none"
|
|
427
|
+
) {
|
|
428
|
+
return true;
|
|
429
|
+
}
|
|
430
|
+
const properties = computed.transitionProperty;
|
|
431
|
+
if (
|
|
432
|
+
typeof properties !== "string" ||
|
|
433
|
+
properties === "" ||
|
|
434
|
+
properties === "none"
|
|
435
|
+
) {
|
|
436
|
+
return false;
|
|
437
|
+
}
|
|
438
|
+
const coversVisibility = properties
|
|
439
|
+
.split(",")
|
|
440
|
+
.map((property) => property.trim())
|
|
441
|
+
.some(
|
|
442
|
+
(property) =>
|
|
443
|
+
property === "opacity" ||
|
|
444
|
+
property === "transform" ||
|
|
445
|
+
property === "all",
|
|
446
|
+
);
|
|
447
|
+
if (!coversVisibility) return false;
|
|
448
|
+
const durations = computed.transitionDuration;
|
|
449
|
+
if (typeof durations !== "string") return false;
|
|
450
|
+
// A declared transition with a zero duration animates nothing, so it is
|
|
451
|
+
// not holding anything back.
|
|
452
|
+
return durations.split(",").some((duration) => parseFloat(duration) > 0);
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
// Explicit "this is hidden on purpose" signals, which outrank animation
|
|
456
|
+
// evidence. A closed popover carries its open-transition while resting
|
|
457
|
+
// closed, so a transition alone cannot distinguish it from an entrance the
|
|
458
|
+
// capture should finish. An ancestor counts: hiding a whole subtree is the
|
|
459
|
+
// common spelling.
|
|
460
|
+
const hasHiddenIntent = (el, computed) => {
|
|
461
|
+
if (computed && computed.pointerEvents === "none") return true;
|
|
462
|
+
if (typeof el.closest === "function") {
|
|
463
|
+
try {
|
|
464
|
+
if (el.closest('[aria-hidden="true"], [inert]')) return true;
|
|
465
|
+
} catch (_) {
|
|
466
|
+
// Fall through to the self-only checks below.
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
if (el.inert === true) return true;
|
|
470
|
+
if (
|
|
471
|
+
typeof el.getAttribute === "function" &&
|
|
472
|
+
el.getAttribute("aria-hidden") === "true"
|
|
473
|
+
) {
|
|
474
|
+
return true;
|
|
475
|
+
}
|
|
476
|
+
return false;
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
const heldByAnimation = new Set();
|
|
480
|
+
if (typeof document.querySelectorAll === "function") {
|
|
481
|
+
for (const el of document.querySelectorAll("*")) {
|
|
482
|
+
if (isAnimationHeld(el)) heldByAnimation.add(el);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
388
486
|
head.appendChild(style);
|
|
389
487
|
|
|
390
488
|
// With animations disabled above, an element held back by an entrance
|
|
391
489
|
// animation now computes to its pre-animation resting state — typically
|
|
392
490
|
// `opacity: 0`, often paired with a translate/scale that parks it offscreen.
|
|
393
|
-
// Reveal exactly those, and only those:
|
|
394
|
-
//
|
|
395
|
-
//
|
|
491
|
+
// Reveal exactly those, and only those: membership in the pre-reset
|
|
492
|
+
// `heldByAnimation` snapshot is what separates them from UI the app is
|
|
493
|
+
// deliberately holding at `opacity: 0`, which stays hidden. An element at
|
|
494
|
+
// any visible opacity is left untouched either way.
|
|
396
495
|
//
|
|
397
496
|
// The transform half of the reveal STOPS at the SVG boundary. A CSS
|
|
398
497
|
// `transform` overrides the SVG `transform` presentation attribute, so
|
|
@@ -417,6 +516,8 @@ async function forceFinalVisualState(target) {
|
|
|
417
516
|
if (!computed) continue;
|
|
418
517
|
const opacity = parseFloat(computed.opacity);
|
|
419
518
|
if (!Number.isFinite(opacity) || opacity > INVISIBLE_EPSILON) continue;
|
|
519
|
+
if (!heldByAnimation.has(el)) continue;
|
|
520
|
+
if (hasHiddenIntent(el, computed)) continue;
|
|
420
521
|
if (!el.style || typeof el.style.setProperty !== "function") continue;
|
|
421
522
|
const inSvg =
|
|
422
523
|
el.ownerSVGElement != null ||
|
|
@@ -928,6 +1029,59 @@ async function collectInteractiveLabels(frame) {
|
|
|
928
1029
|
}, INTERACTIVE_SELECTOR);
|
|
929
1030
|
}
|
|
930
1031
|
|
|
1032
|
+
// How many candidate labels an error message may echo. Deliberately tighter
|
|
1033
|
+
// than `collectInteractiveLabels`' own 20-label read cap: the labels are the
|
|
1034
|
+
// USER'S page content, so every one printed is a line of their app leaked into
|
|
1035
|
+
// the terminal. Enough to answer "did you mean one of these?", not enough to
|
|
1036
|
+
// transcribe the page.
|
|
1037
|
+
const CANDIDATE_LABEL_CAP = 8;
|
|
1038
|
+
|
|
1039
|
+
// Render the candidate labels for an error message, capped and counted. Shared
|
|
1040
|
+
// by the no-match and action-failed paths so the echo policy lives in one place.
|
|
1041
|
+
async function candidateLabelSummary(frame) {
|
|
1042
|
+
const candidates = await collectInteractiveLabels(frame);
|
|
1043
|
+
if (candidates.length === 0) return "(none found on page)";
|
|
1044
|
+
const shown = candidates.slice(0, CANDIDATE_LABEL_CAP);
|
|
1045
|
+
const rest = candidates.length - shown.length;
|
|
1046
|
+
return rest > 0 ? `${shown.join(", ")} (+${rest} more)` : shown.join(", ");
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
// Compose the "no element matched" error, branched on the form the caller
|
|
1050
|
+
// ACTUALLY passed. Pure and string-returning so both branches are asserted
|
|
1051
|
+
// without a browser.
|
|
1052
|
+
//
|
|
1053
|
+
// The substring advice ("prefer an exact CSS selector") only applies to a text
|
|
1054
|
+
// label. Printing it at a caller who already passed `#id` contradicts its own
|
|
1055
|
+
// input and reads as misdirection — the failure mode that sent two sessions
|
|
1056
|
+
// hunting a selector bug when the page had simply not hydrated. The candidate
|
|
1057
|
+
// labels ride with the text branch for the same reason: they answer "did you
|
|
1058
|
+
// mean one of these?", a question only a text match can have asked, and every
|
|
1059
|
+
// label printed is a line of the user's own page echoed into the terminal.
|
|
1060
|
+
function noMatchGuidance({ targetDesc, matchedByText, candidateLabels }) {
|
|
1061
|
+
const queryParamHint =
|
|
1062
|
+
`Many filter/status/sort states are also reachable directly via a ` +
|
|
1063
|
+
`URL query param (e.g. add "?status=active" to the path) with no ` +
|
|
1064
|
+
`interaction at all.`;
|
|
1065
|
+
if (matchedByText) {
|
|
1066
|
+
return (
|
|
1067
|
+
`preview-interact: no element matched ${targetDesc}. ` +
|
|
1068
|
+
`Text is matched as a case-insensitive SUBSTRING, so a misspelled or ` +
|
|
1069
|
+
`over-specific label matches nothing — prefer an exact/role/testid CSS ` +
|
|
1070
|
+
`selector (e.g. {"selector":"[data-testid=\\"save\\"]"}) for a precise ` +
|
|
1071
|
+
`target. ${queryParamHint} ` +
|
|
1072
|
+
`Candidate interactive labels: ${candidateLabels}`
|
|
1073
|
+
);
|
|
1074
|
+
}
|
|
1075
|
+
return (
|
|
1076
|
+
`preview-interact: no element matched ${targetDesc}. ` +
|
|
1077
|
+
`That is already an exact CSS selector, so nothing on the page matches ` +
|
|
1078
|
+
`it as written — confirm the element is present in THIS state (it may be ` +
|
|
1079
|
+
`behind a tab, dialog, or conditional render), that the id/attribute is ` +
|
|
1080
|
+
`spelled as the markup emits it, and that the page rendered and hydrated ` +
|
|
1081
|
+
`at all. ${queryParamHint}`
|
|
1082
|
+
);
|
|
1083
|
+
}
|
|
1084
|
+
|
|
931
1085
|
// Describe the elements a substring text match resolved, so an ambiguity
|
|
932
1086
|
// warning can name the competing controls (e.g. the preset button "Bet" vs the
|
|
933
1087
|
// disclosure button "…or bet on"). Reads each matched element's inner text via
|
|
@@ -1075,17 +1229,17 @@ async function performInteraction(
|
|
|
1075
1229
|
}
|
|
1076
1230
|
|
|
1077
1231
|
if (matchCount === 0) {
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1232
|
+
// Candidate labels are collected ONLY for the text branch — see
|
|
1233
|
+
// `noMatchGuidance` for why a CSS-selector miss neither needs them nor
|
|
1234
|
+
// should echo the user's page content back at them.
|
|
1081
1235
|
throw new Error(
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1236
|
+
noMatchGuidance({
|
|
1237
|
+
targetDesc,
|
|
1238
|
+
matchedByText,
|
|
1239
|
+
candidateLabels: matchedByText
|
|
1240
|
+
? await candidateLabelSummary(frame)
|
|
1241
|
+
: null,
|
|
1242
|
+
}),
|
|
1089
1243
|
);
|
|
1090
1244
|
}
|
|
1091
1245
|
|
|
@@ -1127,12 +1281,9 @@ async function performInteraction(
|
|
|
1127
1281
|
);
|
|
1128
1282
|
}
|
|
1129
1283
|
} catch (error) {
|
|
1130
|
-
const candidates = await collectInteractiveLabels(frame);
|
|
1131
|
-
const candidateList =
|
|
1132
|
-
candidates.length > 0 ? candidates.join(", ") : "(none found on page)";
|
|
1133
1284
|
throw new Error(
|
|
1134
1285
|
`preview-interact: action "${action}" failed against ${targetDesc}: ${error.message || String(error)}. ` +
|
|
1135
|
-
`Candidate interactive labels: ${
|
|
1286
|
+
`Candidate interactive labels: ${await candidateLabelSummary(frame)}`,
|
|
1136
1287
|
);
|
|
1137
1288
|
}
|
|
1138
1289
|
}
|
|
@@ -1209,6 +1360,7 @@ async function performInteractionSequence(
|
|
|
1209
1360
|
|
|
1210
1361
|
module.exports = {
|
|
1211
1362
|
logCaptureTiming,
|
|
1363
|
+
defaultReadServerState,
|
|
1212
1364
|
resolveTcpTarget,
|
|
1213
1365
|
assertAppPortReachable,
|
|
1214
1366
|
escapeHtmlAttribute,
|
|
@@ -1230,6 +1382,8 @@ module.exports = {
|
|
|
1230
1382
|
loadScenarioInIframe,
|
|
1231
1383
|
loadScenarioTopLevel,
|
|
1232
1384
|
collectInteractiveLabels,
|
|
1385
|
+
candidateLabelSummary,
|
|
1386
|
+
noMatchGuidance,
|
|
1233
1387
|
describeMatchCandidates,
|
|
1234
1388
|
performInteraction,
|
|
1235
1389
|
waitForPredicate,
|