xdbc 1.0.217 → 1.0.219
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/.gitattributes +16 -8
- package/.vscode/settings.json +3 -3
- package/.vscode/tasks.json +23 -23
- package/ASSESSMENT.md +249 -0
- package/README.md +538 -408
- package/__tests__/DBC/AE.test.ts +62 -62
- package/__tests__/DBC/ARRAY.test.ts +91 -91
- package/__tests__/DBC/DEFINED.test.ts +53 -53
- package/__tests__/DBC/DOM.test.ts +786 -0
- package/__tests__/DBC/Decorators.test.ts +367 -367
- package/__tests__/DBC/EQ.test.ts +13 -13
- package/__tests__/DBC/GREATER.test.ts +31 -31
- package/__tests__/DBC/HasAttribute.test.ts +60 -60
- package/__tests__/DBC/IF.test.ts +62 -62
- package/__tests__/DBC/INSTANCE.test.ts +13 -13
- package/__tests__/DBC/JSON.OP.test.ts +47 -47
- package/__tests__/DBC/JSON.Parse.test.ts +17 -17
- package/__tests__/DBC/OR.test.ts +14 -14
- package/__tests__/DBC/PLAIN_OBJECT.test.ts +109 -109
- package/__tests__/DBC/REGEX.test.ts +17 -17
- package/__tests__/DBC/TYPE.test.ts +13 -13
- package/__tests__/DBC/UNDEFINED.test.ts +45 -45
- package/__tests__/DBC/ZOD.test.ts +54 -54
- package/__tests__/DBC/onInfringement.test.ts +262 -0
- package/biome.json +45 -40
- package/dist/DBC/AE.js +172 -0
- package/dist/DBC/ARR/PLAIN_OBJECT.d.ts +0 -3
- package/dist/DBC/ARR/PLAIN_OBJECT.js +95 -0
- package/dist/DBC/ARRAY.d.ts +0 -3
- package/dist/DBC/ARRAY.js +90 -0
- package/dist/DBC/COMPARISON/GREATER.js +21 -0
- package/dist/DBC/COMPARISON/GREATER_OR_EQUAL.js +21 -0
- package/dist/DBC/COMPARISON/LESS.js +21 -0
- package/dist/DBC/COMPARISON/LESS_OR_EQUAL.js +21 -0
- package/dist/DBC/COMPARISON.js +98 -0
- package/dist/DBC/DEFINED.js +87 -0
- package/dist/DBC/DOM.d.ts +123 -0
- package/dist/DBC/DOM.js +362 -0
- package/dist/DBC/EQ/DIFFERENT.js +34 -0
- package/dist/DBC/EQ.js +101 -0
- package/dist/DBC/GREATER.js +99 -0
- package/dist/DBC/HasAttribute.js +101 -0
- package/dist/DBC/IF.js +96 -0
- package/dist/DBC/INSTANCE.js +122 -0
- package/dist/DBC/JSON.OP.js +120 -0
- package/dist/DBC/JSON.Parse.js +104 -0
- package/dist/DBC/OR.js +125 -0
- package/dist/DBC/REGEX.js +136 -0
- package/dist/DBC/TYPE.js +112 -0
- package/dist/DBC/UNDEFINED.js +87 -0
- package/dist/DBC/ZOD.js +99 -0
- package/dist/DBC.d.ts +18 -4
- package/dist/DBC.js +645 -0
- package/dist/Demo.d.ts +10 -0
- package/dist/Demo.js +713 -0
- package/dist/Test.html +18 -0
- package/dist/bundle.js +6140 -405
- package/dist/index.d.ts +22 -0
- package/dist/index.html +18 -0
- package/dist/index.js +22 -0
- package/docs/assets/highlight.css +22 -22
- package/docs/assets/icons.js +17 -17
- package/docs/assets/main.js +60 -60
- package/docs/assets/style.css +1640 -1640
- package/docs/classes/DBC.DBC.html +98 -98
- package/docs/classes/DBC_AE.AE.html +160 -160
- package/docs/classes/DBC_EQ.EQ.html +131 -131
- package/docs/classes/DBC_GREATER.GREATER.html +139 -139
- package/docs/classes/DBC_INSTANCE.INSTANCE.html +130 -130
- package/docs/classes/DBC_JSON.OP.JSON_OP.html +138 -138
- package/docs/classes/DBC_JSON.Parse.JSON_Parse.html +129 -129
- package/docs/classes/DBC_OR.OR.html +137 -137
- package/docs/classes/DBC_REGEX.REGEX.html +136 -136
- package/docs/classes/DBC_TYPE.TYPE.html +130 -130
- package/docs/classes/Demo.Demo.html +14 -14
- package/docs/hierarchy.html +1 -1
- package/docs/index.html +1 -1
- package/docs/modules/DBC.html +1 -1
- package/docs/modules/DBC_AE.html +1 -1
- package/docs/modules/DBC_EQ.html +1 -1
- package/docs/modules/DBC_GREATER.html +1 -1
- package/docs/modules/DBC_INSTANCE.html +1 -1
- package/docs/modules/DBC_JSON.OP.html +1 -1
- package/docs/modules/DBC_JSON.Parse.html +1 -1
- package/docs/modules/DBC_OR.html +1 -1
- package/docs/modules/DBC_REGEX.html +1 -1
- package/docs/modules/DBC_TYPE.html +1 -1
- package/docs/modules/Demo.html +1 -1
- package/jest.config.js +32 -32
- package/package.json +71 -55
- package/src/DBC/AE.ts +269 -288
- package/src/DBC/ARR/PLAIN_OBJECT.ts +122 -133
- package/src/DBC/ARRAY.ts +117 -127
- package/src/DBC/COMPARISON/GREATER.ts +41 -46
- package/src/DBC/COMPARISON/GREATER_OR_EQUAL.ts +41 -45
- package/src/DBC/COMPARISON/LESS.ts +41 -45
- package/src/DBC/COMPARISON/LESS_OR_EQUAL.ts +41 -45
- package/src/DBC/COMPARISON.ts +149 -159
- package/src/DBC/DEFINED.ts +117 -122
- package/src/DBC/DOM.ts +453 -0
- package/src/DBC/EQ/DIFFERENT.ts +51 -57
- package/src/DBC/EQ.ts +154 -163
- package/src/DBC/HasAttribute.ts +149 -154
- package/src/DBC/IF.ts +173 -179
- package/src/DBC/INSTANCE.ts +168 -171
- package/src/DBC/JSON.OP.ts +178 -186
- package/src/DBC/JSON.Parse.ts +150 -157
- package/src/DBC/OR.ts +183 -187
- package/src/DBC/REGEX.ts +195 -196
- package/src/DBC/TYPE.ts +142 -149
- package/src/DBC/UNDEFINED.ts +115 -117
- package/src/DBC/ZOD.ts +130 -135
- package/src/DBC.ts +902 -904
- package/src/Demo.ts +537 -404
- package/src/index.ts +22 -0
- package/tsconfig.json +18 -18
- package/tsconfig.test.json +7 -7
- package/typedoc.json +16 -16
- package/webpack.config.js +27 -27
- package/Assessment.md +0 -507
|
@@ -0,0 +1,786 @@
|
|
|
1
|
+
import { DBC } from "../../src/DBC";
|
|
2
|
+
import { registerDOMContract, scanDOM } from "../../src/DBC/DOM";
|
|
3
|
+
|
|
4
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
/** Creates an <input> with the given attributes, appends it to document.body, and returns it. */
|
|
7
|
+
function makeInput(attrs: Record<string, string>): HTMLInputElement {
|
|
8
|
+
const el = document.createElement("input");
|
|
9
|
+
el.type = "text";
|
|
10
|
+
for (const [k, v] of Object.entries(attrs)) {
|
|
11
|
+
el.setAttribute(k, v);
|
|
12
|
+
}
|
|
13
|
+
document.body.appendChild(el);
|
|
14
|
+
return el;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Fires an "input" event on the element (simulates any user input change). */
|
|
18
|
+
function fireInput(
|
|
19
|
+
el: HTMLInputElement | HTMLTextAreaElement,
|
|
20
|
+
newValue: string,
|
|
21
|
+
): void {
|
|
22
|
+
el.value = newValue;
|
|
23
|
+
el.dispatchEvent(new Event("input"));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Fires a "blur" event on the element. */
|
|
27
|
+
function fireBlur(el: HTMLInputElement | HTMLTextAreaElement): void {
|
|
28
|
+
el.dispatchEvent(new Event("blur"));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Fires compositionstart → sets value → compositionend → input (simulates IME). */
|
|
32
|
+
function fireIME(el: HTMLInputElement, composed: string): void {
|
|
33
|
+
el.dispatchEvent(new Event("compositionstart"));
|
|
34
|
+
el.value = composed;
|
|
35
|
+
el.dispatchEvent(new Event("compositionend"));
|
|
36
|
+
el.dispatchEvent(new Event("input"));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
beforeEach(() => {
|
|
40
|
+
// Clean up any elements added during a test.
|
|
41
|
+
document.body.innerHTML = "";
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// ─── scanDOM — basic binding ───────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
describe("scanDOM — basic binding", () => {
|
|
47
|
+
test("returns a cleanup function", () => {
|
|
48
|
+
const cleanup = scanDOM();
|
|
49
|
+
expect(typeof cleanup).toBe("function");
|
|
50
|
+
cleanup();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("ignores elements without data-xdbc", () => {
|
|
54
|
+
const el = makeInput({ "data-xdbc-regex": "^\\d*$" }); // no data-xdbc marker
|
|
55
|
+
const cleanup = scanDOM();
|
|
56
|
+
fireInput(el, "abc");
|
|
57
|
+
expect(el.value).toBe("abc"); // not blocked
|
|
58
|
+
cleanup();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("ignores data-xdbc elements that have no recognised contract attribute", () => {
|
|
62
|
+
const el = makeInput({ "data-xdbc": "" }); // marker only
|
|
63
|
+
const cleanup = scanDOM();
|
|
64
|
+
fireInput(el, "anything");
|
|
65
|
+
expect(el.value).toBe("anything");
|
|
66
|
+
cleanup();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("cleanup removes all listeners — further blur is no longer validated", () => {
|
|
70
|
+
const el = makeInput({ "data-xdbc": "", "data-xdbc-regex": "^\\d*$" });
|
|
71
|
+
const cleanup = scanDOM();
|
|
72
|
+
cleanup();
|
|
73
|
+
el.value = "abc";
|
|
74
|
+
fireBlur(el); // would revert if listener was still attached
|
|
75
|
+
expect(el.value).toBe("abc");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("can scan a sub-element instead of the whole document", () => {
|
|
79
|
+
const container = document.createElement("div");
|
|
80
|
+
document.body.appendChild(container);
|
|
81
|
+
const inside = document.createElement("input");
|
|
82
|
+
inside.setAttribute("data-xdbc", "");
|
|
83
|
+
inside.setAttribute("data-xdbc-regex", "^\\d*$");
|
|
84
|
+
container.appendChild(inside);
|
|
85
|
+
const outside = makeInput({ "data-xdbc": "", "data-xdbc-regex": "^\\d*$" });
|
|
86
|
+
|
|
87
|
+
const cleanup = scanDOM(container);
|
|
88
|
+
fireInput(inside, "abc");
|
|
89
|
+
fireBlur(inside);
|
|
90
|
+
expect(inside.value).toBe(""); // blocked on blur
|
|
91
|
+
fireInput(outside, "abc");
|
|
92
|
+
fireBlur(outside);
|
|
93
|
+
expect(outside.value).toBe("abc"); // NOT scanned
|
|
94
|
+
cleanup();
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// ─── data-xdbc-regex ──────────────────────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
describe("data-xdbc-regex", () => {
|
|
101
|
+
test("accepts a valid value", () => {
|
|
102
|
+
const el = makeInput({ "data-xdbc": "", "data-xdbc-regex": "^\\d*$" });
|
|
103
|
+
const cleanup = scanDOM();
|
|
104
|
+
fireInput(el, "123");
|
|
105
|
+
expect(el.value).toBe("123");
|
|
106
|
+
cleanup();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("reverts an invalid value to the last valid state", () => {
|
|
110
|
+
const el = makeInput({ "data-xdbc": "", "data-xdbc-regex": "^\\d*$" });
|
|
111
|
+
// el.value is "" when scanDOM() runs, so lastValid starts as ""
|
|
112
|
+
const cleanup = scanDOM();
|
|
113
|
+
fireInput(el, "1a");
|
|
114
|
+
fireBlur(el);
|
|
115
|
+
expect(el.value).toBe(""); // reverted to lastValid = "" on blur
|
|
116
|
+
cleanup();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("preserves the last valid value on revert", () => {
|
|
120
|
+
const el = makeInput({ "data-xdbc": "", "data-xdbc-regex": "^\\d*$" });
|
|
121
|
+
const cleanup = scanDOM();
|
|
122
|
+
fireInput(el, "42");
|
|
123
|
+
fireBlur(el); // commits "42" as lastValid
|
|
124
|
+
fireInput(el, "42x");
|
|
125
|
+
fireBlur(el);
|
|
126
|
+
expect(el.value).toBe("42");
|
|
127
|
+
cleanup();
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("warns and skips element with invalid RegExp pattern", () => {
|
|
131
|
+
const warn = jest.spyOn(console, "warn").mockImplementation(() => { });
|
|
132
|
+
const el = makeInput({ "data-xdbc": "", "data-xdbc-regex": "[invalid" });
|
|
133
|
+
const cleanup = scanDOM();
|
|
134
|
+
// Element still bound; invalid check returns error string, so value is reverted on blur
|
|
135
|
+
fireInput(el, "anything");
|
|
136
|
+
fireBlur(el);
|
|
137
|
+
expect(el.value).toBe(""); // reverted
|
|
138
|
+
warn.mockRestore();
|
|
139
|
+
cleanup();
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// ─── data-xdbc-type ───────────────────────────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
describe("data-xdbc-type", () => {
|
|
146
|
+
test("accepts empty string (TYPE skips null/undefined/empty)", () => {
|
|
147
|
+
const el = makeInput({ "data-xdbc": "", "data-xdbc-type": "number" });
|
|
148
|
+
const cleanup = scanDOM();
|
|
149
|
+
fireInput(el, "");
|
|
150
|
+
expect(el.value).toBe("");
|
|
151
|
+
cleanup();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("accepts a value matching the type", () => {
|
|
155
|
+
// All input values are strings — TYPE 'string' always passes for non-empty input
|
|
156
|
+
const el = makeInput({ "data-xdbc": "", "data-xdbc-type": "string" });
|
|
157
|
+
const cleanup = scanDOM();
|
|
158
|
+
fireInput(el, "hello");
|
|
159
|
+
expect(el.value).toBe("hello");
|
|
160
|
+
cleanup();
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// ─── data-xdbc-eq ─────────────────────────────────────────────────────────────
|
|
165
|
+
|
|
166
|
+
describe("data-xdbc-eq", () => {
|
|
167
|
+
test("accepts the exact configured value", () => {
|
|
168
|
+
const el = makeInput({ "data-xdbc": "", "data-xdbc-eq": "secret" });
|
|
169
|
+
const cleanup = scanDOM();
|
|
170
|
+
fireInput(el, "secret");
|
|
171
|
+
expect(el.value).toBe("secret");
|
|
172
|
+
cleanup();
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("reverts when value does not match", () => {
|
|
176
|
+
const el = makeInput({ "data-xdbc": "", "data-xdbc-eq": "secret" });
|
|
177
|
+
const cleanup = scanDOM();
|
|
178
|
+
fireInput(el, "other");
|
|
179
|
+
fireBlur(el);
|
|
180
|
+
expect(el.value).toBe("");
|
|
181
|
+
cleanup();
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// ─── data-xdbc-different ──────────────────────────────────────────────────────
|
|
186
|
+
|
|
187
|
+
describe("data-xdbc-different", () => {
|
|
188
|
+
test("accepts a value that differs from the configured value", () => {
|
|
189
|
+
const el = makeInput({
|
|
190
|
+
"data-xdbc": "",
|
|
191
|
+
"data-xdbc-different": "forbidden",
|
|
192
|
+
});
|
|
193
|
+
const cleanup = scanDOM();
|
|
194
|
+
fireInput(el, "allowed");
|
|
195
|
+
expect(el.value).toBe("allowed");
|
|
196
|
+
cleanup();
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("reverts the forbidden value", () => {
|
|
200
|
+
const el = makeInput({
|
|
201
|
+
"data-xdbc": "",
|
|
202
|
+
"data-xdbc-different": "forbidden",
|
|
203
|
+
});
|
|
204
|
+
const cleanup = scanDOM();
|
|
205
|
+
fireInput(el, "forbidden");
|
|
206
|
+
fireBlur(el);
|
|
207
|
+
expect(el.value).toBe("");
|
|
208
|
+
cleanup();
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// ─── data-xdbc-defined ────────────────────────────────────────────────────────
|
|
213
|
+
|
|
214
|
+
describe("data-xdbc-defined", () => {
|
|
215
|
+
test("accepts a non-empty string", () => {
|
|
216
|
+
const el = makeInput({ "data-xdbc": "", "data-xdbc-defined": "" });
|
|
217
|
+
const cleanup = scanDOM();
|
|
218
|
+
fireInput(el, "hello");
|
|
219
|
+
expect(el.value).toBe("hello");
|
|
220
|
+
cleanup();
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// ─── data-xdbc-greater / less / or-equal variants ────────────────────────────
|
|
225
|
+
|
|
226
|
+
describe("data-xdbc-greater", () => {
|
|
227
|
+
test("accepts a value greater than the threshold", () => {
|
|
228
|
+
const el = makeInput({ "data-xdbc": "", "data-xdbc-greater": "5" });
|
|
229
|
+
const cleanup = scanDOM();
|
|
230
|
+
fireInput(el, "10");
|
|
231
|
+
expect(el.value).toBe("10");
|
|
232
|
+
cleanup();
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test("reverts a value equal to the threshold (strict greater)", () => {
|
|
236
|
+
const el = makeInput({ "data-xdbc": "", "data-xdbc-greater": "5" });
|
|
237
|
+
const cleanup = scanDOM();
|
|
238
|
+
fireInput(el, "5");
|
|
239
|
+
fireBlur(el);
|
|
240
|
+
expect(el.value).toBe("");
|
|
241
|
+
cleanup();
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test("reverts a value less than the threshold", () => {
|
|
245
|
+
const el = makeInput({ "data-xdbc": "", "data-xdbc-greater": "5" });
|
|
246
|
+
const cleanup = scanDOM();
|
|
247
|
+
fireInput(el, "3");
|
|
248
|
+
fireBlur(el);
|
|
249
|
+
expect(el.value).toBe("");
|
|
250
|
+
cleanup();
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
describe("data-xdbc-greater-or-equal", () => {
|
|
255
|
+
test("accepts a value equal to the threshold", () => {
|
|
256
|
+
const el = makeInput({
|
|
257
|
+
"data-xdbc": "",
|
|
258
|
+
"data-xdbc-greater-or-equal": "5",
|
|
259
|
+
});
|
|
260
|
+
const cleanup = scanDOM();
|
|
261
|
+
fireInput(el, "5");
|
|
262
|
+
expect(el.value).toBe("5");
|
|
263
|
+
cleanup();
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test("reverts a value below the threshold", () => {
|
|
267
|
+
const el = makeInput({
|
|
268
|
+
"data-xdbc": "",
|
|
269
|
+
"data-xdbc-greater-or-equal": "5",
|
|
270
|
+
});
|
|
271
|
+
const cleanup = scanDOM();
|
|
272
|
+
fireInput(el, "4");
|
|
273
|
+
fireBlur(el);
|
|
274
|
+
expect(el.value).toBe("");
|
|
275
|
+
cleanup();
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
describe("data-xdbc-less", () => {
|
|
280
|
+
test("accepts a value less than the threshold", () => {
|
|
281
|
+
const el = makeInput({ "data-xdbc": "", "data-xdbc-less": "10" });
|
|
282
|
+
const cleanup = scanDOM();
|
|
283
|
+
fireInput(el, "3");
|
|
284
|
+
expect(el.value).toBe("3");
|
|
285
|
+
cleanup();
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test("reverts a value equal to the threshold (strict less)", () => {
|
|
289
|
+
const el = makeInput({ "data-xdbc": "", "data-xdbc-less": "10" });
|
|
290
|
+
const cleanup = scanDOM();
|
|
291
|
+
fireInput(el, "10");
|
|
292
|
+
fireBlur(el);
|
|
293
|
+
expect(el.value).toBe("");
|
|
294
|
+
cleanup();
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
describe("data-xdbc-less-or-equal", () => {
|
|
299
|
+
test("accepts a value equal to the threshold", () => {
|
|
300
|
+
const el = makeInput({ "data-xdbc": "", "data-xdbc-less-or-equal": "10" });
|
|
301
|
+
const cleanup = scanDOM();
|
|
302
|
+
fireInput(el, "10");
|
|
303
|
+
expect(el.value).toBe("10");
|
|
304
|
+
cleanup();
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
test("reverts a value above the threshold", () => {
|
|
308
|
+
const el = makeInput({ "data-xdbc": "", "data-xdbc-less-or-equal": "10" });
|
|
309
|
+
const cleanup = scanDOM();
|
|
310
|
+
fireInput(el, "11");
|
|
311
|
+
fireBlur(el);
|
|
312
|
+
expect(el.value).toBe("");
|
|
313
|
+
cleanup();
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
// ─── data-xdbc-or ─────────────────────────────────────────────────────────────
|
|
318
|
+
|
|
319
|
+
describe("data-xdbc-or", () => {
|
|
320
|
+
test("passes when the first fragment matches", () => {
|
|
321
|
+
const el = makeInput({
|
|
322
|
+
"data-xdbc": "",
|
|
323
|
+
"data-xdbc-or": "regex:^\\d+$;;eq:N/A",
|
|
324
|
+
});
|
|
325
|
+
const cleanup = scanDOM();
|
|
326
|
+
fireInput(el, "123");
|
|
327
|
+
expect(el.value).toBe("123");
|
|
328
|
+
cleanup();
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
test("passes when the second fragment matches", () => {
|
|
332
|
+
const el = makeInput({
|
|
333
|
+
"data-xdbc": "",
|
|
334
|
+
"data-xdbc-or": "regex:^\\d+$;;eq:N/A",
|
|
335
|
+
});
|
|
336
|
+
const cleanup = scanDOM();
|
|
337
|
+
fireInput(el, "N/A");
|
|
338
|
+
expect(el.value).toBe("N/A");
|
|
339
|
+
cleanup();
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
test("reverts when no fragment matches", () => {
|
|
343
|
+
const el = makeInput({
|
|
344
|
+
"data-xdbc": "",
|
|
345
|
+
"data-xdbc-or": "regex:^\\d+$;;eq:N/A",
|
|
346
|
+
});
|
|
347
|
+
const cleanup = scanDOM();
|
|
348
|
+
fireInput(el, "abc");
|
|
349
|
+
fireBlur(el);
|
|
350
|
+
expect(el.value).toBe("");
|
|
351
|
+
cleanup();
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
test("colons inside a regex value are handled correctly", () => {
|
|
355
|
+
// "regex:^https?://" — colon after first split position belongs to the pattern
|
|
356
|
+
const el = makeInput({
|
|
357
|
+
"data-xdbc": "",
|
|
358
|
+
"data-xdbc-or": "regex:^https?://;;eq:N/A",
|
|
359
|
+
});
|
|
360
|
+
const cleanup = scanDOM();
|
|
361
|
+
fireInput(el, "https://example.com");
|
|
362
|
+
fireBlur(el); // commits "https://example.com" as lastValid
|
|
363
|
+
expect(el.value).toBe("https://example.com");
|
|
364
|
+
// ftp:// fails both fragments
|
|
365
|
+
fireInput(el, "ftp://nope");
|
|
366
|
+
fireBlur(el);
|
|
367
|
+
expect(el.value).toBe("https://example.com"); // reverted to last valid
|
|
368
|
+
cleanup();
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
test("warns and skips an unknown contract key in or fragment", () => {
|
|
372
|
+
const warn = jest.spyOn(console, "warn").mockImplementation(() => { });
|
|
373
|
+
const el = makeInput({
|
|
374
|
+
"data-xdbc": "",
|
|
375
|
+
"data-xdbc-or": "unknown:foo;;eq:ok",
|
|
376
|
+
});
|
|
377
|
+
const cleanup = scanDOM();
|
|
378
|
+
fireInput(el, "ok");
|
|
379
|
+
expect(el.value).toBe("ok"); // eq:ok passes
|
|
380
|
+
warn.mockRestore();
|
|
381
|
+
cleanup();
|
|
382
|
+
});
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
// ─── Multiple contracts on one element ────────────────────────────────────────
|
|
386
|
+
|
|
387
|
+
describe("multiple contracts on one element", () => {
|
|
388
|
+
test("all must pass — first failure blocks and reverts", () => {
|
|
389
|
+
const el = makeInput({
|
|
390
|
+
"data-xdbc": "",
|
|
391
|
+
"data-xdbc-regex": "^\\d+$",
|
|
392
|
+
"data-xdbc-greater": "0",
|
|
393
|
+
"data-xdbc-less-or-equal": "100",
|
|
394
|
+
});
|
|
395
|
+
const cleanup = scanDOM();
|
|
396
|
+
fireInput(el, "50");
|
|
397
|
+
fireBlur(el); // commits "50" as lastValid
|
|
398
|
+
expect(el.value).toBe("50");
|
|
399
|
+
fireInput(el, "150"); // fails less-or-equal
|
|
400
|
+
fireBlur(el);
|
|
401
|
+
expect(el.value).toBe("50");
|
|
402
|
+
fireInput(el, "abc"); // fails regex
|
|
403
|
+
fireBlur(el);
|
|
404
|
+
expect(el.value).toBe("50");
|
|
405
|
+
cleanup();
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
// ─── onInfringement integration ───────────────────────────────────────────────
|
|
410
|
+
|
|
411
|
+
describe("scanDOM — onInfringement integration", () => {
|
|
412
|
+
test("fires onInfringement callback with precondition context on invalid input", () => {
|
|
413
|
+
const spy = jest.fn();
|
|
414
|
+
DBC.isolated((dbc) => {
|
|
415
|
+
dbc.infringementSettings.throwException = false;
|
|
416
|
+
dbc.infringementSettings.onInfringement = spy;
|
|
417
|
+
const el = makeInput({ "data-xdbc": "", "data-xdbc-regex": "^\\d*$" });
|
|
418
|
+
const cleanup = scanDOM();
|
|
419
|
+
fireInput(el, "abc");
|
|
420
|
+
fireBlur(el); // validation (and callback) fires on blur by default
|
|
421
|
+
cleanup();
|
|
422
|
+
});
|
|
423
|
+
expect(spy).toHaveBeenCalledTimes(1);
|
|
424
|
+
expect(spy.mock.calls[0][1].type).toBe("precondition");
|
|
425
|
+
expect(spy.mock.calls[0][1].value).toBe("abc");
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
test("does not fire onInfringement on valid input", () => {
|
|
429
|
+
const spy = jest.fn();
|
|
430
|
+
DBC.isolated((dbc) => {
|
|
431
|
+
dbc.infringementSettings.throwException = false;
|
|
432
|
+
dbc.infringementSettings.onInfringement = spy;
|
|
433
|
+
const el = makeInput({ "data-xdbc": "", "data-xdbc-regex": "^\\d*$" });
|
|
434
|
+
const cleanup = scanDOM();
|
|
435
|
+
fireInput(el, "123");
|
|
436
|
+
cleanup();
|
|
437
|
+
});
|
|
438
|
+
expect(spy).not.toHaveBeenCalled();
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
test("throwException:true does not propagate out of the event handler", () => {
|
|
442
|
+
DBC.isolated((dbc) => {
|
|
443
|
+
dbc.infringementSettings.throwException = true;
|
|
444
|
+
const el = makeInput({ "data-xdbc": "", "data-xdbc-regex": "^\\d*$" });
|
|
445
|
+
const cleanup = scanDOM();
|
|
446
|
+
fireInput(el, "abc");
|
|
447
|
+
expect(() => fireBlur(el)).not.toThrow();
|
|
448
|
+
cleanup();
|
|
449
|
+
});
|
|
450
|
+
});
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
// ─── IME / composition ────────────────────────────────────────────────────────
|
|
454
|
+
|
|
455
|
+
describe("scanDOM — IME / composition", () => {
|
|
456
|
+
test("does not validate mid-composition, validates on compositionend", () => {
|
|
457
|
+
const el = makeInput({ "data-xdbc": "", "data-xdbc-regex": "^[a-z]*$" });
|
|
458
|
+
const cleanup = scanDOM();
|
|
459
|
+
// Simulate typing a valid composed value
|
|
460
|
+
fireIME(el, "hello");
|
|
461
|
+
expect(el.value).toBe("hello");
|
|
462
|
+
cleanup();
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
test("reverts invalid value after compositionend when validateOn is input", () => {
|
|
466
|
+
const el = makeInput({
|
|
467
|
+
"data-xdbc": "",
|
|
468
|
+
"data-xdbc-validate-on": "input",
|
|
469
|
+
"data-xdbc-regex": "^[a-z]*$",
|
|
470
|
+
});
|
|
471
|
+
const cleanup = scanDOM();
|
|
472
|
+
fireIME(el, "123"); // invalid after composition
|
|
473
|
+
expect(el.value).toBe("");
|
|
474
|
+
cleanup();
|
|
475
|
+
});
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
// ─── registerDOMContract ──────────────────────────────────────────────────────
|
|
479
|
+
|
|
480
|
+
describe("registerDOMContract", () => {
|
|
481
|
+
test("a custom registered contract is picked up by scanDOM", () => {
|
|
482
|
+
registerDOMContract("test-max-length", (value, attr) =>
|
|
483
|
+
value.length <= Number(attr) ? true : `Max length is ${attr}`,
|
|
484
|
+
);
|
|
485
|
+
const el = makeInput({ "data-xdbc": "", "data-xdbc-test-max-length": "5" });
|
|
486
|
+
const cleanup = scanDOM();
|
|
487
|
+
fireInput(el, "hello"); // length 5 — passes
|
|
488
|
+
fireBlur(el); // commits "hello" as lastValid
|
|
489
|
+
expect(el.value).toBe("hello");
|
|
490
|
+
fireInput(el, "toolong"); // length 7 — fails
|
|
491
|
+
fireBlur(el);
|
|
492
|
+
expect(el.value).toBe("hello");
|
|
493
|
+
cleanup();
|
|
494
|
+
});
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
// ─── <textarea> support ───────────────────────────────────────────────────────
|
|
498
|
+
|
|
499
|
+
describe("textarea support", () => {
|
|
500
|
+
test("works with <textarea> exactly like <input>", () => {
|
|
501
|
+
const ta = document.createElement("textarea");
|
|
502
|
+
ta.setAttribute("data-xdbc", "");
|
|
503
|
+
ta.setAttribute("data-xdbc-regex", "^[a-z ]*$");
|
|
504
|
+
document.body.appendChild(ta);
|
|
505
|
+
const cleanup = scanDOM();
|
|
506
|
+
ta.value = "hello";
|
|
507
|
+
ta.dispatchEvent(new Event("input"));
|
|
508
|
+
fireBlur(ta); // commits "hello" as lastValid
|
|
509
|
+
expect(ta.value).toBe("hello");
|
|
510
|
+
ta.value = "HELLO";
|
|
511
|
+
ta.dispatchEvent(new Event("input"));
|
|
512
|
+
fireBlur(ta);
|
|
513
|
+
expect(ta.value).toBe("hello");
|
|
514
|
+
cleanup();
|
|
515
|
+
});
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
// ─── data-xdbc-validate-on ───────────────────────────────────────────────────
|
|
519
|
+
|
|
520
|
+
describe("data-xdbc-validate-on", () => {
|
|
521
|
+
const DOMAIN_REGEX = "^[a-zA-Z0-9][a-zA-Z0-9.\\-]*\\.[a-zA-Z]{2,}$";
|
|
522
|
+
|
|
523
|
+
// ── default behaviour: blur ───────────────────────────────────────────────
|
|
524
|
+
|
|
525
|
+
test("default: allows partial input during typing", () => {
|
|
526
|
+
const el = makeInput({ "data-xdbc": "", "data-xdbc-regex": DOMAIN_REGEX });
|
|
527
|
+
const cleanup = scanDOM();
|
|
528
|
+
fireInput(el, "e");
|
|
529
|
+
expect(el.value).toBe("e");
|
|
530
|
+
fireInput(el, "example.c");
|
|
531
|
+
expect(el.value).toBe("example.c"); // still not a valid domain, but not reverted
|
|
532
|
+
cleanup();
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
test("default: accepts a complete valid value on blur", () => {
|
|
536
|
+
const el = makeInput({ "data-xdbc": "", "data-xdbc-regex": DOMAIN_REGEX });
|
|
537
|
+
const cleanup = scanDOM();
|
|
538
|
+
fireInput(el, "example.com");
|
|
539
|
+
fireBlur(el);
|
|
540
|
+
expect(el.value).toBe("example.com");
|
|
541
|
+
cleanup();
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
test("default: reverts to last valid value on blur when value is invalid", () => {
|
|
545
|
+
const el = makeInput({ "data-xdbc": "", "data-xdbc-regex": DOMAIN_REGEX });
|
|
546
|
+
const cleanup = scanDOM();
|
|
547
|
+
fireInput(el, "example.com");
|
|
548
|
+
fireBlur(el); // commits "example.com"
|
|
549
|
+
fireInput(el, "notadomain");
|
|
550
|
+
fireBlur(el);
|
|
551
|
+
expect(el.value).toBe("example.com");
|
|
552
|
+
cleanup();
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
test("default: reverts to empty string when no valid value was ever committed", () => {
|
|
556
|
+
const el = makeInput({ "data-xdbc": "", "data-xdbc-regex": DOMAIN_REGEX });
|
|
557
|
+
const cleanup = scanDOM();
|
|
558
|
+
fireInput(el, "notadomain");
|
|
559
|
+
fireBlur(el);
|
|
560
|
+
expect(el.value).toBe("");
|
|
561
|
+
cleanup();
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
// ── explicit "input" mode ─────────────────────────────────────────────────
|
|
565
|
+
|
|
566
|
+
test('"input": reverts invalid value on every keystroke', () => {
|
|
567
|
+
const el = makeInput({
|
|
568
|
+
"data-xdbc": "",
|
|
569
|
+
"data-xdbc-validate-on": "input",
|
|
570
|
+
"data-xdbc-regex": "^\\d*$",
|
|
571
|
+
});
|
|
572
|
+
const cleanup = scanDOM();
|
|
573
|
+
fireInput(el, "abc");
|
|
574
|
+
expect(el.value).toBe("");
|
|
575
|
+
cleanup();
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
test('"input": allows a valid value through without needing blur', () => {
|
|
579
|
+
const el = makeInput({
|
|
580
|
+
"data-xdbc": "",
|
|
581
|
+
"data-xdbc-validate-on": "input",
|
|
582
|
+
"data-xdbc-regex": "^\\d*$",
|
|
583
|
+
});
|
|
584
|
+
const cleanup = scanDOM();
|
|
585
|
+
fireInput(el, "123");
|
|
586
|
+
expect(el.value).toBe("123");
|
|
587
|
+
cleanup();
|
|
588
|
+
});
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
// ─── data-xdbc-regex-input ───────────────────────────────────────────────────
|
|
592
|
+
|
|
593
|
+
describe("data-xdbc-regex-input", () => {
|
|
594
|
+
const DOMAIN_REGEX = "^[a-zA-Z0-9][a-zA-Z0-9.\\-]*\\.[a-zA-Z]{2,}$";
|
|
595
|
+
const DOMAIN_CHARS = "^[a-zA-Z0-9.\\-]*$";
|
|
596
|
+
|
|
597
|
+
test("blocks disallowed characters on every keystroke", () => {
|
|
598
|
+
const el = makeInput({
|
|
599
|
+
"data-xdbc": "",
|
|
600
|
+
"data-xdbc-regex-input": DOMAIN_CHARS,
|
|
601
|
+
});
|
|
602
|
+
const cleanup = scanDOM();
|
|
603
|
+
fireInput(el, "exam"); // valid chars
|
|
604
|
+
expect(el.value).toBe("exam");
|
|
605
|
+
fireInput(el, "exam ple"); // space is not allowed
|
|
606
|
+
expect(el.value).toBe("exam");
|
|
607
|
+
cleanup();
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
test("allows partial input that passes the keystroke regex but not the blur regex", () => {
|
|
611
|
+
const el = makeInput({
|
|
612
|
+
"data-xdbc": "",
|
|
613
|
+
"data-xdbc-regex": DOMAIN_REGEX,
|
|
614
|
+
"data-xdbc-regex-input": DOMAIN_CHARS,
|
|
615
|
+
});
|
|
616
|
+
const cleanup = scanDOM();
|
|
617
|
+
// "example" has valid chars but is not a complete domain — keystroke check passes, blur check deferred
|
|
618
|
+
fireInput(el, "example");
|
|
619
|
+
expect(el.value).toBe("example");
|
|
620
|
+
cleanup();
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
test("reverts invalid chars on keystroke and reverts incomplete value on blur", () => {
|
|
624
|
+
const el = makeInput({
|
|
625
|
+
"data-xdbc": "",
|
|
626
|
+
"data-xdbc-regex": DOMAIN_REGEX,
|
|
627
|
+
"data-xdbc-regex-input": DOMAIN_CHARS,
|
|
628
|
+
});
|
|
629
|
+
const cleanup = scanDOM();
|
|
630
|
+
// invalid char — reverted immediately
|
|
631
|
+
fireInput(el, "ex ample");
|
|
632
|
+
expect(el.value).toBe("");
|
|
633
|
+
// valid partial — accepted while typing
|
|
634
|
+
fireInput(el, "example");
|
|
635
|
+
expect(el.value).toBe("example");
|
|
636
|
+
// incomplete on blur — reverted to last blur-committed value ("")
|
|
637
|
+
fireBlur(el);
|
|
638
|
+
expect(el.value).toBe("");
|
|
639
|
+
cleanup();
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
test("complete valid domain passes both keystroke and blur checks", () => {
|
|
643
|
+
const el = makeInput({
|
|
644
|
+
"data-xdbc": "",
|
|
645
|
+
"data-xdbc-regex": DOMAIN_REGEX,
|
|
646
|
+
"data-xdbc-regex-input": DOMAIN_CHARS,
|
|
647
|
+
});
|
|
648
|
+
const cleanup = scanDOM();
|
|
649
|
+
fireInput(el, "example.com");
|
|
650
|
+
expect(el.value).toBe("example.com"); // chars ok, not yet blur-validated
|
|
651
|
+
fireBlur(el);
|
|
652
|
+
expect(el.value).toBe("example.com"); // full pattern ok too
|
|
653
|
+
cleanup();
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
test("regex-input fires even when data-xdbc-validate-on is absent (blur default)", () => {
|
|
657
|
+
const el = makeInput({
|
|
658
|
+
"data-xdbc": "",
|
|
659
|
+
"data-xdbc-regex-input": "^\\d*$",
|
|
660
|
+
});
|
|
661
|
+
const cleanup = scanDOM();
|
|
662
|
+
fireInput(el, "12");
|
|
663
|
+
expect(el.value).toBe("12");
|
|
664
|
+
fireInput(el, "12a");
|
|
665
|
+
expect(el.value).toBe("12"); // reverted on input even though validate-on defaults to blur
|
|
666
|
+
cleanup();
|
|
667
|
+
});
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
// ─── -input variants for built-in contracts ───────────────────────────────────
|
|
671
|
+
|
|
672
|
+
describe("-input variants fire on every keystroke regardless of validate-on", () => {
|
|
673
|
+
test("type-input: accepts string value on every keystroke (DOM values are always strings)", () => {
|
|
674
|
+
// In DOM, input values are always JS strings, so type-input="string" always accepts
|
|
675
|
+
// and type-input="number" always rejects. This test verifies the -input contract is
|
|
676
|
+
// picked up and fires on every keystroke without waiting for blur.
|
|
677
|
+
const el = makeInput({ "data-xdbc": "", "data-xdbc-type-input": "string" });
|
|
678
|
+
const cleanup = scanDOM();
|
|
679
|
+
fireInput(el, "hello");
|
|
680
|
+
expect(el.value).toBe("hello"); // string → accepted immediately
|
|
681
|
+
// Verify it really fires on keystroke and not only on blur
|
|
682
|
+
fireInput(el, "world");
|
|
683
|
+
expect(el.value).toBe("world");
|
|
684
|
+
cleanup();
|
|
685
|
+
});
|
|
686
|
+
|
|
687
|
+
test("eq-input: blocks non-matching value on every keystroke", () => {
|
|
688
|
+
const el = makeInput({ "data-xdbc": "", "data-xdbc-eq-input": "yes" });
|
|
689
|
+
const cleanup = scanDOM();
|
|
690
|
+
fireInput(el, "yes");
|
|
691
|
+
expect(el.value).toBe("yes");
|
|
692
|
+
fireInput(el, "no");
|
|
693
|
+
expect(el.value).toBe("yes"); // reverted
|
|
694
|
+
cleanup();
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
test("different-input: blocks forbidden value on every keystroke", () => {
|
|
698
|
+
const el = makeInput({ "data-xdbc": "", "data-xdbc-different-input": "no" });
|
|
699
|
+
const cleanup = scanDOM();
|
|
700
|
+
fireInput(el, "yes");
|
|
701
|
+
expect(el.value).toBe("yes");
|
|
702
|
+
fireInput(el, "no");
|
|
703
|
+
expect(el.value).toBe("yes"); // reverted
|
|
704
|
+
cleanup();
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
test("greater-input: blocks value ≤ threshold on every keystroke", () => {
|
|
708
|
+
const el = makeInput({ "data-xdbc": "", "data-xdbc-greater-input": "0" });
|
|
709
|
+
const cleanup = scanDOM();
|
|
710
|
+
fireInput(el, "1");
|
|
711
|
+
expect(el.value).toBe("1");
|
|
712
|
+
fireInput(el, "0");
|
|
713
|
+
expect(el.value).toBe("1"); // reverted
|
|
714
|
+
cleanup();
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
test("greater-or-equal-input: blocks value below threshold on every keystroke", () => {
|
|
718
|
+
const el = makeInput({
|
|
719
|
+
"data-xdbc": "",
|
|
720
|
+
"data-xdbc-greater-or-equal-input": "5",
|
|
721
|
+
});
|
|
722
|
+
const cleanup = scanDOM();
|
|
723
|
+
fireInput(el, "5");
|
|
724
|
+
expect(el.value).toBe("5");
|
|
725
|
+
fireInput(el, "4");
|
|
726
|
+
expect(el.value).toBe("5"); // reverted
|
|
727
|
+
cleanup();
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
test("less-input: blocks value ≥ threshold on every keystroke", () => {
|
|
731
|
+
const el = makeInput({ "data-xdbc": "", "data-xdbc-less-input": "10" });
|
|
732
|
+
const cleanup = scanDOM();
|
|
733
|
+
fireInput(el, "9");
|
|
734
|
+
expect(el.value).toBe("9");
|
|
735
|
+
fireInput(el, "10");
|
|
736
|
+
expect(el.value).toBe("9"); // reverted
|
|
737
|
+
cleanup();
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
test("less-or-equal-input: blocks value above threshold on every keystroke", () => {
|
|
741
|
+
const el = makeInput({
|
|
742
|
+
"data-xdbc": "",
|
|
743
|
+
"data-xdbc-less-or-equal-input": "10",
|
|
744
|
+
});
|
|
745
|
+
const cleanup = scanDOM();
|
|
746
|
+
fireInput(el, "10");
|
|
747
|
+
expect(el.value).toBe("10");
|
|
748
|
+
fireInput(el, "11");
|
|
749
|
+
expect(el.value).toBe("10"); // reverted
|
|
750
|
+
cleanup();
|
|
751
|
+
});
|
|
752
|
+
|
|
753
|
+
test("or-input: passes when any fragment matches, reverts on keystroke when none do", () => {
|
|
754
|
+
const el = makeInput({
|
|
755
|
+
"data-xdbc": "",
|
|
756
|
+
"data-xdbc-or-input": "regex:^\\d+$;;eq:N/A",
|
|
757
|
+
});
|
|
758
|
+
const cleanup = scanDOM();
|
|
759
|
+
fireInput(el, "42");
|
|
760
|
+
expect(el.value).toBe("42");
|
|
761
|
+
fireInput(el, "N/A");
|
|
762
|
+
expect(el.value).toBe("N/A");
|
|
763
|
+
fireInput(el, "abc");
|
|
764
|
+
expect(el.value).toBe("N/A"); // reverted on keystroke
|
|
765
|
+
cleanup();
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
test("combining -input (keystroke) and plain (blur) contracts works independently", () => {
|
|
769
|
+
// greater-input blocks ≤ 0 on every keystroke; less-or-equal only enforced on blur
|
|
770
|
+
const el = makeInput({
|
|
771
|
+
"data-xdbc": "",
|
|
772
|
+
"data-xdbc-greater-input": "0",
|
|
773
|
+
"data-xdbc-less-or-equal": "100",
|
|
774
|
+
});
|
|
775
|
+
const cleanup = scanDOM();
|
|
776
|
+
fireInput(el, "50");
|
|
777
|
+
expect(el.value).toBe("50"); // passes keystroke check; blur check deferred
|
|
778
|
+
fireInput(el, "0");
|
|
779
|
+
expect(el.value).toBe("50"); // ≤ 0 reverted immediately by -input
|
|
780
|
+
fireInput(el, "150");
|
|
781
|
+
expect(el.value).toBe("150"); // chars pass keystroke; blur check not yet run
|
|
782
|
+
fireBlur(el);
|
|
783
|
+
expect(el.value).toBe("50"); // > 100 reverted on blur (lastValid was "50")
|
|
784
|
+
cleanup();
|
|
785
|
+
});
|
|
786
|
+
});
|