@ggui-ai/ui-visual-tester 0.18.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/fixture-runtime.js +60 -10
- package/fixture/fixture-runtime.js +67 -15
- package/package.json +4 -4
package/dist/fixture-runtime.js
CHANGED
|
@@ -156,9 +156,48 @@ const TEXT_LIKE_INPUT_TYPES = new Set([
|
|
|
156
156
|
'text', 'email', 'search', 'url', 'tel', 'number', 'password', '',
|
|
157
157
|
'date', 'time', 'month', 'week', 'datetime-local',
|
|
158
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
|
+
}
|
|
159
197
|
function sampleValueFor(el) {
|
|
160
|
-
|
|
161
|
-
|
|
198
|
+
if (!(el instanceof HTMLInputElement))
|
|
199
|
+
return 'probe'; // a textarea has no type to switch on
|
|
200
|
+
switch (el.type) {
|
|
162
201
|
case 'number':
|
|
163
202
|
return '1';
|
|
164
203
|
case 'email':
|
|
@@ -167,16 +206,27 @@ function sampleValueFor(el) {
|
|
|
167
206
|
return 'https://example.com';
|
|
168
207
|
case 'tel':
|
|
169
208
|
return '5550100';
|
|
170
|
-
case 'date':
|
|
171
|
-
|
|
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
|
+
}
|
|
172
214
|
case 'time':
|
|
173
215
|
return '09:30';
|
|
174
|
-
case 'month':
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
return
|
|
178
|
-
|
|
179
|
-
|
|
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
|
+
}
|
|
180
230
|
default:
|
|
181
231
|
return 'probe';
|
|
182
232
|
}
|
|
@@ -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(
|
|
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",
|
|
@@ -32879,9 +32898,31 @@
|
|
|
32879
32898
|
"week",
|
|
32880
32899
|
"datetime-local"
|
|
32881
32900
|
]);
|
|
32901
|
+
var pad2 = (n) => String(n).padStart(2, "0");
|
|
32902
|
+
var partsOf = (at) => ({ y: at.getFullYear(), m: at.getMonth() + 1, d: at.getDate() });
|
|
32903
|
+
function todayAndThirtyDaysOut() {
|
|
32904
|
+
const now = /* @__PURE__ */ new Date();
|
|
32905
|
+
const at = new Date(now);
|
|
32906
|
+
at.setDate(at.getDate() + 30);
|
|
32907
|
+
return { today: partsOf(now), later: partsOf(at) };
|
|
32908
|
+
}
|
|
32909
|
+
function clampToRange(el, value, floor, format) {
|
|
32910
|
+
let out = value;
|
|
32911
|
+
if (format.test(el.min) && el.min > out) out = el.min;
|
|
32912
|
+
if (format.test(el.max) && el.max < out && el.max >= floor) out = el.max;
|
|
32913
|
+
return out;
|
|
32914
|
+
}
|
|
32915
|
+
function isoWeekOf(y, m, d) {
|
|
32916
|
+
const date = new Date(Date.UTC(y, m - 1, d));
|
|
32917
|
+
const day = date.getUTCDay() || 7;
|
|
32918
|
+
date.setUTCDate(date.getUTCDate() + 4 - day);
|
|
32919
|
+
const jan1 = new Date(Date.UTC(date.getUTCFullYear(), 0, 1));
|
|
32920
|
+
const week = Math.ceil(((date.getTime() - jan1.getTime()) / 864e5 + 1) / 7);
|
|
32921
|
+
return `${date.getUTCFullYear()}-W${pad2(week)}`;
|
|
32922
|
+
}
|
|
32882
32923
|
function sampleValueFor(el) {
|
|
32883
|
-
|
|
32884
|
-
switch (type) {
|
|
32924
|
+
if (!(el instanceof HTMLInputElement)) return "probe";
|
|
32925
|
+
switch (el.type) {
|
|
32885
32926
|
case "number":
|
|
32886
32927
|
return "1";
|
|
32887
32928
|
case "email":
|
|
@@ -32890,16 +32931,27 @@
|
|
|
32890
32931
|
return "https://example.com";
|
|
32891
32932
|
case "tel":
|
|
32892
32933
|
return "5550100";
|
|
32893
|
-
case "date":
|
|
32894
|
-
|
|
32934
|
+
case "date": {
|
|
32935
|
+
const { today, later } = todayAndThirtyDaysOut();
|
|
32936
|
+
const iso = (p) => `${p.y}-${pad2(p.m)}-${pad2(p.d)}`;
|
|
32937
|
+
return clampToRange(el, iso(later), iso(today), /^\d{4}-\d{2}-\d{2}$/);
|
|
32938
|
+
}
|
|
32895
32939
|
case "time":
|
|
32896
32940
|
return "09:30";
|
|
32897
|
-
case "month":
|
|
32898
|
-
|
|
32899
|
-
|
|
32900
|
-
return
|
|
32901
|
-
|
|
32902
|
-
|
|
32941
|
+
case "month": {
|
|
32942
|
+
const { today, later } = todayAndThirtyDaysOut();
|
|
32943
|
+
const ym = (p) => `${p.y}-${pad2(p.m)}`;
|
|
32944
|
+
return clampToRange(el, ym(later), ym(today), /^\d{4}-\d{2}$/);
|
|
32945
|
+
}
|
|
32946
|
+
case "week": {
|
|
32947
|
+
const { today, later } = todayAndThirtyDaysOut();
|
|
32948
|
+
return clampToRange(el, isoWeekOf(later.y, later.m, later.d), isoWeekOf(today.y, today.m, today.d), /^\d{4}-W\d{2}$/);
|
|
32949
|
+
}
|
|
32950
|
+
case "datetime-local": {
|
|
32951
|
+
const { today, later } = todayAndThirtyDaysOut();
|
|
32952
|
+
const at = (p, hm) => `${p.y}-${pad2(p.m)}-${pad2(p.d)}T${hm}`;
|
|
32953
|
+
return clampToRange(el, at(later, "09:30"), at(today, "00:00"), /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/);
|
|
32954
|
+
}
|
|
32903
32955
|
default:
|
|
32904
32956
|
return "probe";
|
|
32905
32957
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ggui-ai/ui-visual-tester",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
4
|
"description": "Behavioral validator for ggui-generated component code. Renders a component in real Chromium via Playwright, clicks each action declared in its contract, and verifies the action actually does something — mutating bound state or dispatching to the agent. Catches buttons wired to no-op handlers that structural validators miss. Playwright is an optional peer dependency, injected by the caller.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ggui",
|
|
@@ -48,9 +48,9 @@
|
|
|
48
48
|
"dependencies": {
|
|
49
49
|
"react": "^19.0.0",
|
|
50
50
|
"react-dom": "^19.0.0",
|
|
51
|
-
"@ggui-ai/design": "0.
|
|
52
|
-
"@ggui-ai/wire": "0.
|
|
53
|
-
"@ggui-ai/protocol": "0.
|
|
51
|
+
"@ggui-ai/design": "0.19.0",
|
|
52
|
+
"@ggui-ai/wire": "0.19.0",
|
|
53
|
+
"@ggui-ai/protocol": "0.19.0"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
56
|
"@playwright/test": "^1.60.0",
|