Skip to content

Commit 0a6552f

Browse files
Merge pull request #84 from RtlZeroMemory/full-widget-hardening-pass
Harden widget and form invariants with deterministic test coverage
2 parents 38efadb + 0986d4c commit 0a6552f

39 files changed

Lines changed: 5479 additions & 133 deletions

packages/core/src/forms/__tests__/form.async-validation.test.ts

Lines changed: 442 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
import { assert, describe, test } from "@rezi-ui/testkit";
2+
import type { StateUpdater } from "../../app/updateQueue.js";
3+
import { bind, bindChecked, bindSelect, bindTransform } from "../bind.js";
4+
5+
function createStateHarness<S extends Record<string, unknown>>(
6+
initial: S,
7+
): {
8+
getState: () => S;
9+
update: (updater: StateUpdater<S>) => void;
10+
getUpdateCount: () => number;
11+
} {
12+
let state = initial;
13+
let updateCount = 0;
14+
return {
15+
getState: () => state,
16+
update: (updater) => {
17+
updateCount++;
18+
if (typeof updater === "function") {
19+
state = updater(state);
20+
return;
21+
}
22+
state = updater;
23+
},
24+
getUpdateCount: () => updateCount,
25+
};
26+
}
27+
28+
describe("form.bind - input binding", () => {
29+
test("bind returns string value for existing string field", () => {
30+
const h = createStateHarness({ name: "Ada" });
31+
const props = bind(h.getState(), "name", h.update);
32+
assert.equal(props.value, "Ada");
33+
});
34+
35+
test("bind converts numeric value to string", () => {
36+
const h = createStateHarness({ age: 42 });
37+
const props = bind(h.getState(), "age", h.update);
38+
assert.equal(props.value, "42");
39+
});
40+
41+
test("bind converts undefined value to empty string", () => {
42+
const h = createStateHarness({ name: undefined as unknown as string });
43+
const props = bind(h.getState(), "name", h.update);
44+
assert.equal(props.value, "");
45+
});
46+
47+
test("bind converts null value to empty string", () => {
48+
const h = createStateHarness({ name: null as unknown as string });
49+
const props = bind(h.getState(), "name", h.update);
50+
assert.equal(props.value, "");
51+
});
52+
53+
test("bind onInput updates top-level field", () => {
54+
const h = createStateHarness({ name: "Ada" });
55+
const props = bind(h.getState(), "name", h.update);
56+
57+
assert.ok(props.onInput);
58+
props.onInput?.("Grace", 5);
59+
assert.equal(h.getState().name, "Grace");
60+
assert.equal(h.getUpdateCount(), 1);
61+
});
62+
63+
test("bind onInput updates nested field path", () => {
64+
const h = createStateHarness({
65+
profile: { address: { city: "Paris", country: "FR" }, role: "admin" },
66+
status: "active",
67+
});
68+
const before = h.getState();
69+
const beforeProfile = before.profile;
70+
const beforeAddress = before.profile.address;
71+
72+
const props = bind(h.getState(), "profile.address.city", h.update);
73+
assert.ok(props.onInput);
74+
props.onInput?.("Berlin", 6);
75+
76+
const after = h.getState();
77+
assert.equal(after.profile.address.city, "Berlin");
78+
assert.equal(after.profile.address.country, "FR");
79+
assert.equal(after.status, "active");
80+
assert.notEqual(after, before);
81+
assert.notEqual(after.profile, beforeProfile);
82+
assert.notEqual(after.profile.address, beforeAddress);
83+
});
84+
85+
test("bind creates missing nested objects for path assignment", () => {
86+
const h = createStateHarness<{ address?: { city?: string } }>({});
87+
const props = bind(h.getState(), "address.city", h.update);
88+
assert.ok(props.onInput);
89+
props.onInput?.("Tokyo", 5);
90+
91+
assert.deepEqual(h.getState(), { address: { city: "Tokyo" } });
92+
});
93+
94+
test("bind preserves references on untouched branches", () => {
95+
const h = createStateHarness({
96+
left: { value: "A" },
97+
right: { value: "B" },
98+
});
99+
const beforeRight = h.getState().right;
100+
101+
const props = bind(h.getState(), "left.value", h.update);
102+
assert.ok(props.onInput);
103+
props.onInput?.("AA", 2);
104+
105+
const after = h.getState();
106+
assert.equal(after.left.value, "AA");
107+
assert.equal(after.right.value, "B");
108+
assert.equal(after.right, beforeRight);
109+
});
110+
111+
test("bind ignores empty path string updates", () => {
112+
const h = createStateHarness({ name: "Ada" });
113+
const props = bind(h.getState(), "", h.update);
114+
assert.ok(props.onInput);
115+
props.onInput?.("Grace", 5);
116+
117+
assert.deepEqual(h.getState(), { name: "Ada" });
118+
});
119+
});
120+
121+
describe("form.bind - transform binding", () => {
122+
test("bindTransform uses custom get formatter", () => {
123+
const h = createStateHarness({ cents: 1234 });
124+
const props = bindTransform(h.getState(), "cents", h.update, {
125+
get: (value) => `$${(Number(value) / 100).toFixed(2)}`,
126+
set: (value) => Math.round(Number(value.replace("$", "")) * 100),
127+
});
128+
assert.equal(props.value, "$12.34");
129+
});
130+
131+
test("bindTransform onInput uses custom set parser", () => {
132+
const h = createStateHarness({ qty: 1 });
133+
const props = bindTransform(h.getState(), "qty", h.update, {
134+
get: (value) => String(value),
135+
set: (value) => Number(value),
136+
});
137+
138+
assert.ok(props.onInput);
139+
props.onInput?.("7", 1);
140+
assert.equal(h.getState().qty, 7);
141+
});
142+
143+
test("bindTransform supports nested field paths", () => {
144+
const h = createStateHarness({ profile: { rank: 3 } });
145+
const props = bindTransform(h.getState(), "profile.rank", h.update, {
146+
get: (value) => `#${String(value)}`,
147+
set: (value) => Number(value.slice(1)),
148+
});
149+
150+
assert.ok(props.onInput);
151+
props.onInput?.("#9", 2);
152+
assert.equal(h.getState().profile.rank, 9);
153+
});
154+
155+
test("bindTransform creates nested path when missing", () => {
156+
const h = createStateHarness<{ metrics?: { score?: number } }>({});
157+
const props = bindTransform(h.getState(), "metrics.score", h.update, {
158+
get: (value) => String(value ?? 0),
159+
set: (value) => Number(value),
160+
});
161+
162+
assert.ok(props.onInput);
163+
props.onInput?.("12", 2);
164+
assert.deepEqual(h.getState(), { metrics: { score: 12 } });
165+
});
166+
167+
test("bindTransform get receives undefined for missing path", () => {
168+
const seen: unknown[] = [];
169+
const h = createStateHarness<{ profile?: { alias?: string } }>({});
170+
const props = bindTransform(h.getState(), "profile.alias", h.update, {
171+
get: (value) => {
172+
seen.push(value);
173+
return value === undefined ? "" : String(value);
174+
},
175+
set: (value) => value,
176+
});
177+
178+
assert.equal(props.value, "");
179+
assert.deepEqual(seen, [undefined]);
180+
});
181+
});
182+
183+
describe("form.bind - checkbox binding", () => {
184+
test("bindChecked coerces truthy and falsy values", () => {
185+
const a = createStateHarness({ remember: "yes" as unknown as boolean });
186+
const b = createStateHarness({ remember: 0 as unknown as boolean });
187+
188+
assert.equal(bindChecked(a.getState(), "remember", a.update).checked, true);
189+
assert.equal(bindChecked(b.getState(), "remember", b.update).checked, false);
190+
});
191+
192+
test("bindChecked onChange updates top-level boolean", () => {
193+
const h = createStateHarness({ remember: false });
194+
const props = bindChecked(h.getState(), "remember", h.update);
195+
assert.ok(props.onChange);
196+
props.onChange?.(true);
197+
assert.equal(h.getState().remember, true);
198+
});
199+
200+
test("bindChecked onChange supports nested path assignment", () => {
201+
const h = createStateHarness<{ settings?: { marketing?: boolean } }>({});
202+
const props = bindChecked(h.getState(), "settings.marketing", h.update);
203+
assert.ok(props.onChange);
204+
props.onChange?.(true);
205+
206+
assert.deepEqual(h.getState(), { settings: { marketing: true } });
207+
});
208+
});
209+
210+
describe("form.bind - select binding", () => {
211+
test("bindSelect converts nullish field value to empty string", () => {
212+
const h = createStateHarness<{ country?: string }>({});
213+
const props = bindSelect(h.getState(), "country", h.update);
214+
assert.equal(props.value, "");
215+
});
216+
217+
test("bindSelect reads nested field value", () => {
218+
const h = createStateHarness({ profile: { locale: "fr-FR" } });
219+
const props = bindSelect(h.getState(), "profile.locale", h.update);
220+
assert.equal(props.value, "fr-FR");
221+
});
222+
223+
test("bindSelect onChange updates top-level field", () => {
224+
const h = createStateHarness({ country: "US" });
225+
const props = bindSelect(h.getState(), "country", h.update);
226+
assert.ok(props.onChange);
227+
props.onChange?.("DE");
228+
assert.equal(h.getState().country, "DE");
229+
});
230+
231+
test("bindSelect onChange supports nested path", () => {
232+
const h = createStateHarness<{ profile?: { locale?: string } }>({});
233+
const props = bindSelect(h.getState(), "profile.locale", h.update);
234+
assert.ok(props.onChange);
235+
props.onChange?.("en-US");
236+
237+
assert.deepEqual(h.getState(), { profile: { locale: "en-US" } });
238+
});
239+
240+
test("bindSelect does not mutate previous nested object", () => {
241+
const h = createStateHarness({ profile: { locale: "en", timezone: "UTC" } });
242+
const beforeProfile = h.getState().profile;
243+
const props = bindSelect(h.getState(), "profile.locale", h.update);
244+
245+
assert.ok(props.onChange);
246+
props.onChange?.("de");
247+
248+
const after = h.getState();
249+
assert.equal(after.profile.locale, "de");
250+
assert.equal(after.profile.timezone, "UTC");
251+
assert.notEqual(after.profile, beforeProfile);
252+
});
253+
});

0 commit comments

Comments
 (0)