@ggui-ai/ui-gen 0.25.0 → 0.26.0
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 +2 -2
- package/dist/{adapter-Cg_rkpGq.d.ts → adapter-BJwn74rx.d.ts} +49 -1
- package/dist/adapters/generation-dispatch.d.ts +8 -5
- package/dist/adapters/generation-dispatch.js +1118 -193
- package/dist/adapters/generation-dispatch.js.map +1 -1
- package/dist/adapters/index.js +27 -6
- package/dist/adapters/index.js.map +1 -1
- package/dist/advanced/index.d.ts +10 -9
- package/dist/advanced/index.js +1126 -202
- package/dist/advanced/index.js.map +1 -1
- package/dist/blueprint-validator.js +416 -92
- package/dist/blueprint-validator.js.map +1 -1
- package/dist/boilerplate.d.ts +2 -2
- package/dist/boilerplate.js +10 -3
- package/dist/boilerplate.js.map +1 -1
- package/dist/check/index.d.ts +3 -2
- package/dist/check/index.js +9 -2
- package/dist/check/index.js.map +1 -1
- package/dist/{create-ui-generator-DFnnArrv.d.ts → create-ui-generator-N4gRXpcL.d.ts} +3 -4
- package/dist/{design-mode-DLufhrNp.d.ts → design-mode-DGWN_Gkm.d.ts} +1 -1
- package/dist/{dispatch-CStl9eAw.d.ts → dispatch-BUoXRj_r.d.ts} +2 -2
- package/dist/evaluation/axis-checks/index.d.ts +5 -4
- package/dist/evaluation/axis-checks/index.js.map +1 -1
- package/dist/evaluation/axis-checks/registry.d.ts +4 -3
- package/dist/evaluation/index.d.ts +107 -539
- package/dist/evaluation/index.js +552 -55
- package/dist/evaluation/index.js.map +1 -1
- package/dist/evaluation/types.d.ts +17 -0
- package/dist/evaluation/types.js.map +1 -1
- package/dist/{generate-9IeNT4ZY.d.ts → generate-tSO_shQ0.d.ts} +1 -1
- package/dist/harness/check/runtime-render/index.d.ts +5 -5
- package/dist/harness/check/runtime-render/index.js +416 -92
- package/dist/harness/check/runtime-render/index.js.map +1 -1
- package/dist/harness/check/runtime-render/render-check-worker.js +393 -88
- package/dist/harness/check/runtime-render/render-check-worker.js.map +1 -1
- package/dist/harness/index.d.ts +4 -5
- package/dist/harness/index.js +36 -5
- package/dist/harness/index.js.map +1 -1
- package/dist/harness/prompts.d.ts +2 -2
- package/dist/harness/prompts.js +10 -1
- package/dist/harness/prompts.js.map +1 -1
- package/dist/harness/result-types.d.ts +17 -3
- package/dist/harness/result-types.js.map +1 -1
- package/dist/harness/runtime.d.ts +14 -6
- package/dist/harness/runtime.js +11 -4
- package/dist/harness/runtime.js.map +1 -1
- package/dist/harness/types-public.d.ts +4 -2
- package/dist/harness/types-public.js +10 -3
- package/dist/harness/types-public.js.map +1 -1
- package/dist/harness/validator-trace-sink.d.ts +4 -3
- package/dist/index.d.ts +46 -11
- package/dist/index.js +1128 -200
- package/dist/index.js.map +1 -1
- package/dist/patch.d.ts +3 -3
- package/dist/{prompts-YU9piOgs.d.ts → prompts-BhNUuRoZ.d.ts} +1 -1
- package/dist/tools/render-check-worker.js +8 -1
- package/dist/tools/render-check-worker.js.map +1 -1
- package/dist/{types-public-BJTLpzWn.d.ts → types-public-B7wn4ce5.d.ts} +384 -3
- package/dist/validation/index.d.ts +1 -1
- package/dist/validation/index.js +1 -1
- package/dist/validation/index.js.map +1 -1
- package/dist/visual-evaluator-C9L-o6ap.d.ts +639 -0
- package/dist/workflows.d.ts +3 -3
- package/package.json +12 -12
- package/src/adapters/generation-dispatch.ts +2 -0
- package/src/boilerplate/free-design-prompt.ts +3 -3
- package/src/boilerplate/hard-sections.ts +1 -1
- package/src/boilerplate/templates/layouts/chat-mobile.tsx.tmpl +3 -1
- package/src/boilerplate/templates/layouts/chat-universal.tsx.tmpl +3 -1
- package/src/boilerplate/templates/layouts/fullscreen-desktop.tsx.tmpl +3 -1
- package/src/boilerplate/templates/layouts/fullscreen-universal.tsx.tmpl +3 -1
- package/src/check/run-tier0.ts +6 -3
- package/src/contract-context.ts +1 -1
- package/src/evaluation/cost-tracker.ts +54 -6
- package/src/evaluation/criteria/edge-probe.ts +286 -0
- package/src/evaluation/criteria/edge.ts +202 -0
- package/src/evaluation/criteria/resolve.ts +55 -7
- package/src/evaluation/index.ts +5 -1
- package/src/evaluation/judge-spend.ts +48 -0
- package/src/evaluation/types-public.ts +91 -0
- package/src/evaluation/types.ts +10 -0
- package/src/evaluation/visual-evaluator.ts +236 -40
- package/src/harness/check/runtime-render/adapter.ts +18 -0
- package/src/harness/check/runtime-render/probe-pending.ts +111 -0
- package/src/harness/check/runtime-render/render-check-host.ts +10 -6
- package/src/harness/check/runtime-render/render-check-worker.ts +6 -3
- package/src/harness/check/runtime-render/render-check.ts +417 -133
- package/src/harness/check/runtime-render/walk.ts +200 -0
- package/src/harness/coding/assemble-result.ts +2 -0
- package/src/harness/coding/generate-task-runner.ts +27 -0
- package/src/harness/coding/init-session.ts +5 -3
- package/src/harness/coding/run-coding-turn.ts +5 -1
- package/src/harness/coding/run-eval-round.ts +63 -3
- package/src/harness/in-loop-evaluation.ts +39 -0
- package/src/harness/llm-router.ts +58 -1
- package/src/harness/result-types.ts +9 -0
- package/src/harness/runtime.ts +9 -0
- package/src/harness/types-public.ts +10 -1
- package/src/index.ts +5 -0
- package/src/internal/verdict-line.ts +33 -0
- package/src/taught-tokens.ts +32 -0
- package/src/tools/render-check-worker.ts +2 -1
- package/src/tools/render-check.ts +8 -7
- package/src/validation/primitives.ts +1 -1
- package/dist/llm-router-nfO7wPec.d.ts +0 -267
- package/dist/{bank-CT4eEkta.d.ts → bank-Ctdp3FUA.d.ts} +3 -3
|
@@ -17,6 +17,22 @@ var __export = (target, all) => {
|
|
|
17
17
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
18
18
|
};
|
|
19
19
|
|
|
20
|
+
// src/internal/verdict-line.ts
|
|
21
|
+
function parseVerdictLine(stdout) {
|
|
22
|
+
const lines = stdout.split("\n");
|
|
23
|
+
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
24
|
+
const line = lines[i];
|
|
25
|
+
if (line.startsWith(`${VERDICT_PREFIX} `)) return JSON.parse(line.slice(VERDICT_PREFIX.length + 1));
|
|
26
|
+
}
|
|
27
|
+
return void 0;
|
|
28
|
+
}
|
|
29
|
+
var VERDICT_PREFIX;
|
|
30
|
+
var init_verdict_line = __esm({
|
|
31
|
+
"src/internal/verdict-line.ts"() {
|
|
32
|
+
VERDICT_PREFIX = "@@ggui-render-check-verdict@@";
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
20
36
|
// src/harness/check/runtime-render/render-check-host.ts
|
|
21
37
|
var render_check_host_exports = {};
|
|
22
38
|
__export(render_check_host_exports, {
|
|
@@ -124,18 +140,19 @@ function mapSandboxResultToCheckResult(result, t0, boundMs = DEFAULT_RENDER_CHEC
|
|
|
124
140
|
t0
|
|
125
141
|
);
|
|
126
142
|
}
|
|
127
|
-
|
|
128
|
-
if (stdout.length === 0) {
|
|
129
|
-
return unverified("render check worker exited without producing a verdict.", t0);
|
|
130
|
-
}
|
|
143
|
+
let verdict;
|
|
131
144
|
try {
|
|
132
|
-
|
|
145
|
+
verdict = parseVerdictLine(result.stdout);
|
|
133
146
|
} catch (err) {
|
|
134
147
|
return unverified(
|
|
135
148
|
`render check worker verdict was not valid JSON \u2014 ${err instanceof Error ? err.message : String(err)}`,
|
|
136
149
|
t0
|
|
137
150
|
);
|
|
138
151
|
}
|
|
152
|
+
if (verdict === void 0) {
|
|
153
|
+
return unverified("render check worker exited without producing a verdict.", t0);
|
|
154
|
+
}
|
|
155
|
+
return verdict;
|
|
139
156
|
}
|
|
140
157
|
async function runRenderCheckViaWorker(input, options = {}) {
|
|
141
158
|
const t0 = Date.now();
|
|
@@ -154,7 +171,9 @@ async function runRenderCheckViaWorker(input, options = {}) {
|
|
|
154
171
|
gracePeriodMs: bounds.gracePeriodMs,
|
|
155
172
|
maxStdoutBytes: CHECK_STDOUT_CAP,
|
|
156
173
|
nodeHeapMb: bounds.heapMb,
|
|
157
|
-
|
|
174
|
+
// ggui#1398: the check learns its own deadline, so its report-only
|
|
175
|
+
// pending phase can step aside instead of pushing it past the bound.
|
|
176
|
+
stdin: JSON.stringify({ ...input, deadlineAt: t0 + bounds.timeoutMs }),
|
|
158
177
|
// NODE_ENV steers React's production vs development build — keep
|
|
159
178
|
// parity with the caller; everything else stays on the sandbox's
|
|
160
179
|
// default allowlist.
|
|
@@ -171,6 +190,7 @@ async function runRenderCheckViaWorker(input, options = {}) {
|
|
|
171
190
|
var DEFAULT_RENDER_CHECK_HOST_BOUNDS, CHECK_STDOUT_CAP, WORKER_BASENAME;
|
|
172
191
|
var init_render_check_host = __esm({
|
|
173
192
|
"src/harness/check/runtime-render/render-check-host.ts"() {
|
|
193
|
+
init_verdict_line();
|
|
174
194
|
DEFAULT_RENDER_CHECK_HOST_BOUNDS = {
|
|
175
195
|
timeoutMs: 3e4,
|
|
176
196
|
heapMb: 512,
|
|
@@ -698,6 +718,165 @@ function editableSurface(el) {
|
|
|
698
718
|
function parentLink(node) {
|
|
699
719
|
return node;
|
|
700
720
|
}
|
|
721
|
+
|
|
722
|
+
// src/harness/check/runtime-render/probe-pending.ts
|
|
723
|
+
function createProbePendingSource() {
|
|
724
|
+
const pending = /* @__PURE__ */ new Set();
|
|
725
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
726
|
+
const notify = () => {
|
|
727
|
+
for (const listener of [...listeners]) listener();
|
|
728
|
+
};
|
|
729
|
+
return {
|
|
730
|
+
isPending: (actionName) => pending.has(actionName),
|
|
731
|
+
subscribe: (listener) => {
|
|
732
|
+
listeners.add(listener);
|
|
733
|
+
return () => {
|
|
734
|
+
listeners.delete(listener);
|
|
735
|
+
};
|
|
736
|
+
},
|
|
737
|
+
mark: (actionName) => {
|
|
738
|
+
pending.add(actionName);
|
|
739
|
+
notify();
|
|
740
|
+
},
|
|
741
|
+
clear: () => {
|
|
742
|
+
if (pending.size === 0) return;
|
|
743
|
+
pending.clear();
|
|
744
|
+
notify();
|
|
745
|
+
}
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
function lookOf(el) {
|
|
749
|
+
return {
|
|
750
|
+
connected: el.isConnected !== false,
|
|
751
|
+
// React renders `disabled={true}` as `disabled=""`, so presence is the test.
|
|
752
|
+
disabled: el.getAttribute("disabled") !== null || el.getAttribute("aria-disabled") === "true",
|
|
753
|
+
ariaBusy: el.getAttribute("aria-busy") === "true",
|
|
754
|
+
text: el.tagName?.toLowerCase() === "input" ? String(typeof el.value === "string" ? el.value : el.getAttribute("value") ?? "") : el.textContent ?? ""
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
async function settlesWithin(ms, work) {
|
|
758
|
+
let timer;
|
|
759
|
+
const expired = new Promise((resolve) => {
|
|
760
|
+
timer = setTimeout(() => resolve(false), Math.max(0, ms));
|
|
761
|
+
});
|
|
762
|
+
try {
|
|
763
|
+
return await Promise.race([work.then(() => true), expired]);
|
|
764
|
+
} finally {
|
|
765
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
function lookChanged(before, after) {
|
|
769
|
+
if (before.connected && !after.connected) return true;
|
|
770
|
+
if (!before.disabled && after.disabled) return true;
|
|
771
|
+
if (!before.ariaBusy && after.ariaBusy) return true;
|
|
772
|
+
return before.text.trim() !== after.text.trim();
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// src/harness/check/runtime-render/walk.ts
|
|
776
|
+
var LAST_PRESSED_KEPT = 4;
|
|
777
|
+
function isDisabled(el) {
|
|
778
|
+
return el.getAttribute("disabled") !== null || el.getAttribute("aria-disabled") === "true";
|
|
779
|
+
}
|
|
780
|
+
function labelOf(el) {
|
|
781
|
+
const named = el.getAttribute("aria-label") ?? el.getAttribute("placeholder");
|
|
782
|
+
return (named ?? el.textContent ?? "").replace(/\s+/g, " ").trim().slice(0, 60);
|
|
783
|
+
}
|
|
784
|
+
function screenFingerprint(container) {
|
|
785
|
+
const parts = [];
|
|
786
|
+
for (const el of container.querySelectorAll(
|
|
787
|
+
'button, [role="button"], a[href], input, select, textarea, form'
|
|
788
|
+
)) {
|
|
789
|
+
parts.push(
|
|
790
|
+
[el.tagName.toLowerCase(), el.getAttribute("type") ?? "", labelOf(el), isDisabled(el) ? "disabled" : ""].join("|")
|
|
791
|
+
);
|
|
792
|
+
}
|
|
793
|
+
return parts.join("\n");
|
|
794
|
+
}
|
|
795
|
+
function keyed(controls, describe) {
|
|
796
|
+
const seen = /* @__PURE__ */ new Map();
|
|
797
|
+
const out = /* @__PURE__ */ new Map();
|
|
798
|
+
for (const c of controls) {
|
|
799
|
+
const d = describe(c.element);
|
|
800
|
+
const n = seen.get(d) ?? 0;
|
|
801
|
+
seen.set(d, n + 1);
|
|
802
|
+
out.set(`${d}#${n}`, c);
|
|
803
|
+
}
|
|
804
|
+
return out;
|
|
805
|
+
}
|
|
806
|
+
function firstStepToUnpressed(screens, from) {
|
|
807
|
+
const firstStep = /* @__PURE__ */ new Map([[from, void 0]]);
|
|
808
|
+
const queue = [from];
|
|
809
|
+
while (queue.length > 0) {
|
|
810
|
+
const at = queue.shift();
|
|
811
|
+
const screen = screens.get(at);
|
|
812
|
+
if (screen === void 0) continue;
|
|
813
|
+
if (at !== from && screen.order.some((k) => !screen.led.has(k))) return firstStep.get(at);
|
|
814
|
+
for (const [key, to] of screen.led) {
|
|
815
|
+
if (to === at || firstStep.has(to)) continue;
|
|
816
|
+
firstStep.set(to, firstStep.get(at) ?? key);
|
|
817
|
+
queue.push(to);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
return void 0;
|
|
821
|
+
}
|
|
822
|
+
async function walkToAction(input) {
|
|
823
|
+
const lastPressed = [];
|
|
824
|
+
const screenTexts = [];
|
|
825
|
+
const screens = /* @__PURE__ */ new Map();
|
|
826
|
+
let presses = 0;
|
|
827
|
+
let namedControlSeen = false;
|
|
828
|
+
const outcome = (stoppedBy, extra = {}) => ({
|
|
829
|
+
fired: false,
|
|
830
|
+
presses,
|
|
831
|
+
screens: screens.size,
|
|
832
|
+
stoppedBy,
|
|
833
|
+
namedControlSeen,
|
|
834
|
+
lastPressed: [...lastPressed],
|
|
835
|
+
screenTexts: [...screenTexts],
|
|
836
|
+
...extra
|
|
837
|
+
});
|
|
838
|
+
const mounted = await input.mount();
|
|
839
|
+
if (mounted === void 0) return outcome("no-mount");
|
|
840
|
+
const { container } = mounted;
|
|
841
|
+
const arrive = () => {
|
|
842
|
+
mounted.prime();
|
|
843
|
+
const controls = keyed(input.controls(container), input.describe);
|
|
844
|
+
if ([...controls.values()].some((c) => c.namesAction)) namedControlSeen = true;
|
|
845
|
+
const id = screenFingerprint(container);
|
|
846
|
+
if (!screens.has(id)) {
|
|
847
|
+
screens.set(id, { order: [...controls.keys()], led: /* @__PURE__ */ new Map() });
|
|
848
|
+
screenTexts.push(container.textContent ?? "");
|
|
849
|
+
}
|
|
850
|
+
return { id, controls };
|
|
851
|
+
};
|
|
852
|
+
let here = arrive();
|
|
853
|
+
const first = here.id;
|
|
854
|
+
for (; ; ) {
|
|
855
|
+
const screen = screens.get(here.id);
|
|
856
|
+
const onOpenFirstScreen = here.id === first && screen.order.some((k) => !screen.led.has(k));
|
|
857
|
+
if (!onOpenFirstScreen) {
|
|
858
|
+
if (presses >= input.maxPresses) return outcome("press-cap");
|
|
859
|
+
if (Date.now() >= input.stopAt) return outcome("deadline");
|
|
860
|
+
}
|
|
861
|
+
const key = screen.order.find((k) => !screen.led.has(k)) ?? firstStepToUnpressed(screens, here.id);
|
|
862
|
+
if (key === void 0) return outcome("explored");
|
|
863
|
+
const control = here.controls.get(key);
|
|
864
|
+
if (control === void 0) {
|
|
865
|
+
screen.led.set(key, here.id);
|
|
866
|
+
continue;
|
|
867
|
+
}
|
|
868
|
+
presses += 1;
|
|
869
|
+
lastPressed.push(input.describe(control.element));
|
|
870
|
+
if (lastPressed.length > LAST_PRESSED_KEPT) lastPressed.shift();
|
|
871
|
+
if (await input.press(control)) {
|
|
872
|
+
return outcome("dispatched", { fired: true, element: control.element, container });
|
|
873
|
+
}
|
|
874
|
+
here = arrive();
|
|
875
|
+
if (!screen.led.has(key)) screen.led.set(key, here.id);
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
// src/harness/check/runtime-render/render-check.ts
|
|
701
880
|
var RENDER_CHECK_KINDS = [
|
|
702
881
|
"render-no-throw",
|
|
703
882
|
"prop-sensitivity",
|
|
@@ -707,6 +886,10 @@ var RENDER_CHECK_KINDS = [
|
|
|
707
886
|
"optional-props-omitted",
|
|
708
887
|
"stream-rerender"
|
|
709
888
|
];
|
|
889
|
+
var PENDING_PHASE_RESERVE_MS = 5e3;
|
|
890
|
+
var WALK_MAX_PRESSES = 60;
|
|
891
|
+
var WALK_ACTION_CAP_MS = 3e3;
|
|
892
|
+
var WALK_DEADLINE_MARGIN_MS = 2e3;
|
|
710
893
|
async function runRenderCheck(input, options = {}) {
|
|
711
894
|
const g = hostGlobals();
|
|
712
895
|
if ("window" in g && "document" in g) {
|
|
@@ -772,7 +955,9 @@ async function runRenderCheckInProcess(input) {
|
|
|
772
955
|
const wireConfig = createProbeWireConfig(probe);
|
|
773
956
|
uninstallSpy = probe.installPostMessageSpy();
|
|
774
957
|
installProductionActShim();
|
|
775
|
-
const { render, cleanup } = await import('@testing-library/react');
|
|
958
|
+
const { render, cleanup, act } = await import('@testing-library/react');
|
|
959
|
+
const { ActionPendingContext } = await import('@ggui-ai/wire/internal');
|
|
960
|
+
const pendingSource = createProbePendingSource();
|
|
776
961
|
const userEventModule = await import('@testing-library/user-event');
|
|
777
962
|
const userEvent = userEventModule.default ?? userEventModule;
|
|
778
963
|
const originalConsoleError = console.error;
|
|
@@ -809,7 +994,8 @@ async function runRenderCheckInProcess(input) {
|
|
|
809
994
|
};
|
|
810
995
|
const boundaryRef = {
|
|
811
996
|
stack: null,
|
|
812
|
-
error: null
|
|
997
|
+
error: null,
|
|
998
|
+
catches: 0
|
|
813
999
|
};
|
|
814
1000
|
class ProbeErrorBoundary extends React.Component {
|
|
815
1001
|
state = { caught: false };
|
|
@@ -817,6 +1003,7 @@ async function runRenderCheckInProcess(input) {
|
|
|
817
1003
|
return { caught: true };
|
|
818
1004
|
}
|
|
819
1005
|
componentDidCatch(error, errorInfo) {
|
|
1006
|
+
boundaryRef.catches += 1;
|
|
820
1007
|
if (errorInfo.componentStack && !boundaryRef.stack) {
|
|
821
1008
|
boundaryRef.stack = errorInfo.componentStack;
|
|
822
1009
|
}
|
|
@@ -829,19 +1016,19 @@ async function runRenderCheckInProcess(input) {
|
|
|
829
1016
|
return this.props.children;
|
|
830
1017
|
}
|
|
831
1018
|
}
|
|
1019
|
+
const cardTree = () => React.createElement(GguiWireProvider, {
|
|
1020
|
+
config: wireConfig,
|
|
1021
|
+
children: React.createElement(ProbeErrorBoundary, {
|
|
1022
|
+
children: React.createElement(ActionPendingContext.Provider, {
|
|
1023
|
+
value: pendingSource,
|
|
1024
|
+
children: React.createElement(Component, input.mockupProps)
|
|
1025
|
+
})
|
|
1026
|
+
})
|
|
1027
|
+
});
|
|
832
1028
|
let renderResult;
|
|
833
1029
|
const RENDER_TIMEOUT_MS = 5e3;
|
|
834
1030
|
try {
|
|
835
|
-
const renderPromise = Promise.resolve().then(
|
|
836
|
-
() => render(
|
|
837
|
-
React.createElement(GguiWireProvider, {
|
|
838
|
-
config: wireConfig,
|
|
839
|
-
children: React.createElement(ProbeErrorBoundary, {
|
|
840
|
-
children: React.createElement(Component, input.mockupProps)
|
|
841
|
-
})
|
|
842
|
-
})
|
|
843
|
-
)
|
|
844
|
-
);
|
|
1031
|
+
const renderPromise = Promise.resolve().then(() => render(cardTree()));
|
|
845
1032
|
const timeoutPromise = new Promise((_, reject) => {
|
|
846
1033
|
setTimeout(
|
|
847
1034
|
() => reject(
|
|
@@ -893,6 +1080,9 @@ async function runRenderCheckInProcess(input) {
|
|
|
893
1080
|
const user = buildClickUser(userEvent, hostGlobals().document);
|
|
894
1081
|
let actionsChecked = 0;
|
|
895
1082
|
let streamsChecked = 0;
|
|
1083
|
+
let pendingTally;
|
|
1084
|
+
const pendingTargets = [];
|
|
1085
|
+
const walkedTexts = [];
|
|
896
1086
|
try {
|
|
897
1087
|
let inputPriming;
|
|
898
1088
|
try {
|
|
@@ -909,15 +1099,42 @@ async function runRenderCheckInProcess(input) {
|
|
|
909
1099
|
hookName: "useAction",
|
|
910
1100
|
hookArg: name
|
|
911
1101
|
});
|
|
1102
|
+
const walkPriming = {};
|
|
1103
|
+
const mount = async () => {
|
|
1104
|
+
let mounted;
|
|
1105
|
+
try {
|
|
1106
|
+
mounted = render(cardTree());
|
|
1107
|
+
} catch {
|
|
1108
|
+
return void 0;
|
|
1109
|
+
}
|
|
1110
|
+
await flushPromises();
|
|
1111
|
+
const root = mounted.container;
|
|
1112
|
+
return {
|
|
1113
|
+
container: toMinimalElement(root),
|
|
1114
|
+
prime: () => {
|
|
1115
|
+
try {
|
|
1116
|
+
primeInputs(root);
|
|
1117
|
+
} catch (err) {
|
|
1118
|
+
walkPriming.error ??= err instanceof Error ? err.message : String(err);
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
};
|
|
1122
|
+
};
|
|
912
1123
|
const issue = await checkActionWiring({
|
|
913
|
-
|
|
1124
|
+
mount,
|
|
914
1125
|
actionName: name,
|
|
915
1126
|
actionLabel: entry.label,
|
|
916
1127
|
wiring,
|
|
917
1128
|
probe,
|
|
918
|
-
user
|
|
1129
|
+
user,
|
|
1130
|
+
pendingTargets,
|
|
1131
|
+
stopAt: walkStopAt(input.deadlineAt),
|
|
1132
|
+
walkedTexts
|
|
919
1133
|
});
|
|
920
|
-
if (issue)
|
|
1134
|
+
if (issue) {
|
|
1135
|
+
const priming = "primed" in inputPriming && walkPriming.error !== void 0 ? { error: `on a screen the action walk reached: ${walkPriming.error}` } : inputPriming;
|
|
1136
|
+
issues.push({ ...issue, diagnostics: { ...issue.diagnostics, inputPriming: priming } });
|
|
1137
|
+
}
|
|
921
1138
|
}
|
|
922
1139
|
}
|
|
923
1140
|
if (input.contract?.actionSpec) {
|
|
@@ -939,6 +1156,7 @@ async function runRenderCheckInProcess(input) {
|
|
|
939
1156
|
if (input.contract?.propsSpec) {
|
|
940
1157
|
for (const issue of checkPropCoverage({
|
|
941
1158
|
container,
|
|
1159
|
+
walkedTexts,
|
|
942
1160
|
propsSpec: input.contract.propsSpec,
|
|
943
1161
|
mockupProps: input.mockupProps
|
|
944
1162
|
})) {
|
|
@@ -1044,10 +1262,21 @@ async function runRenderCheckInProcess(input) {
|
|
|
1044
1262
|
}
|
|
1045
1263
|
}
|
|
1046
1264
|
}
|
|
1265
|
+
const timeLeft = input.deadlineAt === void 0 ? Number.POSITIVE_INFINITY : input.deadlineAt - Date.now();
|
|
1266
|
+
if (input.contract?.actionSpec && timeLeft >= PENDING_PHASE_RESERVE_MS) {
|
|
1267
|
+
pendingTally = await runPendingPhase({
|
|
1268
|
+
targets: pendingTargets,
|
|
1269
|
+
source: pendingSource,
|
|
1270
|
+
act,
|
|
1271
|
+
catches: () => boundaryRef.catches,
|
|
1272
|
+
actions: Object.entries(input.contract.actionSpec).map(([name, entry]) => ({ name, label: entry.label })),
|
|
1273
|
+
...input.deadlineAt !== void 0 ? { deadlineAt: input.deadlineAt } : {}
|
|
1274
|
+
});
|
|
1275
|
+
}
|
|
1047
1276
|
} finally {
|
|
1048
1277
|
cleanup();
|
|
1049
1278
|
}
|
|
1050
|
-
return finalize(issues, t0, actionsChecked, streamsChecked);
|
|
1279
|
+
return finalize(issues, t0, actionsChecked, streamsChecked, pendingTally);
|
|
1051
1280
|
} finally {
|
|
1052
1281
|
try {
|
|
1053
1282
|
uninstallSpy?.();
|
|
@@ -1259,7 +1488,7 @@ async function setupHappyDom() {
|
|
|
1259
1488
|
delete g.IS_REACT_ACT_ENVIRONMENT;
|
|
1260
1489
|
};
|
|
1261
1490
|
}
|
|
1262
|
-
function finalize(issues, t0, actionsChecked, streamsChecked) {
|
|
1491
|
+
function finalize(issues, t0, actionsChecked, streamsChecked, pendingTally) {
|
|
1263
1492
|
const ok = !issues.some((i) => i.outcome === "failed");
|
|
1264
1493
|
return {
|
|
1265
1494
|
ok,
|
|
@@ -1267,12 +1496,86 @@ function finalize(issues, t0, actionsChecked, streamsChecked) {
|
|
|
1267
1496
|
stats: {
|
|
1268
1497
|
actionsChecked,
|
|
1269
1498
|
streamsChecked,
|
|
1270
|
-
renderMs: Date.now() - t0
|
|
1499
|
+
renderMs: Date.now() - t0,
|
|
1500
|
+
...pendingTally !== void 0 ? {
|
|
1501
|
+
pendingAffordance: {
|
|
1502
|
+
dispatched: pendingTally.dispatched,
|
|
1503
|
+
visible: pendingTally.visible,
|
|
1504
|
+
missing: [...pendingTally.missing],
|
|
1505
|
+
gone: [...pendingTally.gone]
|
|
1506
|
+
}
|
|
1507
|
+
} : {}
|
|
1271
1508
|
}
|
|
1272
1509
|
};
|
|
1273
1510
|
}
|
|
1511
|
+
var PENDING_TARGET_CAP_MS = 2e3;
|
|
1512
|
+
var PENDING_PHASE_TAIL_MS = 1e3;
|
|
1513
|
+
async function runPendingPhase(input) {
|
|
1514
|
+
const tally = { dispatched: 0, visible: 0, missing: [], gone: [] };
|
|
1515
|
+
let stopped = false;
|
|
1516
|
+
for (const target of input.targets) {
|
|
1517
|
+
const budget = Math.min(
|
|
1518
|
+
PENDING_TARGET_CAP_MS,
|
|
1519
|
+
input.deadlineAt === void 0 ? Number.POSITIVE_INFINITY : input.deadlineAt - Date.now() - PENDING_PHASE_TAIL_MS
|
|
1520
|
+
);
|
|
1521
|
+
const found = stopped || budget <= 0 ? void 0 : pendingControlOf(target, input.actions);
|
|
1522
|
+
if (found === void 0) {
|
|
1523
|
+
tally.gone.push(target.actionName);
|
|
1524
|
+
if (budget <= 0) stopped = true;
|
|
1525
|
+
continue;
|
|
1526
|
+
}
|
|
1527
|
+
const el = controlToRead(found, target);
|
|
1528
|
+
const before = lookOf(el);
|
|
1529
|
+
const caughtBefore = input.catches();
|
|
1530
|
+
const settled = await settlesWithin(
|
|
1531
|
+
budget,
|
|
1532
|
+
(async () => {
|
|
1533
|
+
await input.act(async () => {
|
|
1534
|
+
input.source.mark(target.actionName);
|
|
1535
|
+
});
|
|
1536
|
+
await flushPromises();
|
|
1537
|
+
})()
|
|
1538
|
+
);
|
|
1539
|
+
const crashed = input.catches() !== caughtBefore;
|
|
1540
|
+
tally.dispatched += 1;
|
|
1541
|
+
if (settled && !crashed && lookChanged(before, lookOf(el))) tally.visible += 1;
|
|
1542
|
+
else tally.missing.push(target.actionName);
|
|
1543
|
+
if (!settled) {
|
|
1544
|
+
stopped = true;
|
|
1545
|
+
continue;
|
|
1546
|
+
}
|
|
1547
|
+
await input.act(async () => {
|
|
1548
|
+
input.source.clear();
|
|
1549
|
+
});
|
|
1550
|
+
await flushPromises();
|
|
1551
|
+
if (crashed) stopped = true;
|
|
1552
|
+
}
|
|
1553
|
+
return tally;
|
|
1554
|
+
}
|
|
1555
|
+
function pendingControlOf(target, actions) {
|
|
1556
|
+
const movedToAnother = !matchesActionIdentity(target.element, target.actionName, target.actionLabel) && actions.some((a) => a.name !== target.actionName && matchesActionIdentity(target.element, a.name, a.label));
|
|
1557
|
+
if (lookOf(target.element).connected && !movedToAnother) return target.element;
|
|
1558
|
+
if (target.wiringKind !== "click") return void 0;
|
|
1559
|
+
return findActionElements(target.container, target.actionName, target.actionLabel, { identityOnly: true })[0];
|
|
1560
|
+
}
|
|
1561
|
+
function controlToRead(el, target) {
|
|
1562
|
+
if (el.tagName.toLowerCase() !== "form") return el;
|
|
1563
|
+
for (const c of el.querySelectorAll('button, input[type="submit"], input[type="button"], [role="button"]')) {
|
|
1564
|
+
if (matchesActionIdentity(c, target.actionName, target.actionLabel)) return c;
|
|
1565
|
+
}
|
|
1566
|
+
for (const submitter of el.querySelectorAll(
|
|
1567
|
+
'button[type="submit"], input[type="submit"], button:not([type])'
|
|
1568
|
+
)) {
|
|
1569
|
+
return submitter;
|
|
1570
|
+
}
|
|
1571
|
+
return el;
|
|
1572
|
+
}
|
|
1573
|
+
function walkStopAt(deadlineAt) {
|
|
1574
|
+
const cap = Date.now() + WALK_ACTION_CAP_MS;
|
|
1575
|
+
return deadlineAt === void 0 ? cap : Math.min(cap, deadlineAt - PENDING_PHASE_RESERVE_MS - WALK_DEADLINE_MARGIN_MS);
|
|
1576
|
+
}
|
|
1274
1577
|
async function checkActionWiring(input) {
|
|
1275
|
-
const {
|
|
1578
|
+
const { actionName, actionLabel, wiring, probe, user } = input;
|
|
1276
1579
|
const baseDiagnostics = {
|
|
1277
1580
|
observedJsxElements: wiring.observedJsxElements,
|
|
1278
1581
|
observedNativeProps: wiring.observedNativeProps,
|
|
@@ -1296,25 +1599,56 @@ async function checkActionWiring(input) {
|
|
|
1296
1599
|
diagnostics: baseDiagnostics
|
|
1297
1600
|
};
|
|
1298
1601
|
}
|
|
1299
|
-
const
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1602
|
+
const kind = wiring.kind;
|
|
1603
|
+
const walk = await walkToAction({
|
|
1604
|
+
mount: input.mount,
|
|
1605
|
+
controls: (container) => actionControls(container, kind, actionName, actionLabel),
|
|
1606
|
+
press: async (control) => {
|
|
1607
|
+
const before = probe.getFireLog().length;
|
|
1608
|
+
try {
|
|
1609
|
+
await dispatchTrigger(control.element, control.how, user);
|
|
1610
|
+
} catch {
|
|
1611
|
+
return false;
|
|
1612
|
+
}
|
|
1613
|
+
await flushPromises();
|
|
1614
|
+
return probe.getFireLog().slice(before).some((e) => e.kind === "action.fired" && e.name === actionName);
|
|
1615
|
+
},
|
|
1616
|
+
describe: describeElement,
|
|
1617
|
+
stopAt: input.stopAt,
|
|
1618
|
+
maxPresses: WALK_MAX_PRESSES
|
|
1307
1619
|
});
|
|
1308
|
-
|
|
1620
|
+
input.walkedTexts.push(...walk.screenTexts);
|
|
1621
|
+
if (walk.fired) {
|
|
1622
|
+
if (input.pendingTargets && walk.element && walk.container && (kind === "click" || kind === "submit")) {
|
|
1623
|
+
input.pendingTargets.push({
|
|
1624
|
+
actionName,
|
|
1625
|
+
actionLabel,
|
|
1626
|
+
wiringKind: kind,
|
|
1627
|
+
element: walk.element,
|
|
1628
|
+
container: walk.container
|
|
1629
|
+
});
|
|
1630
|
+
}
|
|
1631
|
+
return null;
|
|
1632
|
+
}
|
|
1633
|
+
const allFired = probe.getFireLog().filter((e) => e.kind === "action.fired").map((e) => e.name);
|
|
1634
|
+
const otherActionsFired = Array.from(new Set(allFired)).filter((n) => n !== actionName);
|
|
1635
|
+
const stoppedBy = walk.stoppedBy === "dispatched" ? "explored" : walk.stoppedBy;
|
|
1309
1636
|
return {
|
|
1310
1637
|
check: "action-wiring",
|
|
1311
1638
|
outcome: "unverified",
|
|
1312
1639
|
subject: actionName,
|
|
1313
|
-
reason: `Source confirms ${
|
|
1314
|
-
elementHint:
|
|
1640
|
+
reason: `Source confirms ${kind} wiring exists for action '${actionName}', but synthetic ${kind} did not dispatch it. Likely cause: conditional rendering, required input/form fill, or a multi-step interaction the static probe cannot complete.`,
|
|
1641
|
+
...walk.presses === 0 && stoppedBy === "explored" ? { elementHint: `No ${kind}-eligible element found in DOM` } : {},
|
|
1315
1642
|
diagnostics: {
|
|
1316
1643
|
...baseDiagnostics,
|
|
1317
|
-
actionsFiredFromClicks:
|
|
1644
|
+
actionsFiredFromClicks: otherActionsFired,
|
|
1645
|
+
walk: {
|
|
1646
|
+
presses: walk.presses,
|
|
1647
|
+
screens: walk.screens,
|
|
1648
|
+
stoppedBy,
|
|
1649
|
+
namedControlSeen: walk.namedControlSeen,
|
|
1650
|
+
lastPressed: walk.lastPressed
|
|
1651
|
+
}
|
|
1318
1652
|
}
|
|
1319
1653
|
};
|
|
1320
1654
|
}
|
|
@@ -1382,32 +1716,6 @@ async function checkSelectionIdentity(input) {
|
|
|
1382
1716
|
}
|
|
1383
1717
|
return null;
|
|
1384
1718
|
}
|
|
1385
|
-
async function simulateAndCheck(input) {
|
|
1386
|
-
const { container, user, probe, wiringKind, actionName, actionLabel, eventKind } = input;
|
|
1387
|
-
const candidates = findCandidateElements(container, wiringKind, actionName, actionLabel);
|
|
1388
|
-
if (candidates.length === 0) {
|
|
1389
|
-
return { fired: false, attemptedHint: `No ${wiringKind}-eligible element found in DOM` };
|
|
1390
|
-
}
|
|
1391
|
-
for (const el of candidates) {
|
|
1392
|
-
const before = probe.getFireLog().length;
|
|
1393
|
-
try {
|
|
1394
|
-
await dispatchTrigger(el, wiringKind, user);
|
|
1395
|
-
} catch {
|
|
1396
|
-
continue;
|
|
1397
|
-
}
|
|
1398
|
-
await flushPromises();
|
|
1399
|
-
const newEvents = probe.getFireLog().slice(before);
|
|
1400
|
-
const matched = newEvents.some((e) => e.kind === eventKind && e.name === actionName);
|
|
1401
|
-
if (matched) return { fired: true };
|
|
1402
|
-
}
|
|
1403
|
-
const allFired = probe.getFireLog().filter((e) => e.kind === "action.fired").map((e) => e.name);
|
|
1404
|
-
const otherActionsFired = Array.from(new Set(allFired)).filter((n) => n !== actionName);
|
|
1405
|
-
return {
|
|
1406
|
-
fired: false,
|
|
1407
|
-
attemptedHint: candidates.length ? describeElement(candidates[0]) : void 0,
|
|
1408
|
-
otherActionsFired
|
|
1409
|
-
};
|
|
1410
|
-
}
|
|
1411
1719
|
async function dispatchTrigger(el, kind, user) {
|
|
1412
1720
|
switch (kind) {
|
|
1413
1721
|
case "click":
|
|
@@ -1493,40 +1801,40 @@ function closestForm(el) {
|
|
|
1493
1801
|
}
|
|
1494
1802
|
return null;
|
|
1495
1803
|
}
|
|
1496
|
-
function
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
const all = container.querySelectorAll(
|
|
1502
|
-
'form, button[type="submit"], input[type="submit"]'
|
|
1503
|
-
);
|
|
1504
|
-
return Array.from(all);
|
|
1505
|
-
}
|
|
1506
|
-
case "change": {
|
|
1507
|
-
const all = container.querySelectorAll("select, input, textarea");
|
|
1508
|
-
return Array.from(all);
|
|
1509
|
-
}
|
|
1510
|
-
case "keyboard-enter":
|
|
1511
|
-
return findActionElements(container, name, label);
|
|
1804
|
+
function actionControls(container, kind, name, label) {
|
|
1805
|
+
const clickables = findActionElements(container, name, label);
|
|
1806
|
+
const named = (el) => matchesActionIdentity(el, name, label);
|
|
1807
|
+
if (kind === "click" || kind === "keyboard-enter") {
|
|
1808
|
+
return clickables.map((element) => ({ element, namesAction: named(element), how: kind }));
|
|
1512
1809
|
}
|
|
1810
|
+
const eligible = Array.from(
|
|
1811
|
+
container.querySelectorAll(
|
|
1812
|
+
kind === "submit" ? 'form, button[type="submit"], input[type="submit"]' : "select, input, textarea"
|
|
1813
|
+
)
|
|
1814
|
+
);
|
|
1815
|
+
const seen = new Set(eligible);
|
|
1816
|
+
return [
|
|
1817
|
+
...eligible.map((element) => ({ element, namesAction: named(element), how: kind })),
|
|
1818
|
+
...clickables.filter((el) => !seen.has(el)).map((element) => ({ element, namesAction: named(element), how: "click" }))
|
|
1819
|
+
];
|
|
1513
1820
|
}
|
|
1514
1821
|
function checkPropCoverage(input) {
|
|
1515
|
-
const { container, propsSpec, mockupProps } = input;
|
|
1822
|
+
const { container, walkedTexts, propsSpec, mockupProps } = input;
|
|
1516
1823
|
const issues = [];
|
|
1517
|
-
const
|
|
1824
|
+
const texts = [container.textContent ?? "", ...walkedTexts];
|
|
1518
1825
|
for (const [propName, entry] of Object.entries(propsSpec.properties)) {
|
|
1519
1826
|
if (!entry.required) continue;
|
|
1520
1827
|
const value = mockupProps[propName];
|
|
1521
1828
|
if (value === void 0 || value === null) continue;
|
|
1522
1829
|
const marker = pickScalarMarker(value);
|
|
1523
1830
|
if (marker === null) continue;
|
|
1524
|
-
if (!text.includes(marker)) {
|
|
1831
|
+
if (!texts.some((text) => text.includes(marker))) {
|
|
1832
|
+
const where = walkedTexts.length > 0 ? ", on the first screen or any screen the action walk reached" : "";
|
|
1525
1833
|
issues.push({
|
|
1526
1834
|
check: "prop-coverage",
|
|
1527
1835
|
outcome: "unverified",
|
|
1528
1836
|
subject: propName,
|
|
1529
|
-
reason: `Required prop '${propName}' value (${JSON.stringify(marker).slice(0, 60)}) not visible in rendered DOM`
|
|
1837
|
+
reason: `Required prop '${propName}' value (${JSON.stringify(marker).slice(0, 60)}) not visible in rendered DOM${where}`
|
|
1530
1838
|
});
|
|
1531
1839
|
}
|
|
1532
1840
|
}
|
|
@@ -1566,26 +1874,28 @@ async function checkStreamRerender(input) {
|
|
|
1566
1874
|
}
|
|
1567
1875
|
return null;
|
|
1568
1876
|
}
|
|
1569
|
-
function
|
|
1877
|
+
function matchesActionIdentity(el, name, label) {
|
|
1878
|
+
const norm = (s) => s.toLowerCase().replace(/[\s_-]+/g, "");
|
|
1879
|
+
const aria = (el.getAttribute("aria-label") ?? "").toLowerCase();
|
|
1880
|
+
const txt = (el.textContent ?? "").toLowerCase();
|
|
1881
|
+
const dataAction = (el.getAttribute("data-action") ?? "").toLowerCase();
|
|
1882
|
+
return aria.includes(label.toLowerCase()) || txt.includes(label.toLowerCase()) || norm(aria).includes(norm(name)) || norm(txt).includes(norm(name)) || dataAction === name.toLowerCase() || norm(txt).includes(norm(label));
|
|
1883
|
+
}
|
|
1884
|
+
function findActionElements(container, name, label, options = {}) {
|
|
1570
1885
|
const candidates = [];
|
|
1571
1886
|
const seen = /* @__PURE__ */ new Set();
|
|
1572
|
-
const norm = (s) => s.toLowerCase().replace(/[\s_-]+/g, "");
|
|
1573
|
-
const nameKey = norm(name);
|
|
1574
|
-
const labelKey = norm(label);
|
|
1575
1887
|
const allClickable = container.querySelectorAll(
|
|
1576
1888
|
'button, [role="button"], input[type="submit"], input[type="button"], a[href]'
|
|
1577
1889
|
);
|
|
1578
1890
|
for (const el of allClickable) {
|
|
1579
|
-
|
|
1580
|
-
const txt = (el.textContent ?? "").toLowerCase();
|
|
1581
|
-
const dataAction = (el.getAttribute("data-action") ?? "").toLowerCase();
|
|
1582
|
-
if (aria.includes(label.toLowerCase()) || txt.includes(label.toLowerCase()) || norm(aria).includes(nameKey) || norm(txt).includes(nameKey) || dataAction === name.toLowerCase() || norm(txt).includes(labelKey)) {
|
|
1891
|
+
if (matchesActionIdentity(el, name, label)) {
|
|
1583
1892
|
if (!seen.has(el)) {
|
|
1584
1893
|
seen.add(el);
|
|
1585
1894
|
candidates.push(el);
|
|
1586
1895
|
}
|
|
1587
1896
|
}
|
|
1588
1897
|
}
|
|
1898
|
+
if (options.identityOnly) return candidates;
|
|
1589
1899
|
for (const el of allClickable) {
|
|
1590
1900
|
if (!seen.has(el)) {
|
|
1591
1901
|
seen.add(el);
|
|
@@ -1856,7 +2166,9 @@ function createRuntimeRenderCheck(config = {}) {
|
|
|
1856
2166
|
elapsedMs: Date.now() - t0,
|
|
1857
2167
|
...queuedField(queuedMs),
|
|
1858
2168
|
renderMs: result.stats.renderMs,
|
|
1859
|
-
...load
|
|
2169
|
+
...load,
|
|
2170
|
+
// ggui#1398: report-only — carried to the probe's meta for the bench, never an issue.
|
|
2171
|
+
...result.stats.pendingAffordance !== void 0 ? { pendingAffordance: result.stats.pendingAffordance } : {}
|
|
1860
2172
|
};
|
|
1861
2173
|
}
|
|
1862
2174
|
};
|
|
@@ -1933,6 +2245,18 @@ function toEvalIssue(issue) {
|
|
|
1933
2245
|
"primed" in diag.inputPriming ? `input priming: ${diag.inputPriming.primed} control(s) filled before the click` : `input priming failed: ${diag.inputPriming.error}`
|
|
1934
2246
|
);
|
|
1935
2247
|
}
|
|
2248
|
+
if (diag?.walk) {
|
|
2249
|
+
const w = diag.walk;
|
|
2250
|
+
const stopped = {
|
|
2251
|
+
explored: "every reachable control pressed",
|
|
2252
|
+
"press-cap": "stopped at the press cap",
|
|
2253
|
+
deadline: "stopped at the time budget",
|
|
2254
|
+
"no-mount": "the card could not be mounted for the walk"
|
|
2255
|
+
}[w.stoppedBy];
|
|
2256
|
+
const named = w.namedControlSeen ? "a control naming the action rendered, but pressing it did not dispatch the action" : "no screen rendered a control naming the action";
|
|
2257
|
+
const last = w.lastPressed.length ? `; last pressed: ${w.lastPressed.join(", ")}` : "";
|
|
2258
|
+
diagParts.push(`walk: ${w.presses} press(es) across ${w.screens} screen(s), ${stopped}; ${named}${last}`);
|
|
2259
|
+
}
|
|
1936
2260
|
const diagSuffix = diagParts.length ? ` [observed: ${diagParts.join("; ")}]` : "";
|
|
1937
2261
|
switch (issue.check) {
|
|
1938
2262
|
case "render-no-throw":
|