@praxisjs/css 0.3.4 → 0.3.6
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/CHANGELOG.md +31 -0
- package/dist/builder/css-builder.d.ts.map +1 -1
- package/dist/builder/css-builder.js +68 -74
- package/dist/builder/css-builder.js.map +1 -1
- package/dist/builder/keyframes.d.ts +5 -0
- package/dist/builder/keyframes.d.ts.map +1 -1
- package/dist/builder/keyframes.js +12 -14
- package/dist/builder/keyframes.js.map +1 -1
- package/dist/cx.d.ts.map +1 -1
- package/dist/cx.js +23 -18
- package/dist/cx.js.map +1 -1
- package/dist/decorators/param.d.ts +1 -2
- package/dist/decorators/param.d.ts.map +1 -1
- package/dist/decorators/param.js +9 -17
- package/dist/decorators/param.js.map +1 -1
- package/dist/decorators/style.d.ts.map +1 -1
- package/dist/decorators/style.js +61 -33
- package/dist/decorators/style.js.map +1 -1
- package/dist/decorators/styled.d.ts.map +1 -1
- package/dist/decorators/styled.js +112 -95
- package/dist/decorators/styled.js.map +1 -1
- package/dist/extract.d.ts.map +1 -1
- package/dist/extract.js +19 -35
- package/dist/extract.js.map +1 -1
- package/dist/global.d.ts +3 -0
- package/dist/global.d.ts.map +1 -1
- package/dist/global.js +9 -4
- package/dist/global.js.map +1 -1
- package/dist/internal/kebab.d.ts +5 -0
- package/dist/internal/kebab.d.ts.map +1 -0
- package/dist/internal/kebab.js +23 -0
- package/dist/internal/kebab.js.map +1 -0
- package/dist/internal/lifecycle.d.ts +15 -0
- package/dist/internal/lifecycle.d.ts.map +1 -0
- package/dist/internal/lifecycle.js +16 -0
- package/dist/internal/lifecycle.js.map +1 -0
- package/dist/internal/registry.d.ts +5 -1
- package/dist/internal/registry.d.ts.map +1 -1
- package/dist/internal/registry.js +9 -23
- package/dist/internal/registry.js.map +1 -1
- package/dist/internal/sheet.d.ts +20 -0
- package/dist/internal/sheet.d.ts.map +1 -0
- package/dist/internal/sheet.js +56 -0
- package/dist/internal/sheet.js.map +1 -0
- package/dist/internal/static-mode.d.ts +2 -0
- package/dist/internal/static-mode.d.ts.map +1 -0
- package/dist/internal/static-mode.js +2 -0
- package/dist/internal/static-mode.js.map +1 -0
- package/dist/preflight.d.ts +3 -1
- package/dist/preflight.d.ts.map +1 -1
- package/dist/preflight.js +7 -3
- package/dist/preflight.js.map +1 -1
- package/dist/tokens/decorators.d.ts.map +1 -1
- package/dist/tokens/decorators.js +13 -4
- package/dist/tokens/decorators.js.map +1 -1
- package/dist/tokens/theme-instance.d.ts +5 -0
- package/dist/tokens/theme-instance.d.ts.map +1 -1
- package/dist/tokens/theme-instance.js +25 -17
- package/dist/tokens/theme-instance.js.map +1 -1
- package/dist/tokens/token-sheet.d.ts.map +1 -1
- package/dist/tokens/token-sheet.js +12 -9
- package/dist/tokens/token-sheet.js.map +1 -1
- package/package.json +13 -5
- package/src/builder/css-builder.ts +79 -83
- package/src/builder/keyframes.ts +12 -20
- package/src/cx.ts +20 -13
- package/src/decorators/param.ts +12 -16
- package/src/decorators/style.ts +79 -35
- package/src/decorators/styled.ts +127 -111
- package/src/extract.ts +18 -34
- package/src/global.ts +10 -9
- package/src/internal/kebab.ts +23 -0
- package/src/internal/lifecycle.ts +21 -0
- package/src/internal/registry.ts +9 -35
- package/src/internal/sheet.ts +71 -0
- package/src/internal/static-mode.ts +6 -0
- package/src/preflight.ts +8 -3
- package/src/tokens/decorators.ts +15 -3
- package/src/tokens/theme-instance.ts +26 -16
- package/src/tokens/token-sheet.ts +12 -12
- package/dist/__tests__/css-builder.test.d.ts +0 -2
- package/dist/__tests__/css-builder.test.d.ts.map +0 -1
- package/dist/__tests__/css-builder.test.js +0 -198
- package/dist/__tests__/css-builder.test.js.map +0 -1
- package/dist/__tests__/cx.test.d.ts +0 -2
- package/dist/__tests__/cx.test.d.ts.map +0 -1
- package/dist/__tests__/cx.test.js +0 -47
- package/dist/__tests__/cx.test.js.map +0 -1
- package/dist/__tests__/decorators.test.d.ts +0 -2
- package/dist/__tests__/decorators.test.d.ts.map +0 -1
- package/dist/__tests__/decorators.test.js +0 -264
- package/dist/__tests__/decorators.test.js.map +0 -1
- package/dist/__tests__/extract.test.d.ts +0 -2
- package/dist/__tests__/extract.test.d.ts.map +0 -1
- package/dist/__tests__/extract.test.js +0 -416
- package/dist/__tests__/extract.test.js.map +0 -1
- package/dist/__tests__/global.test.d.ts +0 -2
- package/dist/__tests__/global.test.d.ts.map +0 -1
- package/dist/__tests__/global.test.js +0 -136
- package/dist/__tests__/global.test.js.map +0 -1
- package/dist/__tests__/keyframes.test.d.ts +0 -2
- package/dist/__tests__/keyframes.test.d.ts.map +0 -1
- package/dist/__tests__/keyframes.test.js +0 -90
- package/dist/__tests__/keyframes.test.js.map +0 -1
- package/dist/__tests__/preflight.test.d.ts +0 -2
- package/dist/__tests__/preflight.test.d.ts.map +0 -1
- package/dist/__tests__/preflight.test.js +0 -74
- package/dist/__tests__/preflight.test.js.map +0 -1
- package/dist/__tests__/registry.test.d.ts +0 -2
- package/dist/__tests__/registry.test.d.ts.map +0 -1
- package/dist/__tests__/registry.test.js +0 -132
- package/dist/__tests__/registry.test.js.map +0 -1
- package/dist/__tests__/style.test.d.ts +0 -2
- package/dist/__tests__/style.test.d.ts.map +0 -1
- package/dist/__tests__/style.test.js +0 -281
- package/dist/__tests__/style.test.js.map +0 -1
- package/dist/__tests__/styled.test.d.ts +0 -2
- package/dist/__tests__/styled.test.d.ts.map +0 -1
- package/dist/__tests__/styled.test.js +0 -904
- package/dist/__tests__/styled.test.js.map +0 -1
- package/dist/__tests__/theme-instance.test.d.ts +0 -2
- package/dist/__tests__/theme-instance.test.d.ts.map +0 -1
- package/dist/__tests__/theme-instance.test.js +0 -309
- package/dist/__tests__/theme-instance.test.js.map +0 -1
- package/dist/__tests__/theme-uninit.test.d.ts +0 -2
- package/dist/__tests__/theme-uninit.test.d.ts.map +0 -1
- package/dist/__tests__/theme-uninit.test.js +0 -10
- package/dist/__tests__/theme-uninit.test.js.map +0 -1
- package/dist/__tests__/token-sheet.test.d.ts +0 -2
- package/dist/__tests__/token-sheet.test.d.ts.map +0 -1
- package/dist/__tests__/token-sheet.test.js +0 -74
- package/dist/__tests__/token-sheet.test.js.map +0 -1
- package/src/__tests__/css-builder.test.ts +0 -230
- package/src/__tests__/cx.test.ts +0 -63
- package/src/__tests__/decorators.test.ts +0 -127
- package/src/__tests__/extract.test.ts +0 -459
- package/src/__tests__/global.test.ts +0 -163
- package/src/__tests__/keyframes.test.ts +0 -103
- package/src/__tests__/preflight.test.ts +0 -85
- package/src/__tests__/registry.test.ts +0 -151
- package/src/__tests__/style.test.ts +0 -170
- package/src/__tests__/styled.test.ts +0 -443
- package/src/__tests__/theme-instance.test.ts +0 -374
- package/src/__tests__/theme-uninit.test.ts +0 -12
- package/src/__tests__/token-sheet.test.ts +0 -87
- package/tsconfig.json +0 -8
|
@@ -1,374 +0,0 @@
|
|
|
1
|
-
// @vitest-environment jsdom
|
|
2
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
-
import { ThemeInstance, createThemeInstance, theme } from "../tokens/theme-instance";
|
|
4
|
-
import { TokenSheet } from "../tokens/token-sheet";
|
|
5
|
-
import { createCollector, setCollector } from "../internal/collector";
|
|
6
|
-
|
|
7
|
-
// ─── Fixtures ─────────────────────────────────────────────────────────────────
|
|
8
|
-
|
|
9
|
-
class AppTokens extends TokenSheet {
|
|
10
|
-
colorPrimary!: string;
|
|
11
|
-
colorBg!: string;
|
|
12
|
-
spaceMd!: string;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
class LightTheme extends AppTokens {
|
|
16
|
-
colorPrimary = "#3b82f6";
|
|
17
|
-
colorBg = "#ffffff";
|
|
18
|
-
spaceMd = "16px";
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
class DarkTheme extends LightTheme {
|
|
22
|
-
colorPrimary = "#60a5fa";
|
|
23
|
-
colorBg = "#0f172a";
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
class OtherTheme extends AppTokens {
|
|
27
|
-
colorPrimary = "#10b981";
|
|
28
|
-
colorBg = "#f0fdf4";
|
|
29
|
-
spaceMd = "20px";
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function styleEl(): HTMLStyleElement | null {
|
|
33
|
-
return document.querySelector("style[data-praxis-theme]");
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// ─── Setup / teardown ─────────────────────────────────────────────────────────
|
|
37
|
-
|
|
38
|
-
beforeEach(() => {
|
|
39
|
-
styleEl()?.remove();
|
|
40
|
-
localStorage.clear();
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
afterEach(() => {
|
|
44
|
-
styleEl()?.remove();
|
|
45
|
-
localStorage.clear();
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
// ─── Construction ─────────────────────────────────────────────────────────────
|
|
49
|
-
|
|
50
|
-
describe("ThemeInstance — construction", () => {
|
|
51
|
-
it("injects a <style data-praxis-theme> element on :root", () => {
|
|
52
|
-
const t = new ThemeInstance(LightTheme);
|
|
53
|
-
expect(styleEl()).not.toBeNull();
|
|
54
|
-
t.destroy();
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it("injected CSS contains the correct CSS var names and values", () => {
|
|
58
|
-
const t = new ThemeInstance(LightTheme);
|
|
59
|
-
const css = styleEl()?.textContent ?? "";
|
|
60
|
-
expect(css).toContain("--color-primary: #3b82f6");
|
|
61
|
-
expect(css).toContain("--color-bg: #ffffff");
|
|
62
|
-
expect(css).toContain("--space-md: 16px");
|
|
63
|
-
t.destroy();
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it("current returns an instance of the default theme", () => {
|
|
67
|
-
const t = new ThemeInstance(LightTheme);
|
|
68
|
-
expect(t.current).toBeInstanceOf(LightTheme);
|
|
69
|
-
t.destroy();
|
|
70
|
-
});
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
// ─── switch() ────────────────────────────────────────────────────────────────
|
|
74
|
-
|
|
75
|
-
describe("ThemeInstance — switch()", () => {
|
|
76
|
-
it("updates :root with new theme values", () => {
|
|
77
|
-
const t = new ThemeInstance(LightTheme);
|
|
78
|
-
t.switch(DarkTheme);
|
|
79
|
-
const css = styleEl()?.textContent ?? "";
|
|
80
|
-
expect(css).toContain("--color-primary: #60a5fa");
|
|
81
|
-
expect(css).toContain("--color-bg: #0f172a");
|
|
82
|
-
t.destroy();
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it("current returns an instance of the new theme after switch", () => {
|
|
86
|
-
const t = new ThemeInstance(LightTheme);
|
|
87
|
-
t.switch(DarkTheme);
|
|
88
|
-
expect(t.current).toBeInstanceOf(DarkTheme);
|
|
89
|
-
t.destroy();
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it("inherits values from parent theme for non-overridden keys", () => {
|
|
93
|
-
const t = new ThemeInstance(LightTheme);
|
|
94
|
-
t.switch(DarkTheme);
|
|
95
|
-
const css = styleEl()?.textContent ?? "";
|
|
96
|
-
// spaceMd is not overridden in DarkTheme — inherits from LightTheme
|
|
97
|
-
expect(css).toContain("--space-md: 16px");
|
|
98
|
-
t.destroy();
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it("can switch back to the original theme", () => {
|
|
102
|
-
const t = new ThemeInstance(LightTheme);
|
|
103
|
-
t.switch(DarkTheme);
|
|
104
|
-
t.switch(LightTheme);
|
|
105
|
-
const css = styleEl()?.textContent ?? "";
|
|
106
|
-
expect(css).toContain("--color-primary: #3b82f6");
|
|
107
|
-
t.destroy();
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it("only one <style> element exists after multiple switches", () => {
|
|
111
|
-
const t = new ThemeInstance(LightTheme);
|
|
112
|
-
t.switch(DarkTheme);
|
|
113
|
-
t.switch(OtherTheme);
|
|
114
|
-
expect(document.querySelectorAll("style[data-praxis-theme]").length).toBe(1);
|
|
115
|
-
t.destroy();
|
|
116
|
-
});
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
// ─── destroy() ───────────────────────────────────────────────────────────────
|
|
120
|
-
|
|
121
|
-
describe("ThemeInstance — destroy()", () => {
|
|
122
|
-
it("removes the <style> element", () => {
|
|
123
|
-
const t = new ThemeInstance(LightTheme);
|
|
124
|
-
expect(styleEl()).not.toBeNull();
|
|
125
|
-
t.destroy();
|
|
126
|
-
expect(styleEl()).toBeNull();
|
|
127
|
-
});
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
// ─── persist ─────────────────────────────────────────────────────────────────
|
|
131
|
-
|
|
132
|
-
describe("ThemeInstance — persist: true", () => {
|
|
133
|
-
it("saves CSS vars to localStorage on switch()", () => {
|
|
134
|
-
const t = new ThemeInstance(LightTheme, { persist: true });
|
|
135
|
-
t.switch(DarkTheme);
|
|
136
|
-
const saved = JSON.parse(localStorage.getItem("praxis-theme") ?? "{}") as Record<string, string>;
|
|
137
|
-
expect(saved["--color-primary"]).toBe("#60a5fa");
|
|
138
|
-
expect(saved["--color-bg"]).toBe("#0f172a");
|
|
139
|
-
t.destroy();
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
it("restores saved vars from localStorage on construction", () => {
|
|
143
|
-
localStorage.setItem("praxis-theme", JSON.stringify({
|
|
144
|
-
"--color-primary": "#ef4444",
|
|
145
|
-
"--color-bg": "#fff1f2",
|
|
146
|
-
}));
|
|
147
|
-
const t = new ThemeInstance(LightTheme, { persist: true });
|
|
148
|
-
const css = styleEl()?.textContent ?? "";
|
|
149
|
-
expect(css).toContain("--color-primary: #ef4444");
|
|
150
|
-
expect(css).toContain("--color-bg: #fff1f2");
|
|
151
|
-
t.destroy();
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
it("falls back to default theme when localStorage is empty", () => {
|
|
155
|
-
const t = new ThemeInstance(LightTheme, { persist: true });
|
|
156
|
-
const css = styleEl()?.textContent ?? "";
|
|
157
|
-
expect(css).toContain("--color-primary: #3b82f6");
|
|
158
|
-
t.destroy();
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
it("does not touch localStorage when persist is false", () => {
|
|
162
|
-
const t = new ThemeInstance(LightTheme, { persist: false });
|
|
163
|
-
t.switch(DarkTheme);
|
|
164
|
-
expect(localStorage.getItem("praxis-theme")).toBeNull();
|
|
165
|
-
t.destroy();
|
|
166
|
-
});
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
// ─── syncTabs ────────────────────────────────────────────────────────────────
|
|
170
|
-
|
|
171
|
-
function makeBCMock(overrides: Partial<{
|
|
172
|
-
addEventListener: (type: string, handler: (e: MessageEvent) => void) => void;
|
|
173
|
-
postMessage: (data: unknown) => void;
|
|
174
|
-
close: () => void;
|
|
175
|
-
}> = {}) {
|
|
176
|
-
const channel = {
|
|
177
|
-
addEventListener: vi.fn(),
|
|
178
|
-
postMessage: vi.fn(),
|
|
179
|
-
close: vi.fn(),
|
|
180
|
-
...overrides,
|
|
181
|
-
};
|
|
182
|
-
// BroadcastChannel must be a real constructor (not an arrow fn)
|
|
183
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
184
|
-
function MockBC(this: unknown) { return channel; }
|
|
185
|
-
vi.stubGlobal("BroadcastChannel", MockBC);
|
|
186
|
-
return channel;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
describe("ThemeInstance — syncTabs: true", () => {
|
|
190
|
-
afterEach(() => { vi.unstubAllGlobals(); });
|
|
191
|
-
|
|
192
|
-
it("closes the BroadcastChannel on destroy()", () => {
|
|
193
|
-
const channel = makeBCMock();
|
|
194
|
-
const t = new ThemeInstance(LightTheme, { syncTabs: true });
|
|
195
|
-
t.destroy();
|
|
196
|
-
expect(channel.close).toHaveBeenCalled();
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
it("posts vars to the channel on switch()", () => {
|
|
200
|
-
const channel = makeBCMock();
|
|
201
|
-
const t = new ThemeInstance(LightTheme, { syncTabs: true });
|
|
202
|
-
t.switch(DarkTheme);
|
|
203
|
-
expect(channel.postMessage).toHaveBeenCalledWith(
|
|
204
|
-
expect.objectContaining({ vars: expect.objectContaining({ "--color-primary": "#60a5fa" }) }),
|
|
205
|
-
);
|
|
206
|
-
t.destroy();
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
it("applies incoming broadcast vars to :root", () => {
|
|
210
|
-
let handler: (e: MessageEvent) => void = () => {};
|
|
211
|
-
makeBCMock({
|
|
212
|
-
addEventListener: (_type: string, fn: (e: MessageEvent) => void) => { handler = fn; },
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
const t = new ThemeInstance(LightTheme, { syncTabs: true });
|
|
216
|
-
handler({ data: { vars: { "--color-primary": "#f97316" } } } as MessageEvent);
|
|
217
|
-
|
|
218
|
-
expect(styleEl()?.textContent).toContain("--color-primary: #f97316");
|
|
219
|
-
t.destroy();
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
it("skips applyVarsToRoot when message has no vars", () => {
|
|
223
|
-
let handler: (e: MessageEvent) => void = () => {};
|
|
224
|
-
makeBCMock({
|
|
225
|
-
addEventListener: (_type: string, fn: (e: MessageEvent) => void) => { handler = fn; },
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
const t = new ThemeInstance(LightTheme, { syncTabs: true });
|
|
229
|
-
const before = styleEl()?.textContent;
|
|
230
|
-
handler({ data: {} } as MessageEvent);
|
|
231
|
-
expect(styleEl()?.textContent).toBe(before);
|
|
232
|
-
t.destroy();
|
|
233
|
-
});
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
// ─── createThemeInstance / theme() ───────────────────────────────────────────
|
|
237
|
-
|
|
238
|
-
describe("createThemeInstance / theme()", () => {
|
|
239
|
-
it("creates and returns a ThemeInstance", () => {
|
|
240
|
-
const t = createThemeInstance(LightTheme);
|
|
241
|
-
expect(t).toBeInstanceOf(ThemeInstance);
|
|
242
|
-
expect(theme()).toBe(t);
|
|
243
|
-
t.destroy();
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
it("passes config to ThemeInstance (persist)", () => {
|
|
247
|
-
const t = createThemeInstance(LightTheme, { persist: true });
|
|
248
|
-
t.switch(DarkTheme);
|
|
249
|
-
expect(localStorage.getItem("praxis-theme")).not.toBeNull();
|
|
250
|
-
t.destroy();
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
it("subsequent call overwrites the singleton", () => {
|
|
254
|
-
const t1 = createThemeInstance(LightTheme);
|
|
255
|
-
const t2 = createThemeInstance(DarkTheme);
|
|
256
|
-
expect(theme()).toBe(t2);
|
|
257
|
-
expect(theme()).not.toBe(t1);
|
|
258
|
-
t2.destroy();
|
|
259
|
-
});
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
// ─── #loadFromStorage error branch ───────────────────────────────────────────
|
|
263
|
-
|
|
264
|
-
describe("ThemeInstance — corrupt localStorage", () => {
|
|
265
|
-
it("falls back to default theme when stored JSON is invalid", () => {
|
|
266
|
-
localStorage.setItem("praxis-theme", "{ not: valid }}}");
|
|
267
|
-
const t = new ThemeInstance(LightTheme, { persist: true });
|
|
268
|
-
expect(styleEl()?.textContent).toContain("--color-primary: #3b82f6");
|
|
269
|
-
t.destroy();
|
|
270
|
-
});
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
// ─── Numeric token values (instanceToVars number branch) ─────────────────────
|
|
274
|
-
|
|
275
|
-
describe("ThemeInstance — numeric token values", () => {
|
|
276
|
-
it("converts number token values to CSS var strings and skips non-string/non-number values", () => {
|
|
277
|
-
class NumericTheme {
|
|
278
|
-
opacity = 0.9; // number
|
|
279
|
-
fontSize = 14; // number
|
|
280
|
-
colorText = "#111"; // string
|
|
281
|
-
nested = { x: 1 }; // object — must be skipped
|
|
282
|
-
}
|
|
283
|
-
const t = new ThemeInstance(NumericTheme as new () => unknown);
|
|
284
|
-
const css = styleEl()?.textContent ?? "";
|
|
285
|
-
expect(css).toContain("--opacity: 0.9");
|
|
286
|
-
expect(css).toContain("--font-size: 14");
|
|
287
|
-
expect(css).toContain("--color-text: #111");
|
|
288
|
-
expect(css).not.toContain("--nested");
|
|
289
|
-
t.destroy();
|
|
290
|
-
});
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
// ─── SSR — document undefined ────────────────────────────────────────────────
|
|
294
|
-
|
|
295
|
-
describe("ThemeInstance — SSR (document === undefined)", () => {
|
|
296
|
-
afterEach(() => { vi.unstubAllGlobals(); });
|
|
297
|
-
|
|
298
|
-
it("constructor does not throw when document is undefined", () => {
|
|
299
|
-
vi.stubGlobal("document", undefined);
|
|
300
|
-
expect(() => new ThemeInstance(LightTheme)).not.toThrow();
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
it("switch() does not throw when document is undefined", () => {
|
|
304
|
-
vi.stubGlobal("document", undefined);
|
|
305
|
-
const t = new ThemeInstance(LightTheme);
|
|
306
|
-
expect(() => t.switch(DarkTheme)).not.toThrow();
|
|
307
|
-
});
|
|
308
|
-
|
|
309
|
-
it("destroy() does not throw when document is undefined", () => {
|
|
310
|
-
vi.stubGlobal("document", undefined);
|
|
311
|
-
const t = new ThemeInstance(LightTheme);
|
|
312
|
-
expect(() => t.destroy()).not.toThrow();
|
|
313
|
-
});
|
|
314
|
-
|
|
315
|
-
it("broadcast message handler does not throw when document is undefined", () => {
|
|
316
|
-
let handler: (e: MessageEvent) => void = () => {};
|
|
317
|
-
makeBCMock({
|
|
318
|
-
addEventListener: (_type: string, fn: (e: MessageEvent) => void) => { handler = fn; },
|
|
319
|
-
});
|
|
320
|
-
|
|
321
|
-
const t = new ThemeInstance(LightTheme, { syncTabs: true });
|
|
322
|
-
vi.stubGlobal("document", undefined);
|
|
323
|
-
// applyVarsToRoot is called directly from the broadcast handler — its own
|
|
324
|
-
// SSR guard (line 18) must short-circuit without throwing
|
|
325
|
-
expect(() => handler({ data: { vars: { "--color-primary": "#f97316" } } } as MessageEvent)).not.toThrow();
|
|
326
|
-
t.destroy();
|
|
327
|
-
});
|
|
328
|
-
});
|
|
329
|
-
|
|
330
|
-
// ─── Collector routing (applyVarsToRoot) ─────────────────────────────────────
|
|
331
|
-
|
|
332
|
-
describe("ThemeInstance — collector routing", () => {
|
|
333
|
-
afterEach(() => { setCollector(null); });
|
|
334
|
-
|
|
335
|
-
it("routes :root injection through the collector when one is active", () => {
|
|
336
|
-
const col = createCollector();
|
|
337
|
-
setCollector(col);
|
|
338
|
-
new ThemeInstance(LightTheme);
|
|
339
|
-
expect(col.getCSS()).toContain("--color-primary: #3b82f6");
|
|
340
|
-
expect(document.querySelector("style[data-praxis-theme]")).toBeNull();
|
|
341
|
-
});
|
|
342
|
-
|
|
343
|
-
it("collector receives :root CSS on switch() too", () => {
|
|
344
|
-
const col = createCollector();
|
|
345
|
-
new ThemeInstance(LightTheme); // initial inject without collector
|
|
346
|
-
setCollector(col);
|
|
347
|
-
const t = new ThemeInstance(DarkTheme);
|
|
348
|
-
expect(col.getCSS()).toContain("--color-primary: #60a5fa");
|
|
349
|
-
t.destroy();
|
|
350
|
-
});
|
|
351
|
-
});
|
|
352
|
-
|
|
353
|
-
// ─── BroadcastChannel unavailable ────────────────────────────────────────────
|
|
354
|
-
|
|
355
|
-
describe("ThemeInstance — BroadcastChannel unavailable", () => {
|
|
356
|
-
afterEach(() => { vi.unstubAllGlobals(); });
|
|
357
|
-
|
|
358
|
-
it("does not throw when BroadcastChannel is undefined with syncTabs: true", () => {
|
|
359
|
-
vi.stubGlobal("BroadcastChannel", undefined);
|
|
360
|
-
const t = new ThemeInstance(LightTheme, { syncTabs: true });
|
|
361
|
-
expect(styleEl()).not.toBeNull();
|
|
362
|
-
t.destroy();
|
|
363
|
-
});
|
|
364
|
-
});
|
|
365
|
-
|
|
366
|
-
// ─── theme() before createThemeInstance ──────────────────────────────────────
|
|
367
|
-
|
|
368
|
-
describe("theme() — not yet initialized", () => {
|
|
369
|
-
it("throws a descriptive error when called before createThemeInstance", async () => {
|
|
370
|
-
vi.resetModules();
|
|
371
|
-
const { theme: freshTheme } = await import("../tokens/theme-instance");
|
|
372
|
-
expect(() => freshTheme()).toThrow("[PraxisJS] theme() called before @Themed()");
|
|
373
|
-
});
|
|
374
|
-
});
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
// @vitest-environment jsdom
|
|
2
|
-
// Isolated file — createThemeInstance() is never called here, so _instance starts null.
|
|
3
|
-
import { describe, it, expect } from "vitest";
|
|
4
|
-
import { theme } from "../tokens/theme-instance";
|
|
5
|
-
|
|
6
|
-
describe("theme() — uninitialized", () => {
|
|
7
|
-
it("throws when @Themed() was never applied", () => {
|
|
8
|
-
expect(() => theme()).toThrow(
|
|
9
|
-
"[PraxisJS] theme() called before @Themed() was applied to the root component.",
|
|
10
|
-
);
|
|
11
|
-
});
|
|
12
|
-
});
|
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { TokenSheet, tokenVars } from "../tokens/token-sheet";
|
|
3
|
-
|
|
4
|
-
// ─── Fixtures ─────────────────────────────────────────────────────────────────
|
|
5
|
-
|
|
6
|
-
class AppTokens extends TokenSheet {
|
|
7
|
-
colorPrimary!: string;
|
|
8
|
-
colorBg!: string;
|
|
9
|
-
spaceMd!: string;
|
|
10
|
-
radiusFull!: string;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
class LightTheme extends AppTokens {
|
|
14
|
-
colorPrimary = "#3b82f6";
|
|
15
|
-
colorBg = "#ffffff";
|
|
16
|
-
spaceMd = "16px";
|
|
17
|
-
radiusFull = "9999px";
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
class DarkTheme extends LightTheme {
|
|
21
|
-
colorPrimary = "#60a5fa";
|
|
22
|
-
colorBg = "#0f172a";
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
// ─── TokenSheet static refs ───────────────────────────────────────────────────
|
|
26
|
-
|
|
27
|
-
const asRecord = (cls: unknown) => cls as Record<string, unknown>;
|
|
28
|
-
|
|
29
|
-
describe("TokenSheet — static CSS var references", () => {
|
|
30
|
-
it("camelCase key → var(--kebab-case)", () => {
|
|
31
|
-
expect(asRecord(AppTokens).colorPrimary).toBe("var(--color-primary)");
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it("multi-capital camelCase → correct kebab", () => {
|
|
35
|
-
class T extends TokenSheet { spacingXxLarge!: string; }
|
|
36
|
-
expect(asRecord(T).spacingXxLarge).toBe("var(--spacing-xx-large)");
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it("single-word key → var(--word)", () => {
|
|
40
|
-
class T extends TokenSheet { size!: string; }
|
|
41
|
-
expect(asRecord(T).size).toBe("var(--size)");
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it("subclass inherits static refs from the skeleton", () => {
|
|
45
|
-
expect(asRecord(LightTheme).colorPrimary).toBe("var(--color-primary)");
|
|
46
|
-
expect(asRecord(DarkTheme).colorPrimary).toBe("var(--color-primary)");
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it("built-in static props are not intercepted", () => {
|
|
50
|
-
expect(typeof (AppTokens as unknown as { prototype: unknown }).prototype).toBe("object");
|
|
51
|
-
expect(typeof (AppTokens as unknown as { name: unknown }).name).toBe("string");
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it("props starting with _ are not intercepted", () => {
|
|
55
|
-
expect(asRecord(AppTokens)._internal).toBeUndefined();
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it("props starting with $ are not intercepted", () => {
|
|
59
|
-
expect(asRecord(AppTokens).$root).toBeUndefined();
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it("different token names produce different var strings", () => {
|
|
63
|
-
expect(asRecord(AppTokens).colorPrimary).not.toBe(asRecord(AppTokens).colorBg);
|
|
64
|
-
});
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
// ─── tokenVars ────────────────────────────────────────────────────────────────
|
|
68
|
-
|
|
69
|
-
describe("tokenVars()", () => {
|
|
70
|
-
it("returns CSS var refs for all declared fields", () => {
|
|
71
|
-
const t = tokenVars(AppTokens);
|
|
72
|
-
expect(t.colorPrimary).toBe("var(--color-primary)");
|
|
73
|
-
expect(t.colorBg).toBe("var(--color-bg)");
|
|
74
|
-
expect(t.spaceMd).toBe("var(--space-md)");
|
|
75
|
-
expect(t.radiusFull).toBe("var(--radius-full)");
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it("works on theme subclasses (inherits skeleton fields)", () => {
|
|
79
|
-
const t = tokenVars(DarkTheme);
|
|
80
|
-
expect(t.colorPrimary).toBe("var(--color-primary)");
|
|
81
|
-
expect(t.colorBg).toBe("var(--color-bg)");
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it("is the same object reference as the class (no extra allocation)", () => {
|
|
85
|
-
expect(tokenVars(AppTokens)).toBe(AppTokens as unknown);
|
|
86
|
-
});
|
|
87
|
-
});
|