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.
Files changed (120) hide show
  1. package/.gitattributes +16 -8
  2. package/.vscode/settings.json +3 -3
  3. package/.vscode/tasks.json +23 -23
  4. package/ASSESSMENT.md +249 -0
  5. package/README.md +538 -408
  6. package/__tests__/DBC/AE.test.ts +62 -62
  7. package/__tests__/DBC/ARRAY.test.ts +91 -91
  8. package/__tests__/DBC/DEFINED.test.ts +53 -53
  9. package/__tests__/DBC/DOM.test.ts +786 -0
  10. package/__tests__/DBC/Decorators.test.ts +367 -367
  11. package/__tests__/DBC/EQ.test.ts +13 -13
  12. package/__tests__/DBC/GREATER.test.ts +31 -31
  13. package/__tests__/DBC/HasAttribute.test.ts +60 -60
  14. package/__tests__/DBC/IF.test.ts +62 -62
  15. package/__tests__/DBC/INSTANCE.test.ts +13 -13
  16. package/__tests__/DBC/JSON.OP.test.ts +47 -47
  17. package/__tests__/DBC/JSON.Parse.test.ts +17 -17
  18. package/__tests__/DBC/OR.test.ts +14 -14
  19. package/__tests__/DBC/PLAIN_OBJECT.test.ts +109 -109
  20. package/__tests__/DBC/REGEX.test.ts +17 -17
  21. package/__tests__/DBC/TYPE.test.ts +13 -13
  22. package/__tests__/DBC/UNDEFINED.test.ts +45 -45
  23. package/__tests__/DBC/ZOD.test.ts +54 -54
  24. package/__tests__/DBC/onInfringement.test.ts +262 -0
  25. package/biome.json +45 -40
  26. package/dist/DBC/AE.js +172 -0
  27. package/dist/DBC/ARR/PLAIN_OBJECT.d.ts +0 -3
  28. package/dist/DBC/ARR/PLAIN_OBJECT.js +95 -0
  29. package/dist/DBC/ARRAY.d.ts +0 -3
  30. package/dist/DBC/ARRAY.js +90 -0
  31. package/dist/DBC/COMPARISON/GREATER.js +21 -0
  32. package/dist/DBC/COMPARISON/GREATER_OR_EQUAL.js +21 -0
  33. package/dist/DBC/COMPARISON/LESS.js +21 -0
  34. package/dist/DBC/COMPARISON/LESS_OR_EQUAL.js +21 -0
  35. package/dist/DBC/COMPARISON.js +98 -0
  36. package/dist/DBC/DEFINED.js +87 -0
  37. package/dist/DBC/DOM.d.ts +123 -0
  38. package/dist/DBC/DOM.js +362 -0
  39. package/dist/DBC/EQ/DIFFERENT.js +34 -0
  40. package/dist/DBC/EQ.js +101 -0
  41. package/dist/DBC/GREATER.js +99 -0
  42. package/dist/DBC/HasAttribute.js +101 -0
  43. package/dist/DBC/IF.js +96 -0
  44. package/dist/DBC/INSTANCE.js +122 -0
  45. package/dist/DBC/JSON.OP.js +120 -0
  46. package/dist/DBC/JSON.Parse.js +104 -0
  47. package/dist/DBC/OR.js +125 -0
  48. package/dist/DBC/REGEX.js +136 -0
  49. package/dist/DBC/TYPE.js +112 -0
  50. package/dist/DBC/UNDEFINED.js +87 -0
  51. package/dist/DBC/ZOD.js +99 -0
  52. package/dist/DBC.d.ts +18 -4
  53. package/dist/DBC.js +645 -0
  54. package/dist/Demo.d.ts +10 -0
  55. package/dist/Demo.js +713 -0
  56. package/dist/Test.html +18 -0
  57. package/dist/bundle.js +6140 -405
  58. package/dist/index.d.ts +22 -0
  59. package/dist/index.html +18 -0
  60. package/dist/index.js +22 -0
  61. package/docs/assets/highlight.css +22 -22
  62. package/docs/assets/icons.js +17 -17
  63. package/docs/assets/main.js +60 -60
  64. package/docs/assets/style.css +1640 -1640
  65. package/docs/classes/DBC.DBC.html +98 -98
  66. package/docs/classes/DBC_AE.AE.html +160 -160
  67. package/docs/classes/DBC_EQ.EQ.html +131 -131
  68. package/docs/classes/DBC_GREATER.GREATER.html +139 -139
  69. package/docs/classes/DBC_INSTANCE.INSTANCE.html +130 -130
  70. package/docs/classes/DBC_JSON.OP.JSON_OP.html +138 -138
  71. package/docs/classes/DBC_JSON.Parse.JSON_Parse.html +129 -129
  72. package/docs/classes/DBC_OR.OR.html +137 -137
  73. package/docs/classes/DBC_REGEX.REGEX.html +136 -136
  74. package/docs/classes/DBC_TYPE.TYPE.html +130 -130
  75. package/docs/classes/Demo.Demo.html +14 -14
  76. package/docs/hierarchy.html +1 -1
  77. package/docs/index.html +1 -1
  78. package/docs/modules/DBC.html +1 -1
  79. package/docs/modules/DBC_AE.html +1 -1
  80. package/docs/modules/DBC_EQ.html +1 -1
  81. package/docs/modules/DBC_GREATER.html +1 -1
  82. package/docs/modules/DBC_INSTANCE.html +1 -1
  83. package/docs/modules/DBC_JSON.OP.html +1 -1
  84. package/docs/modules/DBC_JSON.Parse.html +1 -1
  85. package/docs/modules/DBC_OR.html +1 -1
  86. package/docs/modules/DBC_REGEX.html +1 -1
  87. package/docs/modules/DBC_TYPE.html +1 -1
  88. package/docs/modules/Demo.html +1 -1
  89. package/jest.config.js +32 -32
  90. package/package.json +71 -55
  91. package/src/DBC/AE.ts +269 -288
  92. package/src/DBC/ARR/PLAIN_OBJECT.ts +122 -133
  93. package/src/DBC/ARRAY.ts +117 -127
  94. package/src/DBC/COMPARISON/GREATER.ts +41 -46
  95. package/src/DBC/COMPARISON/GREATER_OR_EQUAL.ts +41 -45
  96. package/src/DBC/COMPARISON/LESS.ts +41 -45
  97. package/src/DBC/COMPARISON/LESS_OR_EQUAL.ts +41 -45
  98. package/src/DBC/COMPARISON.ts +149 -159
  99. package/src/DBC/DEFINED.ts +117 -122
  100. package/src/DBC/DOM.ts +453 -0
  101. package/src/DBC/EQ/DIFFERENT.ts +51 -57
  102. package/src/DBC/EQ.ts +154 -163
  103. package/src/DBC/HasAttribute.ts +149 -154
  104. package/src/DBC/IF.ts +173 -179
  105. package/src/DBC/INSTANCE.ts +168 -171
  106. package/src/DBC/JSON.OP.ts +178 -186
  107. package/src/DBC/JSON.Parse.ts +150 -157
  108. package/src/DBC/OR.ts +183 -187
  109. package/src/DBC/REGEX.ts +195 -196
  110. package/src/DBC/TYPE.ts +142 -149
  111. package/src/DBC/UNDEFINED.ts +115 -117
  112. package/src/DBC/ZOD.ts +130 -135
  113. package/src/DBC.ts +902 -904
  114. package/src/Demo.ts +537 -404
  115. package/src/index.ts +22 -0
  116. package/tsconfig.json +18 -18
  117. package/tsconfig.test.json +7 -7
  118. package/typedoc.json +16 -16
  119. package/webpack.config.js +27 -27
  120. 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
+ });