@ggui-ai/ui-visual-tester 0.18.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":"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"}
@@ -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,106 +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
- 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';
182
- }
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))
211
- continue;
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
154
  function countNamedDispatches(actionName) {
254
155
  return (window.__ggui_test_dispatches__ ?? []).filter((d) => d.actionName === actionName).length;
255
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
+ }
@@ -20658,7 +20658,7 @@
20658
20658
  alignItems: "center",
20659
20659
  justifyContent: "center",
20660
20660
  gap: "var(--ggui-spacing-2, 8px)",
20661
- borderRadius: "var(--ggui-shape-radius-md, 8px)",
20661
+ borderRadius: "var(--ggui-shape-radius-control, var(--ggui-shape-radius-md, 8px))",
20662
20662
  fontWeight: "var(--ggui-font-weight-medium, 500)",
20663
20663
  cursor: isDisabled ? "not-allowed" : "pointer",
20664
20664
  opacity: isDisabled ? 0.5 : 1,
@@ -20704,7 +20704,7 @@
20704
20704
  }, children: [label, required && (0, import_jsx_runtime9.jsx)("span", { "aria-hidden": "true", style: { color: "var(--ggui-color-error-500, #ef4444)", marginLeft: "2px" }, children: "*" })] }), (0, import_jsx_runtime9.jsx)("input", { id: inputId, type, placeholder, value, onChange: (e) => onChange?.(e.target.value), disabled, required, "aria-invalid": hasError || void 0, "aria-describedby": hasMessage ? messageId : void 0, style: {
20705
20705
  ...sizeStyles2[size],
20706
20706
  width: "100%",
20707
- borderRadius: "var(--ggui-shape-radius-md, 8px)",
20707
+ borderRadius: "var(--ggui-shape-radius-control, var(--ggui-shape-radius-md, 8px))",
20708
20708
  border: `1px solid ${hasError ? "var(--ggui-color-error-500, #ef4444)" : "var(--ggui-color-outline, #d4d4d8)"}`,
20709
20709
  backgroundColor: disabled ? "var(--ggui-color-sunken, #fafafa)" : "var(--ggui-color-sunken, #ffffff)",
20710
20710
  color: "var(--ggui-color-onContainer, #18181b)",
@@ -20738,7 +20738,7 @@
20738
20738
  }, children: [label, required && (0, import_jsx_runtime10.jsx)("span", { "aria-hidden": "true", style: { color: "var(--ggui-color-error-500, #ef4444)", marginLeft: "2px" }, children: "*" })] }), (0, import_jsx_runtime10.jsx)("textarea", { id: textareaId, placeholder, value, onChange: (e) => onChange?.(e.target.value), rows, disabled, required, maxLength, "aria-invalid": hasError || void 0, "aria-describedby": hasMessage ? messageId : void 0, style: {
20739
20739
  padding: "10px 12px",
20740
20740
  width: "100%",
20741
- borderRadius: "var(--ggui-shape-radius-md, 8px)",
20741
+ borderRadius: "var(--ggui-shape-radius-control, var(--ggui-shape-radius-md, 8px))",
20742
20742
  border: `1px solid ${hasError ? "var(--ggui-color-error-500, #ef4444)" : "var(--ggui-color-outline, #d4d4d8)"}`,
20743
20743
  backgroundColor: disabled ? "var(--ggui-color-sunken, #fafafa)" : "var(--ggui-color-sunken, #ffffff)",
20744
20744
  color: "var(--ggui-color-onContainer, #18181b)",
@@ -20778,7 +20778,7 @@
20778
20778
  }, children: [label, required && (0, import_jsx_runtime11.jsx)("span", { "aria-hidden": "true", style: { color: "var(--ggui-color-error-500, #ef4444)", marginLeft: "2px" }, children: "*" })] }), (0, import_jsx_runtime11.jsxs)("select", { id: selectId, value, onChange: (e) => onChange?.(e.target.value), disabled, required, "aria-invalid": hasError || void 0, "aria-describedby": hasMessage ? messageId : void 0, style: {
20779
20779
  ...sizeStyles3[size],
20780
20780
  width: "100%",
20781
- borderRadius: "var(--ggui-shape-radius-md, 8px)",
20781
+ borderRadius: "var(--ggui-shape-radius-control, var(--ggui-shape-radius-md, 8px))",
20782
20782
  border: `1px solid ${hasError ? "var(--ggui-color-error-500, #ef4444)" : "var(--ggui-color-outline, #d4d4d8)"}`,
20783
20783
  backgroundColor: disabled ? "var(--ggui-color-sunken, #fafafa)" : "var(--ggui-color-sunken, #ffffff)",
20784
20784
  color: value ? "var(--ggui-color-onContainer, #18181b)" : "var(--ggui-color-onSunken, #52525b)",
@@ -28618,6 +28618,7 @@
28618
28618
  if (onDispatchSuppressed) {
28619
28619
  try {
28620
28620
  onDispatchSuppressed({
28621
+ reason: "duplicate-dispatch",
28621
28622
  actionName,
28622
28623
  payloadSignature: decision.signature,
28623
28624
  payload: data,
@@ -29372,11 +29373,26 @@
29372
29373
  return internalSeq;
29373
29374
  });
29374
29375
  const validateEnvelope = opts.validateEnvelope ?? validateOutboundActionEnvelope;
29376
+ const spentOneShots = /* @__PURE__ */ new Set();
29375
29377
  return {
29376
29378
  app: opts.app,
29377
29379
  render: opts.render,
29378
29380
  auth: opts.auth,
29381
+ ...opts.onDispatchSuppressed !== void 0 ? { onDispatchSuppressed: opts.onDispatchSuppressed } : {},
29379
29382
  dispatch: (actionName, data) => {
29383
+ const actionSpec = opts.getActiveActionSpec();
29384
+ const isOneShot = actionSpec?.[actionName]?.oneShot === true;
29385
+ if (isOneShot && spentOneShots.has(actionName)) {
29386
+ console.warn(`[ggui] dispatch('${actionName}') suppressed: this action is declared oneShot and already fired for this render. A second gesture is NOT sent to the agent \u2014 the submitted control is spent for the render's lifetime. If the action is meant to repeat, remove \`oneShot\` from its contract entry.`);
29387
+ opts.onDispatchSuppressed?.({
29388
+ reason: "one-shot-spent",
29389
+ actionName,
29390
+ payloadSignature: payloadSignature(actionName, data),
29391
+ payload: data,
29392
+ suppressedAt: Date.now()
29393
+ });
29394
+ return;
29395
+ }
29380
29396
  const envelope = buildActionEnvelope({
29381
29397
  sessionId: opts.render.sessionId,
29382
29398
  type: "data:submit",
@@ -29386,11 +29402,13 @@
29386
29402
  },
29387
29403
  clientSeq: nextClientSeq()
29388
29404
  });
29389
- const result = validateEnvelope(opts.getActiveActionSpec(), envelope);
29405
+ const result = validateEnvelope(actionSpec, envelope);
29390
29406
  if (!result.valid) {
29391
29407
  opts.onViolation(new ClientContractViolationError("outbound-action", result.violations));
29392
29408
  return;
29393
29409
  }
29410
+ if (isOneShot)
29411
+ spentOneShots.add(actionName);
29394
29412
  opts.emitEnvelope(envelope);
29395
29413
  },
29396
29414
  subscribe: (channelName, handler) => {
@@ -29543,6 +29561,7 @@
29543
29561
  "--ggui-font-weight-normal",
29544
29562
  "--ggui-font-weight-semibold",
29545
29563
  "--ggui-letter-spacing-heading",
29564
+ "--ggui-shape-radius-control",
29546
29565
  "--ggui-shape-radius-full",
29547
29566
  "--ggui-shape-radius-lg",
29548
29567
  "--ggui-shape-radius-md",
@@ -32775,6 +32794,123 @@
32775
32794
  }
32776
32795
  }
32777
32796
 
32797
+ // src/prime-inputs.ts
32798
+ var TEXT_LIKE_INPUT_TYPES = /* @__PURE__ */ new Set([
32799
+ "text",
32800
+ "email",
32801
+ "search",
32802
+ "url",
32803
+ "tel",
32804
+ "number",
32805
+ "password",
32806
+ "",
32807
+ "date",
32808
+ "time",
32809
+ "month",
32810
+ "week",
32811
+ "datetime-local"
32812
+ ]);
32813
+ var pad2 = (n) => String(n).padStart(2, "0");
32814
+ var partsOf = (at) => ({ y: at.getFullYear(), m: at.getMonth() + 1, d: at.getDate() });
32815
+ function todayAndThirtyDaysOut() {
32816
+ const now = /* @__PURE__ */ new Date();
32817
+ const at = new Date(now);
32818
+ at.setDate(at.getDate() + 30);
32819
+ return { today: partsOf(now), later: partsOf(at) };
32820
+ }
32821
+ function clampToRange(el, value, floor, format) {
32822
+ let out = value;
32823
+ if (format.test(el.min) && el.min > out) out = el.min;
32824
+ if (format.test(el.max) && el.max < out && el.max >= floor) out = el.max;
32825
+ return out;
32826
+ }
32827
+ function isoWeekOf(y, m, d) {
32828
+ const date = new Date(Date.UTC(y, m - 1, d));
32829
+ const day = date.getUTCDay() || 7;
32830
+ date.setUTCDate(date.getUTCDate() + 4 - day);
32831
+ const jan1 = new Date(Date.UTC(date.getUTCFullYear(), 0, 1));
32832
+ const week = Math.ceil(((date.getTime() - jan1.getTime()) / 864e5 + 1) / 7);
32833
+ return `${date.getUTCFullYear()}-W${pad2(week)}`;
32834
+ }
32835
+ function sampleValueFor(el) {
32836
+ if (!(el instanceof HTMLInputElement)) return "probe";
32837
+ switch (el.type) {
32838
+ case "number":
32839
+ return "1";
32840
+ case "email":
32841
+ return "probe@example.com";
32842
+ case "url":
32843
+ return "https://example.com";
32844
+ case "tel":
32845
+ return "5550100";
32846
+ case "date": {
32847
+ const { today, later } = todayAndThirtyDaysOut();
32848
+ const iso = (p) => `${p.y}-${pad2(p.m)}-${pad2(p.d)}`;
32849
+ return clampToRange(el, iso(later), iso(today), /^\d{4}-\d{2}-\d{2}$/);
32850
+ }
32851
+ case "time":
32852
+ return "09:30";
32853
+ case "month": {
32854
+ const { today, later } = todayAndThirtyDaysOut();
32855
+ const ym = (p) => `${p.y}-${pad2(p.m)}`;
32856
+ return clampToRange(el, ym(later), ym(today), /^\d{4}-\d{2}$/);
32857
+ }
32858
+ case "week": {
32859
+ const { today, later } = todayAndThirtyDaysOut();
32860
+ return clampToRange(el, isoWeekOf(later.y, later.m, later.d), isoWeekOf(today.y, today.m, today.d), /^\d{4}-W\d{2}$/);
32861
+ }
32862
+ case "datetime-local": {
32863
+ const { today, later } = todayAndThirtyDaysOut();
32864
+ const at = (p, hm) => `${p.y}-${pad2(p.m)}-${pad2(p.d)}T${hm}`;
32865
+ return clampToRange(el, at(later, "09:30"), at(today, "00:00"), /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/);
32866
+ }
32867
+ default:
32868
+ return "probe";
32869
+ }
32870
+ }
32871
+ function setNativeValue(el, value) {
32872
+ const proto = el instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : el instanceof HTMLSelectElement ? HTMLSelectElement.prototype : HTMLInputElement.prototype;
32873
+ const setter = Object.getOwnPropertyDescriptor(proto, "value")?.set;
32874
+ if (setter) setter.call(el, value);
32875
+ else el.value = value;
32876
+ el.dispatchEvent(new Event("input", { bubbles: true }));
32877
+ el.dispatchEvent(new Event("change", { bubbles: true }));
32878
+ }
32879
+ function primeInputs(root) {
32880
+ let primed = 0;
32881
+ const editable = (el) => !el.disabled && !(el instanceof HTMLSelectElement) ? !el.readOnly : !el.disabled;
32882
+ for (const el of Array.from(root.querySelectorAll("input, textarea"))) {
32883
+ if (!(el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) || !editable(el)) continue;
32884
+ if (el instanceof HTMLInputElement && !TEXT_LIKE_INPUT_TYPES.has(el.type)) continue;
32885
+ if (el.value.trim().length > 0) continue;
32886
+ setNativeValue(el, sampleValueFor(el));
32887
+ if (el.value.trim().length > 0) primed += 1;
32888
+ }
32889
+ for (const el of Array.from(root.querySelectorAll("select"))) {
32890
+ if (!(el instanceof HTMLSelectElement) || el.disabled) continue;
32891
+ const current = el.options[el.selectedIndex];
32892
+ if (current !== void 0 && current.value.trim().length > 0) continue;
32893
+ const first = Array.from(el.options).find((o) => o.value.trim().length > 0 && !o.disabled);
32894
+ if (first === void 0) continue;
32895
+ setNativeValue(el, first.value);
32896
+ primed += 1;
32897
+ }
32898
+ const seenGroups = /* @__PURE__ */ new Set();
32899
+ for (const el of Array.from(root.querySelectorAll('input[type="radio"]'))) {
32900
+ if (!(el instanceof HTMLInputElement) || el.disabled) continue;
32901
+ const group = el.name || "";
32902
+ if (seenGroups.has(group)) continue;
32903
+ seenGroups.add(group);
32904
+ const members = group ? Array.from(root.querySelectorAll(`input[type="radio"][name="${CSS.escape(group)}"]`)).filter((m) => m instanceof HTMLInputElement) : [el];
32905
+ if (members.some((m) => m.checked)) continue;
32906
+ const first = members.find((m) => !m.disabled);
32907
+ if (first === void 0) continue;
32908
+ first.click();
32909
+ primed += 1;
32910
+ }
32911
+ return primed;
32912
+ }
32913
+
32778
32914
  // src/fixture-runtime.ts
32779
32915
  function parseActionEntry(raw) {
32780
32916
  if (raw === null || typeof raw !== "object") return null;
@@ -32864,88 +33000,6 @@
32864
33000
  }
32865
33001
  return { named: [...byValue, ...byText], fallback: rest, disabled, namedDisabled, allClickables: every };
32866
33002
  }
32867
- var TEXT_LIKE_INPUT_TYPES = /* @__PURE__ */ new Set([
32868
- "text",
32869
- "email",
32870
- "search",
32871
- "url",
32872
- "tel",
32873
- "number",
32874
- "password",
32875
- "",
32876
- "date",
32877
- "time",
32878
- "month",
32879
- "week",
32880
- "datetime-local"
32881
- ]);
32882
- function sampleValueFor(el) {
32883
- const type = el instanceof HTMLInputElement ? el.type : "textarea";
32884
- switch (type) {
32885
- case "number":
32886
- return "1";
32887
- case "email":
32888
- return "probe@example.com";
32889
- case "url":
32890
- return "https://example.com";
32891
- case "tel":
32892
- return "5550100";
32893
- case "date":
32894
- return "2026-01-15";
32895
- case "time":
32896
- return "09:30";
32897
- case "month":
32898
- return "2026-01";
32899
- case "week":
32900
- return "2026-W03";
32901
- case "datetime-local":
32902
- return "2026-01-15T09:30";
32903
- default:
32904
- return "probe";
32905
- }
32906
- }
32907
- function setNativeValue(el, value) {
32908
- const proto = el instanceof HTMLTextAreaElement ? HTMLTextAreaElement.prototype : el instanceof HTMLSelectElement ? HTMLSelectElement.prototype : HTMLInputElement.prototype;
32909
- const setter = Object.getOwnPropertyDescriptor(proto, "value")?.set;
32910
- if (setter) setter.call(el, value);
32911
- else el.value = value;
32912
- el.dispatchEvent(new Event("input", { bubbles: true }));
32913
- el.dispatchEvent(new Event("change", { bubbles: true }));
32914
- }
32915
- function primeInputs(root) {
32916
- let primed = 0;
32917
- const editable = (el) => !el.disabled && !(el instanceof HTMLSelectElement) ? !el.readOnly : !el.disabled;
32918
- for (const el of Array.from(root.querySelectorAll("input, textarea"))) {
32919
- if (!(el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) || !editable(el)) continue;
32920
- if (el instanceof HTMLInputElement && !TEXT_LIKE_INPUT_TYPES.has(el.type)) continue;
32921
- if (el.value.trim().length > 0) continue;
32922
- setNativeValue(el, sampleValueFor(el));
32923
- if (el.value.trim().length > 0) primed += 1;
32924
- }
32925
- for (const el of Array.from(root.querySelectorAll("select"))) {
32926
- if (!(el instanceof HTMLSelectElement) || el.disabled) continue;
32927
- const current = el.options[el.selectedIndex];
32928
- if (current !== void 0 && current.value.trim().length > 0) continue;
32929
- const first = Array.from(el.options).find((o) => o.value.trim().length > 0 && !o.disabled);
32930
- if (first === void 0) continue;
32931
- setNativeValue(el, first.value);
32932
- primed += 1;
32933
- }
32934
- const seenGroups = /* @__PURE__ */ new Set();
32935
- for (const el of Array.from(root.querySelectorAll('input[type="radio"]'))) {
32936
- if (!(el instanceof HTMLInputElement) || el.disabled) continue;
32937
- const group = el.name || "";
32938
- if (seenGroups.has(group)) continue;
32939
- seenGroups.add(group);
32940
- const members = group ? Array.from(root.querySelectorAll(`input[type="radio"][name="${CSS.escape(group)}"]`)).filter((m) => m instanceof HTMLInputElement) : [el];
32941
- if (members.some((m) => m.checked)) continue;
32942
- const first = members.find((m) => !m.disabled);
32943
- if (first === void 0) continue;
32944
- first.click();
32945
- primed += 1;
32946
- }
32947
- return primed;
32948
- }
32949
33003
  function countNamedDispatches(actionName) {
32950
33004
  return (window.__ggui_test_dispatches__ ?? []).filter((d) => d.actionName === actionName).length;
32951
33005
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ggui-ai/ui-visual-tester",
3
- "version": "0.18.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.18.0",
52
- "@ggui-ai/wire": "0.18.0",
53
- "@ggui-ai/protocol": "0.18.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",