@nodegx/mcp 0.2.5 → 0.2.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/README.md +14 -7
- package/bin/noodl-mcp.js +14 -0
- package/dist/backend/LICENSE +21 -0
- package/dist/backend/cli.js +6534 -2836
- package/dist/backend/folder/start.cmd +4 -0
- package/dist/backend/folder/start.js +466 -0
- package/dist/backend/folder/start.sh +4 -0
- package/dist/backend/vm/provision-app.sh +235 -0
- package/dist/cloud-bundle.cjs +5051 -2381
- package/dist/kit-extract.cjs +5264 -2595
- package/dist/kit-extract.inputs.json +143 -113
- package/dist/node-catalog.json +44 -7
- package/dist/noodl-mcp.cjs +19819 -6657
- package/dist/package.json +1 -1
- package/dist/render-harness/data/node-catalog-enriched.json +1611 -257
- package/dist/render-harness/data/node-catalog.json +44 -7
- package/dist/render-harness/drive-steps.js +196 -21
- package/dist/render-harness/measure-from-disk.js +174 -22
- package/dist/render-harness/render-from-disk.js +136 -136
- package/dist/src/external/viewer/noodl.viewer.js +1 -1
- package/package.json +2 -3
- package/src/index.ts +0 -14
|
@@ -7262,11 +7262,9 @@
|
|
|
7262
7262
|
],
|
|
7263
7263
|
"dynamicPorts": {
|
|
7264
7264
|
"mechanisms": [
|
|
7265
|
-
"runtime-discovered"
|
|
7266
|
-
"editor-adapter"
|
|
7265
|
+
"runtime-discovered"
|
|
7267
7266
|
],
|
|
7268
|
-
"description": "
|
|
7269
|
-
"editorAdapter": "QueryRecordsAdapter"
|
|
7267
|
+
"description": "Every input is drawn from the parameters, so the static port list is empty: \"collectionName\" (the class), \"visualFilter\", \"visualSort\", \"storageEnableLimit\" + \"storageLimit\"/\"storageSkip\", \"storageEnableCount\", \"search\", \"realtime\", and the \"runOnChange-<input>\" boxes. A server-side filter is \"visualFilter\": {\"combinator\": \"and\", \"rules\": [{\"property\": \"<field>\", \"operator\": \"equal to\", \"input\": \"<name>\"}]}; each rule's \"input\" mints the input port \"qp-<name>\" a connection feeds, or the rule carries a literal \"value\" instead. Any other parameter name is stored and never read."
|
|
7270
7268
|
},
|
|
7271
7269
|
"parameterEncoding": {
|
|
7272
7270
|
"known": true,
|
|
@@ -7805,7 +7803,7 @@
|
|
|
7805
7803
|
"name": "signal"
|
|
7806
7804
|
},
|
|
7807
7805
|
"isSignal": true,
|
|
7808
|
-
"description": "Fires when the Id binds a record
|
|
7806
|
+
"description": "Fires when the Id binds a record already in memory, when a record the Id names is first read from the backend, and when a Fetch finishes reading it; use Done to act on the answer to a Fetch"
|
|
7809
7807
|
},
|
|
7810
7808
|
{
|
|
7811
7809
|
"name": "id",
|
|
@@ -29189,6 +29187,17 @@
|
|
|
29189
29187
|
"description": "Read and write rules stored on the record as it is written, each rule adding its own Target, Read and Write ports; the NodeGX backend enforces them on every query and every realtime event, and backends with no per-record access control ignore them",
|
|
29190
29188
|
"index": 1000
|
|
29191
29189
|
},
|
|
29190
|
+
{
|
|
29191
|
+
"name": "addTo",
|
|
29192
|
+
"displayName": "Add To",
|
|
29193
|
+
"group": "General",
|
|
29194
|
+
"plug": "input",
|
|
29195
|
+
"type": {
|
|
29196
|
+
"name": "string"
|
|
29197
|
+
},
|
|
29198
|
+
"isSignal": false,
|
|
29199
|
+
"description": "Property names, comma-separated (e.g. points). For these, the value wired in is an amount added to what the backend holds, in one step on the backend, instead of replacing it: two devices adding at once both count. Wire the amount (1, -1, 5). The record then shows the backend's total. Leave empty to write every value as given."
|
|
29200
|
+
},
|
|
29192
29201
|
{
|
|
29193
29202
|
"name": "idSource",
|
|
29194
29203
|
"displayName": "Id Source",
|
|
@@ -29478,6 +29487,9 @@
|
|
|
29478
29487
|
"onlyIfUnchanged": [
|
|
29479
29488
|
"always: read by the next save — the fields the backend must still hold as this record holds them, or the save is refused"
|
|
29480
29489
|
],
|
|
29490
|
+
"addTo": [
|
|
29491
|
+
"always: read by the next Do — the fields whose value is an amount the backend adds, not a value it stores"
|
|
29492
|
+
],
|
|
29481
29493
|
"accessControl": [
|
|
29482
29494
|
"always: read by the next call — the access rules sent with the record"
|
|
29483
29495
|
],
|
|
@@ -46461,6 +46473,18 @@
|
|
|
46461
46473
|
"description": "How the element is placed: In Layout follows its siblings, Absolute ignores them, Sticky pins to the parent edge on overflow, Fixed stays put and takes no space",
|
|
46462
46474
|
"index": 11
|
|
46463
46475
|
},
|
|
46476
|
+
{
|
|
46477
|
+
"name": "repeatWhileSaving",
|
|
46478
|
+
"displayName": "Repeat While Saving",
|
|
46479
|
+
"group": "General",
|
|
46480
|
+
"plug": "input",
|
|
46481
|
+
"type": {
|
|
46482
|
+
"name": "boolean"
|
|
46483
|
+
},
|
|
46484
|
+
"isSignal": false,
|
|
46485
|
+
"default": false,
|
|
46486
|
+
"description": "Off (the default): when a click starts a save, the next click is ignored until the backend has answered, and for at least half a second, so a double press saves once. Controls wired to the same node share this. On: every click fires, for a control that saves each quick tap"
|
|
46487
|
+
},
|
|
46464
46488
|
{
|
|
46465
46489
|
"name": "scrollIntoView",
|
|
46466
46490
|
"displayName": "Scroll Into View",
|
|
@@ -52008,11 +52032,12 @@
|
|
|
52008
52032
|
],
|
|
52009
52033
|
"items": [
|
|
52010
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",
|
|
52011
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"
|
|
52012
52037
|
],
|
|
52013
52038
|
"value": [
|
|
52014
52039
|
"null: ignored",
|
|
52015
|
-
"the value it already holds, as text: nothing",
|
|
52040
|
+
"the value it already holds, as text: nothing (it is no longer the seed)",
|
|
52016
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)"
|
|
52017
52042
|
],
|
|
52018
52043
|
"mounted": [
|
|
@@ -58536,6 +58561,18 @@
|
|
|
58536
58561
|
"description": "How the element is placed: In Layout follows its siblings, Absolute ignores them, Sticky pins to the parent edge on overflow, Fixed stays put and takes no space",
|
|
58537
58562
|
"index": 11
|
|
58538
58563
|
},
|
|
58564
|
+
{
|
|
58565
|
+
"name": "repeatWhileSaving",
|
|
58566
|
+
"displayName": "Repeat While Saving",
|
|
58567
|
+
"group": "General",
|
|
58568
|
+
"plug": "input",
|
|
58569
|
+
"type": {
|
|
58570
|
+
"name": "boolean"
|
|
58571
|
+
},
|
|
58572
|
+
"isSignal": false,
|
|
58573
|
+
"default": false,
|
|
58574
|
+
"description": "Off (the default): when Enter starts a save, the next Enter is ignored until the backend has answered, and for at least half a second, so a double press saves once. Controls wired to the same node share this. On: every Enter fires, for a control that saves each quick tap"
|
|
58575
|
+
},
|
|
58539
58576
|
{
|
|
58540
58577
|
"name": "runOnChange-startValue",
|
|
58541
58578
|
"displayName": "Value",
|
|
@@ -64381,7 +64418,7 @@
|
|
|
64381
64418
|
"group": "Request",
|
|
64382
64419
|
"plug": "output",
|
|
64383
64420
|
"type": {
|
|
64384
|
-
"name": "
|
|
64421
|
+
"name": "string"
|
|
64385
64422
|
},
|
|
64386
64423
|
"isSignal": false,
|
|
64387
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) {
|