@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,496 @@
1
+ /**
2
+ * Quark side of the Nucleus DevTools probe. Mirrors
3
+ * `neutron/support/tests/devtools-hook.test.ts`: install a hook, drive a
4
+ * sheet, assert the publications and the renderer's `inspect()` snapshot.
5
+ */
6
+ import { Quark } from "../../index";
7
+ import type { QuarkRenderer } from "../../src/devtools-hook";
8
+ import {
9
+ type DevtoolsHook,
10
+ type DevtoolsRenderer,
11
+ NUCLEUS_DEVTOOLS_HOOK_KEY,
12
+ pathMatches,
13
+ type PublicizeMeta,
14
+ type PublicizePath,
15
+ } from "@excom/kit-devtools";
16
+ import {
17
+ afterEach,
18
+ describe,
19
+ expect,
20
+ it,
21
+ vi,
22
+ } from "@excom/heft-rig/profiles/default/config/test-utils";
23
+ import { createSheet, flush } from "./helpers";
24
+
25
+ type Publication = { path: PublicizePath; meta: PublicizeMeta };
26
+
27
+ describe("Quark DevTools hook", () => {
28
+ const publications: Publication[] = [];
29
+ const injected: DevtoolsRenderer[] = [];
30
+
31
+ const ofPath = (...tokens: string[]) =>
32
+ publications.filter((p) => pathMatches(p.path, tokens));
33
+
34
+ const quarkRenderer = () =>
35
+ injected.find((r) => r.kind === "quark") as QuarkRenderer | undefined;
36
+
37
+ const installHook = () => {
38
+ const hook: DevtoolsHook = {
39
+ version: 1,
40
+ inject: (renderer) => {
41
+ injected.push(renderer);
42
+ },
43
+ publicize: (path, meta) => {
44
+ publications.push({ path, meta });
45
+ },
46
+ };
47
+ Quark.attachDevtools(hook);
48
+ return hook;
49
+ };
50
+
51
+ afterEach(() => {
52
+ document.body.innerHTML = "";
53
+ delete (globalThis as any)[NUCLEUS_DEVTOOLS_HOOK_KEY];
54
+ publications.length = 0;
55
+ injected.length = 0;
56
+ vi.restoreAllMocks();
57
+ });
58
+
59
+ it("is dormant when no hook is installed", async () => {
60
+ const { quark, register } = createSheet(
61
+ `<p bind-x></p>`,
62
+ `[bind-x] { content: "hi"; }`,
63
+ );
64
+ register();
65
+ await flush();
66
+ expect(publications).toHaveLength(0);
67
+ quark.unregister();
68
+ });
69
+
70
+ it("injects the quark renderer on attach", () => {
71
+ installHook();
72
+ const renderer = quarkRenderer();
73
+ expect(renderer).toBeDefined();
74
+ expect(renderer!.version).toBe(1);
75
+ expect(renderer!.kind).toBe("quark");
76
+ });
77
+
78
+ it("publicizes sheet registered / unregistered on the host", async () => {
79
+ installHook();
80
+ const { root, quark, register } = createSheet(
81
+ `<p bind-x></p>`,
82
+ `[bind-x] { content: "hi"; }`,
83
+ );
84
+ register();
85
+ await flush();
86
+
87
+ const registered = ofPath("quark", "sheet", "registered");
88
+ expect(registered).toHaveLength(1);
89
+ expect(registered[0].meta.tag).toBe("section");
90
+ expect(registered[0].meta.sheetId).toBe(quark.id);
91
+ expect(registered[0].meta.ruleCount).toBe(1);
92
+ expect(registered[0].meta.isScoped).toBe(true);
93
+ expect((registered[0].meta.weakElement as WeakRef<Element>).deref()).toBe(
94
+ root,
95
+ );
96
+
97
+ quark.unregister();
98
+ const unregistered = ofPath("quark", "sheet", "unregistered");
99
+ expect(unregistered).toHaveLength(1);
100
+ expect(unregistered[0].meta.sheetId).toBe(quark.id);
101
+ });
102
+
103
+ it("publicizes one apply per property with selector, key, expression and result", async () => {
104
+ installHook();
105
+ const { root, quark, register } = createSheet(
106
+ `<article><p bind-x></p></article>`,
107
+ `article {
108
+ $greeting: "hi";
109
+ p[bind-x] { content: $greeting; data-kind: "greeting"; }
110
+ }`,
111
+ );
112
+ register();
113
+ await flush();
114
+
115
+ const applies = ofPath("quark", "apply");
116
+ const p = root.querySelector("p")!;
117
+ const article = root.querySelector("article")!;
118
+
119
+ const variable = applies.find((a) => a.meta.key === "$greeting")!;
120
+ expect(variable).toBeDefined();
121
+ expect(variable.meta.kind).toBe("variable");
122
+ expect(variable.meta.selector).toBe("article");
123
+ expect(variable.meta.expression).toBe('"hi"');
124
+ expect(variable.meta.result).toBe("hi");
125
+ expect(variable.meta.tag).toBe("article");
126
+ expect(variable.meta.isFirstRun).toBe(true);
127
+ expect((variable.meta.weakElement as WeakRef<Element>).deref()).toBe(
128
+ article,
129
+ );
130
+
131
+ const content = applies.find((a) => a.meta.key === "content")!;
132
+ expect(content.meta.kind).toBe("content");
133
+ expect(content.meta.selector).toBe("article p[bind-x]");
134
+ expect(content.meta.result).toBe("hi");
135
+ expect((content.meta.weakElement as WeakRef<Element>).deref()).toBe(p);
136
+
137
+ const attribute = applies.find((a) => a.meta.key === "data-kind")!;
138
+ expect(attribute.meta.kind).toBe("attribute");
139
+ expect(attribute.meta.result).toBe("greeting");
140
+ expect(attribute.meta.isWipe).toBe(false);
141
+ expect(attribute.meta.isNoop).toBe(false);
142
+ expect(typeof attribute.meta.sheetId).toBe("number");
143
+ expect(typeof attribute.meta.ruleId).toBe("number");
144
+ expect(typeof attribute.meta.runId).toBe("string");
145
+ quark.unregister();
146
+ });
147
+
148
+ it("marks preserve as no-op and none as wipe", async () => {
149
+ installHook();
150
+ const { quark, register } = createSheet(
151
+ `<p bind-x>stale</p>`,
152
+ `[bind-x] { data-a: preserve; data-b: none; }`,
153
+ );
154
+ register();
155
+ await flush();
156
+ const a = ofPath("quark", "apply").find((p) => p.meta.key === "data-a")!;
157
+ const b = ofPath("quark", "apply").find((p) => p.meta.key === "data-b")!;
158
+ expect(a.meta.isNoop).toBe(true);
159
+ expect(a.meta.result).toBeUndefined();
160
+ expect(b.meta.isWipe).toBe(true);
161
+ expect(b.meta.result).toBeNull();
162
+ quark.unregister();
163
+ });
164
+
165
+ it("publicizes listener applies with functions tagged, not retained", async () => {
166
+ installHook();
167
+ const handler = () => {};
168
+ const { quark, register } = createSheet(
169
+ `<button bind-x></button>`,
170
+ `[bind-x] { @on click (handle: handler); }`,
171
+ { handler },
172
+ );
173
+ register();
174
+ await flush();
175
+ const listener = ofPath("quark", "apply").find(
176
+ (p) => p.meta.key === "@on click (handle: handler)",
177
+ )!;
178
+ expect(listener.meta.kind).toBe("listener");
179
+ expect(listener.meta.result).toEqual([{ $constructor: "Function" }]);
180
+ quark.unregister();
181
+ });
182
+
183
+ it("publicizes an error when an expression fails", async () => {
184
+ installHook();
185
+ vi.spyOn(console, "error").mockImplementation(() => {});
186
+ const { quark, register } = createSheet(
187
+ `<p bind-x></p>`,
188
+ `[bind-x] { content: "x".splice(0); }`,
189
+ );
190
+ register();
191
+ await flush();
192
+ const errors = ofPath("quark", "error");
193
+ expect(errors).toHaveLength(1);
194
+ expect(errors[0].meta.key).toBe("content");
195
+ expect(errors[0].meta.selector).toBe("[bind-x]");
196
+ expect(errors[0].meta.expression).toBe('"x".splice(0)');
197
+ expect(typeof errors[0].meta.errorMessage).toBe("string");
198
+ // a failed expression still records an apply as a no-op
199
+ const apply = ofPath("quark", "apply").find((p) => p.meta.key === "content")!;
200
+ expect(apply.meta.isNoop).toBe(true);
201
+ quark.unregister();
202
+ });
203
+
204
+ it("publishes content results as the author meant them, never as render plumbing", async () => {
205
+ installHook();
206
+ const { quark, register } = createSheet(
207
+ `<section>
208
+ <template id="t"><span id="from-template">hi</span></template>
209
+ <div bind-tpl></div>
210
+ <div bind-html></div>
211
+ <ul bind-list><template><li class="row"></li></template></ul>
212
+ <p bind-text></p>
213
+ </section>`,
214
+ `[bind-tpl] { content: template("#t"); }
215
+ [bind-html] { content: dangerous-html("<b>bold</b>"); }
216
+ [bind-list] { $items: items; content: iterate($items, none, "id"); }
217
+ [bind-text] { content: "plain"; }`,
218
+ { items: [{ id: "a" }, { id: "b" }] },
219
+ );
220
+ register();
221
+ await flush();
222
+ await flush();
223
+ const content = (selector: string) =>
224
+ ofPath("quark", "apply").find(
225
+ (p) => p.meta.key === "content" && p.meta.selector === selector,
226
+ )!;
227
+ // template(): the promised { type: "nodes", value: [fragment] } → the nodes
228
+ expect(content("[bind-tpl]").meta.result).toEqual([
229
+ { $element: "span", id: "from-template" },
230
+ ]);
231
+ // dangerous-html(): the sync { type: "html", value } → the string
232
+ expect(content("[bind-html]").meta.result).toBe("<b>bold</b>");
233
+ // iterate(): the promised rows → the row elements
234
+ expect(content("[bind-list]").meta.result).toEqual([
235
+ { $element: "li", id: null },
236
+ { $element: "li", id: null },
237
+ ]);
238
+ expect(content("[bind-text]").meta.result).toBe("plain");
239
+ for (const p of ofPath("quark", "apply")) {
240
+ expect(JSON.stringify(p.meta.result)).not.toContain("Promise");
241
+ }
242
+ quark.unregister();
243
+ });
244
+
245
+ it("inspect() snapshots vars, attributes, style props, listeners", async () => {
246
+ installHook();
247
+ const handler = () => {};
248
+ const { root, quark, register } = createSheet(
249
+ `<article><p bind-x></p></article>`,
250
+ `article {
251
+ $count: 3;
252
+ $box: "open";
253
+ $user: user;
254
+ @on click (handle: handler);
255
+ data-count: $count;
256
+ --tone: "warm";
257
+ p[bind-x] { content: $user.name; }
258
+ }`,
259
+ { handler, user: { name: "Ada" } },
260
+ );
261
+ register();
262
+ await flush();
263
+
264
+ const renderer = quarkRenderer()!;
265
+ const article = root.querySelector("article")!;
266
+ const p = root.querySelector("p")!;
267
+
268
+ expect(renderer.isQuarkElement(article)).toBe(true);
269
+ expect(renderer.inspect(document.createElement("div"))).toBeNull();
270
+
271
+ const snap = renderer.inspect(article)!;
272
+ expect(snap.tag).toBe("article");
273
+ expect(snap.vars).toEqual({
274
+ $count: 3,
275
+ $box: "open",
276
+ $user: { name: "Ada" },
277
+ });
278
+ expect(snap.attributes).toEqual({ "data-count": "3" });
279
+ expect(snap.styleProperties).toEqual({ "--tone": "warm" });
280
+ expect(snap.listeners).toEqual({
281
+ "click (handle: handler)": [
282
+ { $constructor: "Function", $expression: "handler" },
283
+ ],
284
+ });
285
+ expect(snap.loop).toBeNull();
286
+ expect(snap.sheets).toEqual([quark.hash]);
287
+
288
+ const child = renderer.inspect(p)!;
289
+ expect(child.vars).toEqual({});
290
+ // content is tracked internally but is not an attribute
291
+ expect(child.attributes).toEqual({});
292
+ quark.unregister();
293
+ });
294
+
295
+ it("inspect() exposes iterate() row context", async () => {
296
+ installHook();
297
+ const { root, quark, register } = createSheet(
298
+ `<ul><template><li></li></template></ul>`,
299
+ `ul { $items: items; content: iterate($items, none, "id"); }`,
300
+ { items: [{ id: "a" }, { id: "b" }] },
301
+ );
302
+ register();
303
+ await flush();
304
+ await flush();
305
+ const rows = [...root.querySelectorAll("li")];
306
+ expect(rows).toHaveLength(2);
307
+ const snap = quarkRenderer()!.inspect(rows[1])!;
308
+ expect(snap.loop).toEqual({ index: 1, key: "b" });
309
+ quark.unregister();
310
+ });
311
+
312
+ it("inspect() reports partial loop context and unreadable vars", async () => {
313
+ installHook();
314
+ const { getQuarkInternal } = await import("../../src/quark-internal");
315
+ const el = document.createElement("li");
316
+ const internal = getQuarkInternal(el);
317
+ internal.setLoopIndex(2);
318
+ expect(quarkRenderer()!.inspect(el)!.loop).toEqual({ index: 2, key: null });
319
+ internal.setLoopIndex(undefined);
320
+ internal.setLoopKey("k");
321
+ expect(quarkRenderer()!.inspect(el)!.loop).toEqual({ index: null, key: "k" });
322
+ // a stored value whose read throws is reported, not propagated
323
+ const broken: unknown[] = ["hash"];
324
+ Object.defineProperty(broken, 1, {
325
+ get() {
326
+ throw new Error("boom");
327
+ },
328
+ });
329
+ internal.vars.$broken = broken;
330
+ const snap = quarkRenderer()!.inspect(el)!;
331
+ expect(snap.vars).toEqual({ $broken: "[unreadable]" });
332
+ });
333
+
334
+ it("inspect() drops removed listeners, empty style values and unknown handler sources", async () => {
335
+ installHook();
336
+ const handler = () => {};
337
+ const { root, quark, register } = createSheet(
338
+ `<button bind-x></button>`,
339
+ `[bind-x] { @on click (handle: handler); --tone: ""; }`,
340
+ { handler },
341
+ );
342
+ register();
343
+ await flush();
344
+ const button = root.querySelector("button")!;
345
+ const renderer = quarkRenderer()!;
346
+ expect(renderer.inspect(button)!.styleProperties).toEqual({ "--tone": null });
347
+ expect(renderer.inspect(button)!.listeners).toEqual({
348
+ "click (handle: handler)": [
349
+ { $constructor: "Function", $expression: "handler" },
350
+ ],
351
+ });
352
+ // removing the slot empties the rule's array
353
+ const { getQuarkInternal } = await import("../../src/quark-internal");
354
+ const internal = getQuarkInternal(button);
355
+ const ruleId = quark.rules[0].id;
356
+ const slot = "@on click (handle: handler)";
357
+ internal.removeAllListeners(quark.hash, ruleId, slot);
358
+ expect(internal.getListeners(quark.hash, ruleId, slot)).toEqual([]);
359
+ expect(renderer.inspect(button)!.listeners).toEqual({});
360
+ // re-attach, then unregister: the rule can no longer be looked up
361
+ internal.setOrderedListeners(quark.hash, ruleId, slot, [handler], {
362
+ eventTypes: ["click"],
363
+ target: button,
364
+ options: {},
365
+ });
366
+ quark.unregister();
367
+ expect(renderer.inspect(button)!.listeners).toEqual({
368
+ "click (handle: handler)": [{ $constructor: "Function", $expression: null }],
369
+ });
370
+ });
371
+
372
+ describe("agent tooling", () => {
373
+ it("sheets() lists registered sheets with their rules and declarations", async () => {
374
+ installHook();
375
+ const { root, quark, register } = createSheet(
376
+ `<p bind-x></p>`,
377
+ `[bind-x] { $n: 1; content: "hi"; @on click (handle: log); [bind-y] { title: "t"; } }`,
378
+ { log: () => {} },
379
+ );
380
+ register();
381
+ await flush();
382
+ const sheet = quarkRenderer()!
383
+ .sheets()
384
+ .find((s) => s.sheetId === quark.id)!;
385
+ expect(sheet).toMatchObject({
386
+ hash: quark.hash,
387
+ host: root,
388
+ isScoped: true,
389
+ isRegistered: true,
390
+ ruleCount: 2,
391
+ src: quark.src,
392
+ });
393
+ expect(sheet.scopeId).toBe(quark.scopeId);
394
+ expect(sheet.rules[0]).toMatchObject({
395
+ sheetId: quark.id,
396
+ ruleId: quark.rules[0].id,
397
+ selector: "[bind-x]",
398
+ isScoped: true,
399
+ declarations: [
400
+ { key: "$n", value: "1" },
401
+ { key: "content", value: '"hi"' },
402
+ ],
403
+ listeners: [
404
+ {
405
+ key: "@on click (handle: log)",
406
+ events: ["click"],
407
+ handlers: "log",
408
+ hasBlock: false,
409
+ },
410
+ ],
411
+ });
412
+ expect(sheet.rules[0].numberOfRuns).toBeGreaterThan(0);
413
+ expect(sheet.rules[1].selector).toBe("[bind-x] [bind-y]");
414
+ quark.unregister();
415
+ expect(
416
+ quarkRenderer()!
417
+ .sheets()
418
+ .find((s) => s.sheetId === quark.id),
419
+ ).toBeUndefined();
420
+ });
421
+
422
+ it("sheets() reports block listeners and unregistered-but-alive sheets", async () => {
423
+ installHook();
424
+ const { quark, register } = createSheet(
425
+ `<button bind-x></button>`,
426
+ `[bind-x] { @on click { data-hit: "1"; } }`,
427
+ );
428
+ register();
429
+ await flush();
430
+ const sheet = quarkRenderer()!
431
+ .sheets()
432
+ .find((s) => s.sheetId === quark.id)!;
433
+ expect(sheet.rules[0].listeners).toEqual([
434
+ { key: "@on click", events: ["click"], handlers: "", hasBlock: true },
435
+ ]);
436
+ quark.unregister();
437
+ });
438
+
439
+ it("matchingRules() returns the rules matching an element, scope-aware, in order", async () => {
440
+ installHook();
441
+ const a = createSheet(
442
+ `<p bind-x class="one"></p><p bind-x class="two"></p>`,
443
+ `[bind-x] { content: "a"; } .two { content: "b"; } :scope { $root: 1; }`,
444
+ );
445
+ a.register();
446
+ const b = createSheet(
447
+ `<p bind-x class="three"></p>`,
448
+ `[bind-x] { content: "c"; }`,
449
+ );
450
+ b.register();
451
+ await flush();
452
+ const renderer = quarkRenderer()!;
453
+ const one = a.root.querySelector(".one")!;
454
+ const two = a.root.querySelector(".two")!;
455
+ const three = b.root.querySelector(".three")!;
456
+ expect(renderer.matchingRules(one).map((r) => r.selector)).toEqual(["[bind-x]"]);
457
+ expect(renderer.matchingRules(two).map((r) => [r.sheetId, r.selector])).toEqual([
458
+ [a.quark.id, "[bind-x]"],
459
+ [a.quark.id, ".two"],
460
+ ]);
461
+ // sheet b is host-scoped: its rule never reaches sheet a's elements
462
+ expect(renderer.matchingRules(three).map((r) => r.sheetId)).toEqual([b.quark.id]);
463
+ // `:scope` rules match the host itself
464
+ expect(renderer.matchingRules(a.root).map((r) => r.selector)).toEqual([":scope"]);
465
+ expect(renderer.matchingRules(document.body)).toEqual([]);
466
+ a.quark.unregister();
467
+ b.quark.unregister();
468
+ expect(renderer.matchingRules(one)).toEqual([]);
469
+ });
470
+
471
+ it("evaluate() resolves bindings, attr(), prop() and @use modules on an element", async () => {
472
+ installHook();
473
+ const { root, quark, register } = createSheet(
474
+ `<p bind-x data-n="3"></p>`,
475
+ `[bind-x] { $n: 2; }`,
476
+ { double: (n: number) => n * 2 },
477
+ );
478
+ register();
479
+ await flush();
480
+ const el = root.querySelector("p")!;
481
+ (el as any).provision = { ok: true };
482
+ const renderer = quarkRenderer()!;
483
+ expect(renderer.evaluate(el, "$n + 1")).toBe(3);
484
+ expect(renderer.evaluate(el, 'attr("data-n")')).toBe("3");
485
+ expect(renderer.evaluate(el, 'prop("provision").ok')).toBe(true);
486
+ expect(renderer.evaluate(el, "double($n)", quark.id)).toBe(4);
487
+ expect(renderer.evaluate(el, '"n is #{$n}"')).toBe("n is 2");
488
+ // an unknown sheet id still evaluates, without modules
489
+ expect(() => renderer.evaluate(el, "double($n)", -1)).toThrow(/double/);
490
+ expect(() => renderer.evaluate(el, "nope(")).toThrow();
491
+ quark.unregister();
492
+ // no sheets at all: bindings and attr() still work, modules do not
493
+ expect(renderer.evaluate(el, 'attr("data-n")')).toBe("3");
494
+ });
495
+ });
496
+ });