@ggui-ai/ui-visual-tester 0.19.0 → 0.20.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.map +1 -1
- package/dist/fixture-runtime.js +1 -150
- package/dist/prime-inputs.d.ts +12 -0
- package/dist/prime-inputs.d.ts.map +1 -0
- package/dist/prime-inputs.js +159 -0
- package/fixture/fixture-runtime.js +91 -89
- package/package.json +9 -4
|
@@ -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;
|
|
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
|
@@ -26,6 +26,7 @@ import * as Wire from '@ggui-ai/wire';
|
|
|
26
26
|
// in-repo consumers of the compile pipeline use.
|
|
27
27
|
import { hoistImports, loadModule } from '@ggui-ai/design/module-loader';
|
|
28
28
|
import { rewriteImports, stripMarkers, } from '@ggui-ai/design/rendering';
|
|
29
|
+
import { primeInputs } from './prime-inputs.js';
|
|
29
30
|
function parseActionEntry(raw) {
|
|
30
31
|
if (raw === null || typeof raw !== 'object')
|
|
31
32
|
return null;
|
|
@@ -150,156 +151,6 @@ function collectActionCandidates(root, name, label) {
|
|
|
150
151
|
}
|
|
151
152
|
return { named: [...byValue, ...byText], fallback: rest, disabled, namedDisabled, allClickables: every };
|
|
152
153
|
}
|
|
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
|
-
/**
|
|
160
|
-
* #1040: dates are primed RELATIVE TO NOW — a form routinely refuses a date in the past
|
|
161
|
-
* ("preferred follow-up date"), and a constant sample date is a past date. Thirty days out,
|
|
162
|
-
* clamped into the input's own [min, max] when those are set in the input's format (ISO
|
|
163
|
-
* strings compare lexically). Time-of-day values have no past to fall into.
|
|
164
|
-
*/
|
|
165
|
-
const pad2 = (n) => String(n).padStart(2, '0');
|
|
166
|
-
const partsOf = (at) => ({ y: at.getFullYear(), m: at.getMonth() + 1, d: at.getDate() });
|
|
167
|
-
/** Today and thirty days out, as the parts a date-like value is built from. */
|
|
168
|
-
function todayAndThirtyDaysOut() {
|
|
169
|
-
const now = new Date();
|
|
170
|
-
const at = new Date(now);
|
|
171
|
-
at.setDate(at.getDate() + 30);
|
|
172
|
-
return { today: partsOf(now), later: partsOf(at) };
|
|
173
|
-
}
|
|
174
|
-
/**
|
|
175
|
-
* Clamp a value into the input's [min, max] when each is set in the input's own format
|
|
176
|
-
* (ISO-shaped strings compare lexically). `min` later than the value wins; `max` earlier than
|
|
177
|
-
* the value wins only while it is not before `floor` (today in the same format) — a max in
|
|
178
|
-
* the past is a form that can never be satisfied, and the probe reports that as it is.
|
|
179
|
-
*/
|
|
180
|
-
function clampToRange(el, value, floor, format) {
|
|
181
|
-
let out = value;
|
|
182
|
-
if (format.test(el.min) && el.min > out)
|
|
183
|
-
out = el.min;
|
|
184
|
-
if (format.test(el.max) && el.max < out && el.max >= floor)
|
|
185
|
-
out = el.max;
|
|
186
|
-
return out;
|
|
187
|
-
}
|
|
188
|
-
function isoWeekOf(y, m, d) {
|
|
189
|
-
// ISO-8601 week: Thursday of the same week decides the year.
|
|
190
|
-
const date = new Date(Date.UTC(y, m - 1, d));
|
|
191
|
-
const day = date.getUTCDay() || 7;
|
|
192
|
-
date.setUTCDate(date.getUTCDate() + 4 - day);
|
|
193
|
-
const jan1 = new Date(Date.UTC(date.getUTCFullYear(), 0, 1));
|
|
194
|
-
const week = Math.ceil(((date.getTime() - jan1.getTime()) / 86_400_000 + 1) / 7);
|
|
195
|
-
return `${date.getUTCFullYear()}-W${pad2(week)}`;
|
|
196
|
-
}
|
|
197
|
-
function sampleValueFor(el) {
|
|
198
|
-
if (!(el instanceof HTMLInputElement))
|
|
199
|
-
return 'probe'; // a textarea has no type to switch on
|
|
200
|
-
switch (el.type) {
|
|
201
|
-
case 'number':
|
|
202
|
-
return '1';
|
|
203
|
-
case 'email':
|
|
204
|
-
return 'probe@example.com';
|
|
205
|
-
case 'url':
|
|
206
|
-
return 'https://example.com';
|
|
207
|
-
case 'tel':
|
|
208
|
-
return '5550100';
|
|
209
|
-
case 'date': {
|
|
210
|
-
const { today, later } = todayAndThirtyDaysOut();
|
|
211
|
-
const iso = (p) => `${p.y}-${pad2(p.m)}-${pad2(p.d)}`;
|
|
212
|
-
return clampToRange(el, iso(later), iso(today), /^\d{4}-\d{2}-\d{2}$/);
|
|
213
|
-
}
|
|
214
|
-
case 'time':
|
|
215
|
-
return '09:30';
|
|
216
|
-
case 'month': {
|
|
217
|
-
const { today, later } = todayAndThirtyDaysOut();
|
|
218
|
-
const ym = (p) => `${p.y}-${pad2(p.m)}`;
|
|
219
|
-
return clampToRange(el, ym(later), ym(today), /^\d{4}-\d{2}$/);
|
|
220
|
-
}
|
|
221
|
-
case 'week': {
|
|
222
|
-
const { today, later } = todayAndThirtyDaysOut();
|
|
223
|
-
return clampToRange(el, isoWeekOf(later.y, later.m, later.d), isoWeekOf(today.y, today.m, today.d), /^\d{4}-W\d{2}$/);
|
|
224
|
-
}
|
|
225
|
-
case 'datetime-local': {
|
|
226
|
-
const { today, later } = todayAndThirtyDaysOut();
|
|
227
|
-
const at = (p, hm) => `${p.y}-${pad2(p.m)}-${pad2(p.d)}T${hm}`;
|
|
228
|
-
return clampToRange(el, at(later, '09:30'), at(today, '00:00'), /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/);
|
|
229
|
-
}
|
|
230
|
-
default:
|
|
231
|
-
return 'probe';
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
/** Set a value the way React notices it: through the prototype's native setter, then an input + change event. */
|
|
235
|
-
function setNativeValue(el, value) {
|
|
236
|
-
const proto = el instanceof HTMLTextAreaElement
|
|
237
|
-
? HTMLTextAreaElement.prototype
|
|
238
|
-
: el instanceof HTMLSelectElement
|
|
239
|
-
? HTMLSelectElement.prototype
|
|
240
|
-
: HTMLInputElement.prototype;
|
|
241
|
-
const setter = Object.getOwnPropertyDescriptor(proto, 'value')?.set;
|
|
242
|
-
if (setter)
|
|
243
|
-
setter.call(el, value);
|
|
244
|
-
else
|
|
245
|
-
el.value = value;
|
|
246
|
-
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
247
|
-
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
248
|
-
}
|
|
249
|
-
/**
|
|
250
|
-
* The generic minimum a user does before pressing Send / Submit / Complete
|
|
251
|
-
* (#1021): give every empty text-like input and textarea a value, every
|
|
252
|
-
* unset select its first real option, every unchecked radio group its first
|
|
253
|
-
* radio. Checkboxes are never touched — they are often the action itself.
|
|
254
|
-
* Returns how many controls were primed.
|
|
255
|
-
*/
|
|
256
|
-
function primeInputs(root) {
|
|
257
|
-
let primed = 0;
|
|
258
|
-
const editable = (el) => !el.disabled && !(el instanceof HTMLSelectElement) ? !el.readOnly : !el.disabled;
|
|
259
|
-
for (const el of Array.from(root.querySelectorAll('input, textarea'))) {
|
|
260
|
-
if (!(el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) || !editable(el))
|
|
261
|
-
continue;
|
|
262
|
-
if (el instanceof HTMLInputElement && !TEXT_LIKE_INPUT_TYPES.has(el.type))
|
|
263
|
-
continue;
|
|
264
|
-
if (el.value.trim().length > 0)
|
|
265
|
-
continue;
|
|
266
|
-
setNativeValue(el, sampleValueFor(el));
|
|
267
|
-
if (el.value.trim().length > 0)
|
|
268
|
-
primed += 1;
|
|
269
|
-
}
|
|
270
|
-
for (const el of Array.from(root.querySelectorAll('select'))) {
|
|
271
|
-
if (!(el instanceof HTMLSelectElement) || el.disabled)
|
|
272
|
-
continue;
|
|
273
|
-
const current = el.options[el.selectedIndex];
|
|
274
|
-
if (current !== undefined && current.value.trim().length > 0)
|
|
275
|
-
continue;
|
|
276
|
-
const first = Array.from(el.options).find((o) => o.value.trim().length > 0 && !o.disabled);
|
|
277
|
-
if (first === undefined)
|
|
278
|
-
continue;
|
|
279
|
-
setNativeValue(el, first.value);
|
|
280
|
-
primed += 1;
|
|
281
|
-
}
|
|
282
|
-
const seenGroups = new Set();
|
|
283
|
-
for (const el of Array.from(root.querySelectorAll('input[type="radio"]'))) {
|
|
284
|
-
if (!(el instanceof HTMLInputElement) || el.disabled)
|
|
285
|
-
continue;
|
|
286
|
-
const group = el.name || '';
|
|
287
|
-
if (seenGroups.has(group))
|
|
288
|
-
continue;
|
|
289
|
-
seenGroups.add(group);
|
|
290
|
-
const members = group
|
|
291
|
-
? Array.from(root.querySelectorAll(`input[type="radio"][name="${CSS.escape(group)}"]`)).filter((m) => m instanceof HTMLInputElement)
|
|
292
|
-
: [el];
|
|
293
|
-
if (members.some((m) => m.checked))
|
|
294
|
-
continue;
|
|
295
|
-
const first = members.find((m) => !m.disabled);
|
|
296
|
-
if (first === undefined)
|
|
297
|
-
continue;
|
|
298
|
-
first.click();
|
|
299
|
-
primed += 1;
|
|
300
|
-
}
|
|
301
|
-
return primed;
|
|
302
|
-
}
|
|
303
154
|
function countNamedDispatches(actionName) {
|
|
304
155
|
return (window.__ggui_test_dispatches__ ?? []).filter((d) => d.actionName === actionName).length;
|
|
305
156
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare function sampleValueFor(el: HTMLInputElement | HTMLTextAreaElement): string;
|
|
2
|
+
/** Set a value the way React notices it: through the prototype's native setter, then an input + change event. */
|
|
3
|
+
export declare function setNativeValue(el: HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement, value: string): void;
|
|
4
|
+
/**
|
|
5
|
+
* The generic minimum a user does before pressing Send / Submit / Complete
|
|
6
|
+
* (#1021): give every empty text-like input and textarea a value, every
|
|
7
|
+
* unset select its first real option, every unchecked radio group its first
|
|
8
|
+
* radio. Checkboxes are never touched — they are often the action itself.
|
|
9
|
+
* Returns how many controls were primed.
|
|
10
|
+
*/
|
|
11
|
+
export declare function primeInputs(root: HTMLElement): number;
|
|
12
|
+
//# sourceMappingURL=prime-inputs.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prime-inputs.d.ts","sourceRoot":"","sources":["../src/prime-inputs.ts"],"names":[],"mappings":"AAsDA,wBAAgB,cAAc,CAAC,EAAE,EAAE,gBAAgB,GAAG,mBAAmB,GAAG,MAAM,CAmCjF;AAED,iHAAiH;AACjH,wBAAgB,cAAc,CAAC,EAAE,EAAE,gBAAgB,GAAG,mBAAmB,GAAG,iBAAiB,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAWlH;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,WAAW,GAAG,MAAM,CAoCrD"}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// oss/packages/ui-visual-tester/src/prime-inputs.ts
|
|
2
|
+
//
|
|
3
|
+
// Shared input-priming for the "generic minimum a user does before pressing
|
|
4
|
+
// Send / Submit / Complete" (#1021), incl. the #1040 relative-to-now date rule.
|
|
5
|
+
// Extracted from fixture-runtime.ts so BOTH the Playwright fixture runtime and
|
|
6
|
+
// the ui-gen runtime-render action-wiring probe (ggui#1187) prime inputs the
|
|
7
|
+
// SAME way — one typed-values table, no divergence. Pure DOM + Date only (no
|
|
8
|
+
// Playwright), so it is safe to import into the lightweight happy-dom check
|
|
9
|
+
// worker via the `@ggui-ai/ui-visual-tester/prime-inputs` subpath.
|
|
10
|
+
// #1040: date-like types too — a wizard's first step is routinely gated on one ("preferred follow-up
|
|
11
|
+
// date"), and a Next that never enables hides every step behind it from the probe.
|
|
12
|
+
const TEXT_LIKE_INPUT_TYPES = new Set([
|
|
13
|
+
'text', 'email', 'search', 'url', 'tel', 'number', 'password', '',
|
|
14
|
+
'date', 'time', 'month', 'week', 'datetime-local',
|
|
15
|
+
]);
|
|
16
|
+
/**
|
|
17
|
+
* #1040: dates are primed RELATIVE TO NOW — a form routinely refuses a date in the past
|
|
18
|
+
* ("preferred follow-up date"), and a constant sample date is a past date. Thirty days out,
|
|
19
|
+
* clamped into the input's own [min, max] when those are set in the input's format (ISO
|
|
20
|
+
* strings compare lexically). Time-of-day values have no past to fall into.
|
|
21
|
+
*/
|
|
22
|
+
const pad2 = (n) => String(n).padStart(2, '0');
|
|
23
|
+
const partsOf = (at) => ({ y: at.getFullYear(), m: at.getMonth() + 1, d: at.getDate() });
|
|
24
|
+
/** Today and thirty days out, as the parts a date-like value is built from. */
|
|
25
|
+
function todayAndThirtyDaysOut() {
|
|
26
|
+
const now = new Date();
|
|
27
|
+
const at = new Date(now);
|
|
28
|
+
at.setDate(at.getDate() + 30);
|
|
29
|
+
return { today: partsOf(now), later: partsOf(at) };
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Clamp a value into the input's [min, max] when each is set in the input's own format
|
|
33
|
+
* (ISO-shaped strings compare lexically). `min` later than the value wins; `max` earlier than
|
|
34
|
+
* the value wins only while it is not before `floor` (today in the same format) — a max in
|
|
35
|
+
* the past is a form that can never be satisfied, and the probe reports that as it is.
|
|
36
|
+
*/
|
|
37
|
+
function clampToRange(el, value, floor, format) {
|
|
38
|
+
let out = value;
|
|
39
|
+
if (format.test(el.min) && el.min > out)
|
|
40
|
+
out = el.min;
|
|
41
|
+
if (format.test(el.max) && el.max < out && el.max >= floor)
|
|
42
|
+
out = el.max;
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
function isoWeekOf(y, m, d) {
|
|
46
|
+
// ISO-8601 week: Thursday of the same week decides the year.
|
|
47
|
+
const date = new Date(Date.UTC(y, m - 1, d));
|
|
48
|
+
const day = date.getUTCDay() || 7;
|
|
49
|
+
date.setUTCDate(date.getUTCDate() + 4 - day);
|
|
50
|
+
const jan1 = new Date(Date.UTC(date.getUTCFullYear(), 0, 1));
|
|
51
|
+
const week = Math.ceil(((date.getTime() - jan1.getTime()) / 86_400_000 + 1) / 7);
|
|
52
|
+
return `${date.getUTCFullYear()}-W${pad2(week)}`;
|
|
53
|
+
}
|
|
54
|
+
export function sampleValueFor(el) {
|
|
55
|
+
if (!(el instanceof HTMLInputElement))
|
|
56
|
+
return 'probe'; // a textarea has no type to switch on
|
|
57
|
+
switch (el.type) {
|
|
58
|
+
case 'number':
|
|
59
|
+
return '1';
|
|
60
|
+
case 'email':
|
|
61
|
+
return 'probe@example.com';
|
|
62
|
+
case 'url':
|
|
63
|
+
return 'https://example.com';
|
|
64
|
+
case 'tel':
|
|
65
|
+
return '5550100';
|
|
66
|
+
case 'date': {
|
|
67
|
+
const { today, later } = todayAndThirtyDaysOut();
|
|
68
|
+
const iso = (p) => `${p.y}-${pad2(p.m)}-${pad2(p.d)}`;
|
|
69
|
+
return clampToRange(el, iso(later), iso(today), /^\d{4}-\d{2}-\d{2}$/);
|
|
70
|
+
}
|
|
71
|
+
case 'time':
|
|
72
|
+
return '09:30';
|
|
73
|
+
case 'month': {
|
|
74
|
+
const { today, later } = todayAndThirtyDaysOut();
|
|
75
|
+
const ym = (p) => `${p.y}-${pad2(p.m)}`;
|
|
76
|
+
return clampToRange(el, ym(later), ym(today), /^\d{4}-\d{2}$/);
|
|
77
|
+
}
|
|
78
|
+
case 'week': {
|
|
79
|
+
const { today, later } = todayAndThirtyDaysOut();
|
|
80
|
+
return clampToRange(el, isoWeekOf(later.y, later.m, later.d), isoWeekOf(today.y, today.m, today.d), /^\d{4}-W\d{2}$/);
|
|
81
|
+
}
|
|
82
|
+
case 'datetime-local': {
|
|
83
|
+
const { today, later } = todayAndThirtyDaysOut();
|
|
84
|
+
const at = (p, hm) => `${p.y}-${pad2(p.m)}-${pad2(p.d)}T${hm}`;
|
|
85
|
+
return clampToRange(el, at(later, '09:30'), at(today, '00:00'), /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/);
|
|
86
|
+
}
|
|
87
|
+
default:
|
|
88
|
+
return 'probe';
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/** Set a value the way React notices it: through the prototype's native setter, then an input + change event. */
|
|
92
|
+
export function setNativeValue(el, value) {
|
|
93
|
+
const proto = el instanceof HTMLTextAreaElement
|
|
94
|
+
? HTMLTextAreaElement.prototype
|
|
95
|
+
: el instanceof HTMLSelectElement
|
|
96
|
+
? HTMLSelectElement.prototype
|
|
97
|
+
: HTMLInputElement.prototype;
|
|
98
|
+
const setter = Object.getOwnPropertyDescriptor(proto, 'value')?.set;
|
|
99
|
+
if (setter)
|
|
100
|
+
setter.call(el, value);
|
|
101
|
+
else
|
|
102
|
+
el.value = value;
|
|
103
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
104
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* The generic minimum a user does before pressing Send / Submit / Complete
|
|
108
|
+
* (#1021): give every empty text-like input and textarea a value, every
|
|
109
|
+
* unset select its first real option, every unchecked radio group its first
|
|
110
|
+
* radio. Checkboxes are never touched — they are often the action itself.
|
|
111
|
+
* Returns how many controls were primed.
|
|
112
|
+
*/
|
|
113
|
+
export function primeInputs(root) {
|
|
114
|
+
let primed = 0;
|
|
115
|
+
const editable = (el) => !el.disabled && !(el instanceof HTMLSelectElement) ? !el.readOnly : !el.disabled;
|
|
116
|
+
for (const el of Array.from(root.querySelectorAll('input, textarea'))) {
|
|
117
|
+
if (!(el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) || !editable(el))
|
|
118
|
+
continue;
|
|
119
|
+
if (el instanceof HTMLInputElement && !TEXT_LIKE_INPUT_TYPES.has(el.type))
|
|
120
|
+
continue;
|
|
121
|
+
if (el.value.trim().length > 0)
|
|
122
|
+
continue;
|
|
123
|
+
setNativeValue(el, sampleValueFor(el));
|
|
124
|
+
if (el.value.trim().length > 0)
|
|
125
|
+
primed += 1;
|
|
126
|
+
}
|
|
127
|
+
for (const el of Array.from(root.querySelectorAll('select'))) {
|
|
128
|
+
if (!(el instanceof HTMLSelectElement) || el.disabled)
|
|
129
|
+
continue;
|
|
130
|
+
const current = el.options[el.selectedIndex];
|
|
131
|
+
if (current !== undefined && current.value.trim().length > 0)
|
|
132
|
+
continue;
|
|
133
|
+
const first = Array.from(el.options).find((o) => o.value.trim().length > 0 && !o.disabled);
|
|
134
|
+
if (first === undefined)
|
|
135
|
+
continue;
|
|
136
|
+
setNativeValue(el, first.value);
|
|
137
|
+
primed += 1;
|
|
138
|
+
}
|
|
139
|
+
const seenGroups = new Set();
|
|
140
|
+
for (const el of Array.from(root.querySelectorAll('input[type="radio"]'))) {
|
|
141
|
+
if (!(el instanceof HTMLInputElement) || el.disabled)
|
|
142
|
+
continue;
|
|
143
|
+
const group = el.name || '';
|
|
144
|
+
if (seenGroups.has(group))
|
|
145
|
+
continue;
|
|
146
|
+
seenGroups.add(group);
|
|
147
|
+
const members = group
|
|
148
|
+
? Array.from(root.querySelectorAll(`input[type="radio"][name="${CSS.escape(group)}"]`)).filter((m) => m instanceof HTMLInputElement)
|
|
149
|
+
: [el];
|
|
150
|
+
if (members.some((m) => m.checked))
|
|
151
|
+
continue;
|
|
152
|
+
const first = members.find((m) => !m.disabled);
|
|
153
|
+
if (first === undefined)
|
|
154
|
+
continue;
|
|
155
|
+
first.click();
|
|
156
|
+
primed += 1;
|
|
157
|
+
}
|
|
158
|
+
return primed;
|
|
159
|
+
}
|
|
@@ -32794,95 +32794,7 @@
|
|
|
32794
32794
|
}
|
|
32795
32795
|
}
|
|
32796
32796
|
|
|
32797
|
-
// src/
|
|
32798
|
-
function parseActionEntry(raw) {
|
|
32799
|
-
if (raw === null || typeof raw !== "object") return null;
|
|
32800
|
-
const r = raw;
|
|
32801
|
-
if (typeof r.label !== "string") return null;
|
|
32802
|
-
return { label: r.label };
|
|
32803
|
-
}
|
|
32804
|
-
function parseContract(raw) {
|
|
32805
|
-
const out = {};
|
|
32806
|
-
if (raw === null || typeof raw !== "object") return { actionSpec: out };
|
|
32807
|
-
const v = raw;
|
|
32808
|
-
if (v.actionSpec === null || typeof v.actionSpec !== "object") {
|
|
32809
|
-
return { actionSpec: out };
|
|
32810
|
-
}
|
|
32811
|
-
for (const [name, entry] of Object.entries(
|
|
32812
|
-
v.actionSpec
|
|
32813
|
-
)) {
|
|
32814
|
-
const parsed = parseActionEntry(entry);
|
|
32815
|
-
if (parsed !== null) out[name] = parsed;
|
|
32816
|
-
}
|
|
32817
|
-
return { actionSpec: out };
|
|
32818
|
-
}
|
|
32819
|
-
function installRegistry() {
|
|
32820
|
-
const registry = {
|
|
32821
|
-
react: React3,
|
|
32822
|
-
reactDom: ReactDomClient,
|
|
32823
|
-
primitives: primitives_exports,
|
|
32824
|
-
components: components_exports,
|
|
32825
|
-
compositions: compositions_exports,
|
|
32826
|
-
interact: interact_exports,
|
|
32827
|
-
appComponents: {},
|
|
32828
|
-
wire: dist_exports,
|
|
32829
|
-
adapters: {}
|
|
32830
|
-
};
|
|
32831
|
-
window.__ggui__ = registry;
|
|
32832
|
-
window.__REACT = React3;
|
|
32833
|
-
window.__GGUI_PRIMITIVES = primitives_exports;
|
|
32834
|
-
window.__GGUI_COMPONENTS = components_exports;
|
|
32835
|
-
window.__GGUI_COMPOSITIONS = compositions_exports;
|
|
32836
|
-
window.__GGUI_INTERACT = interact_exports;
|
|
32837
|
-
window.__GGUI_APP_COMPONENTS = registry.appComponents;
|
|
32838
|
-
return registry;
|
|
32839
|
-
}
|
|
32840
|
-
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]';
|
|
32841
|
-
var FALLBACK_CLICK_WAIT_MS = 600;
|
|
32842
|
-
var MAX_NAVIGATOR_RECLICKS = 8;
|
|
32843
|
-
var ACTION_BUDGET_FLOOR_MS = 6e3;
|
|
32844
|
-
function norm(text) {
|
|
32845
|
-
return text.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
32846
|
-
}
|
|
32847
|
-
function isControlDisabled(el) {
|
|
32848
|
-
return el.hasAttribute("disabled") || el.getAttribute("aria-disabled") === "true";
|
|
32849
|
-
}
|
|
32850
|
-
function collectActionCandidates(root, name, label) {
|
|
32851
|
-
const every = Array.from(root.querySelectorAll(CLICKABLE_SELECTOR)).filter(
|
|
32852
|
-
(el) => el instanceof HTMLElement
|
|
32853
|
-
);
|
|
32854
|
-
const isDisabled = isControlDisabled;
|
|
32855
|
-
const lcName = name.toLowerCase();
|
|
32856
|
-
const lcLabel = label.toLowerCase();
|
|
32857
|
-
const nameKey = norm(name);
|
|
32858
|
-
const labelKey = norm(label);
|
|
32859
|
-
const namesAction = (el) => {
|
|
32860
|
-
const dataAction = (el.getAttribute("data-action") ?? "").toLowerCase();
|
|
32861
|
-
if (dataAction === lcName) return "value";
|
|
32862
|
-
const aria = (el.getAttribute("aria-label") ?? "").toLowerCase();
|
|
32863
|
-
const txt = (el.textContent ?? "").trim().toLowerCase();
|
|
32864
|
-
const ariaNames = aria.length > 0 && (aria.includes(lcLabel) || nameKey.length > 0 && norm(aria).includes(nameKey));
|
|
32865
|
-
const textNames = txt.length > 0 && (txt.includes(lcLabel) || nameKey.length > 0 && norm(txt).includes(nameKey) || labelKey.length > 0 && norm(txt).includes(labelKey));
|
|
32866
|
-
return ariaNames || textNames ? "text" : null;
|
|
32867
|
-
};
|
|
32868
|
-
const disabledEls = every.filter(isDisabled);
|
|
32869
|
-
const disabled = disabledEls.length;
|
|
32870
|
-
const namedDisabled = disabledEls.filter((el) => namesAction(el) !== null).length;
|
|
32871
|
-
const all = every.filter(
|
|
32872
|
-
(el) => !isDisabled(el) && // an already-checked radio fires no change on click — nothing to observe
|
|
32873
|
-
!(el instanceof HTMLInputElement && el.type === "radio" && el.checked)
|
|
32874
|
-
);
|
|
32875
|
-
const byValue = [];
|
|
32876
|
-
const byText = [];
|
|
32877
|
-
const rest = [];
|
|
32878
|
-
for (const el of all) {
|
|
32879
|
-
const how = namesAction(el);
|
|
32880
|
-
if (how === "value") byValue.push(el);
|
|
32881
|
-
else if (how === "text") byText.push(el);
|
|
32882
|
-
else rest.push(el);
|
|
32883
|
-
}
|
|
32884
|
-
return { named: [...byValue, ...byText], fallback: rest, disabled, namedDisabled, allClickables: every };
|
|
32885
|
-
}
|
|
32797
|
+
// src/prime-inputs.ts
|
|
32886
32798
|
var TEXT_LIKE_INPUT_TYPES = /* @__PURE__ */ new Set([
|
|
32887
32799
|
"text",
|
|
32888
32800
|
"email",
|
|
@@ -32998,6 +32910,96 @@
|
|
|
32998
32910
|
}
|
|
32999
32911
|
return primed;
|
|
33000
32912
|
}
|
|
32913
|
+
|
|
32914
|
+
// src/fixture-runtime.ts
|
|
32915
|
+
function parseActionEntry(raw) {
|
|
32916
|
+
if (raw === null || typeof raw !== "object") return null;
|
|
32917
|
+
const r = raw;
|
|
32918
|
+
if (typeof r.label !== "string") return null;
|
|
32919
|
+
return { label: r.label };
|
|
32920
|
+
}
|
|
32921
|
+
function parseContract(raw) {
|
|
32922
|
+
const out = {};
|
|
32923
|
+
if (raw === null || typeof raw !== "object") return { actionSpec: out };
|
|
32924
|
+
const v = raw;
|
|
32925
|
+
if (v.actionSpec === null || typeof v.actionSpec !== "object") {
|
|
32926
|
+
return { actionSpec: out };
|
|
32927
|
+
}
|
|
32928
|
+
for (const [name, entry] of Object.entries(
|
|
32929
|
+
v.actionSpec
|
|
32930
|
+
)) {
|
|
32931
|
+
const parsed = parseActionEntry(entry);
|
|
32932
|
+
if (parsed !== null) out[name] = parsed;
|
|
32933
|
+
}
|
|
32934
|
+
return { actionSpec: out };
|
|
32935
|
+
}
|
|
32936
|
+
function installRegistry() {
|
|
32937
|
+
const registry = {
|
|
32938
|
+
react: React3,
|
|
32939
|
+
reactDom: ReactDomClient,
|
|
32940
|
+
primitives: primitives_exports,
|
|
32941
|
+
components: components_exports,
|
|
32942
|
+
compositions: compositions_exports,
|
|
32943
|
+
interact: interact_exports,
|
|
32944
|
+
appComponents: {},
|
|
32945
|
+
wire: dist_exports,
|
|
32946
|
+
adapters: {}
|
|
32947
|
+
};
|
|
32948
|
+
window.__ggui__ = registry;
|
|
32949
|
+
window.__REACT = React3;
|
|
32950
|
+
window.__GGUI_PRIMITIVES = primitives_exports;
|
|
32951
|
+
window.__GGUI_COMPONENTS = components_exports;
|
|
32952
|
+
window.__GGUI_COMPOSITIONS = compositions_exports;
|
|
32953
|
+
window.__GGUI_INTERACT = interact_exports;
|
|
32954
|
+
window.__GGUI_APP_COMPONENTS = registry.appComponents;
|
|
32955
|
+
return registry;
|
|
32956
|
+
}
|
|
32957
|
+
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]';
|
|
32958
|
+
var FALLBACK_CLICK_WAIT_MS = 600;
|
|
32959
|
+
var MAX_NAVIGATOR_RECLICKS = 8;
|
|
32960
|
+
var ACTION_BUDGET_FLOOR_MS = 6e3;
|
|
32961
|
+
function norm(text) {
|
|
32962
|
+
return text.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
32963
|
+
}
|
|
32964
|
+
function isControlDisabled(el) {
|
|
32965
|
+
return el.hasAttribute("disabled") || el.getAttribute("aria-disabled") === "true";
|
|
32966
|
+
}
|
|
32967
|
+
function collectActionCandidates(root, name, label) {
|
|
32968
|
+
const every = Array.from(root.querySelectorAll(CLICKABLE_SELECTOR)).filter(
|
|
32969
|
+
(el) => el instanceof HTMLElement
|
|
32970
|
+
);
|
|
32971
|
+
const isDisabled = isControlDisabled;
|
|
32972
|
+
const lcName = name.toLowerCase();
|
|
32973
|
+
const lcLabel = label.toLowerCase();
|
|
32974
|
+
const nameKey = norm(name);
|
|
32975
|
+
const labelKey = norm(label);
|
|
32976
|
+
const namesAction = (el) => {
|
|
32977
|
+
const dataAction = (el.getAttribute("data-action") ?? "").toLowerCase();
|
|
32978
|
+
if (dataAction === lcName) return "value";
|
|
32979
|
+
const aria = (el.getAttribute("aria-label") ?? "").toLowerCase();
|
|
32980
|
+
const txt = (el.textContent ?? "").trim().toLowerCase();
|
|
32981
|
+
const ariaNames = aria.length > 0 && (aria.includes(lcLabel) || nameKey.length > 0 && norm(aria).includes(nameKey));
|
|
32982
|
+
const textNames = txt.length > 0 && (txt.includes(lcLabel) || nameKey.length > 0 && norm(txt).includes(nameKey) || labelKey.length > 0 && norm(txt).includes(labelKey));
|
|
32983
|
+
return ariaNames || textNames ? "text" : null;
|
|
32984
|
+
};
|
|
32985
|
+
const disabledEls = every.filter(isDisabled);
|
|
32986
|
+
const disabled = disabledEls.length;
|
|
32987
|
+
const namedDisabled = disabledEls.filter((el) => namesAction(el) !== null).length;
|
|
32988
|
+
const all = every.filter(
|
|
32989
|
+
(el) => !isDisabled(el) && // an already-checked radio fires no change on click — nothing to observe
|
|
32990
|
+
!(el instanceof HTMLInputElement && el.type === "radio" && el.checked)
|
|
32991
|
+
);
|
|
32992
|
+
const byValue = [];
|
|
32993
|
+
const byText = [];
|
|
32994
|
+
const rest = [];
|
|
32995
|
+
for (const el of all) {
|
|
32996
|
+
const how = namesAction(el);
|
|
32997
|
+
if (how === "value") byValue.push(el);
|
|
32998
|
+
else if (how === "text") byText.push(el);
|
|
32999
|
+
else rest.push(el);
|
|
33000
|
+
}
|
|
33001
|
+
return { named: [...byValue, ...byText], fallback: rest, disabled, namedDisabled, allClickables: every };
|
|
33002
|
+
}
|
|
33001
33003
|
function countNamedDispatches(actionName) {
|
|
33002
33004
|
return (window.__ggui_test_dispatches__ ?? []).filter((d) => d.actionName === actionName).length;
|
|
33003
33005
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ggui-ai/ui-visual-tester",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.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",
|
|
@@ -35,6 +35,11 @@
|
|
|
35
35
|
"types": "./dist/index.d.ts",
|
|
36
36
|
"import": "./dist/index.js",
|
|
37
37
|
"default": "./dist/index.js"
|
|
38
|
+
},
|
|
39
|
+
"./prime-inputs": {
|
|
40
|
+
"types": "./dist/prime-inputs.d.ts",
|
|
41
|
+
"import": "./dist/prime-inputs.js",
|
|
42
|
+
"default": "./dist/prime-inputs.js"
|
|
38
43
|
}
|
|
39
44
|
},
|
|
40
45
|
"peerDependencies": {
|
|
@@ -48,9 +53,9 @@
|
|
|
48
53
|
"dependencies": {
|
|
49
54
|
"react": "^19.0.0",
|
|
50
55
|
"react-dom": "^19.0.0",
|
|
51
|
-
"@ggui-ai/design": "0.
|
|
52
|
-
"@ggui-ai/
|
|
53
|
-
"@ggui-ai/
|
|
56
|
+
"@ggui-ai/design": "0.20.0",
|
|
57
|
+
"@ggui-ai/protocol": "0.20.0",
|
|
58
|
+
"@ggui-ai/wire": "0.20.0"
|
|
54
59
|
},
|
|
55
60
|
"devDependencies": {
|
|
56
61
|
"@playwright/test": "^1.60.0",
|