@furystack/shades 11.0.34 → 11.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/CHANGELOG.md +52 -0
- package/README.md +86 -0
- package/esm/compile-route.spec.d.ts +2 -0
- package/esm/compile-route.spec.d.ts.map +1 -0
- package/esm/compile-route.spec.js +34 -0
- package/esm/compile-route.spec.js.map +1 -0
- package/esm/components/route-link.d.ts.map +1 -1
- package/esm/components/route-link.js +4 -5
- package/esm/components/route-link.js.map +1 -1
- package/esm/components/route-link.spec.js +1 -1
- package/esm/components/route-link.spec.js.map +1 -1
- package/esm/css-generator.d.ts +50 -0
- package/esm/css-generator.d.ts.map +1 -0
- package/esm/css-generator.js +107 -0
- package/esm/css-generator.js.map +1 -0
- package/esm/css-generator.spec.d.ts +2 -0
- package/esm/css-generator.spec.d.ts.map +1 -0
- package/esm/css-generator.spec.js +162 -0
- package/esm/css-generator.spec.js.map +1 -0
- package/esm/index.d.ts +2 -0
- package/esm/index.d.ts.map +1 -1
- package/esm/index.js +2 -0
- package/esm/index.js.map +1 -1
- package/esm/models/css-object.d.ts +33 -0
- package/esm/models/css-object.d.ts.map +1 -0
- package/esm/models/css-object.js +2 -0
- package/esm/models/css-object.js.map +1 -0
- package/esm/models/index.d.ts +1 -0
- package/esm/models/index.d.ts.map +1 -1
- package/esm/models/index.js +1 -0
- package/esm/models/index.js.map +1 -1
- package/esm/shade.d.ts +18 -2
- package/esm/shade.d.ts.map +1 -1
- package/esm/shade.js +8 -0
- package/esm/shade.js.map +1 -1
- package/esm/shade.spec.d.ts +2 -0
- package/esm/shade.spec.d.ts.map +1 -0
- package/esm/shade.spec.js +197 -0
- package/esm/shade.spec.js.map +1 -0
- package/esm/style-manager.d.ts +65 -0
- package/esm/style-manager.d.ts.map +1 -0
- package/esm/style-manager.js +95 -0
- package/esm/style-manager.js.map +1 -0
- package/esm/style-manager.spec.d.ts +2 -0
- package/esm/style-manager.spec.d.ts.map +1 -0
- package/esm/style-manager.spec.js +179 -0
- package/esm/style-manager.spec.js.map +1 -0
- package/esm/styled-element.spec.d.ts +2 -0
- package/esm/styled-element.spec.d.ts.map +1 -0
- package/esm/styled-element.spec.js +86 -0
- package/esm/styled-element.spec.js.map +1 -0
- package/esm/styled-shade.spec.d.ts +2 -0
- package/esm/styled-shade.spec.d.ts.map +1 -0
- package/esm/styled-shade.spec.js +66 -0
- package/esm/styled-shade.spec.js.map +1 -0
- package/package.json +15 -10
- package/src/compile-route.spec.ts +39 -0
- package/src/components/route-link.spec.tsx +1 -1
- package/src/components/route-link.tsx +4 -5
- package/src/css-generator.spec.ts +183 -0
- package/src/css-generator.ts +117 -0
- package/src/index.ts +2 -0
- package/src/models/css-object.ts +34 -0
- package/src/models/index.ts +1 -0
- package/src/shade.spec.tsx +238 -0
- package/src/shade.ts +29 -2
- package/src/style-manager.spec.ts +229 -0
- package/src/style-manager.ts +104 -0
- package/src/styled-element.spec.tsx +117 -0
- package/src/styled-shade.spec.ts +86 -0
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
|
2
|
+
import { Shade } from './shade.js'
|
|
3
|
+
import { StyleManager } from './style-manager.js'
|
|
4
|
+
|
|
5
|
+
describe('StyleManager', () => {
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
StyleManager.clear()
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
StyleManager.clear()
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
describe('registerComponentStyles', () => {
|
|
15
|
+
it('should register styles for a component', () => {
|
|
16
|
+
const result = StyleManager.registerComponentStyles('test-component', {
|
|
17
|
+
color: 'red',
|
|
18
|
+
padding: '10px',
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
expect(result).toBe(true)
|
|
22
|
+
expect(StyleManager.isRegistered('test-component')).toBe(true)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('should inject CSS into a style element', () => {
|
|
26
|
+
StyleManager.registerComponentStyles('test-component', {
|
|
27
|
+
color: 'red',
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
const styleElement = document.querySelector('[data-shades-styles]')
|
|
31
|
+
expect(styleElement).not.toBeNull()
|
|
32
|
+
expect(styleElement?.textContent).toContain('test-component')
|
|
33
|
+
expect(styleElement?.textContent).toContain('color: red')
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('should not register the same component twice', () => {
|
|
37
|
+
const result1 = StyleManager.registerComponentStyles('test-component', {
|
|
38
|
+
color: 'red',
|
|
39
|
+
})
|
|
40
|
+
const result2 = StyleManager.registerComponentStyles('test-component', {
|
|
41
|
+
color: 'blue',
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
expect(result1).toBe(true)
|
|
45
|
+
expect(result2).toBe(false)
|
|
46
|
+
|
|
47
|
+
// Should only have the first style
|
|
48
|
+
const styleElement = document.querySelector('[data-shades-styles]')
|
|
49
|
+
expect(styleElement?.textContent).toContain('color: red')
|
|
50
|
+
expect(styleElement?.textContent).not.toContain('color: blue')
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('should handle pseudo-selectors', () => {
|
|
54
|
+
StyleManager.registerComponentStyles('test-button', {
|
|
55
|
+
backgroundColor: 'blue',
|
|
56
|
+
'&:hover': { backgroundColor: 'darkblue' },
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
const styleElement = document.querySelector('[data-shades-styles]')
|
|
60
|
+
expect(styleElement?.textContent).toContain('test-button:hover')
|
|
61
|
+
expect(styleElement?.textContent).toContain('background-color: darkblue')
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('should return false for empty CSS object', () => {
|
|
65
|
+
const result = StyleManager.registerComponentStyles('empty-component', {})
|
|
66
|
+
|
|
67
|
+
expect(result).toBe(false)
|
|
68
|
+
expect(StyleManager.isRegistered('empty-component')).toBe(false)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('should add comments with component name', () => {
|
|
72
|
+
StyleManager.registerComponentStyles('my-component', {
|
|
73
|
+
color: 'red',
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
const styleElement = document.querySelector('[data-shades-styles]')
|
|
77
|
+
expect(styleElement?.textContent).toContain('/* my-component */')
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('should generate attribute selector for customized built-in elements', () => {
|
|
81
|
+
StyleManager.registerComponentStyles(
|
|
82
|
+
'my-link',
|
|
83
|
+
{
|
|
84
|
+
color: 'blue',
|
|
85
|
+
textDecoration: 'none',
|
|
86
|
+
},
|
|
87
|
+
'a',
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
const styleElement = document.querySelector('[data-shades-styles]')
|
|
91
|
+
expect(styleElement?.textContent).toContain('a[is="my-link"]')
|
|
92
|
+
expect(styleElement?.textContent).toContain('color: blue')
|
|
93
|
+
expect(styleElement?.textContent).toContain('text-decoration: none')
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('should handle pseudo-selectors for customized built-in elements', () => {
|
|
97
|
+
StyleManager.registerComponentStyles(
|
|
98
|
+
'my-button',
|
|
99
|
+
{
|
|
100
|
+
backgroundColor: 'gray',
|
|
101
|
+
'&:hover': { backgroundColor: 'darkgray' },
|
|
102
|
+
'&:active': { transform: 'scale(0.98)' },
|
|
103
|
+
},
|
|
104
|
+
'button',
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
const styleElement = document.querySelector('[data-shades-styles]')
|
|
108
|
+
expect(styleElement?.textContent).toContain('button[is="my-button"]')
|
|
109
|
+
expect(styleElement?.textContent).toContain('button[is="my-button"]:hover')
|
|
110
|
+
expect(styleElement?.textContent).toContain('button[is="my-button"]:active')
|
|
111
|
+
})
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
describe('isRegistered', () => {
|
|
115
|
+
it('should return true for registered components', () => {
|
|
116
|
+
StyleManager.registerComponentStyles('test-component', { color: 'red' })
|
|
117
|
+
|
|
118
|
+
expect(StyleManager.isRegistered('test-component')).toBe(true)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('should return false for unregistered components', () => {
|
|
122
|
+
expect(StyleManager.isRegistered('unknown-component')).toBe(false)
|
|
123
|
+
})
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
describe('getRegisteredComponents', () => {
|
|
127
|
+
it('should return all registered component names', () => {
|
|
128
|
+
StyleManager.registerComponentStyles('component-a', { color: 'red' })
|
|
129
|
+
StyleManager.registerComponentStyles('component-b', { color: 'blue' })
|
|
130
|
+
|
|
131
|
+
const registered = StyleManager.getRegisteredComponents()
|
|
132
|
+
|
|
133
|
+
expect(registered.has('component-a')).toBe(true)
|
|
134
|
+
expect(registered.has('component-b')).toBe(true)
|
|
135
|
+
expect(registered.size).toBe(2)
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it('should return empty set when no components registered', () => {
|
|
139
|
+
const registered = StyleManager.getRegisteredComponents()
|
|
140
|
+
expect(registered.size).toBe(0)
|
|
141
|
+
})
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
describe('clear', () => {
|
|
145
|
+
it('should clear all registered components', () => {
|
|
146
|
+
StyleManager.registerComponentStyles('test-component', { color: 'red' })
|
|
147
|
+
expect(StyleManager.isRegistered('test-component')).toBe(true)
|
|
148
|
+
|
|
149
|
+
StyleManager.clear()
|
|
150
|
+
|
|
151
|
+
expect(StyleManager.isRegistered('test-component')).toBe(false)
|
|
152
|
+
expect(StyleManager.getRegisteredComponents().size).toBe(0)
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
it('should remove style element on clear', () => {
|
|
156
|
+
StyleManager.registerComponentStyles('test-component', { color: 'red' })
|
|
157
|
+
let styleElement = document.querySelector('[data-shades-styles]')
|
|
158
|
+
expect(styleElement).not.toBeNull()
|
|
159
|
+
|
|
160
|
+
StyleManager.clear()
|
|
161
|
+
|
|
162
|
+
styleElement = document.querySelector('[data-shades-styles]')
|
|
163
|
+
expect(styleElement).toBeNull()
|
|
164
|
+
})
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
describe('reusing style element', () => {
|
|
168
|
+
it('should use the same style element for multiple components', () => {
|
|
169
|
+
StyleManager.registerComponentStyles('component-a', { color: 'red' })
|
|
170
|
+
StyleManager.registerComponentStyles('component-b', { color: 'blue' })
|
|
171
|
+
|
|
172
|
+
const styleElements = document.querySelectorAll('[data-shades-styles]')
|
|
173
|
+
expect(styleElements.length).toBe(1)
|
|
174
|
+
|
|
175
|
+
const styleElement = styleElements[0]
|
|
176
|
+
expect(styleElement?.textContent).toContain('component-a')
|
|
177
|
+
expect(styleElement?.textContent).toContain('component-b')
|
|
178
|
+
})
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
describe('Shade integration', () => {
|
|
182
|
+
it('should register CSS styles when Shade component is created with css property', () => {
|
|
183
|
+
Shade({
|
|
184
|
+
shadowDomName: 'shade-css-test-component',
|
|
185
|
+
css: {
|
|
186
|
+
color: 'red',
|
|
187
|
+
padding: '10px',
|
|
188
|
+
'&:hover': { color: 'blue' },
|
|
189
|
+
},
|
|
190
|
+
render: () => null,
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
expect(StyleManager.isRegistered('shade-css-test-component')).toBe(true)
|
|
194
|
+
|
|
195
|
+
const styleElement = document.querySelector('[data-shades-styles]')
|
|
196
|
+
expect(styleElement?.textContent).toContain('shade-css-test-component')
|
|
197
|
+
expect(styleElement?.textContent).toContain('color: red')
|
|
198
|
+
expect(styleElement?.textContent).toContain('shade-css-test-component:hover')
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
it('should register CSS with attribute selector for customized built-in elements', () => {
|
|
202
|
+
Shade({
|
|
203
|
+
shadowDomName: 'shade-css-test-button',
|
|
204
|
+
elementBase: HTMLButtonElement,
|
|
205
|
+
elementBaseName: 'button',
|
|
206
|
+
css: {
|
|
207
|
+
backgroundColor: 'blue',
|
|
208
|
+
'&:hover': { backgroundColor: 'darkblue' },
|
|
209
|
+
},
|
|
210
|
+
render: () => null,
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
expect(StyleManager.isRegistered('shade-css-test-button')).toBe(true)
|
|
214
|
+
|
|
215
|
+
const styleElement = document.querySelector('[data-shades-styles]')
|
|
216
|
+
expect(styleElement?.textContent).toContain('button[is="shade-css-test-button"]')
|
|
217
|
+
expect(styleElement?.textContent).toContain('button[is="shade-css-test-button"]:hover')
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
it('should not register styles when Shade component has no css property', () => {
|
|
221
|
+
Shade({
|
|
222
|
+
shadowDomName: 'shade-no-css-component',
|
|
223
|
+
render: () => null,
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
expect(StyleManager.isRegistered('shade-no-css-component')).toBe(false)
|
|
227
|
+
})
|
|
228
|
+
})
|
|
229
|
+
})
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { generateCSS } from './css-generator.js'
|
|
2
|
+
import type { CSSObject } from './models/css-object.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Singleton that manages component CSS injection.
|
|
6
|
+
* Creates and maintains a shared `<style>` element in the document head,
|
|
7
|
+
* and tracks registered component styles to avoid duplicates.
|
|
8
|
+
*/
|
|
9
|
+
class StyleManagerClass {
|
|
10
|
+
private styleElement: HTMLStyleElement | null = null
|
|
11
|
+
private registeredComponents = new Set<string>()
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Gets or creates the shared style element
|
|
15
|
+
* @returns The style element for CSS injection
|
|
16
|
+
*/
|
|
17
|
+
private getStyleElement(): HTMLStyleElement {
|
|
18
|
+
if (!this.styleElement) {
|
|
19
|
+
this.styleElement = document.createElement('style')
|
|
20
|
+
this.styleElement.setAttribute('data-shades-styles', '')
|
|
21
|
+
document.head.appendChild(this.styleElement)
|
|
22
|
+
}
|
|
23
|
+
return this.styleElement
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Registers CSS styles for a component.
|
|
28
|
+
* Styles are only injected once per component (based on shadowDomName).
|
|
29
|
+
*
|
|
30
|
+
* @param shadowDomName - The unique component identifier (used as CSS selector)
|
|
31
|
+
* @param cssObject - The CSSObject containing styles and nested selectors
|
|
32
|
+
* @param elementBaseName - Optional base element name for customized built-in elements (e.g., 'a', 'button').
|
|
33
|
+
* When provided, generates selector like `a[is="component-name"]` instead of `component-name`
|
|
34
|
+
* @returns True if styles were injected, false if already registered
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```typescript
|
|
38
|
+
* // Regular custom element
|
|
39
|
+
* StyleManager.registerComponentStyles('my-button', {
|
|
40
|
+
* padding: '12px',
|
|
41
|
+
* '&:hover': { backgroundColor: 'blue' }
|
|
42
|
+
* })
|
|
43
|
+
*
|
|
44
|
+
* // Customized built-in element (extends anchor)
|
|
45
|
+
* StyleManager.registerComponentStyles('my-link', {
|
|
46
|
+
* color: 'blue',
|
|
47
|
+
* '&:hover': { textDecoration: 'underline' }
|
|
48
|
+
* }, 'a')
|
|
49
|
+
* // Generates: a[is="my-link"] { color: blue; }
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
public registerComponentStyles(shadowDomName: string, cssObject: CSSObject, elementBaseName?: string): boolean {
|
|
53
|
+
if (this.registeredComponents.has(shadowDomName)) {
|
|
54
|
+
return false
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const selector = elementBaseName ? `${elementBaseName}[is="${shadowDomName}"]` : shadowDomName
|
|
58
|
+
const css = generateCSS(selector, cssObject)
|
|
59
|
+
if (css) {
|
|
60
|
+
const styleElement = this.getStyleElement()
|
|
61
|
+
styleElement.textContent += `\n/* ${shadowDomName} */\n${css}\n`
|
|
62
|
+
this.registeredComponents.add(shadowDomName)
|
|
63
|
+
return true
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return false
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Checks if a component's styles have already been registered
|
|
71
|
+
* @param shadowDomName - The component identifier to check
|
|
72
|
+
* @returns True if styles are already registered
|
|
73
|
+
*/
|
|
74
|
+
public isRegistered(shadowDomName: string): boolean {
|
|
75
|
+
return this.registeredComponents.has(shadowDomName)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Gets all registered component names (for debugging/testing)
|
|
80
|
+
* @returns Set of registered component names
|
|
81
|
+
*/
|
|
82
|
+
public getRegisteredComponents(): ReadonlySet<string> {
|
|
83
|
+
return this.registeredComponents
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Clears all registered styles (useful for testing)
|
|
88
|
+
*/
|
|
89
|
+
public clear(): void {
|
|
90
|
+
this.registeredComponents.clear()
|
|
91
|
+
if (this.styleElement) {
|
|
92
|
+
this.styleElement.textContent = ''
|
|
93
|
+
this.styleElement.remove()
|
|
94
|
+
this.styleElement = null
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Singleton instance for managing component CSS styles.
|
|
101
|
+
* Use this to register component-level styles that support
|
|
102
|
+
* pseudo-selectors and nested selectors.
|
|
103
|
+
*/
|
|
104
|
+
export const StyleManager = new StyleManagerClass()
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { styledElement } from './styled-element.js'
|
|
3
|
+
|
|
4
|
+
describe('styled-element', () => {
|
|
5
|
+
describe('styledElement', () => {
|
|
6
|
+
it('should apply additional styles to an intrinsic element', () => {
|
|
7
|
+
const additionalStyles = { color: 'red', padding: '10px' }
|
|
8
|
+
const StyledDiv = styledElement('div', additionalStyles)
|
|
9
|
+
|
|
10
|
+
const result = StyledDiv({}, [])
|
|
11
|
+
|
|
12
|
+
expect(result.style.color).toBe('red')
|
|
13
|
+
expect(result.style.padding).toBe('10px')
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('should merge styles when element already has styles', () => {
|
|
17
|
+
const additionalStyles = { color: 'red' }
|
|
18
|
+
const existingStyles = { backgroundColor: 'blue' }
|
|
19
|
+
|
|
20
|
+
const StyledDiv = styledElement('div', additionalStyles)
|
|
21
|
+
const result = StyledDiv({ style: existingStyles }, [])
|
|
22
|
+
|
|
23
|
+
expect(result.style.backgroundColor).toBe('blue')
|
|
24
|
+
expect(result.style.color).toBe('red')
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('should override existing styles with additional styles', () => {
|
|
28
|
+
const additionalStyles = { color: 'red' }
|
|
29
|
+
const existingStyles = { color: 'blue' }
|
|
30
|
+
|
|
31
|
+
const StyledDiv = styledElement('div', additionalStyles)
|
|
32
|
+
const result = StyledDiv({ style: existingStyles }, [])
|
|
33
|
+
|
|
34
|
+
expect(result.style.color).toBe('red')
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('should preserve original props', () => {
|
|
38
|
+
const additionalStyles = { color: 'red' }
|
|
39
|
+
|
|
40
|
+
const StyledDiv = styledElement('div', additionalStyles)
|
|
41
|
+
const result = StyledDiv({ className: 'my-class', id: 'my-id' }, [])
|
|
42
|
+
|
|
43
|
+
expect(result.className).toBe('my-class')
|
|
44
|
+
expect(result.id).toBe('my-id')
|
|
45
|
+
expect(result.style.color).toBe('red')
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('should pass children correctly', () => {
|
|
49
|
+
const additionalStyles = { color: 'red' }
|
|
50
|
+
|
|
51
|
+
const StyledDiv = styledElement('div', additionalStyles)
|
|
52
|
+
const child1 = document.createElement('span')
|
|
53
|
+
child1.textContent = 'child1'
|
|
54
|
+
const child2 = document.createElement('span')
|
|
55
|
+
child2.textContent = 'child2'
|
|
56
|
+
|
|
57
|
+
const result = StyledDiv({}, [child1, child2])
|
|
58
|
+
|
|
59
|
+
expect(result.children.length).toBe(2)
|
|
60
|
+
expect(result.children[0]).toBe(child1)
|
|
61
|
+
expect(result.children[1]).toBe(child2)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('should handle text children', () => {
|
|
65
|
+
const additionalStyles = { color: 'red' }
|
|
66
|
+
|
|
67
|
+
const StyledDiv = styledElement('div', additionalStyles)
|
|
68
|
+
const result = StyledDiv({}, ['Hello', ' ', 'World'])
|
|
69
|
+
|
|
70
|
+
expect(result.textContent).toBe('Hello World')
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('should return an HTMLElement', () => {
|
|
74
|
+
const additionalStyles = { color: 'red' }
|
|
75
|
+
|
|
76
|
+
const StyledDiv = styledElement('div', additionalStyles)
|
|
77
|
+
const result = StyledDiv({}, [])
|
|
78
|
+
|
|
79
|
+
expect(result).toBeInstanceOf(HTMLElement)
|
|
80
|
+
expect(result.tagName.toLowerCase()).toBe('div')
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('should work with different element types', () => {
|
|
84
|
+
const spanStyles = { fontWeight: 'bold' }
|
|
85
|
+
const buttonStyles = { cursor: 'pointer' }
|
|
86
|
+
|
|
87
|
+
const StyledSpan = styledElement('span', spanStyles)
|
|
88
|
+
const StyledButton = styledElement('button', buttonStyles)
|
|
89
|
+
|
|
90
|
+
const spanResult = StyledSpan({}, [])
|
|
91
|
+
const buttonResult = StyledButton({}, [])
|
|
92
|
+
|
|
93
|
+
expect(spanResult.tagName.toLowerCase()).toBe('span')
|
|
94
|
+
expect(spanResult.style.fontWeight).toBe('bold')
|
|
95
|
+
expect(buttonResult.tagName.toLowerCase()).toBe('button')
|
|
96
|
+
expect(buttonResult.style.cursor).toBe('pointer')
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('should handle props being undefined', () => {
|
|
100
|
+
const additionalStyles = { color: 'red' }
|
|
101
|
+
|
|
102
|
+
const StyledDiv = styledElement('div', additionalStyles)
|
|
103
|
+
const result = StyledDiv(undefined as unknown as Record<string, unknown>, [])
|
|
104
|
+
|
|
105
|
+
expect(result.style.color).toBe('red')
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('should handle empty children list', () => {
|
|
109
|
+
const additionalStyles = { color: 'red' }
|
|
110
|
+
|
|
111
|
+
const StyledDiv = styledElement('div', additionalStyles)
|
|
112
|
+
const result = StyledDiv({}, [])
|
|
113
|
+
|
|
114
|
+
expect(result.children.length).toBe(0)
|
|
115
|
+
})
|
|
116
|
+
})
|
|
117
|
+
})
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { styledShade } from './styled-shade.js'
|
|
3
|
+
|
|
4
|
+
describe('styled-shade', () => {
|
|
5
|
+
describe('styledShade', () => {
|
|
6
|
+
it('should apply additional styles to a component', () => {
|
|
7
|
+
const mockElement = vi.fn().mockReturnValue({} as JSX.Element)
|
|
8
|
+
const additionalStyles = { color: 'red', padding: '10px' }
|
|
9
|
+
|
|
10
|
+
const StyledComponent = styledShade(mockElement, additionalStyles)
|
|
11
|
+
StyledComponent({})
|
|
12
|
+
|
|
13
|
+
expect(mockElement).toHaveBeenCalledTimes(1)
|
|
14
|
+
const [props] = mockElement.mock.calls[0] as [{ style: Record<string, string> }]
|
|
15
|
+
expect(props.style.color).toBe('red')
|
|
16
|
+
expect(props.style.padding).toBe('10px')
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('should merge styles when component already has styles', () => {
|
|
20
|
+
const mockElement = vi.fn().mockReturnValue({} as JSX.Element)
|
|
21
|
+
const additionalStyles = { color: 'red' }
|
|
22
|
+
const existingStyles = { backgroundColor: 'blue' }
|
|
23
|
+
|
|
24
|
+
const StyledComponent = styledShade(mockElement, additionalStyles)
|
|
25
|
+
StyledComponent({ style: existingStyles })
|
|
26
|
+
|
|
27
|
+
expect(mockElement).toHaveBeenCalledTimes(1)
|
|
28
|
+
const [props] = mockElement.mock.calls[0] as [{ style: Record<string, string> }]
|
|
29
|
+
expect(props.style.backgroundColor).toBe('blue')
|
|
30
|
+
expect(props.style.color).toBe('red')
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('should override existing styles with additional styles', () => {
|
|
34
|
+
const mockElement = vi.fn().mockReturnValue({} as JSX.Element)
|
|
35
|
+
const additionalStyles = { color: 'red' }
|
|
36
|
+
const existingStyles = { color: 'blue' }
|
|
37
|
+
|
|
38
|
+
const StyledComponent = styledShade(mockElement, additionalStyles)
|
|
39
|
+
StyledComponent({ style: existingStyles })
|
|
40
|
+
|
|
41
|
+
expect(mockElement).toHaveBeenCalledTimes(1)
|
|
42
|
+
const [props] = mockElement.mock.calls[0] as [{ style: Record<string, string> }]
|
|
43
|
+
expect(props.style.color).toBe('red')
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('should preserve original props', () => {
|
|
47
|
+
const mockElement = vi.fn().mockReturnValue({} as JSX.Element)
|
|
48
|
+
const additionalStyles = { color: 'red' }
|
|
49
|
+
|
|
50
|
+
const StyledComponent = styledShade<{ className?: string; style?: Partial<CSSStyleDeclaration> }>(
|
|
51
|
+
mockElement,
|
|
52
|
+
additionalStyles,
|
|
53
|
+
)
|
|
54
|
+
StyledComponent({ className: 'my-class' })
|
|
55
|
+
|
|
56
|
+
expect(mockElement).toHaveBeenCalledTimes(1)
|
|
57
|
+
const [props] = mockElement.mock.calls[0] as [{ className: string; style: Record<string, string> }]
|
|
58
|
+
expect(props.className).toBe('my-class')
|
|
59
|
+
expect(props.style.color).toBe('red')
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('should pass children correctly', () => {
|
|
63
|
+
const mockElement = vi.fn().mockReturnValue({} as JSX.Element)
|
|
64
|
+
const additionalStyles = { color: 'red' }
|
|
65
|
+
const children = ['child1', 'child2'] as unknown as JSX.Element[]
|
|
66
|
+
|
|
67
|
+
const StyledComponent = styledShade(mockElement, additionalStyles)
|
|
68
|
+
StyledComponent({}, children)
|
|
69
|
+
|
|
70
|
+
expect(mockElement).toHaveBeenCalledTimes(1)
|
|
71
|
+
const [, passedChildren] = mockElement.mock.calls[0] as [unknown, JSX.Element[]]
|
|
72
|
+
expect(passedChildren).toBe(children)
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('should return a function that returns JSX.Element', () => {
|
|
76
|
+
const expectedElement = { tagName: 'div' } as unknown as JSX.Element
|
|
77
|
+
const mockElement = vi.fn().mockReturnValue(expectedElement)
|
|
78
|
+
const additionalStyles = { color: 'red' }
|
|
79
|
+
|
|
80
|
+
const StyledComponent = styledShade(mockElement, additionalStyles)
|
|
81
|
+
const result = StyledComponent({})
|
|
82
|
+
|
|
83
|
+
expect(result).toBe(expectedElement)
|
|
84
|
+
})
|
|
85
|
+
})
|
|
86
|
+
})
|