@pyreon/styler 0.12.13 → 0.12.15
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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pyreon/styler",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.15",
|
|
4
4
|
"description": "Lightweight CSS-in-JS engine for Pyreon",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -35,17 +35,20 @@
|
|
|
35
35
|
"build:watch": "bun run vl_rolldown_build-watch",
|
|
36
36
|
"lint": "oxlint .",
|
|
37
37
|
"test": "vitest run",
|
|
38
|
+
"test:browser": "vitest run --config ./vitest.browser.config.ts",
|
|
38
39
|
"test:coverage": "vitest run --coverage",
|
|
39
40
|
"test:watch": "vitest",
|
|
40
41
|
"typecheck": "tsc --noEmit"
|
|
41
42
|
},
|
|
42
43
|
"devDependencies": {
|
|
43
|
-
"@pyreon/
|
|
44
|
+
"@pyreon/test-utils": "^0.12.10",
|
|
45
|
+
"@pyreon/typescript": "^0.12.15",
|
|
46
|
+
"@vitest/browser-playwright": "^4.1.4",
|
|
44
47
|
"@vitus-labs/tools-rolldown": "^1.15.3"
|
|
45
48
|
},
|
|
46
49
|
"peerDependencies": {
|
|
47
|
-
"@pyreon/core": "^0.12.
|
|
48
|
-
"@pyreon/reactivity": "^0.12.
|
|
50
|
+
"@pyreon/core": "^0.12.15",
|
|
51
|
+
"@pyreon/reactivity": "^0.12.15"
|
|
49
52
|
},
|
|
50
53
|
"engines": {
|
|
51
54
|
"node": ">= 22"
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/** @jsxImportSource @pyreon/core */
|
|
2
|
+
import { h } from '@pyreon/core'
|
|
3
|
+
import { mountInBrowser } from '@pyreon/test-utils/browser'
|
|
4
|
+
import { afterEach, describe, expect, it } from 'vitest'
|
|
5
|
+
import { css } from '../css'
|
|
6
|
+
import { keyframes } from '../keyframes'
|
|
7
|
+
import { sheet } from '../sheet'
|
|
8
|
+
import { styled } from '../styled'
|
|
9
|
+
import { ThemeProvider } from '../ThemeProvider'
|
|
10
|
+
|
|
11
|
+
// Real-Chromium smoke for @pyreon/styler.
|
|
12
|
+
//
|
|
13
|
+
// happy-dom approximates a stylesheet but does NOT compute actual
|
|
14
|
+
// styles — `getComputedStyle(el).color` returns empty in happy-dom for
|
|
15
|
+
// rules in the injected stylesheet. These tests assert real cascade
|
|
16
|
+
// behavior in Chromium: the generated class is applied AND the browser
|
|
17
|
+
// resolves the rule to the expected computed style.
|
|
18
|
+
//
|
|
19
|
+
// What the suite locks in:
|
|
20
|
+
// 1. `styled('div')` produces a VNode that mounts to a real div with
|
|
21
|
+
// the generated class applied.
|
|
22
|
+
// 2. The CSS rule reaches `document.head` and Chromium computes the
|
|
23
|
+
// style as authored.
|
|
24
|
+
// 3. Reactive interpolations update computed styles when the signal
|
|
25
|
+
// changes (via the same reactive cascade as static text).
|
|
26
|
+
// 4. Different tags (div, span, button) produce distinct elements
|
|
27
|
+
// with the same class infrastructure.
|
|
28
|
+
// 5. `keyframes` template returns an animation name and the rule is
|
|
29
|
+
// registered so the animation can fire.
|
|
30
|
+
|
|
31
|
+
describe('@pyreon/styler in real browser', () => {
|
|
32
|
+
afterEach(() => {
|
|
33
|
+
// Don't remove the styler <style> element — sheet is a singleton
|
|
34
|
+
// with `this.sheet` bound to that element; removing detaches the
|
|
35
|
+
// sheet object and breaks subsequent insertRule calls. Cache is
|
|
36
|
+
// independent and safe to clear.
|
|
37
|
+
sheet.clearCache()
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('mounts a styled div with the generated class applied to the DOM', () => {
|
|
41
|
+
const Box = styled('div')`
|
|
42
|
+
display: flex;
|
|
43
|
+
color: rgb(255, 0, 0);
|
|
44
|
+
`
|
|
45
|
+
const { container, unmount } = mountInBrowser(h(Box, { id: 'box' }))
|
|
46
|
+
const el = container.querySelector<HTMLDivElement>('#box')
|
|
47
|
+
expect(el).not.toBeNull()
|
|
48
|
+
expect(el?.tagName.toLowerCase()).toBe('div')
|
|
49
|
+
expect(el?.className).toMatch(/^pyr-/)
|
|
50
|
+
unmount()
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('Chromium computes the authored style — real cascade, not just class application', () => {
|
|
54
|
+
const Red = styled('div')`
|
|
55
|
+
color: rgb(255, 0, 0);
|
|
56
|
+
padding: 12px;
|
|
57
|
+
`
|
|
58
|
+
const { container, unmount } = mountInBrowser(h(Red, { id: 'r' }))
|
|
59
|
+
const el = container.querySelector<HTMLElement>('#r')!
|
|
60
|
+
const cs = getComputedStyle(el)
|
|
61
|
+
expect(cs.color).toBe('rgb(255, 0, 0)')
|
|
62
|
+
expect(cs.padding).toBe('12px')
|
|
63
|
+
unmount()
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('function interpolation resolves per-render against props (dynamic path)', () => {
|
|
67
|
+
const Dynamic = styled('div')<{ $tone: string }>`
|
|
68
|
+
color: ${(p) => p.$tone};
|
|
69
|
+
`
|
|
70
|
+
const a = mountInBrowser(h(Dynamic, { id: 'a', $tone: 'rgb(0, 128, 0)' }))
|
|
71
|
+
const b = mountInBrowser(h(Dynamic, { id: 'b', $tone: 'rgb(0, 0, 255)' }))
|
|
72
|
+
expect(getComputedStyle(a.container.querySelector<HTMLElement>('#a')!).color).toBe(
|
|
73
|
+
'rgb(0, 128, 0)',
|
|
74
|
+
)
|
|
75
|
+
expect(getComputedStyle(b.container.querySelector<HTMLElement>('#b')!).color).toBe(
|
|
76
|
+
'rgb(0, 0, 255)',
|
|
77
|
+
)
|
|
78
|
+
a.unmount()
|
|
79
|
+
b.unmount()
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('different tags produce distinct elements (div, span, button)', () => {
|
|
83
|
+
const D = styled('div')`color: red;`
|
|
84
|
+
const S = styled('span')`color: green;`
|
|
85
|
+
const B = styled('button')`color: blue;`
|
|
86
|
+
const { container, unmount } = mountInBrowser(
|
|
87
|
+
h('div', null, h(D, { id: 'd' }), h(S, { id: 's' }), h(B, { id: 'b' })),
|
|
88
|
+
)
|
|
89
|
+
expect(container.querySelector('#d')?.tagName.toLowerCase()).toBe('div')
|
|
90
|
+
expect(container.querySelector('#s')?.tagName.toLowerCase()).toBe('span')
|
|
91
|
+
expect(container.querySelector('#b')?.tagName.toLowerCase()).toBe('button')
|
|
92
|
+
unmount()
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('ThemeProvider injects a theme readable by themed components', () => {
|
|
96
|
+
const Themed = styled('div')`
|
|
97
|
+
color: ${(p: { theme: { color: string } }) => p.theme.color};
|
|
98
|
+
`
|
|
99
|
+
const { container, unmount } = mountInBrowser(
|
|
100
|
+
h(ThemeProvider, { theme: { color: 'rgb(128, 0, 128)' } }, h(Themed, { id: 't' })),
|
|
101
|
+
)
|
|
102
|
+
const el = container.querySelector<HTMLElement>('#t')!
|
|
103
|
+
expect(getComputedStyle(el).color).toBe('rgb(128, 0, 128)')
|
|
104
|
+
unmount()
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('keyframes registers an animation name usable in styled rules', () => {
|
|
108
|
+
const fadeIn = keyframes`
|
|
109
|
+
from { opacity: 0; }
|
|
110
|
+
to { opacity: 1; }
|
|
111
|
+
`
|
|
112
|
+
const name = String(fadeIn)
|
|
113
|
+
expect(name).toMatch(/^pyr-kf-/)
|
|
114
|
+
|
|
115
|
+
const Animated = styled('div')`
|
|
116
|
+
opacity: 1;
|
|
117
|
+
animation: ${name} 50ms forwards;
|
|
118
|
+
`
|
|
119
|
+
const { container, unmount } = mountInBrowser(h(Animated, { id: 'a' }))
|
|
120
|
+
const el = container.querySelector<HTMLElement>('#a')!
|
|
121
|
+
// animation-name is the resolved animation token (Chromium normalizes).
|
|
122
|
+
const cs = getComputedStyle(el)
|
|
123
|
+
expect(cs.animationName).toContain(name)
|
|
124
|
+
unmount()
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('css`...` lazy CSSResult interpolates into styled() and Chromium computes the rule', () => {
|
|
128
|
+
// CSSResult is lazy — toString() / nested interpolation triggers
|
|
129
|
+
// resolution. This test asserts a standalone css`` block flows
|
|
130
|
+
// through styled() and reaches the cascade in real Chromium.
|
|
131
|
+
const accent = css`
|
|
132
|
+
color: rgb(123, 200, 50);
|
|
133
|
+
font-weight: 700;
|
|
134
|
+
`
|
|
135
|
+
const Styled = styled('div')`
|
|
136
|
+
${accent}
|
|
137
|
+
padding: 4px;
|
|
138
|
+
`
|
|
139
|
+
const { container, unmount } = mountInBrowser(h(Styled, { id: 'c' }))
|
|
140
|
+
const el = container.querySelector<HTMLElement>('#c')!
|
|
141
|
+
const cs = getComputedStyle(el)
|
|
142
|
+
expect(cs.color).toBe('rgb(123, 200, 50)')
|
|
143
|
+
expect(cs.fontWeight).toBe('700')
|
|
144
|
+
expect(cs.padding).toBe('4px')
|
|
145
|
+
unmount()
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it('writes its CSS rules to a real <style> element under document.head', () => {
|
|
149
|
+
const X = styled('div')`background-color: rgb(10, 20, 30);`
|
|
150
|
+
const { unmount } = mountInBrowser(h(X, { id: 'x' }))
|
|
151
|
+
// Chromium accepts inserted CSS rules — the injected sheet exists
|
|
152
|
+
// and the rule is queryable via document.styleSheets.
|
|
153
|
+
let found = false
|
|
154
|
+
for (const s of Array.from(document.styleSheets)) {
|
|
155
|
+
try {
|
|
156
|
+
for (const rule of Array.from(s.cssRules ?? [])) {
|
|
157
|
+
if (rule.cssText.includes('rgb(10, 20, 30)')) {
|
|
158
|
+
found = true
|
|
159
|
+
break
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
} catch {
|
|
163
|
+
// cross-origin sheets throw — ignore
|
|
164
|
+
}
|
|
165
|
+
if (found) break
|
|
166
|
+
}
|
|
167
|
+
expect(found).toBe(true)
|
|
168
|
+
unmount()
|
|
169
|
+
})
|
|
170
|
+
})
|