@nodegx/mcp 0.2.6 → 0.2.8
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/README.md +16 -11
- package/dist/backend/cli.js +5801 -2527
- package/dist/backend/folder/start.js +89 -1
- package/dist/backend/vm/provision-app.sh +41 -0
- package/dist/cloud-bundle.cjs +4265 -1666
- package/dist/kit-extract.cjs +4742 -2156
- package/dist/kit-extract.inputs.json +31 -1
- package/dist/node-catalog.json +10 -9
- package/dist/noodl-mcp.cjs +12019 -7425
- package/dist/package.json +1 -1
- package/dist/render-harness/data/node-catalog-enriched.json +1353 -254
- package/dist/render-harness/data/node-catalog.json +10 -9
- package/dist/render-harness/drive-steps.js +271 -40
- package/dist/render-harness/measure-from-disk.js +174 -22
- package/dist/src/external/viewer/noodl.viewer.js +1 -1
- package/package.json +1 -1
|
@@ -47268,7 +47268,7 @@
|
|
|
47268
47268
|
"always: the size it holds now, a bare number read in the unit it held; drawn at the next paint"
|
|
47269
47269
|
],
|
|
47270
47270
|
"visible": [
|
|
47271
|
-
"always: shown or hidden at
|
|
47271
|
+
"always: shown or hidden at once, its space kept; hidden, a Focus does not reach it"
|
|
47272
47272
|
]
|
|
47273
47273
|
},
|
|
47274
47274
|
"warnings": {
|
|
@@ -49453,7 +49453,7 @@
|
|
|
49453
49453
|
"always: the page, or the scrolling Group holding it, scrolls until the box can be seen"
|
|
49454
49454
|
],
|
|
49455
49455
|
"visible": [
|
|
49456
|
-
"always: shown or hidden at
|
|
49456
|
+
"always: shown or hidden at once, its space kept; hidden, a Focus does not reach it"
|
|
49457
49457
|
]
|
|
49458
49458
|
},
|
|
49459
49459
|
"warnings": {
|
|
@@ -52032,11 +52032,12 @@
|
|
|
52032
52032
|
],
|
|
52033
52033
|
"items": [
|
|
52034
52034
|
"nothing handed (undefined): the list it has stays",
|
|
52035
|
+
"the value is still the placed node's seed and the list does not offer it: the list is kept, and the value is cleared to '' and sent",
|
|
52035
52036
|
"otherwise: the list is kept as handed and drawn at the next paint; the value stays, so a list handed again keeps the pick"
|
|
52036
52037
|
],
|
|
52037
52038
|
"value": [
|
|
52038
52039
|
"null: ignored",
|
|
52039
|
-
"the value it already holds, as text: nothing",
|
|
52040
|
+
"the value it already holds, as text: nothing (it is no longer the seed)",
|
|
52040
52041
|
"a change: kept as text, Value follows, and the list selects it at the next paint if it names an option — Changed does not fire (only a person's pick fires it)"
|
|
52041
52042
|
],
|
|
52042
52043
|
"mounted": [
|
|
@@ -52064,7 +52065,7 @@
|
|
|
52064
52065
|
"always: the size it holds now, a bare number read in the unit it held; drawn at the next paint"
|
|
52065
52066
|
],
|
|
52066
52067
|
"visible": [
|
|
52067
|
-
"always: shown or hidden at
|
|
52068
|
+
"always: shown or hidden at once, its space kept; hidden, a Focus does not reach it"
|
|
52068
52069
|
]
|
|
52069
52070
|
},
|
|
52070
52071
|
"warnings": {
|
|
@@ -54246,7 +54247,7 @@
|
|
|
54246
54247
|
"always: the page, or the scrolling Group holding it, scrolls until the button can be seen"
|
|
54247
54248
|
],
|
|
54248
54249
|
"visible": [
|
|
54249
|
-
"always: shown or hidden at
|
|
54250
|
+
"always: shown or hidden at once, its space kept; hidden, it takes no click and a Focus does not reach it"
|
|
54250
54251
|
]
|
|
54251
54252
|
},
|
|
54252
54253
|
"warnings": {
|
|
@@ -56651,7 +56652,7 @@
|
|
|
56651
56652
|
"always: the size it holds now, a bare number read in the unit it held; drawn at the next paint"
|
|
56652
56653
|
],
|
|
56653
56654
|
"visible": [
|
|
56654
|
-
"always: shown or hidden at
|
|
56655
|
+
"always: shown or hidden at once, its space kept; hidden, a Focus does not reach it"
|
|
56655
56656
|
]
|
|
56656
56657
|
},
|
|
56657
56658
|
"warnings": {
|
|
@@ -59521,7 +59522,7 @@
|
|
|
59521
59522
|
"always: the size it holds now, a bare number read in the unit it held; drawn at the next paint"
|
|
59522
59523
|
],
|
|
59523
59524
|
"visible": [
|
|
59524
|
-
"always: shown or hidden at
|
|
59525
|
+
"always: shown or hidden at once, its space kept; hidden, a Focus does not reach it"
|
|
59525
59526
|
],
|
|
59526
59527
|
"opacity": [
|
|
59527
59528
|
"always: how see-through it is; at exactly 0 it takes no pointer, and an act goes to what is behind it"
|
|
@@ -64363,7 +64364,7 @@
|
|
|
64363
64364
|
},
|
|
64364
64365
|
"isSignal": false,
|
|
64365
64366
|
"default": false,
|
|
64366
|
-
"description": "Whether a request with no valid session token is allowed to run this function at all"
|
|
64367
|
+
"description": "Whether a request with no valid session token is allowed to run this function at all. A run the app starts itself — a schedule, a row change, a run from Studio — is always allowed and sees nobody signed in; a webhook is a request like any other"
|
|
64367
64368
|
},
|
|
64368
64369
|
{
|
|
64369
64370
|
"name": "params",
|
|
@@ -64417,7 +64418,7 @@
|
|
|
64417
64418
|
"group": "Request",
|
|
64418
64419
|
"plug": "output",
|
|
64419
64420
|
"type": {
|
|
64420
|
-
"name": "
|
|
64421
|
+
"name": "string"
|
|
64421
64422
|
},
|
|
64422
64423
|
"isSignal": false,
|
|
64423
64424
|
"description": "Id of the user the session token resolved to, and blank for an unauthenticated request"
|
|
@@ -654,6 +654,80 @@ var require_src = __commonJS({
|
|
|
654
654
|
}
|
|
655
655
|
}
|
|
656
656
|
|
|
657
|
+
// P112-OWN019-LOOKBLIND / OWN-026 \u2014 text a person cannot read against what is painted behind it. Large 4's number pad
|
|
658
|
+
// set white digits on a #FAF5FF button (1.07:1) and it survived five changes: the model tapped the keys, read
|
|
659
|
+
// "Correct!" and never saw that nobody could read them, because nothing here measured contrast. The ground is the
|
|
660
|
+
// text's ancestors' background colours composited from the first opaque one (white under them all, as a browser
|
|
661
|
+
// paints), the ink is the text's colour faded by every ancestor's opacity, and the ratio is WCAG's. It abstains,
|
|
662
|
+
// counted, wherever ancestors are not what is painted underneath: a background image or gradient, a text shadow, an
|
|
663
|
+
// SVG text. A disabled or aria-hidden control is exempt (WCAG 1.4.3's inactive components). Known limit: a sibling
|
|
664
|
+
// laid over the text by position is not an ancestor, so it is not seen. Raw pairs here; the judgement is summarise's.
|
|
665
|
+
layout.lowContrast = [];
|
|
666
|
+
layout.contrastChecked = 0;
|
|
667
|
+
layout.contrastAbstained = 0;
|
|
668
|
+
const CONTRAST_FLOOR = 3;
|
|
669
|
+
{
|
|
670
|
+
const parseColor = (c) => {
|
|
671
|
+
const m = String(c || '').match(/rgba?\\(([^)]+)\\)/);
|
|
672
|
+
if (!m) return null;
|
|
673
|
+
const p = m[1].split(/[ ,\\/]+/).filter(Boolean).map(Number);
|
|
674
|
+
if (p.length < 3 || p.some((x) => isNaN(x))) return null;
|
|
675
|
+
return { r: p[0], g: p[1], b: p[2], a: p.length > 3 ? p[3] : 1 };
|
|
676
|
+
};
|
|
677
|
+
const over = (top, under) => ({ r: top.r * top.a + under.r * (1 - top.a), g: top.g * top.a + under.g * (1 - top.a), b: top.b * top.a + under.b * (1 - top.a), a: 1 });
|
|
678
|
+
const lum = (c) => {
|
|
679
|
+
const f = (x) => { const v = x / 255; return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); };
|
|
680
|
+
return 0.2126 * f(c.r) + 0.7152 * f(c.g) + 0.0722 * f(c.b);
|
|
681
|
+
};
|
|
682
|
+
const contrastOf = (a, b) => { const x = lum(a), y = lum(b); return (Math.max(x, y) + 0.05) / (Math.min(x, y) + 0.05); };
|
|
683
|
+
const hexOf = (c) => '#' + [c.r, c.g, c.b].map((v) => Math.round(v).toString(16).padStart(2, '0')).join('');
|
|
684
|
+
const inactive = (e) => e.disabled === true || (typeof e.getAttribute === 'function' && (e.getAttribute('aria-disabled') === 'true' || e.getAttribute('aria-hidden') === 'true'));
|
|
685
|
+
// What is behind the text, or null to abstain; 'exempt' for an inactive control.
|
|
686
|
+
const groundOf = (el) => {
|
|
687
|
+
const stack = [];
|
|
688
|
+
let opacity = 1;
|
|
689
|
+
for (let e = el; e; e = e.parentElement) {
|
|
690
|
+
if (inactive(e)) return 'exempt';
|
|
691
|
+
const cs = styleOf(e);
|
|
692
|
+
if (!cs) break;
|
|
693
|
+
const image = cs.backgroundImage;
|
|
694
|
+
if (image && image !== 'none' && !/^url\\("data:image\\/svg/.test(image)) return null;
|
|
695
|
+
const bg = parseColor(cs.backgroundColor);
|
|
696
|
+
const o = Number(cs.opacity);
|
|
697
|
+
if (o >= 0 && o <= 1) opacity *= o;
|
|
698
|
+
if (bg && bg.a > 0) {
|
|
699
|
+
stack.push(bg);
|
|
700
|
+
if (bg.a >= 0.999) break;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
let g = { r: 255, g: 255, b: 255, a: 1 };
|
|
704
|
+
while (stack.length) g = over(stack.pop(), g);
|
|
705
|
+
return { ground: g, opacity };
|
|
706
|
+
};
|
|
707
|
+
const pairs = new Map();
|
|
708
|
+
for (const el of textEls) {
|
|
709
|
+
if (el.namespaceURI && el.namespaceURI !== 'http://www.w3.org/1999/xhtml') { layout.contrastAbstained += 1; continue; }
|
|
710
|
+
const cs = styleOf(el);
|
|
711
|
+
const fill = parseColor(cs.webkitTextFillColor);
|
|
712
|
+
const fg = fill || parseColor(cs.color);
|
|
713
|
+
if (!fg || fg.a === 0) continue;
|
|
714
|
+
if (cs.textShadow && cs.textShadow !== 'none') { layout.contrastAbstained += 1; continue; }
|
|
715
|
+
const at = groundOf(el);
|
|
716
|
+
if (at === 'exempt') continue;
|
|
717
|
+
if (at === null) { layout.contrastAbstained += 1; continue; }
|
|
718
|
+
if (at.opacity < 0.05) continue;
|
|
719
|
+
const ink = over({ r: fg.r, g: fg.g, b: fg.b, a: fg.a * at.opacity }, at.ground);
|
|
720
|
+
const ratio = Math.round(contrastOf(ink, at.ground) * 100) / 100;
|
|
721
|
+
layout.contrastChecked += 1;
|
|
722
|
+
if (ratio >= CONTRAST_FLOOR) continue;
|
|
723
|
+
const key = hexOf(ink) + '|' + hexOf(at.ground);
|
|
724
|
+
const seen = pairs.get(key);
|
|
725
|
+
if (seen) seen.count += 1;
|
|
726
|
+
else pairs.set(key, { text: el.textContent.trim().slice(0, 40), fg: hexOf(ink), bg: hexOf(at.ground), ratio, size: round(parseFloat(cs.fontSize) || 0), weight: String(cs.fontWeight || ''), cls: cls(el), count: 1 });
|
|
727
|
+
}
|
|
728
|
+
layout.lowContrast = [...pairs.values()].sort((a, b) => a.ratio - b.ratio).slice(0, 10);
|
|
729
|
+
}
|
|
730
|
+
|
|
657
731
|
const contentBottom = visible.reduce((lowest, el) => {
|
|
658
732
|
const r = el.getBoundingClientRect();
|
|
659
733
|
return r.width > 0 && r.height > 0 && r.bottom > lowest ? r.bottom : lowest;
|
|
@@ -821,7 +895,12 @@ var require_src = __commonJS({
|
|
|
821
895
|
* P112-OWN019-LETTEREDGE (Richard 2026-10-15, TL2's hero). Words within 3 px of the inner left or right edge of the
|
|
822
896
|
* box that frames them (a border or a fill) — a letter in an outlined chip set left because its Text stretched.
|
|
823
897
|
*/
|
|
824
|
-
TextAgainstEdge: "text-against-edge"
|
|
898
|
+
TextAgainstEdge: "text-against-edge",
|
|
899
|
+
/**
|
|
900
|
+
* P112-OWN019-LOOKBLIND (OWN-026's number pad: white digits on #FAF5FF, 1.07:1, through five changes). Text under 3:1
|
|
901
|
+
* against the background composited behind it — unreadable to anyone, and invisible to an agent that only reads text.
|
|
902
|
+
*/
|
|
903
|
+
LowContrastText: "low-contrast-text"
|
|
825
904
|
};
|
|
826
905
|
var SEVERITY_ORDER = { error: 0, warning: 1, info: 2 };
|
|
827
906
|
var ROW_GAP_MIN_TALLEST = 48;
|
|
@@ -867,6 +946,7 @@ var require_src = __commonJS({
|
|
|
867
946
|
const findings = [];
|
|
868
947
|
const add = (f) => findings.push(f);
|
|
869
948
|
let controlsJudged = false;
|
|
949
|
+
const contrastPairs = /* @__PURE__ */ new Map();
|
|
870
950
|
for (const [name, v] of Object.entries(viewports)) {
|
|
871
951
|
if (!v || v.error) continue;
|
|
872
952
|
const isDesktop = v.requested.width >= DESKTOP_WIDTH;
|
|
@@ -1112,6 +1192,12 @@ var require_src = __commonJS({
|
|
|
1112
1192
|
evidence: edges.slice(0, 6)
|
|
1113
1193
|
});
|
|
1114
1194
|
}
|
|
1195
|
+
for (const c of v.layout && v.layout.lowContrast || []) {
|
|
1196
|
+
const key = c.fg + "|" + c.bg;
|
|
1197
|
+
const prev = contrastPairs.get(key);
|
|
1198
|
+
if (!prev) contrastPairs.set(key, { ...c, viewport: name });
|
|
1199
|
+
else if (c.count > prev.count) prev.count = c.count;
|
|
1200
|
+
}
|
|
1115
1201
|
if (v.consoleErrors && v.consoleErrors.length) {
|
|
1116
1202
|
add({
|
|
1117
1203
|
code: RenderFinding.ConsoleError,
|
|
@@ -1122,6 +1208,17 @@ var require_src = __commonJS({
|
|
|
1122
1208
|
});
|
|
1123
1209
|
}
|
|
1124
1210
|
}
|
|
1211
|
+
if (contrastPairs.size) {
|
|
1212
|
+
const worst = [...contrastPairs.values()].sort((a, b) => a.ratio - b.ratio);
|
|
1213
|
+
const named = worst.slice(0, 3).map((c) => `"${c.text}" ${c.fg} on ${c.bg}, ${c.ratio}:1${c.count > 1 ? ` (and ${c.count - 1} more like it)` : ""}`);
|
|
1214
|
+
add({
|
|
1215
|
+
code: RenderFinding.LowContrastText,
|
|
1216
|
+
severity: "warning",
|
|
1217
|
+
viewport: worst[0].viewport,
|
|
1218
|
+
message: `${plural(worst.length, "colour pair", "colour pairs")} of text ${worst.length === 1 ? "reads" : "read"} under 3:1 against what is behind ${worst.length === 1 ? "it" : "them"}, so a person cannot read ${worst.length === 1 ? "it" : "them"}: ${named.join("; ")}. Pressing it still works, so a drive will not notice: set the text's colour or its background from the style vocabulary so they differ in lightness, not only in hue. Look at the ${worst[0].viewport} screenshot.`,
|
|
1219
|
+
evidence: worst.slice(0, 6)
|
|
1220
|
+
});
|
|
1221
|
+
}
|
|
1125
1222
|
const seen = /* @__PURE__ */ new Set();
|
|
1126
1223
|
const unique = findings.filter((f) => {
|
|
1127
1224
|
const key = `${f.code}|${f.viewport}|${f.message}`;
|
|
@@ -2087,6 +2184,32 @@ var require_render_report = __commonJS({
|
|
|
2087
2184
|
}
|
|
2088
2185
|
};
|
|
2089
2186
|
}
|
|
2187
|
+
var PAGE_ANSWER_MS = Number(process.env.NODEGX_RENDER_PAGE_ANSWER_MS) || 3e4;
|
|
2188
|
+
async function pageAnswers(attaching, chromePath) {
|
|
2189
|
+
const neverAnswered = (refused) => {
|
|
2190
|
+
const message = "Chrome started (its debugging port opened) but its page " + (refused ? `refused the connection (${refused.message || refused}). ` : `never answered within ${Math.round(PAGE_ANSWER_MS / 1e3)} s. `) + `That is what Chrome does inside another sandbox (an agent harness that runs commands under sandbox-exec, a container): its own sandbox cannot start and its pages crash. Run the render outside that sandbox, or set CHROME_PATH to a script that starts ${chromePath} with --no-sandbox.`;
|
|
2191
|
+
const error = new Error(message);
|
|
2192
|
+
error.problems = [message];
|
|
2193
|
+
error.actionable = true;
|
|
2194
|
+
return error;
|
|
2195
|
+
};
|
|
2196
|
+
if (!attaching) throw neverAnswered();
|
|
2197
|
+
let timer;
|
|
2198
|
+
const deadline = new Promise((_, reject) => {
|
|
2199
|
+
timer = setTimeout(() => reject(neverAnswered()), PAGE_ANSWER_MS);
|
|
2200
|
+
});
|
|
2201
|
+
try {
|
|
2202
|
+
const answered = attaching.then(
|
|
2203
|
+
async (tab) => (await evaluate(tab.client, "1"), tab),
|
|
2204
|
+
(refused) => {
|
|
2205
|
+
throw neverAnswered(refused);
|
|
2206
|
+
}
|
|
2207
|
+
);
|
|
2208
|
+
return await Promise.race([answered, deadline]);
|
|
2209
|
+
} finally {
|
|
2210
|
+
clearTimeout(timer);
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2090
2213
|
async function withRenderedPage2(options, fn) {
|
|
2091
2214
|
const { projectDir, backendPort, editorTokens = false, chromeArgs = [], beforeBoot } = options;
|
|
2092
2215
|
reapOrphanedRenderProcesses();
|
|
@@ -2186,7 +2309,10 @@ var require_render_report = __commonJS({
|
|
|
2186
2309
|
if (!targets) throw new Error(`Chrome never opened a debugging port (${pre.chrome}).`);
|
|
2187
2310
|
const target = targets.find((t) => t.type === "page");
|
|
2188
2311
|
const settles = [];
|
|
2189
|
-
const main2 = await
|
|
2312
|
+
const main2 = await pageAnswers(
|
|
2313
|
+
target ? attachTab({ wsUrl: target.webSocketDebuggerUrl, servePort, settles }) : void 0,
|
|
2314
|
+
pre.chrome
|
|
2315
|
+
);
|
|
2190
2316
|
client = main2.client;
|
|
2191
2317
|
const openTab = async (label) => {
|
|
2192
2318
|
if (!browserClient) {
|
|
@@ -2251,9 +2377,11 @@ var require_render_report = __commonJS({
|
|
|
2251
2377
|
editorTokens = false,
|
|
2252
2378
|
renderRoutedPages = true,
|
|
2253
2379
|
page: requestedPage,
|
|
2254
|
-
concurrency = PAGE_TABS
|
|
2380
|
+
concurrency = PAGE_TABS,
|
|
2381
|
+
imageBudget
|
|
2255
2382
|
} = options;
|
|
2256
2383
|
const started = Date.now();
|
|
2384
|
+
const imageBytesEach = imageBudget ? Math.floor(imageBudget / Math.max(1, viewports.length)) : 0;
|
|
2257
2385
|
const routes = routedPages2(projectDir);
|
|
2258
2386
|
let focus;
|
|
2259
2387
|
if (requestedPage !== void 0) {
|
|
@@ -2285,24 +2413,45 @@ var require_render_report = __commonJS({
|
|
|
2285
2413
|
if (focusedOffStart) await page.navigate(focus.url);
|
|
2286
2414
|
const measured = {};
|
|
2287
2415
|
const screenshots = [];
|
|
2416
|
+
const take = (vp, client, format, height, quality) => client.send("Page.captureScreenshot", {
|
|
2417
|
+
format,
|
|
2418
|
+
...format === "jpeg" ? { quality } : {},
|
|
2419
|
+
captureBeyondViewport: screenshot === "full",
|
|
2420
|
+
optimizeForSpeed: true,
|
|
2421
|
+
...screenshot === "full" ? { clip: { x: 0, y: 0, width: vp.width, height, scale: deviceScaleFactor } } : {}
|
|
2422
|
+
});
|
|
2288
2423
|
const capture = async (vp, raw, component, subject2, client = page.client) => {
|
|
2289
|
-
const
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2424
|
+
const fullHeight = Math.min(raw.pageHeight, 16384);
|
|
2425
|
+
const shot = await take(vp, client, "png", fullHeight);
|
|
2426
|
+
const base = { name: vp.name, page: component, subject: subject2 };
|
|
2427
|
+
if (!imageBytesEach || shot.data.length <= imageBytesEach) return { ...base, mimeType: "image/png", base64: shot.data };
|
|
2428
|
+
return { ...base, ...await fitted(vp, client, fullHeight) };
|
|
2429
|
+
};
|
|
2430
|
+
const fitted = async (vp, client, fullHeight) => {
|
|
2431
|
+
const screenHeight = Math.max(1, vp.height);
|
|
2432
|
+
const screens = Math.max(1, Math.ceil(fullHeight / screenHeight));
|
|
2433
|
+
let quality = 70;
|
|
2434
|
+
let height = fullHeight;
|
|
2435
|
+
let shot = await take(vp, client, "jpeg", height, quality);
|
|
2436
|
+
if (screenshot === "full" && shot.data.length > imageBytesEach && screens > 1) {
|
|
2437
|
+
let keep = Math.min(screens - 1, Math.max(1, Math.floor(screens * imageBytesEach / shot.data.length)));
|
|
2438
|
+
for (; ; ) {
|
|
2439
|
+
height = Math.min(fullHeight, keep * screenHeight);
|
|
2440
|
+
shot = await take(vp, client, "jpeg", height, quality);
|
|
2441
|
+
if (shot.data.length <= imageBytesEach || keep === 1) break;
|
|
2442
|
+
keep -= 1;
|
|
2443
|
+
}
|
|
2444
|
+
}
|
|
2445
|
+
if (shot.data.length > imageBytesEach) {
|
|
2446
|
+
quality = 40;
|
|
2447
|
+
shot = await take(vp, client, "jpeg", height, quality);
|
|
2448
|
+
}
|
|
2449
|
+
const shown = screenshot === "full" ? Math.min(screens, Math.ceil(height / screenHeight)) : 1;
|
|
2450
|
+
return {
|
|
2451
|
+
mimeType: "image/jpeg",
|
|
2452
|
+
base64: shot.data,
|
|
2453
|
+
fit: { format: "jpeg", quality, screens: shown, of: screenshot === "full" ? screens : 1, overBudget: shot.data.length > imageBytesEach }
|
|
2454
|
+
};
|
|
2306
2455
|
};
|
|
2307
2456
|
const expression = measureExpression(placeholders, probesFor(subjectComponent));
|
|
2308
2457
|
let loggedThrough = 0;
|
|
@@ -2449,6 +2598,7 @@ var require_render_report = __commonJS({
|
|
|
2449
2598
|
// Exported so a session-teardown hook, a CI step or a spec can reap without
|
|
2450
2599
|
// starting a drive — the leak outlives the run that caused it.
|
|
2451
2600
|
reapOrphanedRenderProcesses,
|
|
2601
|
+
pageAnswers,
|
|
2452
2602
|
routedPages: routedPages2,
|
|
2453
2603
|
resolvePageRequest: resolvePageRequest2,
|
|
2454
2604
|
reachableComponents,
|
|
@@ -2751,7 +2901,32 @@ function lookExpression(target) {
|
|
|
2751
2901
|
const r = box.getBoundingClientRect();
|
|
2752
2902
|
const visible = r.width > 0 && r.height > 0 && st.visibility !== 'hidden' && st.display !== 'none' && opacity > 0.05;
|
|
2753
2903
|
const color = getComputedStyle(leaf).color;
|
|
2754
|
-
|
|
2904
|
+
// P112-OWN019-LOOKBLIND \u2014 can a person read it: WCAG contrast of the ink (faded by every ancestor's opacity) against
|
|
2905
|
+
// the ground composited from the text outwards (white under all of them, as a browser paints). null, never a guess,
|
|
2906
|
+
// where a picture or gradient is behind the text or it casts a shadow. The same reading as render_report's
|
|
2907
|
+
// low-contrast-text, so a page behind a sign-in can be checked from a drive.
|
|
2908
|
+
let contrast = null;
|
|
2909
|
+
const ink = rgba(getComputedStyle(leaf).webkitTextFillColor) || rgba(color);
|
|
2910
|
+
if (ink && ink.a > 0 && (getComputedStyle(leaf).textShadow || 'none') === 'none') {
|
|
2911
|
+
const over = (t, u) => ({ r: t.r * t.a + u.r * (1 - t.a), g: t.g * t.a + u.g * (1 - t.a), b: t.b * t.a + u.b * (1 - t.a), a: 1 });
|
|
2912
|
+
const lum = (c) => { const f = (x) => { const v = x / 255; return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); }; return 0.2126 * f(c.r) + 0.7152 * f(c.g) + 0.0722 * f(c.b); };
|
|
2913
|
+
const stack = [];
|
|
2914
|
+
let picture = false;
|
|
2915
|
+
for (let e = leaf; e; e = e.parentElement) {
|
|
2916
|
+
const cs = getComputedStyle(e);
|
|
2917
|
+
if (cs.backgroundImage && cs.backgroundImage !== 'none' && !/^url\\("data:image\\/svg/.test(cs.backgroundImage)) { picture = true; break; }
|
|
2918
|
+
const bg = rgba(cs.backgroundColor);
|
|
2919
|
+
if (bg && bg.a > 0) { stack.push(bg); if (bg.a >= 0.999) break; }
|
|
2920
|
+
}
|
|
2921
|
+
if (!picture) {
|
|
2922
|
+
let ground = { r: 255, g: 255, b: 255, a: 1 };
|
|
2923
|
+
while (stack.length) ground = over(stack.pop(), ground);
|
|
2924
|
+
const fg = over({ r: ink.r, g: ink.g, b: ink.b, a: ink.a * opacity }, ground);
|
|
2925
|
+
const x = lum(fg), y = lum(ground);
|
|
2926
|
+
contrast = Math.round(((Math.max(x, y) + 0.05) / (Math.min(x, y) + 0.05)) * 100) / 100;
|
|
2927
|
+
}
|
|
2928
|
+
}
|
|
2929
|
+
return { text: label, visible, opacity: Math.round(opacity * 100) / 100, background: painted, backgroundName: name(painted), color, colorName: name(color), contrast, readable: contrast === null ? null : contrast >= 3 };
|
|
2755
2930
|
})()`;
|
|
2756
2931
|
}
|
|
2757
2932
|
function readExpression(r) {
|
|
@@ -2789,6 +2964,62 @@ function matches(value, expected) {
|
|
|
2789
2964
|
}
|
|
2790
2965
|
return JSON.stringify(value) === JSON.stringify(expected);
|
|
2791
2966
|
}
|
|
2967
|
+
function watchShape(step, n) {
|
|
2968
|
+
const w = step.watch && typeof step.watch === "object" ? step.watch : {};
|
|
2969
|
+
if (w.look === void 0) throw new UsageError(`Step ${n}: watch takes { look: T } or { look: [T, T, \u2026] }.`);
|
|
2970
|
+
const unknown = Object.keys(w).filter((k) => !["look", "for_ms", "expect"].includes(k));
|
|
2971
|
+
if (unknown.length) throw new UsageError(`Step ${n}: watch does not take ${unknown.map((k) => JSON.stringify(k)).join(", ")} \u2014 it takes look, for_ms and expect.`);
|
|
2972
|
+
if (step.expect !== void 0 && w.expect !== void 0) throw new UsageError(`Step ${n}: give watch one expect, beside it or inside it, not both.`);
|
|
2973
|
+
const many = Array.isArray(w.look);
|
|
2974
|
+
const targets = many ? w.look : [w.look];
|
|
2975
|
+
if (targets.length === 0 || targets.length > 8) throw new UsageError(`Step ${n}: watch looks at 1 to 8 targets.`);
|
|
2976
|
+
return {
|
|
2977
|
+
many,
|
|
2978
|
+
targets,
|
|
2979
|
+
forMs: Math.min(1e4, Math.max(100, Number(step.for_ms ?? w.for_ms ?? 1500))),
|
|
2980
|
+
expect: step.expect !== void 0 ? step.expect : w.expect,
|
|
2981
|
+
expr: `Promise.all([${targets.map((t) => lookExpression(t)).join(", ")}])`
|
|
2982
|
+
};
|
|
2983
|
+
}
|
|
2984
|
+
function checkStep(step, n, { outDir } = {}) {
|
|
2985
|
+
const verb = verbOf(step, n);
|
|
2986
|
+
const at = (fn) => {
|
|
2987
|
+
try {
|
|
2988
|
+
return fn();
|
|
2989
|
+
} catch (error) {
|
|
2990
|
+
if (!error.usage) throw new UsageError(`Step ${n}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2991
|
+
throw error.message.startsWith("Step ") ? error : new UsageError(`Step ${n}: ${error.message}`);
|
|
2992
|
+
}
|
|
2993
|
+
};
|
|
2994
|
+
if (verb === "tap") at(() => finder(step.tap));
|
|
2995
|
+
else if (verb === "type") {
|
|
2996
|
+
if (typeof step.type !== "string") throw new UsageError(`Step ${n}: type takes a string.`);
|
|
2997
|
+
if (step.into !== void 0) at(() => finder(step.into));
|
|
2998
|
+
} else if (verb === "key") {
|
|
2999
|
+
if (!KEYS[step.key]) throw new UsageError(`Step ${n}: key "${step.key}" is not one of ${Object.keys(KEYS).join(", ")}.`);
|
|
3000
|
+
} else if (verb === "wait_for") at(() => waitExpression(step.wait_for));
|
|
3001
|
+
else if (verb === "read") {
|
|
3002
|
+
const r = step.read && typeof step.read === "object" ? step.read : null;
|
|
3003
|
+
if (!r || !RUN_READS.some((k) => r[k] !== void 0)) at(() => readExpression(step.read));
|
|
3004
|
+
} else if (verb === "navigate") {
|
|
3005
|
+
if (typeof step.navigate !== "string") throw new UsageError(`Step ${n}: navigate takes a route string.`);
|
|
3006
|
+
} else if (verb === "watch") watchShape(step, n);
|
|
3007
|
+
else if (verb === "viewport") at(() => parseViewports(String(step.viewport)));
|
|
3008
|
+
else if (verb === "screenshot") {
|
|
3009
|
+
if (!outDir) throw new UsageError(`Step ${n}: screenshot needs --out-dir (render_report's out_dir rule).`);
|
|
3010
|
+
} else if (verb === "seed_storage") {
|
|
3011
|
+
const seed = step.seed_storage;
|
|
3012
|
+
if (typeof seed === "string") {
|
|
3013
|
+
if (typeof step.edit !== "string") throw new UsageError(`Step ${n}: seed_storage "${seed}" edits a saved value and needs edit: "js that changes v".`);
|
|
3014
|
+
} else if (!seed || typeof seed !== "object" || Array.isArray(seed)) {
|
|
3015
|
+
throw new UsageError(`Step ${n}: seed_storage takes { key: value } or a key with edit: "js that changes v".`);
|
|
3016
|
+
}
|
|
3017
|
+
} else if (verb === "stub_route") parseStub(step, n);
|
|
3018
|
+
else if (verb === "reduced_motion") {
|
|
3019
|
+
if (typeof step.reduced_motion !== "boolean") throw new UsageError(`Step ${n}: reduced_motion takes true or false.`);
|
|
3020
|
+
}
|
|
3021
|
+
return verb;
|
|
3022
|
+
}
|
|
2792
3023
|
async function runSteps(page, steps, { outDir, stubs = routeStubs(page), network, beforeBoot = 0 }) {
|
|
2793
3024
|
const rows = [];
|
|
2794
3025
|
const pendingSeeds = [];
|
|
@@ -2914,17 +3145,7 @@ async function runSteps(page, steps, { outDir, stubs = routeStubs(page), network
|
|
|
2914
3145
|
}
|
|
2915
3146
|
row.url = await evaluate("location.pathname + location.hash");
|
|
2916
3147
|
} else if (verb === "watch") {
|
|
2917
|
-
const
|
|
2918
|
-
if (w.look === void 0) throw new UsageError(`Step ${i + 1}: watch takes { look: T } or { look: [T, T, \u2026] }.`);
|
|
2919
|
-
const unknown = Object.keys(w).filter((k) => !["look", "for_ms", "expect"].includes(k));
|
|
2920
|
-
if (unknown.length) throw new UsageError(`Step ${i + 1}: watch does not take ${unknown.map((k) => JSON.stringify(k)).join(", ")} \u2014 it takes look, for_ms and expect.`);
|
|
2921
|
-
if (step.expect !== void 0 && w.expect !== void 0) throw new UsageError(`Step ${i + 1}: give watch one expect, beside it or inside it, not both.`);
|
|
2922
|
-
const forMs = Math.min(1e4, Math.max(100, Number(step.for_ms ?? w.for_ms ?? 1500)));
|
|
2923
|
-
const expect = step.expect !== void 0 ? step.expect : w.expect;
|
|
2924
|
-
const many = Array.isArray(w.look);
|
|
2925
|
-
const targets = many ? w.look : [w.look];
|
|
2926
|
-
if (targets.length === 0 || targets.length > 8) throw new UsageError(`Step ${i + 1}: watch looks at 1 to 8 targets.`);
|
|
2927
|
-
const expr = `Promise.all([${targets.map((t) => lookExpression(t)).join(", ")}])`;
|
|
3148
|
+
const { many, targets, forMs, expect, expr } = watchShape(step, i + 1);
|
|
2928
3149
|
const keyOf = (v) => v && typeof v === "object" ? JSON.stringify({ text: v.text, visible: v.visible, backgroundName: v.backgroundName, colorName: v.colorName, opacity: v.opacity === void 0 ? void 0 : Math.round(v.opacity * 10) / 10 }) : "null";
|
|
2929
3150
|
const states = targets.map(() => []);
|
|
2930
3151
|
const t0 = Date.now();
|
|
@@ -3007,8 +3228,8 @@ async function main() {
|
|
|
3007
3228
|
return i === -1 ? void 0 : argv[i + 1];
|
|
3008
3229
|
};
|
|
3009
3230
|
const projectDir = argv.find((a, i) => !a.startsWith("--") && !VALUE_FLAGS.has(argv[i - 1]));
|
|
3010
|
-
const usage = (message) => {
|
|
3011
|
-
process.stdout.write(JSON.stringify({ error: { message, problems:
|
|
3231
|
+
const usage = (message, problems2 = [message]) => {
|
|
3232
|
+
process.stdout.write(JSON.stringify({ error: { message, problems: problems2, actionable: true } }));
|
|
3012
3233
|
process.exit(2);
|
|
3013
3234
|
};
|
|
3014
3235
|
if (!projectDir) usage("drive-steps.js <project-dir> --steps <json|@file> [--page p] [--viewport v] [--out-dir d]");
|
|
@@ -3018,12 +3239,22 @@ async function main() {
|
|
|
3018
3239
|
if (!raw) throw new UsageError("--steps is required: a JSON array of steps.");
|
|
3019
3240
|
steps = JSON.parse(raw.startsWith("@") ? fs.readFileSync(raw.slice(1), "utf8") : raw);
|
|
3020
3241
|
if (!Array.isArray(steps) || steps.length === 0) throw new UsageError("--steps must be a non-empty JSON array.");
|
|
3021
|
-
steps.forEach((s, i) => {
|
|
3022
|
-
if (verbOf(s, i + 1) === "stub_route") parseStub(s, i + 1);
|
|
3023
|
-
});
|
|
3024
3242
|
} catch (error) {
|
|
3025
3243
|
usage(error.message);
|
|
3026
3244
|
}
|
|
3245
|
+
const problems = [];
|
|
3246
|
+
steps.forEach((step, i) => {
|
|
3247
|
+
try {
|
|
3248
|
+
checkStep(step, i + 1, { outDir: flag("--out-dir") });
|
|
3249
|
+
} catch (error) {
|
|
3250
|
+
if (!error.usage) throw error;
|
|
3251
|
+
problems.push(error.message);
|
|
3252
|
+
}
|
|
3253
|
+
});
|
|
3254
|
+
if (problems.length) {
|
|
3255
|
+
problems.push("No step ran: every step is checked before the first one does.");
|
|
3256
|
+
usage(problems.join(" "), problems);
|
|
3257
|
+
}
|
|
3027
3258
|
let viewport;
|
|
3028
3259
|
try {
|
|
3029
3260
|
[viewport] = parseViewports(flag("--viewport") || "desktop");
|
|
@@ -3075,9 +3306,9 @@ async function main() {
|
|
|
3075
3306
|
process.stdout.write(JSON.stringify(result));
|
|
3076
3307
|
} catch (error) {
|
|
3077
3308
|
if (error.usage) usage(error.message);
|
|
3078
|
-
const
|
|
3079
|
-
process.stdout.write(JSON.stringify({ error: { message:
|
|
3309
|
+
const problems2 = error.problems || [error instanceof Error ? error.message : String(error)];
|
|
3310
|
+
process.stdout.write(JSON.stringify({ error: { message: problems2.join(" "), problems: problems2, actionable: Boolean(error.actionable) } }));
|
|
3080
3311
|
}
|
|
3081
3312
|
}
|
|
3082
3313
|
if (require.main === module) main();
|
|
3083
|
-
module.exports = { runSteps, locateExpression, routeStubs, networkLog, parseStub, VERBS };
|
|
3314
|
+
module.exports = { runSteps, checkStep, locateExpression, routeStubs, networkLog, parseStub, VERBS };
|