@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,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@warn` / `@debug` / `@error` statements inside rules and blocks: they
|
|
3
|
+
* evaluate on the matched element and report to the logger and the
|
|
4
|
+
* DevTools hook; `@warn` / `@error` once per element and rule, `@debug` on
|
|
5
|
+
* every application. Nothing is written to the document.
|
|
6
|
+
*/
|
|
7
|
+
import { Quark } from "../../index";
|
|
8
|
+
import { QuarkLogger } from "../../src/utils";
|
|
9
|
+
import type { QuarkRenderer } from "../../src/devtools-hook";
|
|
10
|
+
import {
|
|
11
|
+
type DevtoolsHook,
|
|
12
|
+
NUCLEUS_DEVTOOLS_HOOK_KEY,
|
|
13
|
+
pathMatches,
|
|
14
|
+
type PublicizeMeta,
|
|
15
|
+
type PublicizePath,
|
|
16
|
+
} from "@excom/kit-devtools";
|
|
17
|
+
import {
|
|
18
|
+
afterEach,
|
|
19
|
+
beforeEach,
|
|
20
|
+
describe,
|
|
21
|
+
expect,
|
|
22
|
+
it,
|
|
23
|
+
vi,
|
|
24
|
+
} from "@excom/heft-rig/profiles/default/config/test-utils";
|
|
25
|
+
import { createSheet, flush, mount, unregisterAll } from "./helpers";
|
|
26
|
+
|
|
27
|
+
type Spy = ReturnType<typeof vi.spyOn>;
|
|
28
|
+
type LogArg = { message?: string; values?: unknown[] };
|
|
29
|
+
|
|
30
|
+
const messages = (spy: Spy) =>
|
|
31
|
+
spy.mock.calls
|
|
32
|
+
.map(([arg]) => String((arg as LogArg)?.message ?? arg))
|
|
33
|
+
.filter((m) => m.startsWith("Quark @"));
|
|
34
|
+
const values = (spy: Spy) =>
|
|
35
|
+
spy.mock.calls
|
|
36
|
+
.map(([arg]) => arg as LogArg)
|
|
37
|
+
.filter((arg) => String(arg?.message).startsWith("Quark @"))
|
|
38
|
+
.map((arg) => arg.values);
|
|
39
|
+
|
|
40
|
+
describe("@warn / @debug / @error", () => {
|
|
41
|
+
let warn: Spy;
|
|
42
|
+
let debug: Spy;
|
|
43
|
+
let error: Spy;
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
warn = vi.spyOn(QuarkLogger, "warn").mockImplementation(() => {});
|
|
46
|
+
debug = vi.spyOn(QuarkLogger, "debug").mockImplementation(() => {});
|
|
47
|
+
error = vi.spyOn(QuarkLogger, "error").mockImplementation(() => {});
|
|
48
|
+
});
|
|
49
|
+
afterEach(() => {
|
|
50
|
+
unregisterAll();
|
|
51
|
+
document.body.innerHTML = "";
|
|
52
|
+
vi.restoreAllMocks();
|
|
53
|
+
delete (globalThis as Record<string, unknown>)[
|
|
54
|
+
NUCLEUS_DEVTOOLS_HOOK_KEY
|
|
55
|
+
];
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("warns once per element and rule, naming the selector; the selector is the condition", async () => {
|
|
59
|
+
const { root } = mount(
|
|
60
|
+
`<img id="a" src="a.png"><img id="b" src="b.png" alt="fine">`,
|
|
61
|
+
`img:not([alt]) { @warn "img needs alt"; }`
|
|
62
|
+
);
|
|
63
|
+
await flush();
|
|
64
|
+
expect(messages(warn)).toEqual([
|
|
65
|
+
"Quark @warn (img:not([alt])): img needs alt",
|
|
66
|
+
]);
|
|
67
|
+
expect(values(warn)).toEqual([["img needs alt"]]);
|
|
68
|
+
const a = root.querySelector("#a")!;
|
|
69
|
+
const b = root.querySelector("#b")!;
|
|
70
|
+
// `a` stops and starts matching again: no second warning for it
|
|
71
|
+
a.setAttribute("alt", "");
|
|
72
|
+
await flush();
|
|
73
|
+
a.removeAttribute("alt");
|
|
74
|
+
await flush();
|
|
75
|
+
expect(messages(warn)).toHaveLength(1);
|
|
76
|
+
// a new offender is its own warning
|
|
77
|
+
b.removeAttribute("alt");
|
|
78
|
+
await flush();
|
|
79
|
+
expect(messages(warn)).toHaveLength(2);
|
|
80
|
+
// nothing was written to the document
|
|
81
|
+
expect([...a.attributes].map((attr) => attr.name)).toEqual(["id", "src"]);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("@debug reports every application and again when a binding it reads changes", async () => {
|
|
85
|
+
const { root } = mount(
|
|
86
|
+
`<section id="host" data-count="1"><span bind-count></span></section>`,
|
|
87
|
+
`#host {
|
|
88
|
+
$count: +attr("data-count");
|
|
89
|
+
@debug "count", $count;
|
|
90
|
+
[bind-count] { content: $count; }
|
|
91
|
+
}`
|
|
92
|
+
);
|
|
93
|
+
await flush();
|
|
94
|
+
expect(values(debug)).toEqual([["count", 1]]);
|
|
95
|
+
root.querySelector("#host")!.setAttribute("data-count", "2");
|
|
96
|
+
await flush();
|
|
97
|
+
expect(values(debug).at(-1)).toEqual(["count", 2]);
|
|
98
|
+
expect(values(debug)).toHaveLength(2);
|
|
99
|
+
expect(root.querySelector("[bind-count]")!.textContent).toBe("2");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("@error reports at error level, inside an @on block with event data", async () => {
|
|
103
|
+
const { root } = mount(
|
|
104
|
+
`<form id="f"><button type="button">go</button></form>`,
|
|
105
|
+
`#f { @on request-failed { @error "request failed", event.detail.code; } }`
|
|
106
|
+
);
|
|
107
|
+
await flush();
|
|
108
|
+
expect(messages(error)).toEqual([]);
|
|
109
|
+
root
|
|
110
|
+
.querySelector("#f")!
|
|
111
|
+
.dispatchEvent(
|
|
112
|
+
new CustomEvent("request-failed", {
|
|
113
|
+
bubbles: true,
|
|
114
|
+
detail: { code: 502 },
|
|
115
|
+
})
|
|
116
|
+
);
|
|
117
|
+
await flush();
|
|
118
|
+
expect(messages(error)).toEqual(["Quark @error (#f): request failed 502"]);
|
|
119
|
+
expect(values(error)).toEqual([["request failed", 502]]);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("reports one value per comma-list item and a single expression as one value", async () => {
|
|
123
|
+
mount(
|
|
124
|
+
`<p id="p"></p>`,
|
|
125
|
+
`#p { $pair: (1, 2); @debug $pair; @debug "a", "b"; @debug (x: 1); }`
|
|
126
|
+
);
|
|
127
|
+
await flush();
|
|
128
|
+
expect(values(debug)).toEqual([[[1, 2]], ["a", "b"], [{ x: 1 }]]);
|
|
129
|
+
expect(messages(debug)[2]).toBe('Quark @debug (#p): {"x":1}');
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("publishes quark/diagnostic with level, values, expression and the element", async () => {
|
|
133
|
+
const publications: { path: PublicizePath; meta: PublicizeMeta }[] = [];
|
|
134
|
+
const hook: DevtoolsHook = {
|
|
135
|
+
version: 1,
|
|
136
|
+
publicize: (path, meta) => {
|
|
137
|
+
publications.push({ path, meta });
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
Quark.attachDevtools(hook);
|
|
141
|
+
const { root } = mount(
|
|
142
|
+
`<img id="a" src="a.png">`,
|
|
143
|
+
`img:not([alt]) { @warn "img needs alt", attr("src"); }`
|
|
144
|
+
);
|
|
145
|
+
await flush();
|
|
146
|
+
const records = publications
|
|
147
|
+
.filter((p) => pathMatches(p.path, ["quark", "diagnostic"]))
|
|
148
|
+
.map((p) => p.meta);
|
|
149
|
+
expect(records).toHaveLength(1);
|
|
150
|
+
expect(records[0]).toMatchObject({
|
|
151
|
+
level: "warn",
|
|
152
|
+
key: "@warn",
|
|
153
|
+
tag: "img",
|
|
154
|
+
selector: "img:not([alt])",
|
|
155
|
+
expression: '"img needs alt", attr("src")',
|
|
156
|
+
values: ["img needs alt", "a.png"],
|
|
157
|
+
message: "img needs alt a.png",
|
|
158
|
+
});
|
|
159
|
+
expect((records[0].weakElement as WeakRef<Element>).deref()).toBe(
|
|
160
|
+
root.querySelector("#a")
|
|
161
|
+
);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("lists diagnostics on the rule for DevTools", async () => {
|
|
165
|
+
const renderers: QuarkRenderer[] = [];
|
|
166
|
+
Quark.attachDevtools({
|
|
167
|
+
version: 1,
|
|
168
|
+
inject: (renderer) => {
|
|
169
|
+
renderers.push(renderer as QuarkRenderer);
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
const { quark, register } = createSheet(
|
|
173
|
+
`<p id="p"></p>`,
|
|
174
|
+
`#p { data-a: 1; @warn "w"; @debug "d", 1; }`
|
|
175
|
+
);
|
|
176
|
+
register();
|
|
177
|
+
await flush();
|
|
178
|
+
const sheet = renderers
|
|
179
|
+
.find((r) => r.kind === "quark")!
|
|
180
|
+
.sheets()
|
|
181
|
+
.find((s) => s.sheetId === quark.id)!;
|
|
182
|
+
expect(sheet.rules[0].diagnostics).toEqual([
|
|
183
|
+
{ level: "warn", expression: '"w"' },
|
|
184
|
+
{ level: "debug", expression: '"d", 1' },
|
|
185
|
+
]);
|
|
186
|
+
expect(sheet.rules[0].declarations).toEqual([
|
|
187
|
+
{ key: "data-a", value: "1" },
|
|
188
|
+
]);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("rejects statements outside a rule at build", () => {
|
|
192
|
+
new Quark({ src: '@warn "top"; @delay 10 { x: 1; }' });
|
|
193
|
+
const built = error.mock.calls.map(([arg]) =>
|
|
194
|
+
String((arg as LogArg)?.message)
|
|
195
|
+
);
|
|
196
|
+
expect(built).toContain("Quark: @warn must be written inside a rule");
|
|
197
|
+
expect(built).toContain("Quark: @delay must be written inside a rule");
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("reports a failing expression as an error, not a diagnostic", async () => {
|
|
201
|
+
mount(`<p id="p"></p>`, `#p { @warn nope(); }`);
|
|
202
|
+
await flush();
|
|
203
|
+
expect(messages(warn)).toEqual([]);
|
|
204
|
+
expect(
|
|
205
|
+
error.mock.calls.some(([arg]) =>
|
|
206
|
+
String((arg as LogArg)?.message).includes(
|
|
207
|
+
"Could not resolve expression"
|
|
208
|
+
)
|
|
209
|
+
)
|
|
210
|
+
).toBe(true);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("works inside @view-transition blocks and nested rules", async () => {
|
|
214
|
+
mount(
|
|
215
|
+
`<div id="host"><span></span><span></span></div>`,
|
|
216
|
+
`#host { @view-transition { span { @warn "in block"; } } }`
|
|
217
|
+
);
|
|
218
|
+
await flush();
|
|
219
|
+
expect(messages(warn)).toEqual([
|
|
220
|
+
"Quark @warn (#host span): in block",
|
|
221
|
+
"Quark @warn (#host span): in block",
|
|
222
|
+
]);
|
|
223
|
+
});
|
|
224
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
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("dispatch view", () => {
|
|
21
|
+
beforeEach(() => installDemoModules());
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
document.body.innerHTML = "";
|
|
24
|
+
restoreDemoModules();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("relays a click to the panel as a custom event the panel's own block turns into State", async () => {
|
|
28
|
+
const { root, quark } = await mountView(readDemo(import.meta.url, "dispatch"));
|
|
29
|
+
const panel = root.querySelector("[aria-label='panel']")!;
|
|
30
|
+
const meter = measureComplexity(quark!);
|
|
31
|
+
click(root.querySelector("button"));
|
|
32
|
+
await flush();
|
|
33
|
+
const budget = meter.take();
|
|
34
|
+
meter.stop();
|
|
35
|
+
expect(panel.getAttribute("data-pings")).toBe("1");
|
|
36
|
+
expect(root.querySelector("output")?.textContent).toBe("pinged 1×");
|
|
37
|
+
click(root.querySelector("button"));
|
|
38
|
+
await flush();
|
|
39
|
+
expect(panel.getAttribute("data-pings")).toBe("2");
|
|
40
|
+
expect(root.querySelector("output")?.textContent).toBe("pinged 2×");
|
|
41
|
+
expectComplexity(budget);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `element.quark`: the JS side of bindings (`setProperty` /
|
|
3
|
+
* `setProperties` / `removeProperty` / `getPropertyValue`) on every
|
|
4
|
+
* element, and how sheets react to those writes.
|
|
5
|
+
*/
|
|
6
|
+
import { Quark } from "../../index";
|
|
7
|
+
import { BINDING_CHANGE_EVENT } from "../../src/bindings";
|
|
8
|
+
import { installElementApi, QuarkElementApi } from "../../src/element-api";
|
|
9
|
+
import {
|
|
10
|
+
afterEach,
|
|
11
|
+
describe,
|
|
12
|
+
expect,
|
|
13
|
+
fixture,
|
|
14
|
+
it,
|
|
15
|
+
vi,
|
|
16
|
+
} from "@excom/heft-rig/profiles/default/config/test-utils";
|
|
17
|
+
import { createSheet, flush, mount, unregisterAll } from "./helpers";
|
|
18
|
+
|
|
19
|
+
describe("element.quark", () => {
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
unregisterAll();
|
|
22
|
+
document.body.innerHTML = "";
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("is a lazy, stable accessor on every element, SVG included", () => {
|
|
26
|
+
const div = document.createElement("div");
|
|
27
|
+
expect(div.quark).toBeInstanceOf(QuarkElementApi);
|
|
28
|
+
expect(div.quark).toBe(div.quark);
|
|
29
|
+
const svg = document.createElementNS("http://www.w3.org/2000/svg", "g");
|
|
30
|
+
expect(svg.quark).toBeInstanceOf(QuarkElementApi);
|
|
31
|
+
expect(svg.quark).not.toBe(div.quark);
|
|
32
|
+
// not enumerable: never serialized alongside real properties
|
|
33
|
+
expect(Object.keys(div)).not.toContain("quark");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("installs once and stays configurable", () => {
|
|
37
|
+
const before = Object.getOwnPropertyDescriptor(Element.prototype, "quark");
|
|
38
|
+
installElementApi();
|
|
39
|
+
expect(
|
|
40
|
+
Object.getOwnPropertyDescriptor(Element.prototype, "quark")?.get
|
|
41
|
+
).toBe(before?.get);
|
|
42
|
+
expect(before?.configurable).toBe(true);
|
|
43
|
+
// a foreign prototype gets its own accessor
|
|
44
|
+
const proto = {};
|
|
45
|
+
installElementApi(proto);
|
|
46
|
+
expect(typeof Object.getOwnPropertyDescriptor(proto, "quark")?.get).toBe(
|
|
47
|
+
"function"
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("writes a binding on the owner and re-runs readers below it", async () => {
|
|
52
|
+
const { root, quark } = mount(
|
|
53
|
+
`<main><span bind-x></span></main>`,
|
|
54
|
+
`main { $x: "a"; }
|
|
55
|
+
[bind-x] { content: $x; }`
|
|
56
|
+
);
|
|
57
|
+
await flush();
|
|
58
|
+
const main = root.querySelector("main")!;
|
|
59
|
+
expect(root.querySelector("[bind-x]")?.textContent).toBe("a");
|
|
60
|
+
expect(main.quark.setProperty("$x", "b")).toBe(true);
|
|
61
|
+
await flush();
|
|
62
|
+
expect(root.querySelector("[bind-x]")?.textContent).toBe("b");
|
|
63
|
+
// same value: no change, no re-run
|
|
64
|
+
const runs = quark.rules[1].numberOfRuns;
|
|
65
|
+
expect(main.quark.setProperty("x", "b")).toBe(false);
|
|
66
|
+
await flush();
|
|
67
|
+
expect(quark.rules[1].numberOfRuns).toBe(runs);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("works on an element no rule matched and shadows an ancestor's binding", async () => {
|
|
71
|
+
const { root } = mount(
|
|
72
|
+
`<main><section><span bind-x></span></section></main>`,
|
|
73
|
+
`main { $x: "outer"; }
|
|
74
|
+
[bind-x] { content: $x; }`
|
|
75
|
+
);
|
|
76
|
+
await flush();
|
|
77
|
+
const section = root.querySelector("section")!;
|
|
78
|
+
section.quark.setProperty("$x", "inner");
|
|
79
|
+
await flush();
|
|
80
|
+
expect(root.querySelector("[bind-x]")?.textContent).toBe("inner");
|
|
81
|
+
expect(section.quark.getPropertyValue("$x")).toBe("inner");
|
|
82
|
+
// removeProperty = unset: the reader falls through to the ancestor
|
|
83
|
+
expect(section.quark.removeProperty("$x")).toBe(true);
|
|
84
|
+
await flush();
|
|
85
|
+
expect(root.querySelector("[bind-x]")?.textContent).toBe("outer");
|
|
86
|
+
expect(section.quark.getPropertyValue("x")).toBe("outer");
|
|
87
|
+
expect(section.quark.removeProperty("$x")).toBe(false);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("getPropertyValue resolves upward like a rule would", () => {
|
|
91
|
+
const root = fixture<HTMLElement>(
|
|
92
|
+
`<div><main><section><span></span></section></main></div>`
|
|
93
|
+
);
|
|
94
|
+
const main = root.querySelector("main")!;
|
|
95
|
+
const span = root.querySelector("span")!;
|
|
96
|
+
expect(span.quark.getPropertyValue("$missing")).toBeUndefined();
|
|
97
|
+
main.quark.setProperty("$list", [1, 2]);
|
|
98
|
+
expect(span.quark.getPropertyValue("$list")).toEqual([1, 2]);
|
|
99
|
+
// by value: a new object is a change, an equal one is not
|
|
100
|
+
expect(main.quark.setProperty("$list", [1, 2])).toBe(false);
|
|
101
|
+
expect(main.quark.setProperty("$list", [1, 2, 3])).toBe(true);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("setProperties stores every value before announcing any of them", async () => {
|
|
105
|
+
const { root } = mount(
|
|
106
|
+
`<main><span bind-sum></span></main>`,
|
|
107
|
+
`main { $a: 1; $b: 1; }
|
|
108
|
+
[bind-sum] { content: "#{$a}+#{$b}"; }`
|
|
109
|
+
);
|
|
110
|
+
await flush();
|
|
111
|
+
const main = root.querySelector("main")!;
|
|
112
|
+
const seen: string[] = [];
|
|
113
|
+
root.addEventListener(BINDING_CHANGE_EVENT, () => {
|
|
114
|
+
seen.push(root.querySelector("[bind-sum]")?.textContent ?? "");
|
|
115
|
+
seen.push(`${main.quark.getPropertyValue("$a")}/${main.quark.getPropertyValue("$b")}`);
|
|
116
|
+
});
|
|
117
|
+
expect(main.quark.setProperties({ $a: 2, b: 3 })).toBe(true);
|
|
118
|
+
await flush();
|
|
119
|
+
expect(root.querySelector("[bind-sum]")?.textContent).toBe("2+3");
|
|
120
|
+
// both values were stored when the first change event fired
|
|
121
|
+
expect(seen[1]).toBe("2/3");
|
|
122
|
+
expect(main.quark.setProperties({ $a: 2, $b: 3 })).toBe(false);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("tags the change event with sheetId null for DevTools", () => {
|
|
126
|
+
const el = fixture<HTMLElement>(`<div></div>`);
|
|
127
|
+
const heard = vi.fn();
|
|
128
|
+
el.addEventListener(BINDING_CHANGE_EVENT, (e) =>
|
|
129
|
+
heard((e as CustomEvent).detail)
|
|
130
|
+
);
|
|
131
|
+
el.quark.setProperty("$k", 1);
|
|
132
|
+
expect(heard).toHaveBeenCalledWith(
|
|
133
|
+
expect.objectContaining({ name: "$k", sheetId: null })
|
|
134
|
+
);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("is State, not an event: a write before the sheet registers is read on the first run", async () => {
|
|
138
|
+
const { root, register } = createSheet(
|
|
139
|
+
`<main><span bind-x></span></main>`,
|
|
140
|
+
`[bind-x] { content: $x; }`
|
|
141
|
+
);
|
|
142
|
+
root.querySelector("main")!.quark.setProperty("$x", "early");
|
|
143
|
+
register();
|
|
144
|
+
await flush();
|
|
145
|
+
expect(root.querySelector("[bind-x]")?.textContent).toBe("early");
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("reaches readers in another sheet below the owner", async () => {
|
|
149
|
+
const root = fixture<HTMLElement>(
|
|
150
|
+
`<div><main><div id="s1"></div><section><div id="s2"></div><span bind-x></span></section></main></div>`
|
|
151
|
+
);
|
|
152
|
+
const outer = new Quark({ src: `:scope { $x: "one"; }`, options: { isScoped: true } });
|
|
153
|
+
outer.register({ sheetElement: root.querySelector("#s1") as HTMLElement });
|
|
154
|
+
const inner = new Quark({ src: `[bind-x] { content: $x; }`, options: { isScoped: true } });
|
|
155
|
+
inner.register({ sheetElement: root.querySelector("#s2") as HTMLElement });
|
|
156
|
+
await flush();
|
|
157
|
+
expect(root.querySelector("[bind-x]")?.textContent).toBe("one");
|
|
158
|
+
root.querySelector("main")!.quark.setProperty("$x", "two");
|
|
159
|
+
await flush();
|
|
160
|
+
expect(root.querySelector("[bind-x]")?.textContent).toBe("two");
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("a rule's def re-running rewrites a JS value (last writer wins)", async () => {
|
|
164
|
+
const { root } = mount(
|
|
165
|
+
`<main><span bind-x></span></main>`,
|
|
166
|
+
`main { $x: attr("data-mode"); }
|
|
167
|
+
[bind-x] { content: $x; }`
|
|
168
|
+
);
|
|
169
|
+
await flush();
|
|
170
|
+
const main = root.querySelector("main")!;
|
|
171
|
+
main.quark.setProperty("$x", "js");
|
|
172
|
+
await flush();
|
|
173
|
+
expect(root.querySelector("[bind-x]")?.textContent).toBe("js");
|
|
174
|
+
// the def's dependency changes: the rule writes again and wins
|
|
175
|
+
main.setAttribute("data-mode", "rule");
|
|
176
|
+
await flush();
|
|
177
|
+
expect(root.querySelector("[bind-x]")?.textContent).toBe("rule");
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("lets a @use handler raise state without a signal box", async () => {
|
|
181
|
+
const { root } = mount(
|
|
182
|
+
`<div data-demo-counter><button></button><output></output></div>`,
|
|
183
|
+
`:scope { $count: 0; }
|
|
184
|
+
button { @on click (handle: bump(closest("[data-demo-counter]"))); }
|
|
185
|
+
output { content: "#{$count}"; }`,
|
|
186
|
+
{
|
|
187
|
+
bump: (owner: Element) => () => {
|
|
188
|
+
owner.quark.setProperty(
|
|
189
|
+
"$count",
|
|
190
|
+
((owner.quark.getPropertyValue("$count") as number) ?? 0) + 1
|
|
191
|
+
);
|
|
192
|
+
},
|
|
193
|
+
}
|
|
194
|
+
);
|
|
195
|
+
await flush();
|
|
196
|
+
const button = root.querySelector("button")!;
|
|
197
|
+
button.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
198
|
+
button.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
|
199
|
+
await flush();
|
|
200
|
+
expect(root.querySelector("output")?.textContent).toBe("2");
|
|
201
|
+
});
|
|
202
|
+
});
|