@excom/quark 0.1.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.
Files changed (130) hide show
  1. package/.rush/temp/chunked-rush-logs/quark.apply-exports.chunks.jsonl +1 -0
  2. package/.rush/temp/chunked-rush-logs/quark.build_package-metas.chunks.jsonl +1 -0
  3. package/.rush/temp/operation/apply-exports/all.log +1 -0
  4. package/.rush/temp/operation/apply-exports/log-chunks.jsonl +1 -0
  5. package/.rush/temp/operation/apply-exports/state.json +3 -0
  6. package/.rush/temp/operation/build_package-metas/all.log +1 -0
  7. package/.rush/temp/operation/build_package-metas/log-chunks.jsonl +1 -0
  8. package/.rush/temp/operation/build_package-metas/state.json +3 -0
  9. package/.rush/temp/shrinkwrap-deps.json +3 -0
  10. package/config/rig.json +6 -0
  11. package/index.ts +4 -0
  12. package/language.ts +7 -0
  13. package/package.json +45 -0
  14. package/rush-logs/quark.apply-exports.cache.log +1 -0
  15. package/rush-logs/quark.apply-exports.log +1 -0
  16. package/rush-logs/quark.build_package-metas.cache.log +1 -0
  17. package/rush-logs/quark.build_package-metas.log +1 -0
  18. package/src/actions.ts +445 -0
  19. package/src/ast.ts +187 -0
  20. package/src/bindings.ts +115 -0
  21. package/src/builtin-modules.ts +401 -0
  22. package/src/constants.ts +47 -0
  23. package/src/cycle-check.ts +121 -0
  24. package/src/devtools-hook.ts +303 -0
  25. package/src/element-api.ts +109 -0
  26. package/src/evaluator.ts +317 -0
  27. package/src/form-controls.ts +69 -0
  28. package/src/language-tables.ts +137 -0
  29. package/src/language.ts +1225 -0
  30. package/src/observer.ts +110 -0
  31. package/src/paint.ts +305 -0
  32. package/src/properties.ts +818 -0
  33. package/src/props.ts +94 -0
  34. package/src/quark-element.ts +7 -0
  35. package/src/quark-internal.ts +365 -0
  36. package/src/quark.ts +817 -0
  37. package/src/resolvers.ts +867 -0
  38. package/src/rule.ts +800 -0
  39. package/src/scope-id.ts +47 -0
  40. package/src/selector-utils.ts +430 -0
  41. package/src/settle.ts +117 -0
  42. package/src/types.ts +155 -0
  43. package/src/utils.ts +79 -0
  44. package/src/variables.ts +359 -0
  45. package/support/demos/css-variables.html +18 -0
  46. package/support/demos/dispatch.html +20 -0
  47. package/support/demos/events.html +9 -0
  48. package/support/demos/iterate.html +13 -0
  49. package/support/demos/js-api.html +17 -0
  50. package/support/demos/provider-list.html +16 -0
  51. package/support/demos/toggle-content.html +14 -0
  52. package/support/demos/unset.html +12 -0
  53. package/support/demos/view-transition.html +36 -0
  54. package/support/docs/ATTRIBUTES.md +37 -0
  55. package/support/docs/AT_RULES.md +45 -0
  56. package/support/docs/BUILTINS.md +57 -0
  57. package/support/docs/CONTENT.md +40 -0
  58. package/support/docs/CSS_VARIABLES.md +16 -0
  59. package/support/docs/DECLARATIONS.md +26 -0
  60. package/support/docs/DELAY.md +37 -0
  61. package/support/docs/DIAGNOSTICS.md +32 -0
  62. package/support/docs/DISPATCH.md +81 -0
  63. package/support/docs/ELEMENT_PROPERTIES.md +34 -0
  64. package/support/docs/EXPRESSIONS.md +31 -0
  65. package/support/docs/INTERNAL.md +8 -0
  66. package/support/docs/JS_API.md +53 -0
  67. package/support/docs/JS_WRITES.md +36 -0
  68. package/support/docs/LIMITATIONS.md +26 -0
  69. package/support/docs/LOOP_GUARD.md +30 -0
  70. package/support/docs/METHODS.md +43 -0
  71. package/support/docs/MODULES.md +111 -0
  72. package/support/docs/NO_REVERSION.md +18 -0
  73. package/support/docs/ON.md +87 -0
  74. package/support/docs/REACTIVITY.md +24 -0
  75. package/support/docs/README.md +143 -0
  76. package/support/docs/SELECTORS.md +87 -0
  77. package/support/docs/SHEETS.md +67 -0
  78. package/support/docs/SYNTAX.md +36 -0
  79. package/support/docs/USE.md +24 -0
  80. package/support/docs/VALUES.md +27 -0
  81. package/support/docs/VARIABLES.md +47 -0
  82. package/support/docs/VIEW_TRANSITION.md +64 -0
  83. package/support/docs-sections.json +65 -0
  84. package/support/package-meta.json +208 -0
  85. package/support/scripts/build-language-docs.mjs +196 -0
  86. package/support/tests/__snapshots__/css-variables.view.test.ts.snap +23 -0
  87. package/support/tests/__snapshots__/dispatch.view.test.ts.snap +23 -0
  88. package/support/tests/__snapshots__/events.view.test.ts.snap +23 -0
  89. package/support/tests/__snapshots__/iterate.view.test.ts.snap +23 -0
  90. package/support/tests/__snapshots__/js-api.view.test.ts.snap +23 -0
  91. package/support/tests/__snapshots__/provider-list.view.test.ts.snap +23 -0
  92. package/support/tests/__snapshots__/quark-features.test.ts.snap +1079 -0
  93. package/support/tests/__snapshots__/quark-with-providers.test.ts.snap +89 -0
  94. package/support/tests/__snapshots__/quark.test.ts.snap +221 -0
  95. package/support/tests/__snapshots__/selectors.test.ts.snap +133 -0
  96. package/support/tests/__snapshots__/toggle-content.view.test.ts.snap +23 -0
  97. package/support/tests/__snapshots__/unset.view.test.ts.snap +23 -0
  98. package/support/tests/__snapshots__/view-transition.view.test.ts.snap +23 -0
  99. package/support/tests/actions.test.ts +437 -0
  100. package/support/tests/builtin-modules.test.ts +337 -0
  101. package/support/tests/builtins.test.ts +189 -0
  102. package/support/tests/css-variables.view.test.ts +35 -0
  103. package/support/tests/delay.test.ts +244 -0
  104. package/support/tests/devtools-hook.test.ts +496 -0
  105. package/support/tests/diagnostics.test.ts +224 -0
  106. package/support/tests/dispatch.view.test.ts +43 -0
  107. package/support/tests/element-api.test.ts +202 -0
  108. package/support/tests/evaluator.test.ts +237 -0
  109. package/support/tests/events.view.test.ts +40 -0
  110. package/support/tests/form-controls.test.ts +452 -0
  111. package/support/tests/helpers.ts +354 -0
  112. package/support/tests/internals.test.ts +568 -0
  113. package/support/tests/iterate.view.test.ts +40 -0
  114. package/support/tests/js-api.view.test.ts +45 -0
  115. package/support/tests/language-docs.test.ts +147 -0
  116. package/support/tests/lifecycle.test.ts +440 -0
  117. package/support/tests/loop-guard.test.ts +323 -0
  118. package/support/tests/provider-list.view.test.ts +47 -0
  119. package/support/tests/quark-features.test.ts +2887 -0
  120. package/support/tests/quark-with-providers.test.ts +169 -0
  121. package/support/tests/quark.test.ts +359 -0
  122. package/support/tests/resolvers.test.ts +346 -0
  123. package/support/tests/selector-analysis.test.ts +281 -0
  124. package/support/tests/selectors.test.ts +670 -0
  125. package/support/tests/toggle-content.view.test.ts +40 -0
  126. package/support/tests/unset.view.test.ts +42 -0
  127. package/support/tests/view-helpers.ts +81 -0
  128. package/support/tests/view-transition.test.ts +1069 -0
  129. package/support/tests/view-transition.view.test.ts +76 -0
  130. package/tsconfig.json +5 -0
