@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,568 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Small internal modules exercised directly: element state, scope markers,
|
|
3
|
+
* the observer / paint schedulers, AST + selector helpers,
|
|
4
|
+
* logging utils and property construction edge cases.
|
|
5
|
+
*/
|
|
6
|
+
import { collectUseRules, declarationStrings, viewBlock } from "../../src/ast";
|
|
7
|
+
import { Quark } from "../../index";
|
|
8
|
+
import { observe, unobserve } from "../../src/observer";
|
|
9
|
+
import { PAINT_QUEUES, reqCommit, schedulePaint } from "../../src/paint";
|
|
10
|
+
import { Attribute, Variable } from "../../src/properties";
|
|
11
|
+
import { getQuarkInternal, QuarkInternal } from "../../src/quark-internal";
|
|
12
|
+
import { outermostElements } from "../../src/rule";
|
|
13
|
+
import { acquireScopeId, releaseScopeId, SCOPE_ATTR } from "../../src/scope-id";
|
|
14
|
+
import { isQuarkBusy, trackPending, whenSettled } from "../../src/settle";
|
|
15
|
+
import {
|
|
16
|
+
buildSelectorLine,
|
|
17
|
+
parseCssSelector,
|
|
18
|
+
splitScopePrefix,
|
|
19
|
+
} from "../../src/selector-utils";
|
|
20
|
+
import { getQuarkHost, getQuarkMin, QuarkLogger } from "../../src/utils";
|
|
21
|
+
import { parse } from "@excom/quark-parser";
|
|
22
|
+
import {
|
|
23
|
+
afterEach,
|
|
24
|
+
describe,
|
|
25
|
+
expect,
|
|
26
|
+
it,
|
|
27
|
+
vi,
|
|
28
|
+
wait,
|
|
29
|
+
} from "@excom/heft-rig/profiles/default/config/test-utils";
|
|
30
|
+
import { flush, mount, unregisterAll } from "./helpers";
|
|
31
|
+
|
|
32
|
+
/** A WeakRef whose target is already gone (GC cannot be forced). */
|
|
33
|
+
const deadRef = <T extends object>(): WeakRef<T> => {
|
|
34
|
+
const ref = Object.create(WeakRef.prototype);
|
|
35
|
+
ref.deref = () => undefined;
|
|
36
|
+
return ref;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
describe("QuarkInternal", () => {
|
|
40
|
+
afterEach(() => {
|
|
41
|
+
vi.restoreAllMocks();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("logs instead of throwing when the element is gone", () => {
|
|
45
|
+
const error = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
46
|
+
expect(getQuarkInternal(deadRef())).toBeUndefined();
|
|
47
|
+
expect(error).toHaveBeenCalledTimes(1);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("reuses the existing state object for an element", () => {
|
|
51
|
+
const el = document.createElement("div");
|
|
52
|
+
const internal = getQuarkInternal(el);
|
|
53
|
+
expect(new QuarkInternal(el)).not.toBe(internal);
|
|
54
|
+
expect(getQuarkInternal(el)).toBe(internal);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("reports whether a binding write changed the stored value", () => {
|
|
58
|
+
const internal = getQuarkInternal(document.createElement("div"));
|
|
59
|
+
expect(internal.setVar("$a", 1)).toBe(true);
|
|
60
|
+
expect(internal.setVar("$a", 1)).toBe(false);
|
|
61
|
+
expect(internal.setVar("$a", 2)).toBe(true);
|
|
62
|
+
expect(internal.setVar("$o", { x: 1 })).toBe(true);
|
|
63
|
+
expect(internal.setVar("$o", { x: 1 })).toBe(false);
|
|
64
|
+
expect(internal.getVar("$o")).toEqual({ x: 1 });
|
|
65
|
+
expect(internal.deleteVar("$o")).toBe(true);
|
|
66
|
+
expect(internal.deleteVar("$o")).toBe(false);
|
|
67
|
+
expect(internal.hasVar("$o")).toBe(false);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("stores modules per sheet, defaulting to the bare bucket", () => {
|
|
71
|
+
const internal = getQuarkInternal(document.createElement("div"));
|
|
72
|
+
internal.setModule("h", undefined, { a: 1 });
|
|
73
|
+
internal.setModule("h", "ns", { b: 2 });
|
|
74
|
+
expect(internal.getModule("h")).toEqual({ a: 1 });
|
|
75
|
+
expect(internal.getModule("h", "ns")).toEqual({ b: 2 });
|
|
76
|
+
expect(internal.getModules("h")).toEqual({
|
|
77
|
+
dfault: { a: 1 },
|
|
78
|
+
ns: { b: 2 },
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("treats undefined attribute and style values as no-ops", () => {
|
|
83
|
+
const el = document.createElement("div");
|
|
84
|
+
el.setAttribute("data-keep", "1");
|
|
85
|
+
el.style.setProperty("--tone", "warm");
|
|
86
|
+
const internal = getQuarkInternal(el);
|
|
87
|
+
internal.setAttr("h", "data-keep", undefined);
|
|
88
|
+
internal.setStyleProperty("h", "--tone", undefined);
|
|
89
|
+
expect(el.getAttribute("data-keep")).toBe("1");
|
|
90
|
+
expect(el.style.getPropertyValue("--tone")).toBe("warm");
|
|
91
|
+
expect(internal.getAllAttrs("h")).toEqual({});
|
|
92
|
+
internal.setAttr("h", "data-flag", true);
|
|
93
|
+
internal.setAttr("h", "data-keep", false);
|
|
94
|
+
expect(el.getAttribute("data-flag")).toBe("");
|
|
95
|
+
expect(el.hasAttribute("data-keep")).toBe(false);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("tracks listeners and attributes per sheet and rule", () => {
|
|
99
|
+
const internal = getQuarkInternal(document.createElement("div"));
|
|
100
|
+
expect(internal.propertyHasBeenSet("h", 1, "variable", "$s")).toBe(false);
|
|
101
|
+
internal.setVar("$s", 1);
|
|
102
|
+
expect(internal.propertyHasBeenSet("h", 1, "variable", "$s")).toBe(true);
|
|
103
|
+
expect(internal.propertyHasBeenSet("h", 1, "listener", "click")).toBe(
|
|
104
|
+
false
|
|
105
|
+
);
|
|
106
|
+
expect(internal.propertyHasBeenSet("h", 1, "attribute", "x")).toBe(false);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("swaps, removes and re-adds ordered listeners in place", () => {
|
|
110
|
+
const el = document.createElement("button");
|
|
111
|
+
const internal = getQuarkInternal(el);
|
|
112
|
+
const a = vi.fn();
|
|
113
|
+
const b = vi.fn();
|
|
114
|
+
const c = vi.fn();
|
|
115
|
+
const meta = { eventTypes: ["click"], target: el, options: {} };
|
|
116
|
+
const slot = "@on click";
|
|
117
|
+
internal.setOrderedListeners("h", 1, slot, [a, b], meta);
|
|
118
|
+
el.click();
|
|
119
|
+
expect(a).toHaveBeenCalledTimes(1);
|
|
120
|
+
expect(b).toHaveBeenCalledTimes(1);
|
|
121
|
+
// same length: index 1 becomes undefined → listener removed in place
|
|
122
|
+
internal.setOrderedListeners("h", 1, slot, [a, undefined], meta);
|
|
123
|
+
el.click();
|
|
124
|
+
expect(a).toHaveBeenCalledTimes(2);
|
|
125
|
+
expect(b).toHaveBeenCalledTimes(1);
|
|
126
|
+
// index 1 had no listener: nothing to remove, new one added
|
|
127
|
+
internal.setOrderedListeners("h", 1, slot, [a, c], meta);
|
|
128
|
+
el.click();
|
|
129
|
+
expect(c).toHaveBeenCalledTimes(1);
|
|
130
|
+
// different length: everything is torn down and re-added
|
|
131
|
+
internal.setOrderedListeners("h", 1, slot, [c], meta);
|
|
132
|
+
el.click();
|
|
133
|
+
expect(a).toHaveBeenCalledTimes(3);
|
|
134
|
+
expect(c).toHaveBeenCalledTimes(2);
|
|
135
|
+
expect(internal.getListeners("h", 1, slot)).toEqual([c]);
|
|
136
|
+
expect(internal.getListenerMeta("h", 1, slot)).toBe(meta);
|
|
137
|
+
expect(internal.propertyHasBeenSet("h", 1, "listener", slot)).toBe(true);
|
|
138
|
+
// a changed target or option re-registers on the new target
|
|
139
|
+
const other = document.createElement("div");
|
|
140
|
+
const captureMeta = { eventTypes: ["click"], target: other, options: { capture: true } };
|
|
141
|
+
internal.setOrderedListeners("h", 1, slot, [c], captureMeta);
|
|
142
|
+
el.click();
|
|
143
|
+
expect(c).toHaveBeenCalledTimes(2);
|
|
144
|
+
other.click();
|
|
145
|
+
expect(c).toHaveBeenCalledTimes(3);
|
|
146
|
+
// holes in the handler list are kept as slots without a DOM listener
|
|
147
|
+
internal.setOrderedListeners("h", 2, slot, [undefined, a], meta);
|
|
148
|
+
expect(internal.getListeners("h", 2, slot)).toEqual([undefined, a]);
|
|
149
|
+
internal.removeAllListeners("h", 2, slot);
|
|
150
|
+
expect(internal.getListeners("h", 2, slot)).toEqual([]);
|
|
151
|
+
expect(internal.getListenerMeta("h", 2, slot)).toBeUndefined();
|
|
152
|
+
el.click();
|
|
153
|
+
expect(a).toHaveBeenCalledTimes(3);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe("prop subscriptions", () => {
|
|
158
|
+
it("refcounts subscriptions per element and name", async () => {
|
|
159
|
+
const { subscribeProp } = await import("../../src/props");
|
|
160
|
+
const { isObservedProperty } = await import("@excom/kit-utils");
|
|
161
|
+
const el = document.createElement("div");
|
|
162
|
+
const first = subscribeProp(el, "thing");
|
|
163
|
+
const second = subscribeProp(el, "thing");
|
|
164
|
+
expect(isObservedProperty(el, "thing")).toBe(true);
|
|
165
|
+
first();
|
|
166
|
+
expect(isObservedProperty(el, "thing")).toBe(true);
|
|
167
|
+
second();
|
|
168
|
+
expect(isObservedProperty(el, "thing")).toBe(false);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe("scope ids", () => {
|
|
173
|
+
it("reasserts a stripped marker on re-acquire and refcounts release", () => {
|
|
174
|
+
const host = document.createElement("div");
|
|
175
|
+
const id = acquireScopeId(host);
|
|
176
|
+
host.removeAttribute(SCOPE_ATTR);
|
|
177
|
+
expect(acquireScopeId(host)).toBe(id);
|
|
178
|
+
expect(host.getAttribute(SCOPE_ATTR)).toBe(id);
|
|
179
|
+
releaseScopeId(host);
|
|
180
|
+
expect(host.getAttribute(SCOPE_ATTR)).toBe(id);
|
|
181
|
+
releaseScopeId(host);
|
|
182
|
+
expect(host.hasAttribute(SCOPE_ATTR)).toBe(false);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("leaves a foreign marker alone and ignores unknown hosts", () => {
|
|
186
|
+
const host = document.createElement("div");
|
|
187
|
+
acquireScopeId(host);
|
|
188
|
+
host.setAttribute(SCOPE_ATTR, "foreign");
|
|
189
|
+
releaseScopeId(host);
|
|
190
|
+
expect(host.getAttribute(SCOPE_ATTR)).toBe("foreign");
|
|
191
|
+
expect(() => releaseScopeId(document.createElement("div"))).not.toThrow();
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
describe("observer", () => {
|
|
196
|
+
it("logs when the host is gone and tolerates a missing observer", () => {
|
|
197
|
+
const error = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
198
|
+
expect(observe(deadRef<HTMLElement>(), [], () => {})).toBeUndefined();
|
|
199
|
+
expect(error).toHaveBeenCalledTimes(1);
|
|
200
|
+
error.mockRestore();
|
|
201
|
+
expect(() => unobserve(null, [])).not.toThrow();
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("reports attribute records and element insertions only", async () => {
|
|
205
|
+
const host = document.createElement("div");
|
|
206
|
+
document.body.appendChild(host);
|
|
207
|
+
const cb = vi.fn();
|
|
208
|
+
const observer = observe(host, ["data-x"], cb)!;
|
|
209
|
+
host.setAttribute("data-x", "1");
|
|
210
|
+
host.setAttribute("data-other", "1");
|
|
211
|
+
host.appendChild(document.createTextNode("t"));
|
|
212
|
+
host.appendChild(document.createElement("span"));
|
|
213
|
+
await wait(0);
|
|
214
|
+
expect(cb.mock.calls.map(([arg]) => arg.attribute)).toEqual([
|
|
215
|
+
"data-x",
|
|
216
|
+
"content",
|
|
217
|
+
]);
|
|
218
|
+
observer.disconnect();
|
|
219
|
+
host.remove();
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("reports a class record only when a named class token flips", async () => {
|
|
223
|
+
const host = document.createElement("div");
|
|
224
|
+
document.body.appendChild(host);
|
|
225
|
+
const cb = vi.fn();
|
|
226
|
+
const observer = observe(host, ["class"], cb, {
|
|
227
|
+
classNames: new Set(["is-on"]),
|
|
228
|
+
})!;
|
|
229
|
+
host.className = "fade";
|
|
230
|
+
host.classList.add("highlight");
|
|
231
|
+
host.setAttribute("class", host.getAttribute("class")!);
|
|
232
|
+
await wait(0);
|
|
233
|
+
expect(cb).not.toHaveBeenCalled();
|
|
234
|
+
|
|
235
|
+
host.classList.add("is-on");
|
|
236
|
+
await wait(0);
|
|
237
|
+
expect(cb).toHaveBeenCalledTimes(1);
|
|
238
|
+
|
|
239
|
+
// removed and re-added in one batch: conservative, the re-add's record
|
|
240
|
+
// still differs from the current value (a rule re-run is harmless)
|
|
241
|
+
host.classList.remove("is-on");
|
|
242
|
+
host.classList.add("is-on");
|
|
243
|
+
await wait(0);
|
|
244
|
+
expect(cb).toHaveBeenCalledTimes(2);
|
|
245
|
+
expect(host.className).toBe("fade highlight is-on");
|
|
246
|
+
observer.disconnect();
|
|
247
|
+
|
|
248
|
+
const all = vi.fn();
|
|
249
|
+
const unfiltered = observe(host, ["class"], all)!;
|
|
250
|
+
host.classList.add("other");
|
|
251
|
+
await wait(0);
|
|
252
|
+
expect(all).toHaveBeenCalledTimes(1);
|
|
253
|
+
unfiltered.disconnect();
|
|
254
|
+
host.remove();
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
describe("paint scheduler", () => {
|
|
259
|
+
// one test below fakes timers; make sure a failure there cannot leak them
|
|
260
|
+
afterEach(() => {
|
|
261
|
+
vi.useRealTimers();
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it("dedupes commit callbacks and runs them once per tick", async () => {
|
|
265
|
+
const a = vi.fn();
|
|
266
|
+
const b = vi.fn();
|
|
267
|
+
reqCommit(a);
|
|
268
|
+
reqCommit(b);
|
|
269
|
+
reqCommit(a);
|
|
270
|
+
await wait(0);
|
|
271
|
+
expect(a).toHaveBeenCalledTimes(1);
|
|
272
|
+
expect(b).toHaveBeenCalledTimes(1);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it("runs high-priority paints before low-priority ones", async () => {
|
|
276
|
+
const order: string[] = [];
|
|
277
|
+
schedulePaint(() => order.push("low"));
|
|
278
|
+
schedulePaint(() => order.push("high"), 0);
|
|
279
|
+
await wait(0);
|
|
280
|
+
expect(order).toEqual(["high", "low"]);
|
|
281
|
+
expect(PAINT_QUEUES.every((q) => q.size === 0)).toBe(true);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it("commits a paint scheduled during a commit in the next commit", async () => {
|
|
285
|
+
const order: string[] = [];
|
|
286
|
+
schedulePaint(() => {
|
|
287
|
+
order.push("first");
|
|
288
|
+
schedulePaint(() => order.push("nested"));
|
|
289
|
+
});
|
|
290
|
+
await wait(0);
|
|
291
|
+
expect(order).toEqual(["first"]);
|
|
292
|
+
await wait(0);
|
|
293
|
+
expect(order).toEqual(["first", "nested"]);
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it("logs a throwing paint and still commits the others", async () => {
|
|
297
|
+
const error = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
298
|
+
const order: string[] = [];
|
|
299
|
+
schedulePaint(() => order.push("before"));
|
|
300
|
+
schedulePaint(() => {
|
|
301
|
+
throw new Error("boom");
|
|
302
|
+
});
|
|
303
|
+
schedulePaint(() => order.push("after"));
|
|
304
|
+
await wait(0);
|
|
305
|
+
expect(order).toEqual(["before", "after"]);
|
|
306
|
+
expect(PAINT_QUEUES.every((q) => q.size === 0)).toBe(true);
|
|
307
|
+
expect(error).toHaveBeenCalled();
|
|
308
|
+
// the failed paint is not retried on the next commit
|
|
309
|
+
schedulePaint(() => order.push("next"));
|
|
310
|
+
await wait(0);
|
|
311
|
+
expect(order).toEqual(["before", "after", "next"]);
|
|
312
|
+
error.mockRestore();
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
// Fake timers: the held paint is asserted to still be held after the plain
|
|
316
|
+
// one commits, which a real `wait(0)` can outlive once the 20ms delay does.
|
|
317
|
+
it("holds a paint with a transition delay back, then commits it", async () => {
|
|
318
|
+
vi.useFakeTimers();
|
|
319
|
+
const order: string[] = [];
|
|
320
|
+
schedulePaint(() => order.push("delayed"), 1, {
|
|
321
|
+
transition: { types: [], timeout: 50, delay: 20, ifActive: "skip", source: "@view-transition" },
|
|
322
|
+
});
|
|
323
|
+
schedulePaint(() => order.push("plain"));
|
|
324
|
+
// a held paint is not queued: nothing is busy on its behalf
|
|
325
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
326
|
+
expect(order).toEqual(["plain"]);
|
|
327
|
+
expect(isQuarkBusy()).toBe(false);
|
|
328
|
+
await vi.advanceTimersByTimeAsync(30);
|
|
329
|
+
expect(order).toEqual(["plain", "delayed"]);
|
|
330
|
+
vi.useRealTimers();
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
describe("settle", () => {
|
|
335
|
+
afterEach(() => {
|
|
336
|
+
unregisterAll();
|
|
337
|
+
document.body.innerHTML = "";
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it("resolves settled once nothing is queued, and reports queued paints as busy", async () => {
|
|
341
|
+
expect(await whenSettled()).toBe("settled");
|
|
342
|
+
let painted = false;
|
|
343
|
+
schedulePaint(() => {
|
|
344
|
+
painted = true;
|
|
345
|
+
});
|
|
346
|
+
expect(isQuarkBusy()).toBe(true);
|
|
347
|
+
expect(await whenSettled()).toBe("settled");
|
|
348
|
+
expect(painted).toBe(true);
|
|
349
|
+
expect(isQuarkBusy()).toBe(false);
|
|
350
|
+
expect(Quark.whenSettled).toBe(whenSettled);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it("waits for tracked async work on a connected element, and ignores it once detached", async () => {
|
|
354
|
+
const host = document.createElement("div");
|
|
355
|
+
document.body.append(host);
|
|
356
|
+
let release!: () => void;
|
|
357
|
+
const work = new Promise<void>((resolve) => (release = resolve));
|
|
358
|
+
trackPending(work, host);
|
|
359
|
+
expect(isQuarkBusy()).toBe(true);
|
|
360
|
+
const settled = whenSettled({ timeout: 500 });
|
|
361
|
+
await wait(5);
|
|
362
|
+
release();
|
|
363
|
+
expect(await settled).toBe("settled");
|
|
364
|
+
// a never-settling promise stops counting when its element leaves
|
|
365
|
+
trackPending(new Promise(() => {}), host);
|
|
366
|
+
expect(isQuarkBusy()).toBe(true);
|
|
367
|
+
host.remove();
|
|
368
|
+
expect(isQuarkBusy()).toBe(false);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it("counts element-less work until it settles", async () => {
|
|
372
|
+
let release!: () => void;
|
|
373
|
+
trackPending(new Promise<void>((resolve) => (release = resolve)));
|
|
374
|
+
expect(isQuarkBusy()).toBe(true);
|
|
375
|
+
release();
|
|
376
|
+
await wait(0);
|
|
377
|
+
expect(isQuarkBusy()).toBe(false);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it("resolves timeout when Quark stays busy past the cap", async () => {
|
|
381
|
+
const host = document.createElement("div");
|
|
382
|
+
document.body.append(host);
|
|
383
|
+
trackPending(new Promise(() => {}), host);
|
|
384
|
+
const start = Date.now();
|
|
385
|
+
expect(await whenSettled({ timeout: 30 })).toBe("timeout");
|
|
386
|
+
expect(Date.now() - start).toBeGreaterThanOrEqual(25);
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it("waits until the owner matches a selector, or leaves the document", async () => {
|
|
390
|
+
const owner = document.createElement("div");
|
|
391
|
+
document.body.append(owner);
|
|
392
|
+
const matched = whenSettled({
|
|
393
|
+
timeout: 500,
|
|
394
|
+
until: { selector: "[is-success]", owner: new WeakRef(owner) },
|
|
395
|
+
});
|
|
396
|
+
await wait(10);
|
|
397
|
+
owner.setAttribute("is-success", "");
|
|
398
|
+
expect(await matched).toBe("until");
|
|
399
|
+
const never = whenSettled({
|
|
400
|
+
timeout: 30,
|
|
401
|
+
until: { selector: "[is-error]", owner: new WeakRef(owner) },
|
|
402
|
+
});
|
|
403
|
+
expect(await never).toBe("timeout");
|
|
404
|
+
const removed = whenSettled({
|
|
405
|
+
timeout: 500,
|
|
406
|
+
until: { selector: "[is-error]", owner: new WeakRef(owner) },
|
|
407
|
+
});
|
|
408
|
+
owner.remove();
|
|
409
|
+
expect(await removed).toBe("until");
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
it("waits until a thenable settles either way", async () => {
|
|
413
|
+
let reject!: (reason: unknown) => void;
|
|
414
|
+
const thenable = new Promise((_, r) => (reject = r));
|
|
415
|
+
const settled = whenSettled({ timeout: 500, until: { thenable } });
|
|
416
|
+
await wait(10);
|
|
417
|
+
reject(new Error("nope"));
|
|
418
|
+
expect(await settled).toBe("until");
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
it("is busy while a registered sheet loads its @use modules", async () => {
|
|
422
|
+
const original = Quark.moduleLoader;
|
|
423
|
+
let release!: (mod: Record<string, unknown>) => void;
|
|
424
|
+
Quark.moduleLoader = () => new Promise((resolve) => (release = resolve));
|
|
425
|
+
try {
|
|
426
|
+
const { root } = mount(`<p id="out"></p>`, `@use "/slow.js" as *; #out { content: greet(); }`);
|
|
427
|
+
await wait(5);
|
|
428
|
+
expect(isQuarkBusy()).toBe(true);
|
|
429
|
+
const settled = whenSettled({ timeout: 500 });
|
|
430
|
+
release({ greet: () => "hi" });
|
|
431
|
+
expect(await settled).toBe("settled");
|
|
432
|
+
expect(root.querySelector("#out")!.textContent).toBe("hi");
|
|
433
|
+
} finally {
|
|
434
|
+
Quark.moduleLoader = original;
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
describe("utils", () => {
|
|
440
|
+
it("falls back to body for document-level hosts", () => {
|
|
441
|
+
expect(getQuarkHost(null)).toBe(document.body);
|
|
442
|
+
expect(getQuarkHost(document.documentElement)).toBe(document.body);
|
|
443
|
+
const div = document.createElement("div");
|
|
444
|
+
expect(getQuarkHost(div)).toBe(div);
|
|
445
|
+
expect(getQuarkMin(undefined as unknown as string)).toBe("");
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
it("formats log payloads with and without run context", () => {
|
|
449
|
+
const full = QuarkLogger.formatArgs([
|
|
450
|
+
"name",
|
|
451
|
+
{
|
|
452
|
+
method: "run",
|
|
453
|
+
sheetId: 1,
|
|
454
|
+
runId: "abc",
|
|
455
|
+
ruleId: 2,
|
|
456
|
+
options: {},
|
|
457
|
+
extra: 1,
|
|
458
|
+
},
|
|
459
|
+
]);
|
|
460
|
+
expect(full[0]).toBe("");
|
|
461
|
+
expect(full).toContain("sheet: 1 | ");
|
|
462
|
+
expect(full).toContain(" | extra: ");
|
|
463
|
+
const bare = QuarkLogger.formatArgs(["name", {}]);
|
|
464
|
+
expect(bare[0]).toBe(" ");
|
|
465
|
+
expect(bare).not.toContain("sheet: 1 | ");
|
|
466
|
+
expect(bare.at(-1)).toBe(" | (name)");
|
|
467
|
+
});
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
describe("ast helpers", () => {
|
|
471
|
+
it("splits a block into the statements Quark executes", () => {
|
|
472
|
+
const src =
|
|
473
|
+
'p { @use "/a.js"; /* note */ content: "a"; @warn "w"; @on click { x: 1; } }';
|
|
474
|
+
const body = (parse(src).body[0] as any).block.body;
|
|
475
|
+
const view = viewBlock(body);
|
|
476
|
+
expect(
|
|
477
|
+
view.declarations.map((d) => declarationStrings(d, src).key)
|
|
478
|
+
).toEqual(["content"]);
|
|
479
|
+
expect(view.diagnostics.map((d) => d.name)).toEqual(["warn"]);
|
|
480
|
+
expect(view.listeners).toHaveLength(1);
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
it("collects @use rules through blocks and skips blockless at-rules", () => {
|
|
484
|
+
const src = '@warn "x"; @scope { @use "/a"; p { @use "/b"; } }';
|
|
485
|
+
const uses = collectUseRules(parse(src).body);
|
|
486
|
+
expect(uses.map((u) => u.url)).toEqual(["/a", "/b"]);
|
|
487
|
+
});
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
describe("selector utils", () => {
|
|
491
|
+
it("substitutes & inside pseudo-class arguments", () => {
|
|
492
|
+
expect(buildSelectorLine("span:not(&)", "div")).toBe("span:not(div)");
|
|
493
|
+
expect(buildSelectorLine("li:nth-child(2)", "ul")).toBe(
|
|
494
|
+
"ul li:nth-child(2)"
|
|
495
|
+
);
|
|
496
|
+
expect(buildSelectorLine("", "ul")).toBe("");
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
it("only honours :scope in the first compound", () => {
|
|
500
|
+
expect(splitScopePrefix("div :scope")).toEqual({
|
|
501
|
+
hostCompound: null,
|
|
502
|
+
rest: "div :scope",
|
|
503
|
+
});
|
|
504
|
+
expect(splitScopePrefix(":scope[is-on] > span")).toEqual({
|
|
505
|
+
hostCompound: "[is-on]",
|
|
506
|
+
rest: "> span",
|
|
507
|
+
});
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
it("collects observed attributes from nested pseudo-classes", () => {
|
|
511
|
+
const { path, attrs } = parseCssSelector(
|
|
512
|
+
"div:not([data-a]) > [data-b]:has([q-x], [data-c])"
|
|
513
|
+
);
|
|
514
|
+
expect(path).toEqual([
|
|
515
|
+
"div:not([data-a])",
|
|
516
|
+
">",
|
|
517
|
+
"[data-b]:has([q-x], [data-c])",
|
|
518
|
+
]);
|
|
519
|
+
expect([...attrs].sort()).toEqual(["data-a", "data-b", "data-c"]);
|
|
520
|
+
// a leading combinator (the remainder of `:scope > span`) has no compound before it
|
|
521
|
+
expect(parseCssSelector("> span").path).toEqual([">", "span"]);
|
|
522
|
+
});
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
describe("outermostElements", () => {
|
|
526
|
+
it("keeps one entry per outermost element, dropping duplicates", () => {
|
|
527
|
+
const outer = document.createElement("div");
|
|
528
|
+
const inner = document.createElement("span");
|
|
529
|
+
outer.appendChild(inner);
|
|
530
|
+
document.body.appendChild(outer);
|
|
531
|
+
expect(outermostElements([inner, outer, inner])).toEqual([outer]);
|
|
532
|
+
expect(outermostElements([inner])).toEqual([inner]);
|
|
533
|
+
outer.remove();
|
|
534
|
+
});
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
describe("Property construction", () => {
|
|
538
|
+
afterEach(() => {
|
|
539
|
+
unregisterAll();
|
|
540
|
+
document.body.innerHTML = "";
|
|
541
|
+
vi.restoreAllMocks();
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
it("logs an invalid expression and stays inert", () => {
|
|
545
|
+
const error = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
546
|
+
const parent = { observedAttrs: new Set<string>() } as any;
|
|
547
|
+
const attribute = new Attribute({ key: "data-x", value: "1 ?", parent });
|
|
548
|
+
expect(attribute.parsedValue).toBeNull();
|
|
549
|
+
expect(attribute.isReactive).toBe(false);
|
|
550
|
+
expect(error).toHaveBeenCalledTimes(1);
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
it("dispatches a def written outside a run immediately", async () => {
|
|
554
|
+
const { root, quark } = mount(
|
|
555
|
+
`<article><p bind-x></p></article>`,
|
|
556
|
+
`article { $n: 1; }
|
|
557
|
+
[bind-x] { content: $n; }`
|
|
558
|
+
);
|
|
559
|
+
await flush();
|
|
560
|
+
const article = root.querySelector("article") as HTMLElement;
|
|
561
|
+
const def = quark.rules[0].variables[0] as Variable;
|
|
562
|
+
def.value = "2";
|
|
563
|
+
// no run in flight (no trace) → writeBinding dispatches the event itself
|
|
564
|
+
def.run(article, {});
|
|
565
|
+
await flush();
|
|
566
|
+
expect(root.querySelector("[bind-x]")?.textContent).toBe("2");
|
|
567
|
+
});
|
|
568
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import "@excom/quark-sheet";
|
|
2
|
+
import {
|
|
3
|
+
afterEach,
|
|
4
|
+
beforeEach,
|
|
5
|
+
describe,
|
|
6
|
+
expect,
|
|
7
|
+
it,
|
|
8
|
+
} from "@excom/heft-rig/profiles/default/config/test-utils";
|
|
9
|
+
import {
|
|
10
|
+
expectComplexity,
|
|
11
|
+
flush,
|
|
12
|
+
installDemoModules,
|
|
13
|
+
measureComplexity,
|
|
14
|
+
mountView,
|
|
15
|
+
readDemo,
|
|
16
|
+
restoreDemoModules,
|
|
17
|
+
} from "./view-helpers";
|
|
18
|
+
|
|
19
|
+
describe("iterate view", () => {
|
|
20
|
+
beforeEach(() => installDemoModules());
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
document.body.innerHTML = "";
|
|
23
|
+
restoreDemoModules();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("renders the planet list", async () => {
|
|
27
|
+
const { root, quark } = await mountView(readDemo(import.meta.url, "iterate"));
|
|
28
|
+
await flush();
|
|
29
|
+
const meter = measureComplexity(quark!);
|
|
30
|
+
const budget = meter.take();
|
|
31
|
+
meter.stop();
|
|
32
|
+
expect([...root.querySelectorAll("li")].map((el) => el.textContent)).toEqual([
|
|
33
|
+
"Mercury",
|
|
34
|
+
"Venus",
|
|
35
|
+
"Earth",
|
|
36
|
+
"Mars",
|
|
37
|
+
]);
|
|
38
|
+
expectComplexity(budget);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import "@excom/quark-sheet";
|
|
2
|
+
import {
|
|
3
|
+
afterEach,
|
|
4
|
+
beforeEach,
|
|
5
|
+
describe,
|
|
6
|
+
expect,
|
|
7
|
+
it,
|
|
8
|
+
} from "@excom/heft-rig/profiles/default/config/test-utils";
|
|
9
|
+
import {
|
|
10
|
+
click,
|
|
11
|
+
expectComplexity,
|
|
12
|
+
flush,
|
|
13
|
+
installDemoModules,
|
|
14
|
+
measureComplexity,
|
|
15
|
+
mountView,
|
|
16
|
+
readDemo,
|
|
17
|
+
restoreDemoModules,
|
|
18
|
+
} from "./view-helpers";
|
|
19
|
+
|
|
20
|
+
describe("js-api view", () => {
|
|
21
|
+
beforeEach(() => installDemoModules());
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
document.body.innerHTML = "";
|
|
24
|
+
restoreDemoModules();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("increments the click count through element.quark", async () => {
|
|
28
|
+
const { root, quark } = await mountView(readDemo(import.meta.url, "js-api"));
|
|
29
|
+
expect(root.querySelector("output")?.textContent).toBe("Clicked 0 times");
|
|
30
|
+
const meter = measureComplexity(quark!);
|
|
31
|
+
click(root.querySelector("button"));
|
|
32
|
+
await flush();
|
|
33
|
+
const budget = meter.take();
|
|
34
|
+
meter.stop();
|
|
35
|
+
expect(root.querySelector("output")?.textContent).toBe("Clicked 1 times");
|
|
36
|
+
click(root.querySelector("button"));
|
|
37
|
+
await flush();
|
|
38
|
+
expect(root.querySelector("output")?.textContent).toBe("Clicked 2 times");
|
|
39
|
+
const owner = root.matches("[data-demo-counter]")
|
|
40
|
+
? root
|
|
41
|
+
: root.querySelector("[data-demo-counter]")!;
|
|
42
|
+
expect(owner.quark.getPropertyValue("$count")).toBe(2);
|
|
43
|
+
expectComplexity(budget);
|
|
44
|
+
});
|
|
45
|
+
});
|