@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,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@delay <ms> { … }`: the block applies once after the pause, if the
|
|
3
|
+
* element is still in the document and the rule still matches; applying
|
|
4
|
+
* the rule again restarts the timer. Real timers with short pauses.
|
|
5
|
+
*/
|
|
6
|
+
import { Quark } from "../../index";
|
|
7
|
+
import { QuarkLogger } from "../../src/utils";
|
|
8
|
+
import type { QuarkRenderer } from "../../src/devtools-hook";
|
|
9
|
+
import {
|
|
10
|
+
type DevtoolsHook,
|
|
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
|
+
wait,
|
|
23
|
+
} from "@excom/heft-rig/profiles/default/config/test-utils";
|
|
24
|
+
import { LoopGuard } from "@excom/kit-utils";
|
|
25
|
+
import { createSheet, flush, mount, unregisterAll } from "./helpers";
|
|
26
|
+
|
|
27
|
+
const waitFor = async (condition: () => boolean, ticks = 60) => {
|
|
28
|
+
for (let i = 0; i < ticks && !condition(); i++) await wait(5);
|
|
29
|
+
expect(condition()).toBe(true);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* `flush()` under fake timers: advance the clock by `ms`, then drain whatever
|
|
34
|
+
* the due timers queued. Advancing also flushes microtasks, so this covers
|
|
35
|
+
* the paint queue and the observer callbacks `flush()` waits on.
|
|
36
|
+
*/
|
|
37
|
+
const tick = async (ms = 0) => {
|
|
38
|
+
await vi.advanceTimersByTimeAsync(ms);
|
|
39
|
+
for (let i = 0; i < 8; i++) await vi.advanceTimersByTimeAsync(0);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
describe("@delay", () => {
|
|
43
|
+
afterEach(() => {
|
|
44
|
+
unregisterAll();
|
|
45
|
+
document.body.innerHTML = "";
|
|
46
|
+
vi.useRealTimers();
|
|
47
|
+
vi.restoreAllMocks();
|
|
48
|
+
LoopGuard.reset();
|
|
49
|
+
delete (globalThis as Record<string, unknown>)[
|
|
50
|
+
NUCLEUS_DEVTOOLS_HOOK_KEY
|
|
51
|
+
];
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Fake timers: `is-visible` is asserted while the 30ms pause is still
|
|
55
|
+
// running, and a real `flush()` can outlast it on a loaded machine.
|
|
56
|
+
it("applies the block after the pause; declarations write the element, nested rules its descendants", async () => {
|
|
57
|
+
vi.useFakeTimers();
|
|
58
|
+
const { root } = mount(
|
|
59
|
+
`<output id="o" is-visible><span>hi</span></output>`,
|
|
60
|
+
`#o[is-visible] { @delay 30 { is-visible: none; span { data-late: ""; } } }`
|
|
61
|
+
);
|
|
62
|
+
await tick();
|
|
63
|
+
const out = root.querySelector("#o")!;
|
|
64
|
+
expect(out.hasAttribute("is-visible")).toBe(true);
|
|
65
|
+
await tick(35);
|
|
66
|
+
expect(out.hasAttribute("is-visible")).toBe(false);
|
|
67
|
+
expect(out.querySelector("span")!.hasAttribute("data-late")).toBe(true);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// Fake timers: the assertion below is that p has *not* fired yet, so the
|
|
71
|
+
// re-application has to land inside the 80ms pause. On a loaded machine a
|
|
72
|
+
// real `wait(40)` can overshoot it and p fires on its first timer.
|
|
73
|
+
it("restarts on every application of the rule, one timer per element", async () => {
|
|
74
|
+
vi.useFakeTimers();
|
|
75
|
+
const { root } = mount(
|
|
76
|
+
`<p id="p" data-tick="1"></p><p id="q" data-tick="1"></p>`,
|
|
77
|
+
`[data-tick] { @delay 80 { data-done: attr("data-tick"); } }`
|
|
78
|
+
);
|
|
79
|
+
await tick();
|
|
80
|
+
const p = root.querySelector("#p")!;
|
|
81
|
+
const q = root.querySelector("#q")!;
|
|
82
|
+
await tick(40);
|
|
83
|
+
p.setAttribute("data-tick", "2");
|
|
84
|
+
await tick();
|
|
85
|
+
await tick(45);
|
|
86
|
+
// 85ms after the first application, but p restarted at 40ms
|
|
87
|
+
expect(p.hasAttribute("data-done")).toBe(false);
|
|
88
|
+
expect(q.getAttribute("data-done")).toBe("1");
|
|
89
|
+
await tick(40);
|
|
90
|
+
expect(p.getAttribute("data-done")).toBe("2");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Fake timers: the drops have to be decided while the 30ms pause is still
|
|
94
|
+
// running, so the removals must land inside it.
|
|
95
|
+
it("drops the block when the element left, the rule stopped matching, or the sheet unregistered", async () => {
|
|
96
|
+
vi.useFakeTimers();
|
|
97
|
+
const { root, quark } = mount(
|
|
98
|
+
`<p id="a" data-x></p><p id="b" data-x></p><p id="c" data-x></p>`,
|
|
99
|
+
`[data-x] { @delay 30 { is-late: ""; } }`
|
|
100
|
+
);
|
|
101
|
+
await tick();
|
|
102
|
+
const a = root.querySelector("#a")!;
|
|
103
|
+
const b = root.querySelector("#b")!;
|
|
104
|
+
const c = root.querySelector("#c")!;
|
|
105
|
+
a.remove();
|
|
106
|
+
b.removeAttribute("data-x");
|
|
107
|
+
await tick();
|
|
108
|
+
await tick(60);
|
|
109
|
+
expect(a.hasAttribute("is-late")).toBe(false);
|
|
110
|
+
expect(b.hasAttribute("is-late")).toBe(false);
|
|
111
|
+
expect(c.hasAttribute("is-late")).toBe(true);
|
|
112
|
+
// unregister clears pending timers
|
|
113
|
+
c.removeAttribute("is-late");
|
|
114
|
+
c.setAttribute("data-x", "again");
|
|
115
|
+
await tick();
|
|
116
|
+
quark.unregister();
|
|
117
|
+
await tick(60);
|
|
118
|
+
expect(c.hasAttribute("is-late")).toBe(false);
|
|
119
|
+
expect(quark.delayTimers.size).toBe(0);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// Fake timers: `data-copied` is asserted while the 50ms pause is still
|
|
123
|
+
// running, both for the single click and between the rapid pair.
|
|
124
|
+
it("inside an @on block: fires per event with event data; rapid events restart", async () => {
|
|
125
|
+
vi.useFakeTimers();
|
|
126
|
+
const { root } = mount(
|
|
127
|
+
`<button id="b" type="button">copy</button>`,
|
|
128
|
+
`#b { @on click { data-copied: ""; @delay 50 { data-copied: none; data-last: event.type; } } }`
|
|
129
|
+
);
|
|
130
|
+
await tick();
|
|
131
|
+
const button = root.querySelector("#b")!;
|
|
132
|
+
const click = () =>
|
|
133
|
+
button.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
134
|
+
click();
|
|
135
|
+
await tick();
|
|
136
|
+
expect(button.getAttribute("data-copied")).toBe("");
|
|
137
|
+
await tick(55);
|
|
138
|
+
expect(button.hasAttribute("data-copied")).toBe(false);
|
|
139
|
+
expect(button.getAttribute("data-last")).toBe("click");
|
|
140
|
+
// rapid clicks: one flash, cleared once after the last click
|
|
141
|
+
click();
|
|
142
|
+
await tick(25);
|
|
143
|
+
click();
|
|
144
|
+
await tick();
|
|
145
|
+
await tick(35);
|
|
146
|
+
// 60ms after the first click, but only 35ms after the one that restarted it
|
|
147
|
+
expect(button.getAttribute("data-copied")).toBe("");
|
|
148
|
+
await tick(25);
|
|
149
|
+
expect(button.hasAttribute("data-copied")).toBe(false);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("chains: a delay inside a delay keeps `target` from the enclosing @on", async () => {
|
|
153
|
+
const { root } = mount(
|
|
154
|
+
`<ul id="list"><li><span>a</span></li></ul>`,
|
|
155
|
+
`#list {
|
|
156
|
+
@on click (target: "span") {
|
|
157
|
+
@delay 10 { data-step: "1"; @delay 10 { data-step: "2"; data-target: target.localName; } }
|
|
158
|
+
}
|
|
159
|
+
}`
|
|
160
|
+
);
|
|
161
|
+
await flush();
|
|
162
|
+
const list = root.querySelector("#list")!;
|
|
163
|
+
list
|
|
164
|
+
.querySelector("span")!
|
|
165
|
+
.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
166
|
+
await waitFor(() => list.getAttribute("data-step") === "2");
|
|
167
|
+
expect(list.getAttribute("data-target")).toBe("span");
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("takes the duration from an expression and warns about non-numbers", async () => {
|
|
171
|
+
const warn = vi.spyOn(QuarkLogger, "warn").mockImplementation(() => {});
|
|
172
|
+
const { root } = mount(
|
|
173
|
+
`<p id="a" data-ms="20"></p><p id="bad"></p>`,
|
|
174
|
+
`[data-ms] { $ms: +attr("data-ms"); @delay $ms { is-late: ""; } }
|
|
175
|
+
#bad { @delay "soon" { is-late: ""; } }`
|
|
176
|
+
);
|
|
177
|
+
await flush();
|
|
178
|
+
await waitFor(() => root.querySelector("#a")!.hasAttribute("is-late"));
|
|
179
|
+
await wait(30);
|
|
180
|
+
expect(root.querySelector("#bad")!.hasAttribute("is-late")).toBe(false);
|
|
181
|
+
expect(
|
|
182
|
+
warn.mock.calls.some(([arg]) =>
|
|
183
|
+
String((arg as { message?: string })?.message).includes(
|
|
184
|
+
'@delay "soon" needs a number of milliseconds'
|
|
185
|
+
)
|
|
186
|
+
)
|
|
187
|
+
).toBe(true);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("keeps the loop guard's causal depth: a self-retriggering chain is cut", async () => {
|
|
191
|
+
LoopGuard.configure({ limit: 4, log: () => {} });
|
|
192
|
+
const { root } = mount(
|
|
193
|
+
`<p id="n" data-n="0"></p>`,
|
|
194
|
+
`[data-n] { @delay 1 { data-n: +attr("data-n") + 1; } }`
|
|
195
|
+
);
|
|
196
|
+
await flush();
|
|
197
|
+
await wait(150);
|
|
198
|
+
await flush();
|
|
199
|
+
const n = Number(root.querySelector("#n")!.getAttribute("data-n"));
|
|
200
|
+
expect(n).toBeGreaterThan(0);
|
|
201
|
+
expect(n).toBeLessThanOrEqual(LoopGuard.limit + 1);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("publishes quark/delay scheduled, fired and dropped records, and lists delays on the rule", async () => {
|
|
205
|
+
const publications: { path: PublicizePath; meta: PublicizeMeta }[] = [];
|
|
206
|
+
const renderers: QuarkRenderer[] = [];
|
|
207
|
+
const hook: DevtoolsHook = {
|
|
208
|
+
version: 1,
|
|
209
|
+
inject: (renderer) => {
|
|
210
|
+
renderers.push(renderer as QuarkRenderer);
|
|
211
|
+
},
|
|
212
|
+
publicize: (path, meta) => {
|
|
213
|
+
publications.push({ path, meta });
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
Quark.attachDevtools(hook);
|
|
217
|
+
const { root, quark, register } = createSheet(
|
|
218
|
+
`<p id="a" data-x></p><p id="b" data-x></p>`,
|
|
219
|
+
// 100ms, not 10: under a loaded test runner the `#b` removal below must
|
|
220
|
+
// land before the timer fires or the `dropped` record becomes `fired`
|
|
221
|
+
`[data-x] { @delay 100 { is-late: ""; } }`
|
|
222
|
+
);
|
|
223
|
+
register();
|
|
224
|
+
await flush();
|
|
225
|
+
root.querySelector("#b")!.remove();
|
|
226
|
+
await waitFor(() => root.querySelector("#a")!.hasAttribute("is-late"));
|
|
227
|
+
await wait(20);
|
|
228
|
+
const records = publications
|
|
229
|
+
.filter((p) => pathMatches(p.path, ["quark", "delay"]))
|
|
230
|
+
.map(({ meta }) => [meta.tag, meta.phase, meta.ms, meta.reason ?? null]);
|
|
231
|
+
expect(records).toEqual([
|
|
232
|
+
["p", "scheduled", 100, null],
|
|
233
|
+
["p", "scheduled", 100, null],
|
|
234
|
+
["p", "fired", 100, null],
|
|
235
|
+
["p", "dropped", 100, "disconnected"],
|
|
236
|
+
]);
|
|
237
|
+
const sheet = renderers
|
|
238
|
+
.find((r) => r.kind === "quark")!
|
|
239
|
+
.sheets()
|
|
240
|
+
.find((s) => s.sheetId === quark.id)!;
|
|
241
|
+
expect(sheet.rules[0].delays).toEqual([{ duration: "100" }]);
|
|
242
|
+
expect(sheet.rules[0].declarations).toEqual([]);
|
|
243
|
+
});
|
|
244
|
+
});
|