@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.
- package/.rush/temp/chunked-rush-logs/quark.apply-exports.chunks.jsonl +1 -0
- package/.rush/temp/chunked-rush-logs/quark.build_package-metas.chunks.jsonl +1 -0
- package/.rush/temp/operation/apply-exports/all.log +1 -0
- package/.rush/temp/operation/apply-exports/log-chunks.jsonl +1 -0
- package/.rush/temp/operation/apply-exports/state.json +3 -0
- package/.rush/temp/operation/build_package-metas/all.log +1 -0
- package/.rush/temp/operation/build_package-metas/log-chunks.jsonl +1 -0
- package/.rush/temp/operation/build_package-metas/state.json +3 -0
- package/.rush/temp/shrinkwrap-deps.json +3 -0
- package/config/rig.json +6 -0
- package/index.ts +4 -0
- package/language.ts +7 -0
- package/package.json +45 -0
- package/rush-logs/quark.apply-exports.cache.log +1 -0
- package/rush-logs/quark.apply-exports.log +1 -0
- package/rush-logs/quark.build_package-metas.cache.log +1 -0
- package/rush-logs/quark.build_package-metas.log +1 -0
- package/src/actions.ts +445 -0
- package/src/ast.ts +187 -0
- package/src/bindings.ts +115 -0
- package/src/builtin-modules.ts +401 -0
- package/src/constants.ts +47 -0
- package/src/cycle-check.ts +121 -0
- package/src/devtools-hook.ts +303 -0
- package/src/element-api.ts +109 -0
- package/src/evaluator.ts +317 -0
- package/src/form-controls.ts +69 -0
- package/src/language-tables.ts +137 -0
- package/src/language.ts +1225 -0
- package/src/observer.ts +110 -0
- package/src/paint.ts +305 -0
- package/src/properties.ts +818 -0
- package/src/props.ts +94 -0
- package/src/quark-element.ts +7 -0
- package/src/quark-internal.ts +365 -0
- package/src/quark.ts +817 -0
- package/src/resolvers.ts +867 -0
- package/src/rule.ts +800 -0
- package/src/scope-id.ts +47 -0
- package/src/selector-utils.ts +430 -0
- package/src/settle.ts +117 -0
- package/src/types.ts +155 -0
- package/src/utils.ts +79 -0
- package/src/variables.ts +359 -0
- package/support/demos/css-variables.html +18 -0
- package/support/demos/dispatch.html +20 -0
- package/support/demos/events.html +9 -0
- package/support/demos/iterate.html +13 -0
- package/support/demos/js-api.html +17 -0
- package/support/demos/provider-list.html +16 -0
- package/support/demos/toggle-content.html +14 -0
- package/support/demos/unset.html +12 -0
- package/support/demos/view-transition.html +36 -0
- package/support/docs/ATTRIBUTES.md +37 -0
- package/support/docs/AT_RULES.md +45 -0
- package/support/docs/BUILTINS.md +57 -0
- package/support/docs/CONTENT.md +40 -0
- package/support/docs/CSS_VARIABLES.md +16 -0
- package/support/docs/DECLARATIONS.md +26 -0
- package/support/docs/DELAY.md +37 -0
- package/support/docs/DIAGNOSTICS.md +32 -0
- package/support/docs/DISPATCH.md +81 -0
- package/support/docs/ELEMENT_PROPERTIES.md +34 -0
- package/support/docs/EXPRESSIONS.md +31 -0
- package/support/docs/INTERNAL.md +8 -0
- package/support/docs/JS_API.md +53 -0
- package/support/docs/JS_WRITES.md +36 -0
- package/support/docs/LIMITATIONS.md +26 -0
- package/support/docs/LOOP_GUARD.md +30 -0
- package/support/docs/METHODS.md +43 -0
- package/support/docs/MODULES.md +111 -0
- package/support/docs/NO_REVERSION.md +18 -0
- package/support/docs/ON.md +87 -0
- package/support/docs/REACTIVITY.md +24 -0
- package/support/docs/README.md +143 -0
- package/support/docs/SELECTORS.md +87 -0
- package/support/docs/SHEETS.md +67 -0
- package/support/docs/SYNTAX.md +36 -0
- package/support/docs/USE.md +24 -0
- package/support/docs/VALUES.md +27 -0
- package/support/docs/VARIABLES.md +47 -0
- package/support/docs/VIEW_TRANSITION.md +64 -0
- package/support/docs-sections.json +65 -0
- package/support/package-meta.json +208 -0
- package/support/scripts/build-language-docs.mjs +196 -0
- package/support/tests/__snapshots__/css-variables.view.test.ts.snap +23 -0
- package/support/tests/__snapshots__/dispatch.view.test.ts.snap +23 -0
- package/support/tests/__snapshots__/events.view.test.ts.snap +23 -0
- package/support/tests/__snapshots__/iterate.view.test.ts.snap +23 -0
- package/support/tests/__snapshots__/js-api.view.test.ts.snap +23 -0
- package/support/tests/__snapshots__/provider-list.view.test.ts.snap +23 -0
- package/support/tests/__snapshots__/quark-features.test.ts.snap +1079 -0
- package/support/tests/__snapshots__/quark-with-providers.test.ts.snap +89 -0
- package/support/tests/__snapshots__/quark.test.ts.snap +221 -0
- package/support/tests/__snapshots__/selectors.test.ts.snap +133 -0
- package/support/tests/__snapshots__/toggle-content.view.test.ts.snap +23 -0
- package/support/tests/__snapshots__/unset.view.test.ts.snap +23 -0
- package/support/tests/__snapshots__/view-transition.view.test.ts.snap +23 -0
- package/support/tests/actions.test.ts +437 -0
- package/support/tests/builtin-modules.test.ts +337 -0
- package/support/tests/builtins.test.ts +189 -0
- package/support/tests/css-variables.view.test.ts +35 -0
- package/support/tests/delay.test.ts +244 -0
- package/support/tests/devtools-hook.test.ts +496 -0
- package/support/tests/diagnostics.test.ts +224 -0
- package/support/tests/dispatch.view.test.ts +43 -0
- package/support/tests/element-api.test.ts +202 -0
- package/support/tests/evaluator.test.ts +237 -0
- package/support/tests/events.view.test.ts +40 -0
- package/support/tests/form-controls.test.ts +452 -0
- package/support/tests/helpers.ts +354 -0
- package/support/tests/internals.test.ts +568 -0
- package/support/tests/iterate.view.test.ts +40 -0
- package/support/tests/js-api.view.test.ts +45 -0
- package/support/tests/language-docs.test.ts +147 -0
- package/support/tests/lifecycle.test.ts +440 -0
- package/support/tests/loop-guard.test.ts +323 -0
- package/support/tests/provider-list.view.test.ts +47 -0
- package/support/tests/quark-features.test.ts +2887 -0
- package/support/tests/quark-with-providers.test.ts +169 -0
- package/support/tests/quark.test.ts +359 -0
- package/support/tests/resolvers.test.ts +346 -0
- package/support/tests/selector-analysis.test.ts +281 -0
- package/support/tests/selectors.test.ts +670 -0
- package/support/tests/toggle-content.view.test.ts +40 -0
- package/support/tests/unset.view.test.ts +42 -0
- package/support/tests/view-helpers.ts +81 -0
- package/support/tests/view-transition.test.ts +1069 -0
- package/support/tests/view-transition.view.test.ts +76 -0
- package/tsconfig.json +5 -0
|
@@ -0,0 +1,670 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Selector features whose dependencies point sideways or upward: logical
|
|
3
|
+
* pseudos with complex args, sibling combinators, `:has()`, structural
|
|
4
|
+
* (position / children) and attribute-backed pseudos, class / id
|
|
5
|
+
* selectors (token-filtered `class` records), and the child-removal
|
|
6
|
+
* observation they need. Each case asserts DOM
|
|
7
|
+
* correctness; the key ones keep a complexity budget as a baseline.
|
|
8
|
+
*
|
|
9
|
+
* happy-dom limits, worked around here: its selector cache goes stale
|
|
10
|
+
* for `:has()` / sibling matches (bypassed below); `:is()` / `:not()`
|
|
11
|
+
* keep only the first compound of a complex arg; `:enabled`,
|
|
12
|
+
* `:required`, `:read-only`, `:any-link`, `:lang()`, `:open` never
|
|
13
|
+
* match. Those are covered by the classifier tests in
|
|
14
|
+
* `selector-analysis.test.ts`.
|
|
15
|
+
*/
|
|
16
|
+
import { Quark } from "../../index";
|
|
17
|
+
import { QuarkLogger } from "../../src/utils";
|
|
18
|
+
import {
|
|
19
|
+
afterAll,
|
|
20
|
+
afterEach,
|
|
21
|
+
beforeAll,
|
|
22
|
+
describe,
|
|
23
|
+
expect,
|
|
24
|
+
fixture,
|
|
25
|
+
it,
|
|
26
|
+
vi,
|
|
27
|
+
} from "@excom/heft-rig/profiles/default/config/test-utils";
|
|
28
|
+
import {
|
|
29
|
+
bypassSelectorCache,
|
|
30
|
+
createSheet,
|
|
31
|
+
expectComplexity,
|
|
32
|
+
flush,
|
|
33
|
+
measureComplexity,
|
|
34
|
+
mount,
|
|
35
|
+
unregisterAll,
|
|
36
|
+
} from "./helpers";
|
|
37
|
+
|
|
38
|
+
const attrsOf = (root: ParentNode, selector: string, attr: string) =>
|
|
39
|
+
[...root.querySelectorAll(selector)].map((el) => el.getAttribute(attr));
|
|
40
|
+
|
|
41
|
+
describe("selector features", () => {
|
|
42
|
+
let restoreSelectorCache: () => void;
|
|
43
|
+
beforeAll(() => {
|
|
44
|
+
restoreSelectorCache = bypassSelectorCache();
|
|
45
|
+
});
|
|
46
|
+
afterAll(() => restoreSelectorCache());
|
|
47
|
+
afterEach(() => {
|
|
48
|
+
unregisterAll();
|
|
49
|
+
document.body.innerHTML = "";
|
|
50
|
+
vi.restoreAllMocks();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe(":is() / :where() / :not()", () => {
|
|
54
|
+
it("observes attributes in simple arguments like the compound's own", async () => {
|
|
55
|
+
const { root } = mount(
|
|
56
|
+
`<ul><li></li><li></li></ul>`,
|
|
57
|
+
`li:is([data-a], [data-b]) { data-hit: ""; }
|
|
58
|
+
li:where(:not([data-a]):not([data-b])) { data-hit: none; }`
|
|
59
|
+
);
|
|
60
|
+
await flush();
|
|
61
|
+
expect(attrsOf(root, "li", "data-hit")).toEqual([null, null]);
|
|
62
|
+
|
|
63
|
+
const [first, second] = root.querySelectorAll("li");
|
|
64
|
+
first.setAttribute("data-a", "");
|
|
65
|
+
second.setAttribute("data-b", "");
|
|
66
|
+
await flush();
|
|
67
|
+
expect(attrsOf(root, "li", "data-hit")).toEqual(["", ""]);
|
|
68
|
+
|
|
69
|
+
first.removeAttribute("data-a");
|
|
70
|
+
await flush();
|
|
71
|
+
expect(attrsOf(root, "li", "data-hit")).toEqual([null, ""]);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("observes :not() arguments on ancestor compounds", async () => {
|
|
75
|
+
const { root, quark, register } = createSheet(
|
|
76
|
+
`<section><ul><li></li><li></li></ul></section>`,
|
|
77
|
+
`section:not([data-off]) li { data-lit: ""; }
|
|
78
|
+
section[data-off] li { data-lit: none; }`
|
|
79
|
+
);
|
|
80
|
+
register();
|
|
81
|
+
await flush();
|
|
82
|
+
expect(attrsOf(root, "li", "data-lit")).toEqual(["", ""]);
|
|
83
|
+
|
|
84
|
+
const meter = measureComplexity(quark);
|
|
85
|
+
root.querySelector("section")!.setAttribute("data-off", "");
|
|
86
|
+
await flush();
|
|
87
|
+
expect(attrsOf(root, "li", "data-lit")).toEqual([null, null]);
|
|
88
|
+
|
|
89
|
+
root.querySelector("section")!.removeAttribute("data-off");
|
|
90
|
+
await flush();
|
|
91
|
+
const budget = meter.take();
|
|
92
|
+
meter.stop();
|
|
93
|
+
expect(attrsOf(root, "li", "data-lit")).toEqual(["", ""]);
|
|
94
|
+
expectComplexity(budget);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
describe("sibling combinators", () => {
|
|
99
|
+
it("~ re-runs the following siblings when the earlier sibling changes", async () => {
|
|
100
|
+
const { root, quark, register } = createSheet(
|
|
101
|
+
`<form><input /><span></span><button></button></form>`,
|
|
102
|
+
`input[data-valid] ~ button { disabled: none; }
|
|
103
|
+
input:not([data-valid]) ~ button { disabled: ""; }`
|
|
104
|
+
);
|
|
105
|
+
register();
|
|
106
|
+
await flush();
|
|
107
|
+
const button = root.querySelector("button")!;
|
|
108
|
+
expect(button.hasAttribute("disabled")).toBe(true);
|
|
109
|
+
|
|
110
|
+
const meter = measureComplexity(quark);
|
|
111
|
+
root.querySelector("input")!.setAttribute("data-valid", "");
|
|
112
|
+
await flush();
|
|
113
|
+
expect(button.hasAttribute("disabled")).toBe(false);
|
|
114
|
+
|
|
115
|
+
root.querySelector("input")!.removeAttribute("data-valid");
|
|
116
|
+
await flush();
|
|
117
|
+
const budget = meter.take();
|
|
118
|
+
meter.stop();
|
|
119
|
+
expect(button.hasAttribute("disabled")).toBe(true);
|
|
120
|
+
expectComplexity(budget);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("+ follows the current marker along a list", async () => {
|
|
124
|
+
const { root } = mount(
|
|
125
|
+
`<ol><li data-current></li><li></li><li></li></ol>`,
|
|
126
|
+
`li[data-current] + li { data-next: ""; }
|
|
127
|
+
li:not([data-current]) + li { data-next: none; }`
|
|
128
|
+
);
|
|
129
|
+
await flush();
|
|
130
|
+
expect(attrsOf(root, "li", "data-next")).toEqual([null, "", null]);
|
|
131
|
+
|
|
132
|
+
const [first, second] = root.querySelectorAll("li");
|
|
133
|
+
first.removeAttribute("data-current");
|
|
134
|
+
second.setAttribute("data-current", "");
|
|
135
|
+
await flush();
|
|
136
|
+
expect(attrsOf(root, "li", "data-next")).toEqual([null, null, ""]);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("re-evaluates adjacency when an element between two siblings is removed or inserted", async () => {
|
|
140
|
+
const { root } = mount(
|
|
141
|
+
`<article><h2></h2><div></div><p></p></article>`,
|
|
142
|
+
`h2 + p { data-lead: ""; }
|
|
143
|
+
:not(h2) + p { data-lead: none; }`
|
|
144
|
+
);
|
|
145
|
+
await flush();
|
|
146
|
+
const p = root.querySelector("p")!;
|
|
147
|
+
expect(p.hasAttribute("data-lead")).toBe(false);
|
|
148
|
+
|
|
149
|
+
root.querySelector("article > div")!.remove();
|
|
150
|
+
await flush();
|
|
151
|
+
expect(p.hasAttribute("data-lead")).toBe(true);
|
|
152
|
+
|
|
153
|
+
root.querySelector("h2")!.after(document.createElement("div"));
|
|
154
|
+
await flush();
|
|
155
|
+
expect(p.hasAttribute("data-lead")).toBe(false);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("reaches subjects below a following sibling", async () => {
|
|
159
|
+
const { root } = mount(
|
|
160
|
+
`<div><nav></nav><main><span></span></main></div>`,
|
|
161
|
+
`nav[data-open] ~ main span { content: "open"; }
|
|
162
|
+
nav:not([data-open]) ~ main span { content: "closed"; }`
|
|
163
|
+
);
|
|
164
|
+
await flush();
|
|
165
|
+
const span = root.querySelector("span")!;
|
|
166
|
+
expect(span.textContent).toBe("closed");
|
|
167
|
+
|
|
168
|
+
root.querySelector("nav")!.setAttribute("data-open", "");
|
|
169
|
+
await flush();
|
|
170
|
+
expect(span.textContent).toBe("open");
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
describe("structural pseudo-classes", () => {
|
|
175
|
+
it("re-positions :first-child / :last-child when a sibling is removed", async () => {
|
|
176
|
+
const { root, quark, register } = createSheet(
|
|
177
|
+
`<ul><li>a</li><li>b</li><li>c</li></ul>`,
|
|
178
|
+
`li:first-child { data-pos: "first"; }
|
|
179
|
+
li:last-child { data-pos: "last"; }
|
|
180
|
+
li:not(:first-child):not(:last-child) { data-pos: "mid"; }`
|
|
181
|
+
);
|
|
182
|
+
register();
|
|
183
|
+
await flush();
|
|
184
|
+
expect(attrsOf(root, "li", "data-pos")).toEqual(["first", "mid", "last"]);
|
|
185
|
+
|
|
186
|
+
const meter = measureComplexity(quark);
|
|
187
|
+
root.querySelector("li")!.remove();
|
|
188
|
+
await flush();
|
|
189
|
+
const budget = meter.take();
|
|
190
|
+
meter.stop();
|
|
191
|
+
expect(attrsOf(root, "li", "data-pos")).toEqual(["first", "last"]);
|
|
192
|
+
expectComplexity(budget);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("re-stripes :nth-child() after a removal and an insertion", async () => {
|
|
196
|
+
const { root } = mount(
|
|
197
|
+
`<ul><li>1</li><li>2</li><li>3</li><li>4</li></ul>`,
|
|
198
|
+
`li:nth-child(odd) { data-stripe: "odd"; }
|
|
199
|
+
li:nth-child(even) { data-stripe: "even"; }`
|
|
200
|
+
);
|
|
201
|
+
await flush();
|
|
202
|
+
expect(attrsOf(root, "li", "data-stripe")).toEqual([
|
|
203
|
+
"odd",
|
|
204
|
+
"even",
|
|
205
|
+
"odd",
|
|
206
|
+
"even",
|
|
207
|
+
]);
|
|
208
|
+
|
|
209
|
+
root.querySelectorAll("li")[1].remove();
|
|
210
|
+
await flush();
|
|
211
|
+
expect(attrsOf(root, "li", "data-stripe")).toEqual([
|
|
212
|
+
"odd",
|
|
213
|
+
"even",
|
|
214
|
+
"odd",
|
|
215
|
+
]);
|
|
216
|
+
|
|
217
|
+
root.querySelector("ul")!.prepend(document.createElement("li"));
|
|
218
|
+
await flush();
|
|
219
|
+
expect(attrsOf(root, "li", "data-stripe")).toEqual([
|
|
220
|
+
"odd",
|
|
221
|
+
"even",
|
|
222
|
+
"odd",
|
|
223
|
+
"even",
|
|
224
|
+
]);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it("observes the attributes of `of S` on the siblings", async () => {
|
|
228
|
+
const { root } = mount(
|
|
229
|
+
`<ul><li data-visible></li><li data-visible></li><li></li></ul>`,
|
|
230
|
+
`li:nth-child(1 of [data-visible]) { data-first-visible: ""; }
|
|
231
|
+
li:not(:nth-child(1 of [data-visible])) { data-first-visible: none; }`
|
|
232
|
+
);
|
|
233
|
+
await flush();
|
|
234
|
+
expect(attrsOf(root, "li", "data-first-visible")).toEqual([
|
|
235
|
+
"",
|
|
236
|
+
null,
|
|
237
|
+
null,
|
|
238
|
+
]);
|
|
239
|
+
|
|
240
|
+
root.querySelector("li")!.removeAttribute("data-visible");
|
|
241
|
+
await flush();
|
|
242
|
+
expect(attrsOf(root, "li", "data-first-visible")).toEqual([
|
|
243
|
+
null,
|
|
244
|
+
"",
|
|
245
|
+
null,
|
|
246
|
+
]);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("tracks :only-child and :empty through insertions and removals", async () => {
|
|
250
|
+
const { root } = mount(
|
|
251
|
+
`<ul></ul>`,
|
|
252
|
+
`ul:empty { data-empty: ""; }
|
|
253
|
+
ul:not(:empty) { data-empty: none; }
|
|
254
|
+
li:only-child { data-only: ""; }
|
|
255
|
+
li:not(:only-child) { data-only: none; }`
|
|
256
|
+
);
|
|
257
|
+
await flush();
|
|
258
|
+
const ul = root.querySelector("ul")!;
|
|
259
|
+
expect(ul.hasAttribute("data-empty")).toBe(true);
|
|
260
|
+
|
|
261
|
+
ul.append(document.createElement("li"));
|
|
262
|
+
await flush();
|
|
263
|
+
expect(ul.hasAttribute("data-empty")).toBe(false);
|
|
264
|
+
expect(attrsOf(root, "li", "data-only")).toEqual([""]);
|
|
265
|
+
|
|
266
|
+
ul.append(document.createElement("li"));
|
|
267
|
+
await flush();
|
|
268
|
+
expect(attrsOf(root, "li", "data-only")).toEqual([null, null]);
|
|
269
|
+
|
|
270
|
+
ul.querySelectorAll("li").forEach((li) => li.remove());
|
|
271
|
+
await flush();
|
|
272
|
+
expect(ul.hasAttribute("data-empty")).toBe(true);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it("moves :last-child when iterate() drops rows", async () => {
|
|
276
|
+
const list = fixture<HTMLElement & { provision?: string[] }>(
|
|
277
|
+
`<x-list><section><div id="sheet"></div><ul><template><li></li></template></ul></section></x-list>`
|
|
278
|
+
);
|
|
279
|
+
list.provision = ["a", "b", "c"];
|
|
280
|
+
const quark = new Quark({
|
|
281
|
+
src: `x-list { $items: prop("provision"); }
|
|
282
|
+
ul { content: iterate($items); }
|
|
283
|
+
li { content: item; }
|
|
284
|
+
li:last-child { data-last: ""; }
|
|
285
|
+
li:not(:last-child) { data-last: none; }`,
|
|
286
|
+
options: { isScoped: false },
|
|
287
|
+
});
|
|
288
|
+
quark.register({ sheetElement: list.querySelector("#sheet")! });
|
|
289
|
+
await flush();
|
|
290
|
+
expect(attrsOf(list, "li", "data-last")).toEqual([null, null, ""]);
|
|
291
|
+
|
|
292
|
+
list.provision = ["a", "b"];
|
|
293
|
+
await flush();
|
|
294
|
+
expect(attrsOf(list, "li", "data-last")).toEqual([null, ""]);
|
|
295
|
+
quark.unregister();
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
describe(":has()", () => {
|
|
300
|
+
it("re-checks the subject when an attribute named in the argument changes below it", async () => {
|
|
301
|
+
const { root, quark, register } = createSheet(
|
|
302
|
+
`<details><summary></summary><div><span></span></div></details>`,
|
|
303
|
+
`details:has([data-active]) { data-highlight: ""; }
|
|
304
|
+
details:not(:has([data-active])) { data-highlight: none; }`
|
|
305
|
+
);
|
|
306
|
+
register();
|
|
307
|
+
await flush();
|
|
308
|
+
const details = root.querySelector("details")!;
|
|
309
|
+
expect(details.hasAttribute("data-highlight")).toBe(false);
|
|
310
|
+
|
|
311
|
+
const meter = measureComplexity(quark);
|
|
312
|
+
root.querySelector("span")!.setAttribute("data-active", "");
|
|
313
|
+
await flush();
|
|
314
|
+
expect(details.hasAttribute("data-highlight")).toBe(true);
|
|
315
|
+
|
|
316
|
+
root.querySelector("span")!.removeAttribute("data-active");
|
|
317
|
+
await flush();
|
|
318
|
+
const budget = meter.take();
|
|
319
|
+
meter.stop();
|
|
320
|
+
expect(details.hasAttribute("data-highlight")).toBe(false);
|
|
321
|
+
expectComplexity(budget);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it("re-checks the subject when elements are inserted or removed below it", async () => {
|
|
325
|
+
const { root } = mount(
|
|
326
|
+
`<ul></ul>`,
|
|
327
|
+
`ul:has(li) { data-has-items: ""; }
|
|
328
|
+
ul:not(:has(li)) { data-has-items: none; }`
|
|
329
|
+
);
|
|
330
|
+
await flush();
|
|
331
|
+
const ul = root.querySelector("ul")!;
|
|
332
|
+
expect(ul.hasAttribute("data-has-items")).toBe(false);
|
|
333
|
+
|
|
334
|
+
ul.append(document.createElement("li"));
|
|
335
|
+
await flush();
|
|
336
|
+
expect(ul.hasAttribute("data-has-items")).toBe(true);
|
|
337
|
+
|
|
338
|
+
ul.querySelector("li")!.remove();
|
|
339
|
+
await flush();
|
|
340
|
+
expect(ul.hasAttribute("data-has-items")).toBe(false);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it("fans out below a :has() ancestor compound", async () => {
|
|
344
|
+
const { root } = mount(
|
|
345
|
+
`<section><div><p></p></div><button></button></section>`,
|
|
346
|
+
`section:has([data-open]) button { disabled: ""; }
|
|
347
|
+
section:not(:has([data-open])) button { disabled: none; }`
|
|
348
|
+
);
|
|
349
|
+
await flush();
|
|
350
|
+
const button = root.querySelector("button")!;
|
|
351
|
+
expect(button.hasAttribute("disabled")).toBe(false);
|
|
352
|
+
|
|
353
|
+
root.querySelector("p")!.setAttribute("data-open", "");
|
|
354
|
+
await flush();
|
|
355
|
+
expect(button.hasAttribute("disabled")).toBe(true);
|
|
356
|
+
|
|
357
|
+
root.querySelector("p")!.removeAttribute("data-open");
|
|
358
|
+
await flush();
|
|
359
|
+
expect(button.hasAttribute("disabled")).toBe(false);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it("re-runs every matching ancestor of the change", async () => {
|
|
363
|
+
const { root } = mount(
|
|
364
|
+
`<div><div><span></span></div></div>`,
|
|
365
|
+
`div:has([data-x]) { data-deep: ""; }
|
|
366
|
+
div:not(:has([data-x])) { data-deep: none; }`
|
|
367
|
+
);
|
|
368
|
+
await flush();
|
|
369
|
+
root.querySelector("span")!.setAttribute("data-x", "");
|
|
370
|
+
await flush();
|
|
371
|
+
expect(attrsOf(root, "div:not(#sheet)", "data-deep")).toEqual(["", ""]);
|
|
372
|
+
|
|
373
|
+
root.querySelector("span")!.removeAttribute("data-x");
|
|
374
|
+
await flush();
|
|
375
|
+
expect(attrsOf(root, "div:not(#sheet)", "data-deep")).toEqual([
|
|
376
|
+
null,
|
|
377
|
+
null,
|
|
378
|
+
]);
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
it("supports child-relative arguments", async () => {
|
|
382
|
+
const { root } = mount(
|
|
383
|
+
`<ul><li><ul><li></li></ul></li></ul>`,
|
|
384
|
+
`ul:has(> li[data-open]) { data-open-child: ""; }
|
|
385
|
+
ul:not(:has(> li[data-open])) { data-open-child: none; }`
|
|
386
|
+
);
|
|
387
|
+
await flush();
|
|
388
|
+
const [outer, inner] = root.querySelectorAll("ul");
|
|
389
|
+
inner.querySelector("li")!.setAttribute("data-open", "");
|
|
390
|
+
await flush();
|
|
391
|
+
expect(outer.hasAttribute("data-open-child")).toBe(false);
|
|
392
|
+
expect(inner.hasAttribute("data-open-child")).toBe(true);
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
it("re-runs sibling-relative arguments from the host", async () => {
|
|
396
|
+
const { root } = mount(
|
|
397
|
+
`<form><label></label><input /></form>`,
|
|
398
|
+
`label:has(+ input[data-invalid]) { data-error: ""; }
|
|
399
|
+
label:not(:has(+ input[data-invalid])) { data-error: none; }`
|
|
400
|
+
);
|
|
401
|
+
await flush();
|
|
402
|
+
const label = root.querySelector("label")!;
|
|
403
|
+
expect(label.hasAttribute("data-error")).toBe(false);
|
|
404
|
+
|
|
405
|
+
root.querySelector("input")!.setAttribute("data-invalid", "");
|
|
406
|
+
await flush();
|
|
407
|
+
expect(label.hasAttribute("data-error")).toBe(true);
|
|
408
|
+
|
|
409
|
+
root.querySelector("input")!.removeAttribute("data-invalid");
|
|
410
|
+
await flush();
|
|
411
|
+
expect(label.hasAttribute("data-error")).toBe(false);
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
it("derives a host aggregate with :scope:has()", async () => {
|
|
415
|
+
const { root, quark, register } = createSheet(
|
|
416
|
+
`<ul><li></li><li></li></ul><button></button>`,
|
|
417
|
+
`:scope:has(li[data-selected]) { data-has-selection: ""; button { disabled: none; } }
|
|
418
|
+
:scope:not(:has(li[data-selected])) { data-has-selection: none; button { disabled: ""; } }`
|
|
419
|
+
);
|
|
420
|
+
register();
|
|
421
|
+
await flush();
|
|
422
|
+
const button = root.querySelector("button")!;
|
|
423
|
+
expect(root.hasAttribute("data-has-selection")).toBe(false);
|
|
424
|
+
expect(button.hasAttribute("disabled")).toBe(true);
|
|
425
|
+
|
|
426
|
+
const meter = measureComplexity(quark);
|
|
427
|
+
root.querySelector("li")!.setAttribute("data-selected", "");
|
|
428
|
+
await flush();
|
|
429
|
+
expect(root.hasAttribute("data-has-selection")).toBe(true);
|
|
430
|
+
expect(button.hasAttribute("disabled")).toBe(false);
|
|
431
|
+
|
|
432
|
+
root.querySelector("li")!.remove();
|
|
433
|
+
await flush();
|
|
434
|
+
const budget = meter.take();
|
|
435
|
+
meter.stop();
|
|
436
|
+
expect(root.hasAttribute("data-has-selection")).toBe(false);
|
|
437
|
+
expect(button.hasAttribute("disabled")).toBe(true);
|
|
438
|
+
expectComplexity(budget);
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
it("lets a global sheet match a :has() subject above its host", async () => {
|
|
442
|
+
const provider = fixture<HTMLElement>(
|
|
443
|
+
`<x-provider><section><div id="sheet"></div><p></p></section></x-provider>`
|
|
444
|
+
);
|
|
445
|
+
const quark = new Quark({
|
|
446
|
+
src: `x-provider:has(p[data-ready]) { data-ready: ""; }
|
|
447
|
+
x-provider:not(:has(p[data-ready])) { data-ready: none; }`,
|
|
448
|
+
options: { isScoped: false },
|
|
449
|
+
});
|
|
450
|
+
quark.register({ sheetElement: provider.querySelector("#sheet")! });
|
|
451
|
+
await flush();
|
|
452
|
+
expect(provider.hasAttribute("data-ready")).toBe(false);
|
|
453
|
+
|
|
454
|
+
provider.querySelector("p")!.setAttribute("data-ready", "");
|
|
455
|
+
await flush();
|
|
456
|
+
expect(provider.hasAttribute("data-ready")).toBe(true);
|
|
457
|
+
quark.unregister();
|
|
458
|
+
});
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
describe("attribute-backed pseudo-classes", () => {
|
|
462
|
+
it("re-runs :disabled when the disabled attribute changes", async () => {
|
|
463
|
+
const { root } = mount(
|
|
464
|
+
`<button></button>`,
|
|
465
|
+
`button:disabled { data-state: "off"; }
|
|
466
|
+
button:not(:disabled) { data-state: "on"; }`
|
|
467
|
+
);
|
|
468
|
+
await flush();
|
|
469
|
+
const button = root.querySelector("button")!;
|
|
470
|
+
expect(button.getAttribute("data-state")).toBe("on");
|
|
471
|
+
|
|
472
|
+
button.setAttribute("disabled", "");
|
|
473
|
+
await flush();
|
|
474
|
+
expect(button.getAttribute("data-state")).toBe("off");
|
|
475
|
+
|
|
476
|
+
button.removeAttribute("disabled");
|
|
477
|
+
await flush();
|
|
478
|
+
expect(button.getAttribute("data-state")).toBe("on");
|
|
479
|
+
});
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
describe("class and id selectors", () => {
|
|
483
|
+
it("re-runs a rule when a class it names is added or removed", async () => {
|
|
484
|
+
const { root } = mount(
|
|
485
|
+
`<ul><li></li><li class="is-done"></li></ul>`,
|
|
486
|
+
`li.is-done { data-hit: ""; }
|
|
487
|
+
li:not(.is-done) { data-hit: none; }`
|
|
488
|
+
);
|
|
489
|
+
await flush();
|
|
490
|
+
expect(attrsOf(root, "li", "data-hit")).toEqual([null, ""]);
|
|
491
|
+
|
|
492
|
+
const [first, second] = root.querySelectorAll("li");
|
|
493
|
+
first.classList.add("is-done");
|
|
494
|
+
second.classList.remove("is-done");
|
|
495
|
+
await flush();
|
|
496
|
+
expect(attrsOf(root, "li", "data-hit")).toEqual(["", null]);
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
it("fans out below an ancestor whose class changes", async () => {
|
|
500
|
+
const { root, quark, register } = createSheet(
|
|
501
|
+
`<article><ul><li></li><li></li></ul></article>`,
|
|
502
|
+
`article.is-open li { data-lit: ""; }
|
|
503
|
+
article:not(.is-open) li { data-lit: none; }`
|
|
504
|
+
);
|
|
505
|
+
register();
|
|
506
|
+
await flush();
|
|
507
|
+
expect(attrsOf(root, "li", "data-lit")).toEqual([null, null]);
|
|
508
|
+
|
|
509
|
+
const meter = measureComplexity(quark);
|
|
510
|
+
root.querySelector("article")!.className = "is-open";
|
|
511
|
+
await flush();
|
|
512
|
+
expect(attrsOf(root, "li", "data-lit")).toEqual(["", ""]);
|
|
513
|
+
|
|
514
|
+
root.querySelector("article")!.className = "";
|
|
515
|
+
await flush();
|
|
516
|
+
const budget = meter.take();
|
|
517
|
+
meter.stop();
|
|
518
|
+
expect(attrsOf(root, "li", "data-lit")).toEqual([null, null]);
|
|
519
|
+
expectComplexity(budget);
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
it("re-checks :has() when a class changes below the subject", async () => {
|
|
523
|
+
const { root } = mount(
|
|
524
|
+
`<ul><li></li><li></li></ul>`,
|
|
525
|
+
`:scope:has(li.is-active) { data-has-active: ""; }
|
|
526
|
+
:scope:not(:has(li.is-active)) { data-has-active: none; }`
|
|
527
|
+
);
|
|
528
|
+
await flush();
|
|
529
|
+
expect(root.hasAttribute("data-has-active")).toBe(false);
|
|
530
|
+
|
|
531
|
+
const li = root.querySelector("li")!;
|
|
532
|
+
li.classList.add("is-active");
|
|
533
|
+
await flush();
|
|
534
|
+
expect(root.hasAttribute("data-has-active")).toBe(true);
|
|
535
|
+
|
|
536
|
+
li.classList.remove("is-active");
|
|
537
|
+
await flush();
|
|
538
|
+
expect(root.hasAttribute("data-has-active")).toBe(false);
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
it("re-runs #id rules when an id changes", async () => {
|
|
542
|
+
const { root } = mount(
|
|
543
|
+
`<p></p><p id="main"></p>`,
|
|
544
|
+
`p#main { data-main: ""; }
|
|
545
|
+
p:not(#main) { data-main: none; }`
|
|
546
|
+
);
|
|
547
|
+
await flush();
|
|
548
|
+
expect(attrsOf(root, "p", "data-main")).toEqual([null, ""]);
|
|
549
|
+
|
|
550
|
+
const [first, second] = root.querySelectorAll("p");
|
|
551
|
+
second.id = "aside";
|
|
552
|
+
first.id = "main";
|
|
553
|
+
await flush();
|
|
554
|
+
expect(attrsOf(root, "p", "data-main")).toEqual(["", null]);
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
it("ignores class changes that touch no class the sheet names", async () => {
|
|
558
|
+
const { root, quark } = mount(
|
|
559
|
+
`<ul><li></li></ul>`,
|
|
560
|
+
`li.is-done { data-hit: ""; }`
|
|
561
|
+
);
|
|
562
|
+
await flush();
|
|
563
|
+
const runSpy = vi.spyOn(quark, "run");
|
|
564
|
+
const li = root.querySelector("li")!;
|
|
565
|
+
li.classList.add("fade-in");
|
|
566
|
+
li.className = "fade-in highlight";
|
|
567
|
+
await flush();
|
|
568
|
+
expect(runSpy).not.toHaveBeenCalled();
|
|
569
|
+
|
|
570
|
+
li.classList.add("is-done");
|
|
571
|
+
await flush();
|
|
572
|
+
expect(runSpy).toHaveBeenCalledTimes(1);
|
|
573
|
+
expect(li.getAttribute("data-hit")).toBe("");
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
it("observes every class change when a rule reads the whole value", async () => {
|
|
577
|
+
const { root } = mount(
|
|
578
|
+
`<ul><li class="a"></li></ul>`,
|
|
579
|
+
`li.a { data-classes: attr("class"); }`
|
|
580
|
+
);
|
|
581
|
+
await flush();
|
|
582
|
+
const li = root.querySelector("li")!;
|
|
583
|
+
expect(li.getAttribute("data-classes")).toBe("a");
|
|
584
|
+
|
|
585
|
+
li.classList.add("b");
|
|
586
|
+
await flush();
|
|
587
|
+
expect(li.getAttribute("data-classes")).toBe("a b");
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
it("reacts to classes Quark writes, including on the same element", async () => {
|
|
591
|
+
const { root } = mount(
|
|
592
|
+
`<ul><li></li><li data-done></li></ul>`,
|
|
593
|
+
`li[data-done] { class: (is-done: true); }
|
|
594
|
+
li.is-done { class: (is-struck: true); data-struck: ""; }`
|
|
595
|
+
);
|
|
596
|
+
await flush();
|
|
597
|
+
const [first, second] = root.querySelectorAll("li");
|
|
598
|
+
expect(second.className).toBe("is-done is-struck");
|
|
599
|
+
expect(second.getAttribute("data-struck")).toBe("");
|
|
600
|
+
|
|
601
|
+
first.setAttribute("data-done", "");
|
|
602
|
+
await flush();
|
|
603
|
+
expect(first.className).toBe("is-done is-struck");
|
|
604
|
+
expect(first.getAttribute("data-struck")).toBe("");
|
|
605
|
+
|
|
606
|
+
// re-run with the classes already in place: no write, no churn
|
|
607
|
+
const observed = vi.fn();
|
|
608
|
+
new MutationObserver(observed).observe(first, { attributes: true });
|
|
609
|
+
first.classList.remove("is-done");
|
|
610
|
+
first.classList.add("is-done");
|
|
611
|
+
await flush();
|
|
612
|
+
expect(first.className).toBe("is-struck is-done");
|
|
613
|
+
expect(observed).toHaveBeenCalledTimes(1);
|
|
614
|
+
});
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
describe("observation plumbing", () => {
|
|
618
|
+
it("does not observe removals for a sheet without position-dependent rules", async () => {
|
|
619
|
+
const { root, quark } = mount(
|
|
620
|
+
`<ul><li></li><li></li></ul>`,
|
|
621
|
+
`li { data-seen: ""; }`
|
|
622
|
+
);
|
|
623
|
+
await flush();
|
|
624
|
+
const runSpy = vi.spyOn(quark, "run");
|
|
625
|
+
root.querySelector("li")!.remove();
|
|
626
|
+
await flush();
|
|
627
|
+
expect(runSpy).not.toHaveBeenCalled();
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
it("observes removals once a rule depends on children or position", async () => {
|
|
631
|
+
const { root, quark } = mount(
|
|
632
|
+
`<ul><li></li><li></li></ul>`,
|
|
633
|
+
`li:last-child { data-last: ""; }`
|
|
634
|
+
);
|
|
635
|
+
await flush();
|
|
636
|
+
const runSpy = vi.spyOn(quark, "run");
|
|
637
|
+
root.querySelector("li")!.remove();
|
|
638
|
+
await flush();
|
|
639
|
+
expect(runSpy).toHaveBeenCalledTimes(1);
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
it("fans out from every path ancestor changed in one batch", async () => {
|
|
643
|
+
const { root } = mount(
|
|
644
|
+
`<div id="a1"><em><b></b></em></div><div id="a2" data-x><em><b></b></em></div>`,
|
|
645
|
+
`div[data-x] em[data-y] b { data-hit: ""; }`
|
|
646
|
+
);
|
|
647
|
+
await flush();
|
|
648
|
+
root.querySelector("#a1")!.setAttribute("data-x", "");
|
|
649
|
+
root.querySelector("#a1 em")!.setAttribute("data-y", "");
|
|
650
|
+
root.querySelector("#a2 em")!.setAttribute("data-y", "");
|
|
651
|
+
await flush();
|
|
652
|
+
expect(attrsOf(root, "b", "data-hit")).toEqual(["", ""]);
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
it("warns once about a pseudo-class it cannot observe", async () => {
|
|
656
|
+
const warn = vi.spyOn(QuarkLogger, "warn").mockImplementation(() => {});
|
|
657
|
+
mount(
|
|
658
|
+
`<a><span></span></a>`,
|
|
659
|
+
`a:hover { data-hot: ""; span { content: "x"; } }
|
|
660
|
+
a:not([href]) { data-plain: ""; }`
|
|
661
|
+
);
|
|
662
|
+
await flush();
|
|
663
|
+
const messages = warn.mock.calls.map(
|
|
664
|
+
(c) => (c[0] as { message: string }).message
|
|
665
|
+
);
|
|
666
|
+
expect(messages.filter((m) => m.includes(":hover"))).toHaveLength(1);
|
|
667
|
+
expect(messages[0]).toContain("not observed");
|
|
668
|
+
});
|
|
669
|
+
});
|
|
670
|
+
});
|