@ggui-ai/ui-visual-tester 0.17.0 → 0.18.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/dist/fixture-runtime.js +83 -11
- package/fixture/fixture-runtime.js +64 -11
- package/package.json +4 -4
package/dist/fixture-runtime.js
CHANGED
|
@@ -82,6 +82,12 @@ const CLICKABLE_SELECTOR = 'button, [role="button"], input[type="submit"], input
|
|
|
82
82
|
// parent <details> — a DOM change that says nothing about the action).
|
|
83
83
|
/** Per-control wait in the fallback pass — enough for a synchronous dispatch + a render. */
|
|
84
84
|
const FALLBACK_CLICK_WAIT_MS = 600;
|
|
85
|
+
/**
|
|
86
|
+
* #1040: how many times ONE control whose click revealed new controls (a wizard's Next, a tab) may be
|
|
87
|
+
* clicked again while the action's own control is still not visible. Bounds a walk through a
|
|
88
|
+
* multi-step form; a toggle that keeps revealing and hiding a panel cannot spin the probe.
|
|
89
|
+
*/
|
|
90
|
+
const MAX_NAVIGATOR_RECLICKS = 8;
|
|
85
91
|
/**
|
|
86
92
|
* ONE deadline covers both passes for an action: max(the caller's wait, this
|
|
87
93
|
* floor). A few loosely-matching decoys can no longer run an action for
|
|
@@ -99,9 +105,13 @@ function norm(text) {
|
|
|
99
105
|
* DOM order. A generated component that wires `useAction('toggleItem')` to an
|
|
100
106
|
* unnamed checkbox is found by pass 2 and judged by what its click dispatches.
|
|
101
107
|
*/
|
|
108
|
+
/** The one disabled predicate: the collector's and the walk's reveal signal must not drift apart. */
|
|
109
|
+
function isControlDisabled(el) {
|
|
110
|
+
return el.hasAttribute('disabled') || el.getAttribute('aria-disabled') === 'true';
|
|
111
|
+
}
|
|
102
112
|
function collectActionCandidates(root, name, label) {
|
|
103
113
|
const every = Array.from(root.querySelectorAll(CLICKABLE_SELECTOR)).filter((el) => el instanceof HTMLElement);
|
|
104
|
-
const isDisabled =
|
|
114
|
+
const isDisabled = isControlDisabled;
|
|
105
115
|
const lcName = name.toLowerCase();
|
|
106
116
|
const lcLabel = label.toLowerCase();
|
|
107
117
|
const nameKey = norm(name);
|
|
@@ -140,7 +150,12 @@ function collectActionCandidates(root, name, label) {
|
|
|
140
150
|
}
|
|
141
151
|
return { named: [...byValue, ...byText], fallback: rest, disabled, namedDisabled, allClickables: every };
|
|
142
152
|
}
|
|
143
|
-
|
|
153
|
+
// #1040: date-like types too — a wizard's first step is routinely gated on one ("preferred follow-up
|
|
154
|
+
// date"), and a Next that never enables hides every step behind it from the probe.
|
|
155
|
+
const TEXT_LIKE_INPUT_TYPES = new Set([
|
|
156
|
+
'text', 'email', 'search', 'url', 'tel', 'number', 'password', '',
|
|
157
|
+
'date', 'time', 'month', 'week', 'datetime-local',
|
|
158
|
+
]);
|
|
144
159
|
function sampleValueFor(el) {
|
|
145
160
|
const type = el instanceof HTMLInputElement ? el.type : 'textarea';
|
|
146
161
|
switch (type) {
|
|
@@ -152,6 +167,16 @@ function sampleValueFor(el) {
|
|
|
152
167
|
return 'https://example.com';
|
|
153
168
|
case 'tel':
|
|
154
169
|
return '5550100';
|
|
170
|
+
case 'date':
|
|
171
|
+
return '2026-01-15';
|
|
172
|
+
case 'time':
|
|
173
|
+
return '09:30';
|
|
174
|
+
case 'month':
|
|
175
|
+
return '2026-01';
|
|
176
|
+
case 'week':
|
|
177
|
+
return '2026-W03';
|
|
178
|
+
case 'datetime-local':
|
|
179
|
+
return '2026-01-15T09:30';
|
|
155
180
|
default:
|
|
156
181
|
return 'probe';
|
|
157
182
|
}
|
|
@@ -189,7 +214,8 @@ function primeInputs(root) {
|
|
|
189
214
|
if (el.value.trim().length > 0)
|
|
190
215
|
continue;
|
|
191
216
|
setNativeValue(el, sampleValueFor(el));
|
|
192
|
-
|
|
217
|
+
if (el.value.trim().length > 0)
|
|
218
|
+
primed += 1;
|
|
193
219
|
}
|
|
194
220
|
for (const el of Array.from(root.querySelectorAll('select'))) {
|
|
195
221
|
if (!(el instanceof HTMLSelectElement) || el.disabled)
|
|
@@ -422,7 +448,8 @@ async function run(input) {
|
|
|
422
448
|
return {
|
|
423
449
|
status: 'action-not-rendered',
|
|
424
450
|
diagnostic: `no clickable control in the rendered DOM for actionSpec.${input.actionName} (label "${entry.label}")` +
|
|
425
|
-
(first.disabled > 0 ? ` — ${first.disabled} control(s) rendered disabled, none naming the action` : '')
|
|
451
|
+
(first.disabled > 0 ? ` — ${first.disabled} control(s) rendered disabled, none naming the action` : '') +
|
|
452
|
+
` (primed ${primed} input(s))`,
|
|
426
453
|
};
|
|
427
454
|
}
|
|
428
455
|
// One deadline for the whole action. Named controls get the caller's wait
|
|
@@ -459,27 +486,71 @@ async function run(input) {
|
|
|
459
486
|
let namedDisabledSeen = first.namedDisabled;
|
|
460
487
|
let navigated = false;
|
|
461
488
|
let knownSignatures = new Set(first.allClickables.map((el) => signatureOf(container, el)));
|
|
489
|
+
// #1040: the walk. A wizard's Next is ONE element at ONE DOM path on every step, so "tried once by
|
|
490
|
+
// path" stopped the probe at step 1 forever, and the terminal control never mounted. A control
|
|
491
|
+
// whose click revealed controls the probe had not seen is a NAVIGATOR: while the action's own
|
|
492
|
+
// control is not visible, the probe clicks it again (bounded) before exploring further — the way
|
|
493
|
+
// a user presses Next until the last step. Inputs a step reveals are primed like the first step's.
|
|
494
|
+
const navigators = new Map();
|
|
495
|
+
let lastClickedSignature;
|
|
496
|
+
// The action's OWN control is keyed by path AND text: a wizard renders Complete at the path Next
|
|
497
|
+
// occupied on every earlier step, and "tried by path" would read it as the Next already clicked.
|
|
498
|
+
const namedKey = (el) => `${signatureOf(container, el)}|${(el.textContent ?? '').trim()}`;
|
|
499
|
+
const triedNamed = new Set();
|
|
500
|
+
// A step is "revealed" when a control the probe had not seen appears OR a control it had only
|
|
501
|
+
// seen disabled becomes enabled (a wizard's Back on step 2, a Next a step's own field unlocked).
|
|
502
|
+
const stateOf = (el) => `${signatureOf(container, el)}|${isControlDisabled(el) ? 'off' : 'on'}`;
|
|
503
|
+
let knownStates = new Set(first.allClickables.map(stateOf));
|
|
462
504
|
for (;;) {
|
|
463
505
|
if (remaining() === 0) {
|
|
464
506
|
exhausted = true;
|
|
465
507
|
break;
|
|
466
508
|
}
|
|
467
|
-
|
|
468
|
-
namedDisabledSeen = Math.max(namedDisabledSeen, live.namedDisabled);
|
|
509
|
+
let live = collectActionCandidates(container, input.actionName, entry.label);
|
|
469
510
|
// Disabled controls count: a step that reveals a disabled Complete IS navigation into the gate.
|
|
470
511
|
const liveSignatures = live.allClickables.map((el) => signatureOf(container, el));
|
|
512
|
+
const liveStates = live.allClickables.map(stateOf);
|
|
513
|
+
// `navigated` (the reason on an unreachable exit) keeps its meaning: controls the probe had not
|
|
514
|
+
// seen appeared. `revealed` (the walk) is wider: that, or a control seen only disabled enabling.
|
|
471
515
|
if (clicked > 0 && liveSignatures.some((sig) => !knownSignatures.has(sig)))
|
|
472
516
|
navigated = true;
|
|
517
|
+
const revealed = clicked > 0 && liveStates.some((state) => !knownStates.has(state));
|
|
473
518
|
knownSignatures = new Set([...knownSignatures, ...liveSignatures]);
|
|
519
|
+
knownStates = new Set([...knownStates, ...liveStates]);
|
|
520
|
+
if (revealed) {
|
|
521
|
+
if (lastClickedSignature !== undefined && !navigators.has(lastClickedSignature))
|
|
522
|
+
navigators.set(lastClickedSignature, 0);
|
|
523
|
+
// The revealed step may carry its own gate ("name", "date") — give it what the first step got.
|
|
524
|
+
if (primeInputs(container) > 0) {
|
|
525
|
+
await delay(150);
|
|
526
|
+
live = collectActionCandidates(container, input.actionName, entry.label);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
namedDisabledSeen = Math.max(namedDisabledSeen, live.namedDisabled);
|
|
474
530
|
const untried = (list) => list.find((el) => el.isConnected && !tried.has(signatureOf(container, el)));
|
|
475
|
-
const nextNamed =
|
|
476
|
-
const
|
|
531
|
+
const nextNamed = live.named.find((el) => el.isConnected && !triedNamed.has(namedKey(el)));
|
|
532
|
+
const navigator = nextNamed === undefined
|
|
533
|
+
? live.fallback.find((el) => {
|
|
534
|
+
const sig = signatureOf(container, el);
|
|
535
|
+
return el.isConnected && (navigators.get(sig) ?? MAX_NAVIGATOR_RECLICKS) < MAX_NAVIGATOR_RECLICKS;
|
|
536
|
+
})
|
|
537
|
+
: undefined;
|
|
538
|
+
// Untried controls first — including the ones the last click revealed — and only when nothing
|
|
539
|
+
// untried remains, the navigator again: a wizard walks (one extra Back click); an "Add item"
|
|
540
|
+
// or a tab is not clicked eight more times before what it revealed gets its turn.
|
|
541
|
+
const next = nextNamed ?? untried(live.fallback) ?? navigator;
|
|
477
542
|
if (next === undefined)
|
|
478
543
|
break;
|
|
479
544
|
const via = nextNamed !== undefined ? 'named' : 'fallback';
|
|
480
545
|
if (via === 'named')
|
|
481
546
|
namedAttempted = true;
|
|
482
|
-
|
|
547
|
+
const nextSignature = signatureOf(container, next);
|
|
548
|
+
if (navigator !== undefined && next === navigator)
|
|
549
|
+
navigators.set(nextSignature, (navigators.get(nextSignature) ?? 0) + 1);
|
|
550
|
+
if (via === 'named')
|
|
551
|
+
triedNamed.add(namedKey(next));
|
|
552
|
+
tried.add(nextSignature);
|
|
553
|
+
lastClickedSignature = nextSignature;
|
|
483
554
|
const hit = await attempt(next, via === 'named' ? Math.min(input.waitMs, remaining()) : Math.min(FALLBACK_CLICK_WAIT_MS, remaining()), via);
|
|
484
555
|
if (hit !== null)
|
|
485
556
|
return hit;
|
|
@@ -490,6 +561,7 @@ async function run(input) {
|
|
|
490
561
|
if (req.requireDom && !bestDom)
|
|
491
562
|
missing.push('DOM change');
|
|
492
563
|
const others = otherActions.size > 0 ? `; other actions dispatched: ${[...otherActions].join(', ')}` : '';
|
|
564
|
+
const navigatorReclicks = [...navigators.values()].reduce((sum, n) => sum + n, 0);
|
|
493
565
|
// #1040: the named control was seen only disabled and never clicked enabled — the probe could not
|
|
494
566
|
// reach the action. Not measured, with the reason: a click made new controls appear (the gate sits
|
|
495
567
|
// behind navigation the probe walked into but could not finish) or none did (the gate is input the
|
|
@@ -500,14 +572,14 @@ async function run(input) {
|
|
|
500
572
|
status: 'action-unreachable',
|
|
501
573
|
reason,
|
|
502
574
|
diagnostic: `actionSpec.${input.actionName} (label "${entry.label}") rendered disabled on every observation (${namedDisabledSeen} matching control(s)) and was never clicked enabled; ` +
|
|
503
|
-
`clicked ${clicked} other control(s) (${tried.size} distinct; primed ${primed} input(s)); new controls appeared after a click=${navigated}; DOM changed=${bestDom}` +
|
|
575
|
+
`clicked ${clicked} other control(s) (${tried.size} distinct; primed ${primed} input(s); navigator re-clicks=${navigatorReclicks}); new controls appeared after a click=${navigated}; DOM changed=${bestDom}` +
|
|
504
576
|
`${exhausted ? '; budget exhausted' : ''}${others}`,
|
|
505
577
|
};
|
|
506
578
|
}
|
|
507
579
|
return {
|
|
508
580
|
status: 'action-no-effect',
|
|
509
581
|
diagnostic: `${req.description}; clicked ${clicked} control(s) (${tried.size} distinct; first collection ${first.named.length} named, ${first.fallback.length} fallback, ${first.disabled} disabled; primed ${primed} input(s)` +
|
|
510
|
-
|
|
582
|
+
`; navigator re-clicks=${navigatorReclicks}${exhausted ? '; budget exhausted' : ''}); best observed dispatch=${bestDispatch}, domChanged=${bestDom}; ` +
|
|
511
583
|
`missing=${missing.join('+')}${others}`,
|
|
512
584
|
};
|
|
513
585
|
}
|
|
@@ -32820,15 +32820,19 @@
|
|
|
32820
32820
|
}
|
|
32821
32821
|
var CLICKABLE_SELECTOR = 'button, [role="button"], input[type="submit"], input[type="button"], input[type="checkbox"], input[type="radio"], [role="switch"], [role="checkbox"], [role="menuitem"], [role="tab"], [role="option"], [data-action]';
|
|
32822
32822
|
var FALLBACK_CLICK_WAIT_MS = 600;
|
|
32823
|
+
var MAX_NAVIGATOR_RECLICKS = 8;
|
|
32823
32824
|
var ACTION_BUDGET_FLOOR_MS = 6e3;
|
|
32824
32825
|
function norm(text) {
|
|
32825
32826
|
return text.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
32826
32827
|
}
|
|
32828
|
+
function isControlDisabled(el) {
|
|
32829
|
+
return el.hasAttribute("disabled") || el.getAttribute("aria-disabled") === "true";
|
|
32830
|
+
}
|
|
32827
32831
|
function collectActionCandidates(root, name, label) {
|
|
32828
32832
|
const every = Array.from(root.querySelectorAll(CLICKABLE_SELECTOR)).filter(
|
|
32829
32833
|
(el) => el instanceof HTMLElement
|
|
32830
32834
|
);
|
|
32831
|
-
const isDisabled =
|
|
32835
|
+
const isDisabled = isControlDisabled;
|
|
32832
32836
|
const lcName = name.toLowerCase();
|
|
32833
32837
|
const lcLabel = label.toLowerCase();
|
|
32834
32838
|
const nameKey = norm(name);
|
|
@@ -32860,7 +32864,21 @@
|
|
|
32860
32864
|
}
|
|
32861
32865
|
return { named: [...byValue, ...byText], fallback: rest, disabled, namedDisabled, allClickables: every };
|
|
32862
32866
|
}
|
|
32863
|
-
var TEXT_LIKE_INPUT_TYPES = /* @__PURE__ */ new Set([
|
|
32867
|
+
var TEXT_LIKE_INPUT_TYPES = /* @__PURE__ */ new Set([
|
|
32868
|
+
"text",
|
|
32869
|
+
"email",
|
|
32870
|
+
"search",
|
|
32871
|
+
"url",
|
|
32872
|
+
"tel",
|
|
32873
|
+
"number",
|
|
32874
|
+
"password",
|
|
32875
|
+
"",
|
|
32876
|
+
"date",
|
|
32877
|
+
"time",
|
|
32878
|
+
"month",
|
|
32879
|
+
"week",
|
|
32880
|
+
"datetime-local"
|
|
32881
|
+
]);
|
|
32864
32882
|
function sampleValueFor(el) {
|
|
32865
32883
|
const type = el instanceof HTMLInputElement ? el.type : "textarea";
|
|
32866
32884
|
switch (type) {
|
|
@@ -32872,6 +32890,16 @@
|
|
|
32872
32890
|
return "https://example.com";
|
|
32873
32891
|
case "tel":
|
|
32874
32892
|
return "5550100";
|
|
32893
|
+
case "date":
|
|
32894
|
+
return "2026-01-15";
|
|
32895
|
+
case "time":
|
|
32896
|
+
return "09:30";
|
|
32897
|
+
case "month":
|
|
32898
|
+
return "2026-01";
|
|
32899
|
+
case "week":
|
|
32900
|
+
return "2026-W03";
|
|
32901
|
+
case "datetime-local":
|
|
32902
|
+
return "2026-01-15T09:30";
|
|
32875
32903
|
default:
|
|
32876
32904
|
return "probe";
|
|
32877
32905
|
}
|
|
@@ -32892,7 +32920,7 @@
|
|
|
32892
32920
|
if (el instanceof HTMLInputElement && !TEXT_LIKE_INPUT_TYPES.has(el.type)) continue;
|
|
32893
32921
|
if (el.value.trim().length > 0) continue;
|
|
32894
32922
|
setNativeValue(el, sampleValueFor(el));
|
|
32895
|
-
primed += 1;
|
|
32923
|
+
if (el.value.trim().length > 0) primed += 1;
|
|
32896
32924
|
}
|
|
32897
32925
|
for (const el of Array.from(root.querySelectorAll("select"))) {
|
|
32898
32926
|
if (!(el instanceof HTMLSelectElement) || el.disabled) continue;
|
|
@@ -33084,7 +33112,7 @@
|
|
|
33084
33112
|
}
|
|
33085
33113
|
return {
|
|
33086
33114
|
status: "action-not-rendered",
|
|
33087
|
-
diagnostic: `no clickable control in the rendered DOM for actionSpec.${input.actionName} (label "${entry.label}")` + (first.disabled > 0 ? ` \u2014 ${first.disabled} control(s) rendered disabled, none naming the action` : "")
|
|
33115
|
+
diagnostic: `no clickable control in the rendered DOM for actionSpec.${input.actionName} (label "${entry.label}")` + (first.disabled > 0 ? ` \u2014 ${first.disabled} control(s) rendered disabled, none naming the action` : "") + ` (primed ${primed} input(s))`
|
|
33088
33116
|
};
|
|
33089
33117
|
}
|
|
33090
33118
|
const deadline = Date.now() + Math.max(input.waitMs, ACTION_BUDGET_FLOOR_MS);
|
|
@@ -33109,23 +33137,47 @@
|
|
|
33109
33137
|
let namedDisabledSeen = first.namedDisabled;
|
|
33110
33138
|
let navigated = false;
|
|
33111
33139
|
let knownSignatures = new Set(first.allClickables.map((el) => signatureOf(container, el)));
|
|
33140
|
+
const navigators = /* @__PURE__ */ new Map();
|
|
33141
|
+
let lastClickedSignature;
|
|
33142
|
+
const namedKey = (el) => `${signatureOf(container, el)}|${(el.textContent ?? "").trim()}`;
|
|
33143
|
+
const triedNamed = /* @__PURE__ */ new Set();
|
|
33144
|
+
const stateOf = (el) => `${signatureOf(container, el)}|${isControlDisabled(el) ? "off" : "on"}`;
|
|
33145
|
+
let knownStates = new Set(first.allClickables.map(stateOf));
|
|
33112
33146
|
for (; ; ) {
|
|
33113
33147
|
if (remaining() === 0) {
|
|
33114
33148
|
exhausted = true;
|
|
33115
33149
|
break;
|
|
33116
33150
|
}
|
|
33117
|
-
|
|
33118
|
-
namedDisabledSeen = Math.max(namedDisabledSeen, live.namedDisabled);
|
|
33151
|
+
let live = collectActionCandidates(container, input.actionName, entry.label);
|
|
33119
33152
|
const liveSignatures = live.allClickables.map((el) => signatureOf(container, el));
|
|
33153
|
+
const liveStates = live.allClickables.map(stateOf);
|
|
33120
33154
|
if (clicked > 0 && liveSignatures.some((sig) => !knownSignatures.has(sig))) navigated = true;
|
|
33155
|
+
const revealed = clicked > 0 && liveStates.some((state) => !knownStates.has(state));
|
|
33121
33156
|
knownSignatures = /* @__PURE__ */ new Set([...knownSignatures, ...liveSignatures]);
|
|
33157
|
+
knownStates = /* @__PURE__ */ new Set([...knownStates, ...liveStates]);
|
|
33158
|
+
if (revealed) {
|
|
33159
|
+
if (lastClickedSignature !== void 0 && !navigators.has(lastClickedSignature)) navigators.set(lastClickedSignature, 0);
|
|
33160
|
+
if (primeInputs(container) > 0) {
|
|
33161
|
+
await delay(150);
|
|
33162
|
+
live = collectActionCandidates(container, input.actionName, entry.label);
|
|
33163
|
+
}
|
|
33164
|
+
}
|
|
33165
|
+
namedDisabledSeen = Math.max(namedDisabledSeen, live.namedDisabled);
|
|
33122
33166
|
const untried = (list) => list.find((el) => el.isConnected && !tried.has(signatureOf(container, el)));
|
|
33123
|
-
const nextNamed =
|
|
33124
|
-
const
|
|
33167
|
+
const nextNamed = live.named.find((el) => el.isConnected && !triedNamed.has(namedKey(el)));
|
|
33168
|
+
const navigator2 = nextNamed === void 0 ? live.fallback.find((el) => {
|
|
33169
|
+
const sig = signatureOf(container, el);
|
|
33170
|
+
return el.isConnected && (navigators.get(sig) ?? MAX_NAVIGATOR_RECLICKS) < MAX_NAVIGATOR_RECLICKS;
|
|
33171
|
+
}) : void 0;
|
|
33172
|
+
const next = nextNamed ?? untried(live.fallback) ?? navigator2;
|
|
33125
33173
|
if (next === void 0) break;
|
|
33126
33174
|
const via = nextNamed !== void 0 ? "named" : "fallback";
|
|
33127
33175
|
if (via === "named") namedAttempted = true;
|
|
33128
|
-
|
|
33176
|
+
const nextSignature = signatureOf(container, next);
|
|
33177
|
+
if (navigator2 !== void 0 && next === navigator2) navigators.set(nextSignature, (navigators.get(nextSignature) ?? 0) + 1);
|
|
33178
|
+
if (via === "named") triedNamed.add(namedKey(next));
|
|
33179
|
+
tried.add(nextSignature);
|
|
33180
|
+
lastClickedSignature = nextSignature;
|
|
33129
33181
|
const hit = await attempt(
|
|
33130
33182
|
next,
|
|
33131
33183
|
via === "named" ? Math.min(input.waitMs, remaining()) : Math.min(FALLBACK_CLICK_WAIT_MS, remaining()),
|
|
@@ -33137,17 +33189,18 @@
|
|
|
33137
33189
|
if (req.requireDispatch && !bestDispatch) missing.push("dispatch");
|
|
33138
33190
|
if (req.requireDom && !bestDom) missing.push("DOM change");
|
|
33139
33191
|
const others = otherActions.size > 0 ? `; other actions dispatched: ${[...otherActions].join(", ")}` : "";
|
|
33192
|
+
const navigatorReclicks = [...navigators.values()].reduce((sum, n) => sum + n, 0);
|
|
33140
33193
|
if (!namedAttempted && namedDisabledSeen > 0) {
|
|
33141
33194
|
const reason = navigated ? "behind-navigation" : "disabled-after-priming";
|
|
33142
33195
|
return {
|
|
33143
33196
|
status: "action-unreachable",
|
|
33144
33197
|
reason,
|
|
33145
|
-
diagnostic: `actionSpec.${input.actionName} (label "${entry.label}") rendered disabled on every observation (${namedDisabledSeen} matching control(s)) and was never clicked enabled; clicked ${clicked} other control(s) (${tried.size} distinct; primed ${primed} input(s)); new controls appeared after a click=${navigated}; DOM changed=${bestDom}${exhausted ? "; budget exhausted" : ""}${others}`
|
|
33198
|
+
diagnostic: `actionSpec.${input.actionName} (label "${entry.label}") rendered disabled on every observation (${namedDisabledSeen} matching control(s)) and was never clicked enabled; clicked ${clicked} other control(s) (${tried.size} distinct; primed ${primed} input(s); navigator re-clicks=${navigatorReclicks}); new controls appeared after a click=${navigated}; DOM changed=${bestDom}${exhausted ? "; budget exhausted" : ""}${others}`
|
|
33146
33199
|
};
|
|
33147
33200
|
}
|
|
33148
33201
|
return {
|
|
33149
33202
|
status: "action-no-effect",
|
|
33150
|
-
diagnostic: `${req.description}; clicked ${clicked} control(s) (${tried.size} distinct; first collection ${first.named.length} named, ${first.fallback.length} fallback, ${first.disabled} disabled; primed ${primed} input(s)${exhausted ? "; budget exhausted" : ""}); best observed dispatch=${bestDispatch}, domChanged=${bestDom}; missing=${missing.join("+")}${others}`
|
|
33203
|
+
diagnostic: `${req.description}; clicked ${clicked} control(s) (${tried.size} distinct; first collection ${first.named.length} named, ${first.fallback.length} fallback, ${first.disabled} disabled; primed ${primed} input(s); navigator re-clicks=${navigatorReclicks}${exhausted ? "; budget exhausted" : ""}); best observed dispatch=${bestDispatch}, domChanged=${bestDom}; missing=${missing.join("+")}${others}`
|
|
33151
33204
|
};
|
|
33152
33205
|
} finally {
|
|
33153
33206
|
window.removeEventListener("error", onWindowError);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ggui-ai/ui-visual-tester",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "Behavioral validator for ggui-generated component code. Renders a component in real Chromium via Playwright, clicks each action declared in its contract, and verifies the action actually does something — mutating bound state or dispatching to the agent. Catches buttons wired to no-op handlers that structural validators miss. Playwright is an optional peer dependency, injected by the caller.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ggui",
|
|
@@ -48,9 +48,9 @@
|
|
|
48
48
|
"dependencies": {
|
|
49
49
|
"react": "^19.0.0",
|
|
50
50
|
"react-dom": "^19.0.0",
|
|
51
|
-
"@ggui-ai/design": "0.
|
|
52
|
-
"@ggui-ai/
|
|
53
|
-
"@ggui-ai/
|
|
51
|
+
"@ggui-ai/design": "0.18.0",
|
|
52
|
+
"@ggui-ai/wire": "0.18.0",
|
|
53
|
+
"@ggui-ai/protocol": "0.18.0"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
56
|
"@playwright/test": "^1.60.0",
|