@ggui-ai/ui-visual-tester 0.16.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.d.ts +12 -0
- package/dist/fixture-runtime.d.ts.map +1 -1
- package/dist/fixture-runtime.js +394 -60
- package/dist/index.d.ts +49 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/validate.d.ts.map +1 -1
- package/dist/validate.js +33 -3
- package/fixture/fixture-runtime.js +1617 -878
- package/package.json +4 -4
|
@@ -29,6 +29,8 @@ type ActionClassification = 'agent-bound' | 'context-bound';
|
|
|
29
29
|
interface RunInput {
|
|
30
30
|
readonly componentCode: string;
|
|
31
31
|
readonly contract: unknown;
|
|
32
|
+
/** Parsed JSON of the commit's fixture props; a plain object or nothing. */
|
|
33
|
+
readonly sampleProps: unknown;
|
|
32
34
|
readonly actionName: string;
|
|
33
35
|
readonly classification: ActionClassification;
|
|
34
36
|
readonly settleMs: number;
|
|
@@ -39,13 +41,23 @@ type RunOutcome = {
|
|
|
39
41
|
readonly diagnostic: string;
|
|
40
42
|
} | {
|
|
41
43
|
readonly status: 'action-not-rendered';
|
|
44
|
+
readonly diagnostic: string;
|
|
42
45
|
} | {
|
|
43
46
|
readonly status: 'action-no-effect';
|
|
44
47
|
readonly diagnostic: string;
|
|
48
|
+
}
|
|
49
|
+
/** #1040: the named control was never reachable enabled — not measured; `reason` says why. */
|
|
50
|
+
| {
|
|
51
|
+
readonly status: 'action-unreachable';
|
|
52
|
+
readonly reason: 'disabled-after-priming' | 'behind-navigation';
|
|
53
|
+
readonly diagnostic: string;
|
|
45
54
|
} | {
|
|
46
55
|
readonly status: 'ok';
|
|
47
56
|
readonly dispatchFired: boolean;
|
|
48
57
|
readonly domChanged: boolean;
|
|
58
|
+
/** Which pass found the control that produced the signal, and how many were clicked to get there. */
|
|
59
|
+
readonly via?: 'named' | 'fallback';
|
|
60
|
+
readonly clicked?: number;
|
|
49
61
|
};
|
|
50
62
|
declare global {
|
|
51
63
|
interface Window {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fixture-runtime.d.ts","sourceRoot":"","sources":["../src/fixture-runtime.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAC/B,OAAO,KAAK,cAAc,MAAM,kBAAkB,CAAC;AACnD,OAAO,KAAK,gBAAgB,MAAM,4BAA4B,CAAC;AAC/D,OAAO,KAAK,gBAAgB,MAAM,4BAA4B,CAAC;AAC/D,OAAO,KAAK,kBAAkB,MAAM,8BAA8B,CAAC;AACnE,OAAO,KAAK,cAAc,MAAM,0BAA0B,CAAC;AAC3D,OAAO,KAAK,IAAI,MAAM,eAAe,CAAC;AA0CtC,UAAU,cAAc;IACtB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;IACvB,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,KAAK,oBAAoB,GAAG,aAAa,GAAG,eAAe,CAAC;AAE5D,UAAU,QAAQ;IAChB,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,cAAc,EAAE,oBAAoB,CAAC;IAC9C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,KAAK,UAAU,GACX;IAAE,QAAQ,CAAC,MAAM,EAAE,eAAe,CAAC;IAAC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;CAAE,GACjE;IAAE,QAAQ,CAAC,MAAM,EAAE,qBAAqB,CAAA;CAAE,
|
|
1
|
+
{"version":3,"file":"fixture-runtime.d.ts","sourceRoot":"","sources":["../src/fixture-runtime.ts"],"names":[],"mappings":"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"}
|
package/dist/fixture-runtime.js
CHANGED
|
@@ -70,27 +70,247 @@ function installRegistry() {
|
|
|
70
70
|
window.__GGUI_APP_COMPONENTS = registry.appComponents;
|
|
71
71
|
return registry;
|
|
72
72
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
73
|
+
/**
|
|
74
|
+
* What a generated component may wire an action to. Kept to real controls
|
|
75
|
+
* (a `<div onClick>` that names nothing is not a contract affordance).
|
|
76
|
+
*/
|
|
77
|
+
const CLICKABLE_SELECTOR = 'button, [role="button"], input[type="submit"], input[type="button"], ' +
|
|
78
|
+
'input[type="checkbox"], input[type="radio"], [role="switch"], [role="checkbox"], ' +
|
|
79
|
+
'[role="menuitem"], [role="tab"], [role="option"], [data-action]';
|
|
80
|
+
// Deliberately NOT candidates: <a href> (a click navigates the fixture page
|
|
81
|
+
// away and aborts every remaining check) and <summary> (its click toggles the
|
|
82
|
+
// parent <details> — a DOM change that says nothing about the action).
|
|
83
|
+
/** Per-control wait in the fallback pass — enough for a synchronous dispatch + a render. */
|
|
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;
|
|
91
|
+
/**
|
|
92
|
+
* ONE deadline covers both passes for an action: max(the caller's wait, this
|
|
93
|
+
* floor). A few loosely-matching decoys can no longer run an action for
|
|
94
|
+
* minutes; the floor keeps very short test waits from starving the fallback.
|
|
95
|
+
*/
|
|
96
|
+
const ACTION_BUDGET_FLOOR_MS = 6000;
|
|
97
|
+
function norm(text) {
|
|
98
|
+
return text.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* The controls to try, the way the harness's render-check finds them (#996):
|
|
102
|
+
* pass 1 = controls that NAME the action — `data-action` by value (the one
|
|
103
|
+
* hook that names the action rather than its label), then aria-label / text
|
|
104
|
+
* carrying the label or the name; pass 2 = every other enabled clickable, in
|
|
105
|
+
* DOM order. A generated component that wires `useAction('toggleItem')` to an
|
|
106
|
+
* unnamed checkbox is found by pass 2 and judged by what its click dispatches.
|
|
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
|
+
}
|
|
112
|
+
function collectActionCandidates(root, name, label) {
|
|
113
|
+
const every = Array.from(root.querySelectorAll(CLICKABLE_SELECTOR)).filter((el) => el instanceof HTMLElement);
|
|
114
|
+
const isDisabled = isControlDisabled;
|
|
115
|
+
const lcName = name.toLowerCase();
|
|
116
|
+
const lcLabel = label.toLowerCase();
|
|
117
|
+
const nameKey = norm(name);
|
|
118
|
+
const labelKey = norm(label);
|
|
119
|
+
// How the harness names a control: `data-action` by value first (#996), then the aria label or the text.
|
|
120
|
+
const namesAction = (el) => {
|
|
121
|
+
const dataAction = (el.getAttribute('data-action') ?? '').toLowerCase();
|
|
122
|
+
if (dataAction === lcName)
|
|
123
|
+
return 'value';
|
|
124
|
+
const aria = (el.getAttribute('aria-label') ?? '').toLowerCase();
|
|
82
125
|
const txt = (el.textContent ?? '').trim().toLowerCase();
|
|
83
|
-
|
|
84
|
-
|
|
126
|
+
const ariaNames = aria.length > 0 && (aria.includes(lcLabel) || (nameKey.length > 0 && norm(aria).includes(nameKey)));
|
|
127
|
+
const textNames = txt.length > 0 &&
|
|
128
|
+
(txt.includes(lcLabel) ||
|
|
129
|
+
(nameKey.length > 0 && norm(txt).includes(nameKey)) ||
|
|
130
|
+
(labelKey.length > 0 && norm(txt).includes(labelKey)));
|
|
131
|
+
return ariaNames || textNames ? 'text' : null;
|
|
132
|
+
};
|
|
133
|
+
const disabledEls = every.filter(isDisabled);
|
|
134
|
+
const disabled = disabledEls.length;
|
|
135
|
+
const namedDisabled = disabledEls.filter((el) => namesAction(el) !== null).length;
|
|
136
|
+
const all = every.filter((el) => !isDisabled(el) &&
|
|
137
|
+
// an already-checked radio fires no change on click — nothing to observe
|
|
138
|
+
!(el instanceof HTMLInputElement && el.type === 'radio' && el.checked));
|
|
139
|
+
const byValue = [];
|
|
140
|
+
const byText = [];
|
|
141
|
+
const rest = [];
|
|
142
|
+
for (const el of all) {
|
|
143
|
+
const how = namesAction(el);
|
|
144
|
+
if (how === 'value')
|
|
145
|
+
byValue.push(el);
|
|
146
|
+
else if (how === 'text')
|
|
147
|
+
byText.push(el);
|
|
148
|
+
else
|
|
149
|
+
rest.push(el);
|
|
150
|
+
}
|
|
151
|
+
return { named: [...byValue, ...byText], fallback: rest, disabled, namedDisabled, allClickables: every };
|
|
152
|
+
}
|
|
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
|
+
]);
|
|
159
|
+
function sampleValueFor(el) {
|
|
160
|
+
const type = el instanceof HTMLInputElement ? el.type : 'textarea';
|
|
161
|
+
switch (type) {
|
|
162
|
+
case 'number':
|
|
163
|
+
return '1';
|
|
164
|
+
case 'email':
|
|
165
|
+
return 'probe@example.com';
|
|
166
|
+
case 'url':
|
|
167
|
+
return 'https://example.com';
|
|
168
|
+
case 'tel':
|
|
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';
|
|
180
|
+
default:
|
|
181
|
+
return 'probe';
|
|
85
182
|
}
|
|
86
|
-
|
|
87
|
-
|
|
183
|
+
}
|
|
184
|
+
/** Set a value the way React notices it: through the prototype's native setter, then an input + change event. */
|
|
185
|
+
function setNativeValue(el, value) {
|
|
186
|
+
const proto = el instanceof HTMLTextAreaElement
|
|
187
|
+
? HTMLTextAreaElement.prototype
|
|
188
|
+
: el instanceof HTMLSelectElement
|
|
189
|
+
? HTMLSelectElement.prototype
|
|
190
|
+
: HTMLInputElement.prototype;
|
|
191
|
+
const setter = Object.getOwnPropertyDescriptor(proto, 'value')?.set;
|
|
192
|
+
if (setter)
|
|
193
|
+
setter.call(el, value);
|
|
194
|
+
else
|
|
195
|
+
el.value = value;
|
|
196
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
197
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* The generic minimum a user does before pressing Send / Submit / Complete
|
|
201
|
+
* (#1021): give every empty text-like input and textarea a value, every
|
|
202
|
+
* unset select its first real option, every unchecked radio group its first
|
|
203
|
+
* radio. Checkboxes are never touched — they are often the action itself.
|
|
204
|
+
* Returns how many controls were primed.
|
|
205
|
+
*/
|
|
206
|
+
function primeInputs(root) {
|
|
207
|
+
let primed = 0;
|
|
208
|
+
const editable = (el) => !el.disabled && !(el instanceof HTMLSelectElement) ? !el.readOnly : !el.disabled;
|
|
209
|
+
for (const el of Array.from(root.querySelectorAll('input, textarea'))) {
|
|
210
|
+
if (!(el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) || !editable(el))
|
|
88
211
|
continue;
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
212
|
+
if (el instanceof HTMLInputElement && !TEXT_LIKE_INPUT_TYPES.has(el.type))
|
|
213
|
+
continue;
|
|
214
|
+
if (el.value.trim().length > 0)
|
|
215
|
+
continue;
|
|
216
|
+
setNativeValue(el, sampleValueFor(el));
|
|
217
|
+
if (el.value.trim().length > 0)
|
|
218
|
+
primed += 1;
|
|
219
|
+
}
|
|
220
|
+
for (const el of Array.from(root.querySelectorAll('select'))) {
|
|
221
|
+
if (!(el instanceof HTMLSelectElement) || el.disabled)
|
|
222
|
+
continue;
|
|
223
|
+
const current = el.options[el.selectedIndex];
|
|
224
|
+
if (current !== undefined && current.value.trim().length > 0)
|
|
225
|
+
continue;
|
|
226
|
+
const first = Array.from(el.options).find((o) => o.value.trim().length > 0 && !o.disabled);
|
|
227
|
+
if (first === undefined)
|
|
228
|
+
continue;
|
|
229
|
+
setNativeValue(el, first.value);
|
|
230
|
+
primed += 1;
|
|
231
|
+
}
|
|
232
|
+
const seenGroups = new Set();
|
|
233
|
+
for (const el of Array.from(root.querySelectorAll('input[type="radio"]'))) {
|
|
234
|
+
if (!(el instanceof HTMLInputElement) || el.disabled)
|
|
235
|
+
continue;
|
|
236
|
+
const group = el.name || '';
|
|
237
|
+
if (seenGroups.has(group))
|
|
238
|
+
continue;
|
|
239
|
+
seenGroups.add(group);
|
|
240
|
+
const members = group
|
|
241
|
+
? Array.from(root.querySelectorAll(`input[type="radio"][name="${CSS.escape(group)}"]`)).filter((m) => m instanceof HTMLInputElement)
|
|
242
|
+
: [el];
|
|
243
|
+
if (members.some((m) => m.checked))
|
|
244
|
+
continue;
|
|
245
|
+
const first = members.find((m) => !m.disabled);
|
|
246
|
+
if (first === undefined)
|
|
247
|
+
continue;
|
|
248
|
+
first.click();
|
|
249
|
+
primed += 1;
|
|
250
|
+
}
|
|
251
|
+
return primed;
|
|
252
|
+
}
|
|
253
|
+
function countNamedDispatches(actionName) {
|
|
254
|
+
return (window.__ggui_test_dispatches__ ?? []).filter((d) => d.actionName === actionName).length;
|
|
255
|
+
}
|
|
256
|
+
function snapshot(container, actionName) {
|
|
257
|
+
return {
|
|
258
|
+
text: container.textContent ?? '',
|
|
259
|
+
html: container.innerHTML,
|
|
260
|
+
named: countNamedDispatches(actionName),
|
|
261
|
+
total: (window.__ggui_test_dispatches__ ?? []).length,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
/** Poll for the required signal after a click. `dispatchFired` counts only dispatches of THIS action. */
|
|
265
|
+
async function observe(container, actionName, before, req, waitMs) {
|
|
266
|
+
const start = Date.now();
|
|
267
|
+
let dispatchFired = false;
|
|
268
|
+
let domChanged = false;
|
|
269
|
+
while (Date.now() - start < waitMs) {
|
|
270
|
+
await delay(50);
|
|
271
|
+
if (!dispatchFired && countNamedDispatches(actionName) > before.named) {
|
|
272
|
+
dispatchFired = true;
|
|
273
|
+
}
|
|
274
|
+
if (!domChanged) {
|
|
275
|
+
const afterText = container.textContent ?? '';
|
|
276
|
+
const afterHtml = container.innerHTML;
|
|
277
|
+
if (afterText !== before.text || afterHtml !== before.html) {
|
|
278
|
+
domChanged = true;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
if (dispatchFired && domChanged)
|
|
282
|
+
break;
|
|
283
|
+
// Early-exit once the REQUIRED signal has fired (don't wait for the other one).
|
|
284
|
+
if (req.requireDispatch && dispatchFired)
|
|
285
|
+
break;
|
|
286
|
+
if (req.requireDom && domChanged)
|
|
287
|
+
break;
|
|
288
|
+
}
|
|
289
|
+
const otherActions = (window.__ggui_test_dispatches__ ?? [])
|
|
290
|
+
.slice(before.total)
|
|
291
|
+
.map((d) => d.actionName)
|
|
292
|
+
.filter((n) => n !== actionName);
|
|
293
|
+
return { dispatchFired, domChanged, otherActions: [...new Set(otherActions)] };
|
|
294
|
+
}
|
|
295
|
+
function satisfied(req, obs) {
|
|
296
|
+
return (req.requireDispatch ? obs.dispatchFired : true) && (req.requireDom ? obs.domChanged : true);
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* A control's identity across re-renders: its path from the mount root by tag
|
|
300
|
+
* and sibling index (plus an input's type). A re-rendered equivalent at the
|
|
301
|
+
* same position counts as already tried; a detached node never gets clicked.
|
|
302
|
+
*/
|
|
303
|
+
function signatureOf(root, el) {
|
|
304
|
+
const parts = [];
|
|
305
|
+
let cur = el;
|
|
306
|
+
while (cur !== null && cur !== root) {
|
|
307
|
+
const parent = cur.parentElement;
|
|
308
|
+
const idx = parent === null ? 0 : Array.prototype.indexOf.call(parent.children, cur);
|
|
309
|
+
const type = cur instanceof HTMLInputElement ? `[${cur.type}]` : '';
|
|
310
|
+
parts.push(`${cur.tagName.toLowerCase()}${type}:${idx}`);
|
|
311
|
+
cur = parent;
|
|
92
312
|
}
|
|
93
|
-
return
|
|
313
|
+
return parts.reverse().join('/');
|
|
94
314
|
}
|
|
95
315
|
async function evaluateComponent(code) {
|
|
96
316
|
const cleaned = stripMarkers(code);
|
|
@@ -104,7 +324,12 @@ async function evaluateComponent(code) {
|
|
|
104
324
|
}
|
|
105
325
|
return Comp;
|
|
106
326
|
}
|
|
107
|
-
function
|
|
327
|
+
function propsOf(value) {
|
|
328
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
329
|
+
? value
|
|
330
|
+
: {};
|
|
331
|
+
}
|
|
332
|
+
function mountTree(Comp, props) {
|
|
108
333
|
const container = document.createElement('div');
|
|
109
334
|
container.id = 'ggui-vt-mount';
|
|
110
335
|
document.body.replaceChildren(container);
|
|
@@ -127,7 +352,9 @@ function mountTree(Comp) {
|
|
|
127
352
|
subscribe: () => () => { },
|
|
128
353
|
};
|
|
129
354
|
const root = ReactDomClient.createRoot(container);
|
|
130
|
-
|
|
355
|
+
// The commit's fixture props: a component that draws its controls from
|
|
356
|
+
// props (a board's tasks, a list's items) mounts EMPTY without them.
|
|
357
|
+
const componentElement = React.createElement(Comp, props);
|
|
131
358
|
const tree = React.createElement(Wire.GguiWireProvider, {
|
|
132
359
|
config: wireConfig,
|
|
133
360
|
children: componentElement,
|
|
@@ -182,7 +409,7 @@ async function run(input) {
|
|
|
182
409
|
let handles = null;
|
|
183
410
|
try {
|
|
184
411
|
try {
|
|
185
|
-
handles = mountTree(Comp);
|
|
412
|
+
handles = mountTree(Comp, propsOf(input.sampleProps));
|
|
186
413
|
}
|
|
187
414
|
catch (err) {
|
|
188
415
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -199,55 +426,162 @@ async function run(input) {
|
|
|
199
426
|
diagnostic: `actionSpec.${input.actionName} missing`,
|
|
200
427
|
};
|
|
201
428
|
}
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
429
|
+
const req = signalRequirement(input.classification);
|
|
430
|
+
const container = handles.container;
|
|
431
|
+
// Prime inputs first (#1021): Send/Submit/Complete are commonly disabled
|
|
432
|
+
// until something is typed or chosen — the minimum a user does before them.
|
|
433
|
+
const primed = primeInputs(container);
|
|
434
|
+
if (primed > 0)
|
|
435
|
+
await delay(150);
|
|
436
|
+
const first = collectActionCandidates(container, input.actionName, entry.label);
|
|
437
|
+
if (first.named.length === 0 && first.fallback.length === 0) {
|
|
438
|
+
if (first.namedDisabled > 0) {
|
|
439
|
+
// #1040: the action's own control is there, disabled, and nothing else is clickable —
|
|
440
|
+
// the action is gated on input the probe did not provide. Not missing, not a miss: not measured.
|
|
441
|
+
return {
|
|
442
|
+
status: 'action-unreachable',
|
|
443
|
+
reason: 'disabled-after-priming',
|
|
444
|
+
diagnostic: `${first.disabled} control(s) rendered, all disabled (${first.namedDisabled} naming the action) — the action is gated on input the probe did not provide ` +
|
|
445
|
+
`(primed ${primed} input(s)); actionSpec.${input.actionName} (label "${entry.label}")`,
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
return {
|
|
449
|
+
status: 'action-not-rendered',
|
|
450
|
+
diagnostic: `no clickable control in the rendered DOM for actionSpec.${input.actionName} (label "${entry.label}")` +
|
|
451
|
+
(first.disabled > 0 ? ` — ${first.disabled} control(s) rendered disabled, none naming the action` : '') +
|
|
452
|
+
` (primed ${primed} input(s))`,
|
|
453
|
+
};
|
|
205
454
|
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
let
|
|
212
|
-
let
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
455
|
+
// One deadline for the whole action. Named controls get the caller's wait
|
|
456
|
+
// (or what is left of the budget); fallback controls get a short slice each.
|
|
457
|
+
const deadline = Date.now() + Math.max(input.waitMs, ACTION_BUDGET_FLOOR_MS);
|
|
458
|
+
const remaining = () => Math.max(0, deadline - Date.now());
|
|
459
|
+
let clicked = 0;
|
|
460
|
+
let bestDispatch = false;
|
|
461
|
+
let bestDom = false;
|
|
462
|
+
const otherActions = new Set();
|
|
463
|
+
const attempt = async (el, waitMs, via) => {
|
|
464
|
+
const before = snapshot(container, input.actionName);
|
|
465
|
+
clicked += 1;
|
|
466
|
+
el.click();
|
|
467
|
+
const obs = await observe(container, input.actionName, before, req, waitMs);
|
|
468
|
+
bestDispatch = bestDispatch || obs.dispatchFired;
|
|
469
|
+
bestDom = bestDom || obs.domChanged;
|
|
470
|
+
for (const a of obs.otherActions)
|
|
471
|
+
otherActions.add(a);
|
|
472
|
+
return satisfied(req, obs)
|
|
473
|
+
? { status: 'ok', dispatchFired: obs.dispatchFired, domChanged: obs.domChanged, via, clicked }
|
|
474
|
+
: null;
|
|
475
|
+
};
|
|
476
|
+
// Re-collect LIVE candidates before EVERY click: an earlier click may have
|
|
477
|
+
// re-rendered the tree (a filter, a tab, an opened editor), detaching what
|
|
478
|
+
// was collected and adding what was not there. A control is tried once,
|
|
479
|
+
// by its DOM path; a detached node is never the one clicked.
|
|
480
|
+
const tried = new Set();
|
|
481
|
+
let exhausted = false;
|
|
482
|
+
// #1040: was the NAMED control ever clicked enabled (a real attempt), was it ever seen disabled (a
|
|
483
|
+
// gate), and did a click make NEW controls appear (a step, a tab, a revealed section — navigation,
|
|
484
|
+
// as distinct from a re-render of what was already there)?
|
|
485
|
+
let namedAttempted = false;
|
|
486
|
+
let namedDisabledSeen = first.namedDisabled;
|
|
487
|
+
let navigated = false;
|
|
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));
|
|
504
|
+
for (;;) {
|
|
505
|
+
if (remaining() === 0) {
|
|
506
|
+
exhausted = true;
|
|
507
|
+
break;
|
|
218
508
|
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
509
|
+
let live = collectActionCandidates(container, input.actionName, entry.label);
|
|
510
|
+
// Disabled controls count: a step that reveals a disabled Complete IS navigation into the gate.
|
|
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.
|
|
515
|
+
if (clicked > 0 && liveSignatures.some((sig) => !knownSignatures.has(sig)))
|
|
516
|
+
navigated = true;
|
|
517
|
+
const revealed = clicked > 0 && liveStates.some((state) => !knownStates.has(state));
|
|
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);
|
|
224
527
|
}
|
|
225
528
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
529
|
+
namedDisabledSeen = Math.max(namedDisabledSeen, live.namedDisabled);
|
|
530
|
+
const untried = (list) => list.find((el) => el.isConnected && !tried.has(signatureOf(container, el)));
|
|
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;
|
|
542
|
+
if (next === undefined)
|
|
234
543
|
break;
|
|
544
|
+
const via = nextNamed !== undefined ? 'named' : 'fallback';
|
|
545
|
+
if (via === 'named')
|
|
546
|
+
namedAttempted = true;
|
|
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;
|
|
554
|
+
const hit = await attempt(next, via === 'named' ? Math.min(input.waitMs, remaining()) : Math.min(FALLBACK_CLICK_WAIT_MS, remaining()), via);
|
|
555
|
+
if (hit !== null)
|
|
556
|
+
return hit;
|
|
235
557
|
}
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
(
|
|
239
|
-
if (!
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
558
|
+
const missing = [];
|
|
559
|
+
if (req.requireDispatch && !bestDispatch)
|
|
560
|
+
missing.push('dispatch');
|
|
561
|
+
if (req.requireDom && !bestDom)
|
|
562
|
+
missing.push('DOM change');
|
|
563
|
+
const others = otherActions.size > 0 ? `; other actions dispatched: ${[...otherActions].join(', ')}` : '';
|
|
564
|
+
const navigatorReclicks = [...navigators.values()].reduce((sum, n) => sum + n, 0);
|
|
565
|
+
// #1040: the named control was seen only disabled and never clicked enabled — the probe could not
|
|
566
|
+
// reach the action. Not measured, with the reason: a click made new controls appear (the gate sits
|
|
567
|
+
// behind navigation the probe walked into but could not finish) or none did (the gate is input the
|
|
568
|
+
// probe did not provide). An enabled named control that was clicked and did nothing stays the miss.
|
|
569
|
+
if (!namedAttempted && namedDisabledSeen > 0) {
|
|
570
|
+
const reason = navigated ? 'behind-navigation' : 'disabled-after-priming';
|
|
245
571
|
return {
|
|
246
|
-
status: 'action-
|
|
247
|
-
|
|
572
|
+
status: 'action-unreachable',
|
|
573
|
+
reason,
|
|
574
|
+
diagnostic: `actionSpec.${input.actionName} (label "${entry.label}") rendered disabled on every observation (${namedDisabledSeen} matching control(s)) and was never clicked enabled; ` +
|
|
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}` +
|
|
576
|
+
`${exhausted ? '; budget exhausted' : ''}${others}`,
|
|
248
577
|
};
|
|
249
578
|
}
|
|
250
|
-
return {
|
|
579
|
+
return {
|
|
580
|
+
status: 'action-no-effect',
|
|
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)` +
|
|
582
|
+
`; navigator re-clicks=${navigatorReclicks}${exhausted ? '; budget exhausted' : ''}); best observed dispatch=${bestDispatch}, domChanged=${bestDom}; ` +
|
|
583
|
+
`missing=${missing.join('+')}${others}`,
|
|
584
|
+
};
|
|
251
585
|
}
|
|
252
586
|
finally {
|
|
253
587
|
window.removeEventListener('error', onWindowError);
|
package/dist/index.d.ts
CHANGED
|
@@ -52,20 +52,27 @@
|
|
|
52
52
|
*
|
|
53
53
|
* For each entry in `contract.actionSpec`:
|
|
54
54
|
*
|
|
55
|
-
* 1. Render the component in a real Chromium tab via Playwright
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
55
|
+
* 1. Render the component in a real Chromium tab via Playwright, with
|
|
56
|
+
* `sampleProps` (the commit's fixture props) — a component that draws its
|
|
57
|
+
* controls from props has nothing to click without them.
|
|
58
|
+
* 2. Collect the controls to try, the way the harness's render-check
|
|
59
|
+
* does (#996): first those that NAME the action (`data-action` by
|
|
60
|
+
* value, then aria-label / text carrying the label or the name), then
|
|
61
|
+
* every other enabled clickable in DOM order.
|
|
62
|
+
* 3. For each candidate: snapshot the DOM, click, wait up to `timeoutMs`
|
|
63
|
+
* (named) or a bounded slice of it (fallback) for the required signal
|
|
64
|
+
* (per the gate above); stop at the first control that produces it.
|
|
65
|
+
* Only dispatches of THIS action count; others are reported.
|
|
66
|
+
* 4. `action-not-rendered` means no clickable control exists at all.
|
|
67
|
+
* 5. `action-no-effect` means controls were clicked and none produced
|
|
68
|
+
* the required signal — the diagnostic says how many, and what was seen.
|
|
62
69
|
* 6. Classify: ok / action-no-effect / action-not-rendered /
|
|
63
70
|
* render-failed.
|
|
64
71
|
*
|
|
65
72
|
* Empty `actionSpec` returns `{ ok: true, failures: [] }` (not
|
|
66
73
|
* applicable — nothing to test).
|
|
67
74
|
*/
|
|
68
|
-
import type { DataContract } from '@ggui-ai/protocol';
|
|
75
|
+
import type { DataContract, JsonObject } from '@ggui-ai/protocol';
|
|
69
76
|
import type { Browser as PlaywrightBrowser, BrowserContext as PlaywrightBrowserContext, Page as PlaywrightPage, LaunchOptions as PlaywrightLaunchOptions } from 'playwright-core';
|
|
70
77
|
/**
|
|
71
78
|
* Minimal Playwright `chromium` surface the validator depends on.
|
|
@@ -85,6 +92,13 @@ export type { PlaywrightBrowser, PlaywrightBrowserContext, PlaywrightPage, };
|
|
|
85
92
|
export interface ValidateContractBehaviorInput {
|
|
86
93
|
readonly componentCode: string;
|
|
87
94
|
readonly contract: DataContract;
|
|
95
|
+
/**
|
|
96
|
+
* The props the component was generated for (the commit's fixture props —
|
|
97
|
+
* the same object the visual judge receives). Without them a component that
|
|
98
|
+
* renders its controls from props (a board's tasks, a list's items) mounts
|
|
99
|
+
* EMPTY and has nothing to click, which reads as action-not-rendered.
|
|
100
|
+
*/
|
|
101
|
+
readonly sampleProps?: JsonObject;
|
|
88
102
|
readonly timeoutMs?: number;
|
|
89
103
|
/**
|
|
90
104
|
* Playwright module. Pass `{ chromium }` from `playwright-core` or
|
|
@@ -93,11 +107,37 @@ export interface ValidateContractBehaviorInput {
|
|
|
93
107
|
*/
|
|
94
108
|
readonly playwright: PlaywrightModule;
|
|
95
109
|
}
|
|
96
|
-
|
|
110
|
+
/**
|
|
111
|
+
* Extensibly-closed (docs/protocol/VERSION-POLICY.md §1.2): members are added
|
|
112
|
+
* in MINOR releases of this package. A reader that meets a kind it does not
|
|
113
|
+
* name MUST treat it as a failure of unknown kind — never as success, never
|
|
114
|
+
* as "not rendered". An exhaustive `switch` over this union stops compiling
|
|
115
|
+
* at the next member; that is the intended nudge at upgrade, not a wire
|
|
116
|
+
* break. The report wire (`benchmark-report.v2`) carries the kind as a
|
|
117
|
+
* string and its readers copy it verbatim (ggui#1014, ggui#1040).
|
|
118
|
+
*/
|
|
119
|
+
export type BehaviorFailureKind = 'action-no-effect' | 'action-not-rendered'
|
|
120
|
+
/**
|
|
121
|
+
* The named action's control was present but the static probe could not reach it enabled
|
|
122
|
+
* (ggui#1040). NOT MEASURED — never a miss, never "not rendered"; symmetric across arms by
|
|
123
|
+
* construction. `BehaviorFailure.reason` says why.
|
|
124
|
+
*/
|
|
125
|
+
| 'action-unreachable' | 'render-failed' | 'timeout';
|
|
126
|
+
/**
|
|
127
|
+
* Why an action was unreachable (ggui#1040). `disabled-after-priming`: the action's control rendered
|
|
128
|
+
* disabled and no click of the probe's made new controls appear — the gate is input the probe did not
|
|
129
|
+
* provide. `behind-navigation`: a click made new controls appear (a step, a tab, a revealed section)
|
|
130
|
+
* and the action's control was still never observed enabled — the gate sits behind navigation the
|
|
131
|
+
* probe walked into but could not finish. Measured by the clickable-candidate set changing, not by a
|
|
132
|
+
* DOM diff.
|
|
133
|
+
*/
|
|
134
|
+
export type BehaviorUnreachableReason = 'disabled-after-priming' | 'behind-navigation';
|
|
97
135
|
export interface BehaviorFailure {
|
|
98
136
|
readonly kind: BehaviorFailureKind;
|
|
99
137
|
readonly actionName?: string;
|
|
100
138
|
readonly diagnostic: string;
|
|
139
|
+
/** Present when `kind` is `action-unreachable` (ggui#1040). */
|
|
140
|
+
readonly reason?: BehaviorUnreachableReason;
|
|
101
141
|
}
|
|
102
142
|
export interface ValidateContractBehaviorResult {
|
|
103
143
|
readonly ok: boolean;
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyEG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAClE,OAAO,KAAK,EACV,OAAO,IAAI,iBAAiB,EAC5B,cAAc,IAAI,wBAAwB,EAC1C,IAAI,IAAI,cAAc,EACtB,aAAa,IAAI,uBAAuB,EACzC,MAAM,iBAAiB,CAAC;AAEzB;;;;;;;GAOG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,QAAQ,EAAE;QACjB,MAAM,CAAC,OAAO,CAAC,EAAE,uBAAuB,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAAC;KACvE,CAAC;CACH;AAED,yDAAyD;AACzD,YAAY,EACV,iBAAiB,EACjB,wBAAwB,EACxB,cAAc,GACf,CAAC;AAEF,MAAM,WAAW,6BAA6B;IAC5C,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAC;IAChC;;;;;OAKG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,UAAU,CAAC;IAClC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B;;;;OAIG;IACH,QAAQ,CAAC,UAAU,EAAE,gBAAgB,CAAC;CACvC;AAED;;;;;;;;GAQG;AACH,MAAM,MAAM,mBAAmB,GAC3B,kBAAkB,GAClB,qBAAqB;AACvB;;;;GAIG;GACD,oBAAoB,GACpB,eAAe,GACf,SAAS,CAAC;AAEd;;;;;;;GAOG;AACH,MAAM,MAAM,yBAAyB,GAAG,wBAAwB,GAAG,mBAAmB,CAAC;AAEvF,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,IAAI,EAAE,mBAAmB,CAAC;IACnC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,+DAA+D;IAC/D,QAAQ,CAAC,MAAM,CAAC,EAAE,yBAAyB,CAAC;CAC7C;AAED,MAAM,WAAW,8BAA8B;IAC7C,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,QAAQ,EAAE,SAAS,eAAe,EAAE,CAAC;CAC/C;AAED,OAAO,EAAE,wBAAwB,EAAE,MAAM,eAAe,CAAC;AAEzD;;;;GAIG;AACH,qBAAa,2BAA4B,SAAQ,KAAK;IACpD,SAAkB,IAAI,iCAAiC;;CASxD"}
|