@@ -0,0 +1,452 @@
1
+ /**
2
+ * Form-control sync: on native form controls the attribute Quark writes is
3
+ * authoritative, and the live (dirty-flag guarded) property follows it.
4
+ * happy-dom implements the dirty value / checkedness flags, so every
5
+ * "user edited" case below really diverges the property from the attribute
6
+ * before a rule writes.
7
+ */
8
+ import { Quark } from "../../index";
9
+ import {
10
+ FORM_CONTROL_ATTRIBUTES,
11
+ TEXT_CONTROL_TAGS,
12
+ isFormControlAttribute,
13
+ } from "../../src/form-controls";
14
+ import {
15
+ afterEach,
16
+ describe,
17
+ expect,
18
+ it,
19
+ } from "@excom/heft-rig/profiles/default/config/test-utils";
20
+ import { createSheet, flush, mount, unregisterAll } from "./helpers";
21
+
22
+ /** Type into a control the way a user would: live value only, no attribute. */
23
+ const userTypes = (input: HTMLInputElement | HTMLTextAreaElement, value: string) => {
24
+ input.value = value;
25
+ input.dispatchEvent(new Event("input", { bubbles: true }));
26
+ };
27
+ const userToggles = (input: HTMLInputElement, checked: boolean) => {
28
+ input.checked = checked;
29
+ input.dispatchEvent(new Event("change", { bubbles: true }));
30
+ };
31
+
32
+ describe("form-control sync", () => {
33
+ afterEach(() => {
34
+ unregisterAll();
35
+ document.body.innerHTML = "";
36
+ });
37
+
38
+ describe("table", () => {
39
+ it("covers exactly input value / checked, option selected, textarea text", () => {
40
+ expect(FORM_CONTROL_ATTRIBUTES).toEqual({
41
+ input: ["value", "checked"],
42
+ option: ["selected"],
43
+ });
44
+ expect(TEXT_CONTROL_TAGS).toEqual(["textarea"]);
45
+ expect(isFormControlAttribute(document.createElement("input"), "value")).toBe(true);
46
+ expect(isFormControlAttribute(document.createElement("input"), "checked")).toBe(true);
47
+ expect(isFormControlAttribute(document.createElement("option"), "selected")).toBe(true);
48
+ expect(isFormControlAttribute(document.createElement("select"), "value")).toBe(false);
49
+ expect(isFormControlAttribute(document.createElement("textarea"), "value")).toBe(false);
50
+ expect(isFormControlAttribute(document.createElement("input"), "placeholder")).toBe(false);
51
+ expect(isFormControlAttribute(document.createElement("super-input"), "value")).toBe(false);
52
+ });
53
+ });
54
+
55
+ describe("input value", () => {
56
+ it("writes the attribute and the live value on a pristine input", async () => {
57
+ const { root } = mount(`<input>`, `input { value: "hello"; }`);
58
+ await flush();
59
+ const input = root.querySelector("input")!;
60
+ expect(input.getAttribute("value")).toBe("hello");
61
+ expect(input.defaultValue).toBe("hello");
62
+ expect(input.value).toBe("hello");
63
+ });
64
+
65
+ it("writes numbers as text", async () => {
66
+ const { root } = mount(`<input type="number">`, `input { value: 4 * 10.5; }`);
67
+ await flush();
68
+ const input = root.querySelector("input")!;
69
+ expect(input.getAttribute("value")).toBe("42");
70
+ expect(input.value).toBe("42");
71
+ });
72
+
73
+ it("overrides a value the user typed when the rule writes a new one", async () => {
74
+ const { root } = mount(
75
+ `<section data-unit="c"><input value="0"></section>`,
76
+ `section[data-unit="f"] input { value: "32"; }`,
77
+ );
78
+ await flush();
79
+ const input = root.querySelector("input")!;
80
+ userTypes(input, "100");
81
+ expect(input.value).toBe("100");
82
+ expect(input.getAttribute("value")).toBe("0");
83
+
84
+ root.querySelector("section")!.setAttribute("data-unit", "f");
85
+ await flush();
86
+ expect(input.getAttribute("value")).toBe("32");
87
+ expect(input.value).toBe("32");
88
+ });
89
+
90
+ it("restores a user-edited input even when the attribute already holds the rule's value", async () => {
91
+ /*
92
+ * the temperature-converter case: the user typed into a field whose
93
+ * attribute never changed; a re-run to the same attribute value must
94
+ * still win over the live value
95
+ */
96
+ const { root } = mount(
97
+ `<section data-source="a"><input name="b" value="0"></section>`,
98
+ `section[data-source="a"] input { value: "0"; }`,
99
+ );
100
+ await flush();
101
+ const input = root.querySelector("input")!;
102
+ expect(input.value).toBe("0");
103
+
104
+ userTypes(input, "100");
105
+ root.querySelector("section")!.setAttribute("data-source", "b");
106
+ await flush();
107
+ expect(input.value).toBe("100"); // rule not matching: nothing written
108
+
109
+ root.querySelector("section")!.setAttribute("data-source", "a");
110
+ await flush();
111
+ expect(input.getAttribute("value")).toBe("0");
112
+ expect(input.value).toBe("0");
113
+ });
114
+
115
+ it("does not touch a control while the rule does not re-run", async () => {
116
+ const { root } = mount(`<input>`, `input { value: "seed"; }`);
117
+ await flush();
118
+ const input = root.querySelector("input")!;
119
+ userTypes(input, "mine");
120
+ await flush();
121
+ expect(input.value).toBe("mine");
122
+ expect(input.getAttribute("value")).toBe("seed");
123
+ });
124
+
125
+ it("`none` removes the attribute and clears the live value", async () => {
126
+ const { root } = mount(
127
+ `<section><input value="keep"></section>`,
128
+ `section[data-clear] input { value: none; }`,
129
+ );
130
+ await flush();
131
+ const input = root.querySelector("input")!;
132
+ userTypes(input, "typed");
133
+ root.querySelector("section")!.setAttribute("data-clear", "");
134
+ await flush();
135
+ expect(input.hasAttribute("value")).toBe(false);
136
+ expect(input.value).toBe("");
137
+ });
138
+
139
+ it("`preserve` leaves both the attribute and the live value alone", async () => {
140
+ const { root } = mount(
141
+ `<section><input value="keep"></section>`,
142
+ `section[data-go] input { value: preserve; }`,
143
+ );
144
+ await flush();
145
+ const input = root.querySelector("input")!;
146
+ userTypes(input, "typed");
147
+ root.querySelector("section")!.setAttribute("data-go", "");
148
+ await flush();
149
+ expect(input.getAttribute("value")).toBe("keep");
150
+ expect(input.value).toBe("typed");
151
+ });
152
+
153
+ it("re-syncs on a reactive re-run driven by a $binding", async () => {
154
+ const { root } = mount(
155
+ `<section data-n="1"><input></section>`,
156
+ `section { $n: attr("data-n"); input { value: $n * 2; } }`,
157
+ );
158
+ await flush();
159
+ const input = root.querySelector("input")!;
160
+ expect(input.value).toBe("2");
161
+ userTypes(input, "999");
162
+ root.querySelector("section")!.setAttribute("data-n", "5");
163
+ await flush();
164
+ expect(input.getAttribute("value")).toBe("10");
165
+ expect(input.value).toBe("10");
166
+ });
167
+
168
+ it("skips file inputs (read-only value) but still writes the attribute", async () => {
169
+ const { root } = mount(`<input type="file">`, `input { value: "x"; }`);
170
+ await flush();
171
+ const input = root.querySelector("input")!;
172
+ expect(input.getAttribute("value")).toBe("x");
173
+ expect(input.value).toBe("");
174
+ });
175
+
176
+ it("keeps `defaultValue` equal to the written attribute", async () => {
177
+ const { root } = mount(`<input>`, `input { value: "d"; }`);
178
+ await flush();
179
+ const input = root.querySelector("input")!;
180
+ userTypes(input, "typed");
181
+ expect(input.defaultValue).toBe("d");
182
+ expect(input.getAttribute("value")).toBe("d");
183
+ });
184
+
185
+ it("does not read the live value back: typing never re-runs a rule", async () => {
186
+ const { root } = mount(
187
+ `<input value="a">`,
188
+ `input { data-mirror: attr("value"); }`,
189
+ );
190
+ await flush();
191
+ const input = root.querySelector("input")!;
192
+ expect(input.getAttribute("data-mirror")).toBe("a");
193
+ userTypes(input, "b");
194
+ await flush();
195
+ expect(input.getAttribute("data-mirror")).toBe("a");
196
+ expect(input.getAttribute("value")).toBe("a");
197
+ expect(input.value).toBe("b");
198
+ });
199
+ });
200
+
201
+ describe("input checked", () => {
202
+ it("checks a checkbox through the attribute", async () => {
203
+ const { root } = mount(
204
+ `<input type="checkbox">`,
205
+ `input { checked: true; }`,
206
+ );
207
+ await flush();
208
+ const box = root.querySelector("input")!;
209
+ expect(box.hasAttribute("checked")).toBe(true);
210
+ expect(box.checked).toBe(true);
211
+ });
212
+
213
+ it("re-checks a box the user unchecked when the rule writes again", async () => {
214
+ const { root } = mount(
215
+ `<section data-on><input type="checkbox"></section>`,
216
+ `section[data-on] input { checked: ""; }`,
217
+ );
218
+ await flush();
219
+ const box = root.querySelector("input")!;
220
+ expect(box.checked).toBe(true);
221
+ userToggles(box, false);
222
+ expect(box.checked).toBe(false);
223
+ expect(box.hasAttribute("checked")).toBe(true);
224
+
225
+ const section = root.querySelector("section")!;
226
+ section.removeAttribute("data-on");
227
+ await flush();
228
+ section.setAttribute("data-on", "");
229
+ await flush();
230
+ expect(box.checked).toBe(true);
231
+ });
232
+
233
+ it("`false` / `none` remove the attribute and uncheck", async () => {
234
+ const { root } = mount(
235
+ `<section><input type="checkbox" checked><input type="checkbox" checked></section>`,
236
+ `section[data-off] input:first-child { checked: false; }
237
+ section[data-off] input:last-child { checked: none; }`,
238
+ );
239
+ await flush();
240
+ const [a, b] = Array.from(root.querySelectorAll("input"));
241
+ expect(a.checked).toBe(true);
242
+ root.querySelector("section")!.setAttribute("data-off", "");
243
+ await flush();
244
+ expect(a.hasAttribute("checked")).toBe(false);
245
+ expect(a.checked).toBe(false);
246
+ expect(b.hasAttribute("checked")).toBe(false);
247
+ expect(b.checked).toBe(false);
248
+ });
249
+
250
+ it("selects a radio through the attribute", async () => {
251
+ // (a browser also unchecks the group mates live; happy-dom does not
252
+ // model radio groups, so only the written radio is asserted here)
253
+ const { root } = mount(
254
+ `<form><input type="radio" name="r" value="1" checked><input type="radio" name="r" value="2"></form>`,
255
+ `input[value="2"] { checked: ""; }`,
256
+ );
257
+ await flush();
258
+ const [one, two] = Array.from(root.querySelectorAll("input"));
259
+ expect(two.hasAttribute("checked")).toBe(true);
260
+ expect(two.checked).toBe(true);
261
+ // the attribute on the first radio is not Quark's to remove
262
+ expect(one.hasAttribute("checked")).toBe(true);
263
+ });
264
+
265
+ it("tracks a data-driven checked state across re-runs", async () => {
266
+ const { root } = mount(
267
+ `<section data-done="true"><input type="checkbox"></section>`,
268
+ `section { $done: attr("data-done") == "true"; input { checked: $done; } }`,
269
+ );
270
+ await flush();
271
+ const box = root.querySelector("input")!;
272
+ expect(box.checked).toBe(true);
273
+ userToggles(box, false);
274
+ root.querySelector("section")!.setAttribute("data-done", "false");
275
+ await flush();
276
+ expect(box.hasAttribute("checked")).toBe(false);
277
+ expect(box.checked).toBe(false);
278
+ root.querySelector("section")!.setAttribute("data-done", "true");
279
+ await flush();
280
+ expect(box.checked).toBe(true);
281
+ });
282
+ });
283
+
284
+ describe("option selected", () => {
285
+ it("selects an option through the attribute so the select's value follows", async () => {
286
+ const { root } = mount(
287
+ `<select><option value="a">A</option><option value="b">B</option></select>`,
288
+ `option[value="b"] { selected: ""; }`,
289
+ );
290
+ await flush();
291
+ const select = root.querySelector("select")!;
292
+ expect(select.value).toBe("b");
293
+ expect(root.querySelector('option[value="b"]')!.hasAttribute("selected")).toBe(true);
294
+ });
295
+
296
+ it("re-selects after the user picked another option", async () => {
297
+ const { root } = mount(
298
+ `<section data-pick="b"><select><option value="a">A</option><option value="b">B</option></select></section>`,
299
+ `section[data-pick="b"] option[value="b"] { selected: ""; }
300
+ section[data-pick="a"] option[value="a"] { selected: ""; }`,
301
+ );
302
+ await flush();
303
+ const select = root.querySelector("select")!;
304
+ const [a, b] = Array.from(root.querySelectorAll("option"));
305
+ expect(select.value).toBe("b");
306
+ a.selected = true;
307
+ select.dispatchEvent(new Event("change", { bubbles: true }));
308
+ expect(select.value).toBe("a");
309
+
310
+ const section = root.querySelector("section")!;
311
+ section.setAttribute("data-pick", "a");
312
+ await flush();
313
+ section.setAttribute("data-pick", "b");
314
+ await flush();
315
+ expect(b.selected).toBe(true);
316
+ expect(select.value).toBe("b");
317
+ });
318
+
319
+ it("`none` deselects", async () => {
320
+ const { root } = mount(
321
+ `<section><select><option value="a">A</option><option value="b" selected>B</option></select></section>`,
322
+ `section[data-reset] option { selected: none; }`,
323
+ );
324
+ await flush();
325
+ const [, b] = Array.from(root.querySelectorAll("option"));
326
+ expect(b.selected).toBe(true);
327
+ root.querySelector("section")!.setAttribute("data-reset", "");
328
+ await flush();
329
+ expect(b.hasAttribute("selected")).toBe(false);
330
+ expect(b.selected).toBe(false);
331
+ });
332
+ });
333
+
334
+ describe("textarea content", () => {
335
+ it("sets the default text and the live value", async () => {
336
+ const { root } = mount(`<textarea></textarea>`, `textarea { content: "draft"; }`);
337
+ await flush();
338
+ const area = root.querySelector("textarea")!;
339
+ expect(area.textContent).toBe("draft");
340
+ expect(area.defaultValue).toBe("draft");
341
+ expect(area.value).toBe("draft");
342
+ });
343
+
344
+ it("overrides text the user typed when the rule writes again", async () => {
345
+ const { root } = mount(
346
+ `<section data-v="one"><textarea></textarea></section>`,
347
+ `section { $v: attr("data-v"); textarea { content: $v; } }`,
348
+ );
349
+ await flush();
350
+ const area = root.querySelector("textarea")!;
351
+ userTypes(area, "typed");
352
+ root.querySelector("section")!.setAttribute("data-v", "two");
353
+ await flush();
354
+ expect(area.textContent).toBe("two");
355
+ expect(area.value).toBe("two");
356
+ });
357
+
358
+ it("restores the live value even when the text already matches (skipped paint)", async () => {
359
+ const { root } = mount(
360
+ `<section data-go><textarea>same</textarea></section>`,
361
+ `section[data-go] textarea { content: "same"; }`,
362
+ );
363
+ await flush();
364
+ const area = root.querySelector("textarea")!;
365
+ userTypes(area, "edited");
366
+ const section = root.querySelector("section")!;
367
+ section.removeAttribute("data-go");
368
+ await flush();
369
+ section.setAttribute("data-go", "");
370
+ await flush();
371
+ expect(area.textContent).toBe("same");
372
+ expect(area.value).toBe("same");
373
+ });
374
+
375
+ it("`none` clears both", async () => {
376
+ const { root } = mount(
377
+ `<section><textarea>seed</textarea></section>`,
378
+ `section[data-clear] textarea { content: none; }`,
379
+ );
380
+ await flush();
381
+ const area = root.querySelector("textarea")!;
382
+ userTypes(area, "typed");
383
+ root.querySelector("section")!.setAttribute("data-clear", "");
384
+ await flush();
385
+ expect(area.textContent).toBe("");
386
+ expect(area.value).toBe("");
387
+ });
388
+
389
+ it("leaves a non-control's text paint alone (no .value expando)", async () => {
390
+ const { root } = mount(`<p></p>`, `p { content: "text"; }`);
391
+ await flush();
392
+ const p = root.querySelector("p")!;
393
+ expect(p.textContent).toBe("text");
394
+ expect("value" in p).toBe(false);
395
+ });
396
+ });
397
+
398
+ describe("custom elements and other natives", () => {
399
+ it("writes only the attribute on a custom element with a `value` attribute", async () => {
400
+ const { root } = mount(`<my-field></my-field>`, `my-field { value: "v"; }`);
401
+ await flush();
402
+ const el = root.querySelector("my-field")!;
403
+ expect(el.getAttribute("value")).toBe("v");
404
+ expect("value" in el).toBe(false);
405
+ expect(Object.prototype.hasOwnProperty.call(el, "value")).toBe(false);
406
+ });
407
+
408
+ it("writes only the attribute on <select> (use `selected` on its options)", async () => {
409
+ const { root } = mount(
410
+ `<select><option value="a">A</option><option value="b">B</option></select>`,
411
+ `select { value: "b"; }`,
412
+ );
413
+ await flush();
414
+ const select = root.querySelector("select")!;
415
+ expect(select.getAttribute("value")).toBe("b");
416
+ expect(select.value).toBe("a");
417
+ });
418
+
419
+ it("treats other input attributes as plain attributes", async () => {
420
+ const { root } = mount(`<input>`, `input { placeholder: "hint"; min: 3; }`);
421
+ await flush();
422
+ const input = root.querySelector("input")!;
423
+ expect(input.getAttribute("placeholder")).toBe("hint");
424
+ expect(input.getAttribute("min")).toBe("3");
425
+ expect(input.value).toBe("");
426
+ });
427
+ });
428
+
429
+ describe("registration timing", () => {
430
+ it("wins over a value typed before the sheet registered", async () => {
431
+ const { root, register } = createSheet(`<input value="0">`, `input { value: "7"; }`);
432
+ const input = root.querySelector("input")!;
433
+ userTypes(input, "typed-before");
434
+ register();
435
+ await flush();
436
+ expect(input.getAttribute("value")).toBe("7");
437
+ expect(input.value).toBe("7");
438
+ });
439
+
440
+ it("does not revert on unregister (no rule reversion)", async () => {
441
+ const { root, quark } = mount(`<input>`, `input { value: "kept"; }`);
442
+ await flush();
443
+ quark.unregister();
444
+ const input = root.querySelector("input")!;
445
+ expect(input.value).toBe("kept");
446
+ expect(input.getAttribute("value")).toBe("kept");
447
+ });
448
+ });
449
+ });
450
+
451
+ // keep the import used even if a future refactor drops the direct assertions
452
+ void Quark;