@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.
@@ -1 +1 @@
1
- {"version":3,"file":"fixture-runtime.d.ts","sourceRoot":"","sources":["../src/fixture-runtime.ts"],"names":[],"mappings":"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"}
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;AA2CtC,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,4EAA4E;IAC5E,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAC9B,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,CAAC;IAAC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;CAAE,GACvE;IAAE,QAAQ,CAAC,MAAM,EAAE,kBAAkB,CAAC;IAAC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;CAAE;AACtE,8FAA8F;GAC5F;IAAE,QAAQ,CAAC,MAAM,EAAE,oBAAoB,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,wBAAwB,GAAG,mBAAmB,CAAC;IAAC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;CAAE,GACvI;IACE,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC;IACtB,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAC7B,qGAAqG;IACrG,QAAQ,CAAC,GAAG,CAAC,EAAE,OAAO,GAAG,UAAU,CAAC;IACpC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B,CAAC;AAEN,OAAO,CAAC,MAAM,CAAC;IACb,UAAU,MAAM;QACd,wBAAwB,CAAC,EAAE,cAAc,EAAE,CAAC;QAC5C,gCAAgC,CAAC,EAAE,CAAC,KAAK,EAAE,QAAQ,KAAK,OAAO,CAAC,UAAU,CAAC,CAAC;QAC5E,QAAQ,CAAC,EAAE,YAAY,CAAC;QACxB,OAAO,CAAC,EAAE,OAAO,KAAK,CAAC;QACvB,iBAAiB,CAAC,EAAE,OAAO,gBAAgB,CAAC;QAC5C,iBAAiB,CAAC,EAAE,OAAO,gBAAgB,CAAC;QAC5C,mBAAmB,CAAC,EAAE,OAAO,kBAAkB,CAAC;QAChD,eAAe,CAAC,EAAE,OAAO,cAAc,CAAC;QACxC,qBAAqB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KACjD;CACF;AAED,UAAU,YAAY;IACpB,KAAK,EAAE,OAAO,KAAK,CAAC;IACpB,QAAQ,EAAE,OAAO,cAAc,CAAC;IAChC,UAAU,EAAE,OAAO,gBAAgB,CAAC;IACpC,UAAU,EAAE,OAAO,gBAAgB,CAAC;IACpC,YAAY,EAAE,OAAO,kBAAkB,CAAC;IACxC,QAAQ,EAAE,OAAO,cAAc,CAAC;IAChC;;;;;;;;OAQG;IACH,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACvC,IAAI,EAAE,OAAO,IAAI,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC"}
@@ -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/fixture-runtime.ts
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.19.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.19.0",
52
- "@ggui-ai/wire": "0.19.0",
53
- "@ggui-ai/protocol": "0.19.0"
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",