@pyreon/styler 0.11.1 → 0.11.2
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/package.json +6 -5
- package/src/ThemeProvider.ts +37 -0
- package/src/__tests__/ThemeProvider.test.ts +67 -0
- package/src/__tests__/benchmark.bench.ts +189 -0
- package/src/__tests__/composition-chain.test.ts +489 -0
- package/src/__tests__/css.test.ts +70 -0
- package/src/__tests__/forward.test.ts +282 -0
- package/src/__tests__/globalStyle.test.ts +72 -0
- package/src/__tests__/hash.test.ts +70 -0
- package/src/__tests__/hybrid-injection.test.ts +205 -0
- package/src/__tests__/index.ts +14 -0
- package/src/__tests__/insertion-effect.test.ts +106 -0
- package/src/__tests__/integration.test.ts +149 -0
- package/src/__tests__/keyframes.test.ts +68 -0
- package/src/__tests__/memory-growth.test.ts +152 -0
- package/src/__tests__/p3-features.test.ts +258 -0
- package/src/__tests__/resolve.test.ts +249 -0
- package/src/__tests__/shared.test.ts +73 -0
- package/src/__tests__/sheet-advanced.test.ts +669 -0
- package/src/__tests__/sheet-split-atrules.test.ts +411 -0
- package/src/__tests__/sheet.test.ts +164 -0
- package/src/__tests__/styled-ssr.test.ts +67 -0
- package/src/__tests__/styled.test.ts +303 -0
- package/src/__tests__/theme.test.ts +33 -0
- package/src/__tests__/useCSS.test.ts +142 -0
- package/src/css.ts +13 -0
- package/src/forward.ts +276 -0
- package/src/globalStyle.ts +48 -0
- package/src/hash.ts +30 -0
- package/src/index.ts +15 -0
- package/src/keyframes.ts +36 -0
- package/src/resolve.ts +172 -0
- package/src/shared.ts +12 -0
- package/src/sheet.ts +387 -0
- package/src/styled.tsx +277 -0
- package/src/useCSS.ts +20 -0
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import type { VNode } from "@pyreon/core"
|
|
2
|
+
import { h } from "@pyreon/core"
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest"
|
|
4
|
+
import { css } from "../css"
|
|
5
|
+
import { createSheet, StyleSheet } from "../sheet"
|
|
6
|
+
import { styled } from "../styled"
|
|
7
|
+
|
|
8
|
+
describe("P3 features", () => {
|
|
9
|
+
describe("shouldForwardProp", () => {
|
|
10
|
+
it("allows custom prop filtering", () => {
|
|
11
|
+
const Comp = styled("div", {
|
|
12
|
+
shouldForwardProp: (prop) => prop !== "color",
|
|
13
|
+
})`display: flex;`
|
|
14
|
+
|
|
15
|
+
const vnode = Comp({ color: "red", title: "hello" }) as VNode
|
|
16
|
+
expect(vnode.props.color).toBeUndefined()
|
|
17
|
+
expect(vnode.props.title).toBe("hello")
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it("custom filter receives all non-system props", () => {
|
|
21
|
+
const forwarded: string[] = []
|
|
22
|
+
const Comp = styled("div", {
|
|
23
|
+
shouldForwardProp: (prop) => {
|
|
24
|
+
forwarded.push(prop)
|
|
25
|
+
return true
|
|
26
|
+
},
|
|
27
|
+
})`display: flex;`
|
|
28
|
+
|
|
29
|
+
Comp({ "data-x": "1", title: "hi" })
|
|
30
|
+
expect(forwarded).toContain("data-x")
|
|
31
|
+
expect(forwarded).toContain("title")
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it("works with dynamic interpolations", () => {
|
|
35
|
+
const Comp = styled("div", {
|
|
36
|
+
shouldForwardProp: (prop) => prop === "title",
|
|
37
|
+
})`color: ${(p: any) => p.$color};`
|
|
38
|
+
|
|
39
|
+
const vnode = Comp({ $color: "red", title: "yes", custom: "no" }) as VNode
|
|
40
|
+
expect(vnode.props.title).toBe("yes")
|
|
41
|
+
expect(vnode.props.custom).toBeUndefined()
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it("does not affect component wrapping (components receive all props)", () => {
|
|
45
|
+
const Inner = (props: { class?: string; myProp?: string }) =>
|
|
46
|
+
h("div", { class: props.class, "data-my": props.myProp })
|
|
47
|
+
|
|
48
|
+
// shouldForwardProp is only for HTML elements
|
|
49
|
+
const Comp = styled(Inner, {
|
|
50
|
+
shouldForwardProp: () => false,
|
|
51
|
+
})`color: red;`
|
|
52
|
+
|
|
53
|
+
const vnode = Comp({ myProp: "hello" }) as VNode
|
|
54
|
+
// Components always receive all props (no filtering)
|
|
55
|
+
// The VNode wraps Inner and should pass myProp through
|
|
56
|
+
expect(vnode.props.myProp).toBe("hello")
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
describe("styled(StyledComponent) — extending", () => {
|
|
61
|
+
it("extends a styled component", () => {
|
|
62
|
+
const Base = styled("div")`color: red;`
|
|
63
|
+
const Extended = styled(Base)`font-size: 20px;`
|
|
64
|
+
|
|
65
|
+
const vnode = Extended({}) as VNode
|
|
66
|
+
// Extended wraps Base, so Base applies its own className
|
|
67
|
+
// and Extended passes its className to Base as a prop
|
|
68
|
+
expect(vnode.props.class).toContain("pyr-")
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it("extended component receives className from outer", () => {
|
|
72
|
+
const Base = styled("div")`color: red;`
|
|
73
|
+
const Extended = styled(Base)`font-size: 20px;`
|
|
74
|
+
|
|
75
|
+
const vnode = Extended({ className: "user-cls" }) as VNode
|
|
76
|
+
expect(vnode.props.class).toContain("user-cls")
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it("multi-level extension works", () => {
|
|
80
|
+
const L1 = styled("div")`display: flex;`
|
|
81
|
+
const L2 = styled(L1)`color: red;`
|
|
82
|
+
const L3 = styled(L2)`font-size: 14px;`
|
|
83
|
+
|
|
84
|
+
const vnode = L3({}) as VNode
|
|
85
|
+
expect(vnode.props.class).toContain("pyr-")
|
|
86
|
+
// L3 wraps L2 which wraps L1 which wraps 'div'.
|
|
87
|
+
// The outermost VNode's type is the next component in the chain.
|
|
88
|
+
expect(typeof vnode.type).toBe("function")
|
|
89
|
+
})
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
describe("HMR cleanup API", () => {
|
|
93
|
+
beforeEach(() => {
|
|
94
|
+
document.querySelectorAll("style[data-pyreon-styler]").forEach((el) => {
|
|
95
|
+
el.remove()
|
|
96
|
+
})
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it("clearCache removes all cached entries", () => {
|
|
100
|
+
const s = createSheet()
|
|
101
|
+
s.insert("color: red;")
|
|
102
|
+
s.insert("color: blue;")
|
|
103
|
+
expect(s.cacheSize).toBe(2)
|
|
104
|
+
|
|
105
|
+
s.clearCache()
|
|
106
|
+
expect(s.cacheSize).toBe(0)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it("clearAll removes cache, SSR buffer, and DOM rules", () => {
|
|
110
|
+
const s = new StyleSheet()
|
|
111
|
+
s.insert("color: red;")
|
|
112
|
+
s.insert("color: blue;")
|
|
113
|
+
expect(s.cacheSize).toBe(2)
|
|
114
|
+
|
|
115
|
+
s.clearAll()
|
|
116
|
+
expect(s.cacheSize).toBe(0)
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it("after clearCache, same CSS gets re-inserted", () => {
|
|
120
|
+
const s = createSheet()
|
|
121
|
+
s.insert("color: red;")
|
|
122
|
+
expect(s.cacheSize).toBe(1)
|
|
123
|
+
|
|
124
|
+
s.clearCache()
|
|
125
|
+
expect(s.cacheSize).toBe(0)
|
|
126
|
+
|
|
127
|
+
// Re-insert — should work since cache was cleared
|
|
128
|
+
s.insert("color: red;")
|
|
129
|
+
expect(s.cacheSize).toBe(1)
|
|
130
|
+
})
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
describe("HMR cleanup API (SSR mode)", () => {
|
|
134
|
+
let originalDocument: typeof document
|
|
135
|
+
|
|
136
|
+
beforeEach(() => {
|
|
137
|
+
originalDocument = globalThis.document
|
|
138
|
+
// @ts-expect-error - intentionally deleting for SSR simulation
|
|
139
|
+
delete globalThis.document
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
afterEach(() => {
|
|
143
|
+
globalThis.document = originalDocument
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
it("clearAll in SSR mode clears buffer and cache", () => {
|
|
147
|
+
const s = createSheet()
|
|
148
|
+
s.insert("color: red;")
|
|
149
|
+
expect(s.getStyles()).toContain("color: red;")
|
|
150
|
+
expect(s.cacheSize).toBe(1)
|
|
151
|
+
|
|
152
|
+
s.clearAll()
|
|
153
|
+
expect(s.getStyles()).toBe("")
|
|
154
|
+
expect(s.cacheSize).toBe(0)
|
|
155
|
+
})
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
describe("CSS nesting (& selectors)", () => {
|
|
159
|
+
it("& selectors pass through to the CSS rule", () => {
|
|
160
|
+
// Native CSS nesting is supported by modern browsers
|
|
161
|
+
// The resolver passes CSS through without transformation
|
|
162
|
+
const Comp = styled("div")`
|
|
163
|
+
color: red;
|
|
164
|
+
&:hover { color: blue; }
|
|
165
|
+
`
|
|
166
|
+
const vnode = Comp({}) as VNode
|
|
167
|
+
expect(vnode.props.class).toMatch(/^pyr-/)
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
it("nested & with pseudo-elements", () => {
|
|
171
|
+
const Comp = styled("div")`
|
|
172
|
+
position: relative;
|
|
173
|
+
&::before { content: ""; display: block; }
|
|
174
|
+
&::after { content: ""; display: block; }
|
|
175
|
+
`
|
|
176
|
+
const vnode = Comp({}) as VNode
|
|
177
|
+
expect(vnode.props.class).toMatch(/^pyr-/)
|
|
178
|
+
})
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
describe("edge cases", () => {
|
|
182
|
+
it("empty template with dynamic interpolation returning nothing", () => {
|
|
183
|
+
const Comp = styled("div")`${(p: any) => p.$show && css`color: red;`}`
|
|
184
|
+
const vnode = Comp({ $show: false }) as VNode
|
|
185
|
+
// When resolved CSS is empty/whitespace, no className
|
|
186
|
+
expect(vnode.props.class).toBeFalsy()
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
it("empty template with dynamic interpolation returning value", () => {
|
|
190
|
+
const Comp = styled("div")`${(p: any) => p.$show && css`color: red;`}`
|
|
191
|
+
const vnode = Comp({ $show: true }) as VNode
|
|
192
|
+
expect(vnode.props.class).toMatch(/^pyr-/)
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
it("deeply nested CSSResult chains resolve correctly", () => {
|
|
196
|
+
const l1 = css`color: red;`
|
|
197
|
+
const l2 = css`${l1} font-size: 14px;`
|
|
198
|
+
const l3 = css`${l2} display: flex;`
|
|
199
|
+
const l4 = css`${l3} padding: 8px;`
|
|
200
|
+
const l5 = css`${l4} margin: 4px;`
|
|
201
|
+
|
|
202
|
+
const resolved = l5.toString()
|
|
203
|
+
expect(resolved).toContain("color: red;")
|
|
204
|
+
expect(resolved).toContain("font-size: 14px;")
|
|
205
|
+
expect(resolved).toContain("display: flex;")
|
|
206
|
+
expect(resolved).toContain("padding: 8px;")
|
|
207
|
+
expect(resolved).toContain("margin: 4px;")
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
it("anonymous component gets fallback displayName", () => {
|
|
211
|
+
const Anon = (() => {
|
|
212
|
+
const fn = () => null
|
|
213
|
+
Object.defineProperty(fn, "name", { value: "" })
|
|
214
|
+
return fn
|
|
215
|
+
})()
|
|
216
|
+
|
|
217
|
+
const Comp = styled(Anon)`color: red;`
|
|
218
|
+
expect((Comp as any).displayName).toBe("styled(Component)")
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
it("handles very large CSS strings", () => {
|
|
222
|
+
const bigCSS = Array.from({ length: 100 }, (_, i) => `prop${i}: val${i};`).join(" ")
|
|
223
|
+
const Comp = styled("div")`${bigCSS}`
|
|
224
|
+
const vnode = Comp({}) as VNode
|
|
225
|
+
expect(vnode.props.class).toMatch(/^pyr-/)
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
it("different dynamic values cause different classNames", () => {
|
|
229
|
+
const Comp = styled("div")`
|
|
230
|
+
color: ${(p: any) => p.$color};
|
|
231
|
+
font-size: ${(p: any) => p.$size};
|
|
232
|
+
`
|
|
233
|
+
|
|
234
|
+
const vnode1 = Comp({ $color: "red", $size: "14px" }) as VNode
|
|
235
|
+
const cls1 = vnode1.props.class as string
|
|
236
|
+
|
|
237
|
+
const vnode2 = Comp({ $color: "blue", $size: "16px" }) as VNode
|
|
238
|
+
const cls2 = vnode2.props.class as string
|
|
239
|
+
|
|
240
|
+
expect(cls1).not.toBe(cls2)
|
|
241
|
+
expect(cls1).toMatch(/^pyr-/)
|
|
242
|
+
expect(cls2).toMatch(/^pyr-/)
|
|
243
|
+
})
|
|
244
|
+
|
|
245
|
+
it("same dynamic values produce same className (dedup cache)", () => {
|
|
246
|
+
const Comp = styled("div")`color: ${(p: any) => p.$color};`
|
|
247
|
+
|
|
248
|
+
const vnode1 = Comp({ $color: "red" }) as VNode
|
|
249
|
+
const cls1 = vnode1.props.class as string
|
|
250
|
+
|
|
251
|
+
// Re-render with same value
|
|
252
|
+
const vnode2 = Comp({ $color: "red" }) as VNode
|
|
253
|
+
const cls2 = vnode2.props.class as string
|
|
254
|
+
|
|
255
|
+
expect(cls1).toBe(cls2)
|
|
256
|
+
})
|
|
257
|
+
})
|
|
258
|
+
})
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest"
|
|
2
|
+
import { css } from "../css"
|
|
3
|
+
import { CSSResult, normalizeCSS, resolve, resolveValue } from "../resolve"
|
|
4
|
+
|
|
5
|
+
// Helper to create a TemplateStringsArray
|
|
6
|
+
const tsa = (strings: readonly string[]): TemplateStringsArray => {
|
|
7
|
+
const arr = [...strings] as string[] & { raw: readonly string[] }
|
|
8
|
+
arr.raw = strings
|
|
9
|
+
return arr
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
describe("resolve", () => {
|
|
13
|
+
describe("primitive interpolations", () => {
|
|
14
|
+
it("resolves strings", () => {
|
|
15
|
+
const result = resolve(tsa(["color: ", ";"]), ["red"], {})
|
|
16
|
+
expect(result).toBe("color: red;")
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it("resolves numbers", () => {
|
|
20
|
+
const result = resolve(tsa(["flex: ", ";"]), [1], {})
|
|
21
|
+
expect(result).toBe("flex: 1;")
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it("resolves null as empty string", () => {
|
|
25
|
+
const result = resolve(tsa(["a", "b"]), [null], {})
|
|
26
|
+
expect(result).toBe("ab")
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it("resolves undefined as empty string", () => {
|
|
30
|
+
const result = resolve(tsa(["a", "b"]), [undefined], {})
|
|
31
|
+
expect(result).toBe("ab")
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it("resolves false as empty string", () => {
|
|
35
|
+
const result = resolve(tsa(["a", "b"]), [false], {})
|
|
36
|
+
expect(result).toBe("ab")
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it("resolves true as empty string", () => {
|
|
40
|
+
const result = resolve(tsa(["a", "b"]), [true], {})
|
|
41
|
+
expect(result).toBe("ab")
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
describe("function interpolations", () => {
|
|
46
|
+
it("calls functions with props and uses return value", () => {
|
|
47
|
+
const fn = (props: Record<string, unknown>) => props.color as string
|
|
48
|
+
const result = resolve(tsa(["color: ", ";"]), [fn], { color: "blue" })
|
|
49
|
+
expect(result).toBe("color: blue;")
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it("resolves nested function results recursively", () => {
|
|
53
|
+
const fn = () => () => "red"
|
|
54
|
+
const result = resolve(tsa(["color: ", ";"]), [fn], {})
|
|
55
|
+
expect(result).toBe("color: red;")
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it("handles functions returning null", () => {
|
|
59
|
+
const fn = () => null
|
|
60
|
+
const result = resolve(tsa(["a", "b"]), [fn], {})
|
|
61
|
+
expect(result).toBe("ab")
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it("handles functions returning false (conditional)", () => {
|
|
65
|
+
const fn = (props: Record<string, unknown>) => (props.active ? "color: red;" : false)
|
|
66
|
+
const result = resolve(tsa(["", ""]), [fn], { active: false })
|
|
67
|
+
expect(result).toBe("")
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it("uses empty object when no props provided", () => {
|
|
71
|
+
const fn = (props: Record<string, unknown>) =>
|
|
72
|
+
Object.keys(props).length === 0 ? "empty" : "has-props"
|
|
73
|
+
const result = resolve(tsa(["", ""]), [fn], {})
|
|
74
|
+
expect(result).toBe("empty")
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
describe("CSSResult interpolations", () => {
|
|
79
|
+
it("resolves nested CSSResult", () => {
|
|
80
|
+
const inner = css`color: red;`
|
|
81
|
+
const result = resolveValue(inner, {})
|
|
82
|
+
expect(result).toBe("color: red;")
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it("resolves deeply nested CSSResults", () => {
|
|
86
|
+
const inner1 = css`color: red;`
|
|
87
|
+
const inner2 = css`${inner1} display: flex;`
|
|
88
|
+
const result = resolveValue(inner2, {})
|
|
89
|
+
expect(result).toBe("color: red; display: flex;")
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
it("resolves CSSResult with function interpolations", () => {
|
|
93
|
+
const inner = css`color: ${((p: Record<string, unknown>) => p.color) as any};`
|
|
94
|
+
const result = resolveValue(inner, { color: "green" })
|
|
95
|
+
expect(result).toBe("color: green;")
|
|
96
|
+
})
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
describe("array interpolations", () => {
|
|
100
|
+
it("resolves arrays of values", () => {
|
|
101
|
+
const result = resolve(tsa(["", ""]), [["a", "b", "c"]], {})
|
|
102
|
+
expect(result).toBe("abc")
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
it("resolves arrays with CSSResults", () => {
|
|
106
|
+
const inner = css`color: red;`
|
|
107
|
+
const result = resolveValue([inner, " display: flex;"], {})
|
|
108
|
+
expect(result).toBe("color: red; display: flex;")
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
describe("combined patterns", () => {
|
|
113
|
+
it("handles multiple interpolation types", () => {
|
|
114
|
+
const result = resolve(
|
|
115
|
+
tsa(["display: ", "; color: ", "; flex: ", ";"]),
|
|
116
|
+
["flex", "red", 1],
|
|
117
|
+
{},
|
|
118
|
+
)
|
|
119
|
+
expect(result).toBe("display: flex; color: red; flex: 1;")
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it("handles conditional CSS with logical AND (truthy)", () => {
|
|
123
|
+
const condition = true
|
|
124
|
+
const conditionalCss = condition && css`color: red;`
|
|
125
|
+
const result = resolveValue(conditionalCss, {})
|
|
126
|
+
expect(result).toBe("color: red;")
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it("handles conditional CSS with logical AND (falsy)", () => {
|
|
130
|
+
const condition = false
|
|
131
|
+
const conditionalCss = condition && css`color: red;`
|
|
132
|
+
const result = resolveValue(conditionalCss, {})
|
|
133
|
+
expect(result).toBe("")
|
|
134
|
+
})
|
|
135
|
+
})
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
describe("CSSResult", () => {
|
|
139
|
+
it("stores strings and values as readonly properties", () => {
|
|
140
|
+
const strings = ["color: ", ";"] as unknown as TemplateStringsArray
|
|
141
|
+
const values = ["red"]
|
|
142
|
+
const result = new CSSResult(strings, values)
|
|
143
|
+
expect(result.strings).toBe(strings)
|
|
144
|
+
expect(result.values).toBe(values)
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it("toString resolves with empty props", () => {
|
|
148
|
+
const result = css`color: red;`
|
|
149
|
+
expect(result.toString()).toBe("color: red;")
|
|
150
|
+
})
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
describe("normalizeCSS", () => {
|
|
154
|
+
describe("comment stripping", () => {
|
|
155
|
+
it("strips CSS block comments", () => {
|
|
156
|
+
expect(normalizeCSS("/* comment */ color: red;")).toBe("color: red;")
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
it("strips multiple block comments", () => {
|
|
160
|
+
expect(normalizeCSS("/* BASE */ color: red; /* HOVER */ font-size: 1rem;")).toBe(
|
|
161
|
+
"color: red; font-size: 1rem;",
|
|
162
|
+
)
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
it("strips multiline block comments", () => {
|
|
166
|
+
expect(normalizeCSS("/* --------\n BASE STATE\n -------- */\nheight: 3rem;")).toBe(
|
|
167
|
+
"height: 3rem;",
|
|
168
|
+
)
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it("strips JS-style line comments", () => {
|
|
172
|
+
expect(normalizeCSS("// this is not valid CSS\ncolor: red;")).toBe("color: red;")
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
it("preserves :// in URLs", () => {
|
|
176
|
+
expect(normalizeCSS("background: url(https://example.com/img.png);")).toContain(
|
|
177
|
+
"https://example.com/img.png",
|
|
178
|
+
)
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
it("strips line comments but preserves URL protocols", () => {
|
|
182
|
+
const result = normalizeCSS("// comment\nbackground: url(https://example.com/img.png);")
|
|
183
|
+
expect(result).toContain("https://example.com/img.png")
|
|
184
|
+
expect(result).not.toContain("// comment")
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
it("handles unterminated block comment", () => {
|
|
188
|
+
expect(normalizeCSS("color: red; /* never closed")).toBe("color: red;")
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
it("handles unterminated line comment", () => {
|
|
192
|
+
expect(normalizeCSS("color: red;\n// trailing comment")).toBe("color: red;")
|
|
193
|
+
})
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
describe("whitespace handling", () => {
|
|
197
|
+
it("collapses whitespace", () => {
|
|
198
|
+
expect(normalizeCSS(" color: red; font-size: 1rem; ")).toBe(
|
|
199
|
+
"color: red; font-size: 1rem;",
|
|
200
|
+
)
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
it("converts tabs and newlines to spaces", () => {
|
|
204
|
+
expect(normalizeCSS("color:\tred;\nfont-size:\t1rem;")).toBe("color: red; font-size: 1rem;")
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
it("collapses multiple spaces", () => {
|
|
208
|
+
expect(normalizeCSS("color: red;")).toBe("color: red;")
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
it("trims leading and trailing whitespace", () => {
|
|
212
|
+
expect(normalizeCSS(" color: red; ")).toBe("color: red;")
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
it("handles carriage returns", () => {
|
|
216
|
+
expect(normalizeCSS("color: red;\r\nfont-size: 1rem;")).toBe("color: red; font-size: 1rem;")
|
|
217
|
+
})
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
describe("semicolon handling", () => {
|
|
221
|
+
it("removes redundant semicolons after {", () => {
|
|
222
|
+
expect(normalizeCSS(".foo {; color: red; }")).toBe(".foo { color: red; }")
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
it("removes redundant semicolons after }", () => {
|
|
226
|
+
expect(normalizeCSS(".foo { color: red; }; .bar { }")).toBe(".foo { color: red; } .bar { }")
|
|
227
|
+
})
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
describe("edge cases", () => {
|
|
231
|
+
it("returns empty string for empty input", () => {
|
|
232
|
+
expect(normalizeCSS("")).toBe("")
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
it("returns empty string for whitespace-only input", () => {
|
|
236
|
+
expect(normalizeCSS(" \n\t ")).toBe("")
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
it("handles CSS with braces", () => {
|
|
240
|
+
expect(normalizeCSS(".foo { color: red; }")).toBe(".foo { color: red; }")
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
it("handles @media rules", () => {
|
|
244
|
+
const result = normalizeCSS("@media (min-width: 48em) { color: blue; }")
|
|
245
|
+
expect(result).toContain("@media")
|
|
246
|
+
expect(result).toContain("color: blue;")
|
|
247
|
+
})
|
|
248
|
+
})
|
|
249
|
+
})
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest"
|
|
2
|
+
import { css } from "../css"
|
|
3
|
+
import { CSSResult } from "../resolve"
|
|
4
|
+
import { isDynamic } from "../shared"
|
|
5
|
+
|
|
6
|
+
describe("isDynamic", () => {
|
|
7
|
+
it("returns true for function values", () => {
|
|
8
|
+
expect(isDynamic(() => "red")).toBe(true)
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it("returns false for string values", () => {
|
|
12
|
+
expect(isDynamic("color: red;")).toBe(false)
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
it("returns false for number values", () => {
|
|
16
|
+
expect(isDynamic(42)).toBe(false)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it("returns false for null and undefined", () => {
|
|
20
|
+
expect(isDynamic(null)).toBe(false)
|
|
21
|
+
expect(isDynamic(undefined)).toBe(false)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it("returns false for boolean values", () => {
|
|
25
|
+
expect(isDynamic(true)).toBe(false)
|
|
26
|
+
expect(isDynamic(false)).toBe(false)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it("returns true for arrays containing functions", () => {
|
|
30
|
+
expect(isDynamic(["a", () => "b"])).toBe(true)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it("returns false for arrays of static values", () => {
|
|
34
|
+
expect(isDynamic(["a", "b", 42])).toBe(false)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it("returns true for CSSResult with dynamic values", () => {
|
|
38
|
+
const result = css`color: ${() => "red"};`
|
|
39
|
+
expect(isDynamic(result)).toBe(true)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it("returns false for CSSResult with only static values", () => {
|
|
43
|
+
const result = css`color: ${"red"};`
|
|
44
|
+
expect(isDynamic(result)).toBe(false)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it("returns true for nested dynamic CSSResult", () => {
|
|
48
|
+
const inner = css`color: ${() => "red"};`
|
|
49
|
+
const outer = css`${inner}`
|
|
50
|
+
expect(isDynamic(outer)).toBe(true)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it("returns false for nested static CSSResult", () => {
|
|
54
|
+
const inner = css`color: red;`
|
|
55
|
+
const outer = css`${inner}`
|
|
56
|
+
expect(isDynamic(outer)).toBe(false)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it("detects deeply nested dynamic values", () => {
|
|
60
|
+
const deep = css`color: ${() => "red"};`
|
|
61
|
+
const mid = css`${deep}`
|
|
62
|
+
const outer = css`${mid}`
|
|
63
|
+
expect(isDynamic(outer)).toBe(true)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it("handles arrays inside CSSResult", () => {
|
|
67
|
+
const result = new CSSResult(
|
|
68
|
+
Object.assign(["", ""], { raw: ["", ""] }) as TemplateStringsArray,
|
|
69
|
+
[["a", () => "b"]],
|
|
70
|
+
)
|
|
71
|
+
expect(isDynamic(result)).toBe(true)
|
|
72
|
+
})
|
|
73
|
+
})
|