894 lines
23 KiB
TypeScript
894 lines
23 KiB
TypeScript
import { describe, it, expect } from 'vitest'
|
|
import { mount } from '@vue/test-utils'
|
|
import Button from 'primevue/button'
|
|
import { h, ref } from 'vue'
|
|
|
|
import { ThemeEngine, ThemeLibrary, type Themeable } from '../src/engine'
|
|
|
|
const getTheme = () => {
|
|
const button: Themeable<typeof Button> = {
|
|
classes: ['test-class'],
|
|
styles: ['width: 50px'],
|
|
propMutator: (props) => {
|
|
if (props.variant) {
|
|
const pt = props.pt as { root: { class?: string } }
|
|
|
|
const classes = pt.root.class?.split(' ') ?? []
|
|
|
|
classes.push(`test-${props.variant}`)
|
|
|
|
pt.root.class = classes.join(' ')
|
|
|
|
props.pt = pt
|
|
}
|
|
|
|
return props
|
|
},
|
|
injectionKeys: {
|
|
class: 'pt',
|
|
},
|
|
component: Button,
|
|
}
|
|
|
|
const buttonTheme: ThemeLibrary = {
|
|
components: {
|
|
Button: button,
|
|
},
|
|
}
|
|
return buttonTheme
|
|
}
|
|
|
|
const getSimpleTheme = () => {
|
|
const button: Themeable<typeof Button> = {
|
|
classes: ['btn-default'],
|
|
styles: ['padding: 8px'],
|
|
injectionKeys: {
|
|
class: 'pt',
|
|
},
|
|
component: Button,
|
|
}
|
|
|
|
return {
|
|
components: {
|
|
Button: button,
|
|
},
|
|
}
|
|
}
|
|
|
|
//eslint-disable-next-line
|
|
const callSetup = (component: any, props: Record<string, unknown>) => {
|
|
// Apply prop defaults from the component definition
|
|
const mergedProps: Record<string, unknown> = {}
|
|
|
|
if (component.props) {
|
|
for (const [key, propDef] of Object.entries(component.props)) {
|
|
if (typeof propDef === 'object' && propDef !== null && 'default' in propDef) {
|
|
mergedProps[key] = propDef.default
|
|
}
|
|
}
|
|
}
|
|
|
|
// Merge in user-provided props
|
|
Object.assign(mergedProps, props)
|
|
|
|
if (component.setup) {
|
|
const result = component.setup(mergedProps, { slots: {}, attrs: {} })
|
|
// Engine always returns a render function; call it and return the vnode's props
|
|
// so tests can inspect the final merged props passed to the original component.
|
|
if (typeof result === 'function') {
|
|
const vnode = result()
|
|
return (vnode?.props as Record<string, unknown>) || {}
|
|
}
|
|
return result
|
|
}
|
|
return mergedProps
|
|
}
|
|
|
|
describe('ThemeEngine', () => {
|
|
it('instantiates properly', () => {
|
|
const theme = getTheme()
|
|
const engine = new ThemeEngine(theme)
|
|
|
|
expect(engine).not.toBeNull()
|
|
expect(engine.lib).toBe(theme)
|
|
})
|
|
|
|
it('returns a wrapped component', () => {
|
|
const theme = getTheme()
|
|
const engine = new ThemeEngine(theme)
|
|
|
|
//eslint-disable-next-line
|
|
const themedButton: any = engine.getComponent<typeof Button>('Button')
|
|
|
|
expect(themedButton).not.toBeNull()
|
|
expect(themedButton.props).toBeDefined()
|
|
expect(themedButton.setup).toBeDefined()
|
|
})
|
|
|
|
it('injects classes from theme definition', () => {
|
|
const theme = getSimpleTheme()
|
|
const engine = new ThemeEngine(theme)
|
|
const themedButton = engine.getComponent<typeof Button>('Button')
|
|
|
|
const result = callSetup(themedButton, {
|
|
pt: {},
|
|
})
|
|
|
|
expect(result.pt?.root?.class).toContain('btn-default')
|
|
})
|
|
|
|
it('injects styles from theme definition', () => {
|
|
const theme = getSimpleTheme()
|
|
const engine = new ThemeEngine(theme)
|
|
const themedButton = engine.getComponent<typeof Button>('Button')
|
|
|
|
const result = callSetup(themedButton, {
|
|
pt: {},
|
|
})
|
|
|
|
expect(result.pt?.root?.style).toContain('padding: 8px')
|
|
})
|
|
|
|
it('applies propMutator to modify component props', () => {
|
|
const theme = getTheme()
|
|
const engine = new ThemeEngine(theme)
|
|
const themedButton = engine.getComponent<typeof Button>('Button')
|
|
|
|
const result = callSetup(themedButton, {
|
|
pt: {},
|
|
variant: 'primary',
|
|
})
|
|
|
|
expect(result.pt?.root?.class).toContain('test-primary')
|
|
})
|
|
|
|
it('handles PT injection key for PrimeVue components', () => {
|
|
const theme = getTheme()
|
|
const engine = new ThemeEngine(theme)
|
|
const themedButton = engine.getComponent<typeof Button>('Button')
|
|
|
|
const result = callSetup(themedButton, {
|
|
pt: {},
|
|
})
|
|
|
|
expect(result.pt).toBeDefined()
|
|
expect(result.pt.root).toBeDefined()
|
|
expect(result.pt.root.class).toBeDefined()
|
|
expect(result.pt.root.style).toBeDefined()
|
|
})
|
|
|
|
it('applies class prefix when stylePlug is provided', () => {
|
|
const simpleTheme = getSimpleTheme()
|
|
const engine = new ThemeEngine(simpleTheme)
|
|
const themedButton = engine.getComponent<typeof Button>('Button')
|
|
|
|
const result = callSetup(themedButton, {
|
|
stylePlug: 'custom',
|
|
pt: {},
|
|
})
|
|
|
|
expect(result.pt?.root?.class).toContain('custom-btn-default')
|
|
})
|
|
|
|
it('merges user-provided PT with theme-injected classes', () => {
|
|
const simpleTheme = getSimpleTheme()
|
|
const engine = new ThemeEngine(simpleTheme)
|
|
const themedButton = engine.getComponent<typeof Button>('Button')
|
|
|
|
const result = callSetup(themedButton, {
|
|
pt: {
|
|
label: {
|
|
class: 'user-label-class',
|
|
},
|
|
},
|
|
})
|
|
|
|
// Theme classes should be injected to root
|
|
expect(result.pt?.root?.class).toContain('btn-default')
|
|
// User-provided PT structure should be preserved
|
|
expect(result.pt?.label?.class).toBe('user-label-class')
|
|
})
|
|
|
|
it('preserves existing PT structure when merging', () => {
|
|
const theme: ThemeLibrary = {
|
|
components: {
|
|
Button: {
|
|
component: Button,
|
|
injectionKeys: {
|
|
class: 'pt',
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
const engine = new ThemeEngine(theme)
|
|
const themedButton = engine.getComponent<typeof Button>('Button')
|
|
|
|
const result = callSetup(themedButton, {
|
|
pt: {
|
|
root: { class: 'existing' },
|
|
label: { class: 'label-class' },
|
|
},
|
|
})
|
|
|
|
expect(result.pt?.root).toBeDefined()
|
|
expect(result.pt?.label?.class).toBe('label-class')
|
|
})
|
|
|
|
it('preserves theme classes when user provides pt.root', () => {
|
|
const theme: ThemeLibrary = {
|
|
components: {
|
|
Button: {
|
|
component: Button,
|
|
classes: ['theme-class'],
|
|
injectionKeys: {
|
|
class: 'pt',
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
const engine = new ThemeEngine(theme)
|
|
const themedButton = engine.getComponent<typeof Button>('Button')
|
|
|
|
const result = callSetup(themedButton, {
|
|
pt: { root: { class: 'user-root' } },
|
|
})
|
|
|
|
expect(result.pt?.root?.class).toContain('theme-class')
|
|
expect(result.pt?.root?.class).toContain('user-root')
|
|
})
|
|
|
|
it('handles missing injectionKeys by defaulting to class and style', () => {
|
|
const theme: ThemeLibrary = {
|
|
components: {
|
|
Button: {
|
|
component: Button,
|
|
classes: ['default-class'],
|
|
styles: ['color: red'],
|
|
},
|
|
},
|
|
}
|
|
|
|
const engine = new ThemeEngine(theme)
|
|
const themedButton = engine.getComponent<typeof Button>('Button')
|
|
|
|
const result = callSetup(themedButton, {})
|
|
|
|
expect(result.class).toBe('default-class')
|
|
expect(result.style).toBe('color: red')
|
|
})
|
|
|
|
it('supports unstyled prop passthrough', () => {
|
|
const theme: ThemeLibrary = {
|
|
components: {
|
|
Button: {
|
|
component: Button,
|
|
unstyled: true,
|
|
},
|
|
},
|
|
}
|
|
|
|
const engine = new ThemeEngine(theme)
|
|
//eslint-disable-next-line
|
|
const themedButton: any = engine.getComponent<typeof Button>('Button')
|
|
|
|
expect(themedButton.props.unstyled.default).toBe(true)
|
|
})
|
|
|
|
it('combines multiple classes in correct order', () => {
|
|
const theme: ThemeLibrary = {
|
|
components: {
|
|
Button: {
|
|
component: Button,
|
|
classes: ['class-a', 'class-b', 'class-c'],
|
|
injectionKeys: {
|
|
class: 'pt',
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
const engine = new ThemeEngine(theme)
|
|
const themedButton = engine.getComponent<typeof Button>('Button')
|
|
|
|
const result = callSetup(themedButton, {
|
|
pt: {},
|
|
})
|
|
|
|
expect(result.pt?.root?.class).toBe('class-a class-b class-c')
|
|
})
|
|
|
|
it('combines multiple styles correctly', () => {
|
|
const theme: ThemeLibrary = {
|
|
components: {
|
|
Button: {
|
|
component: Button,
|
|
styles: ['padding: 8px', 'margin: 4px', 'border: 1px solid'],
|
|
injectionKeys: {
|
|
class: 'pt',
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
const engine = new ThemeEngine(theme)
|
|
const themedButton = engine.getComponent<typeof Button>('Button')
|
|
|
|
const result = callSetup(themedButton, {
|
|
pt: {},
|
|
})
|
|
|
|
expect(result.pt?.root?.style).toBe('padding: 8px; margin: 4px; border: 1px solid')
|
|
})
|
|
|
|
it('properly injects injectedClasses and injectedStyles props', () => {
|
|
const theme: ThemeLibrary = {
|
|
components: {
|
|
Button: {
|
|
component: Button,
|
|
classes: ['injected-class'],
|
|
styles: ['injected-style'],
|
|
injectionKeys: {
|
|
class: 'pt',
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
const engine = new ThemeEngine(theme)
|
|
//eslint-disable-next-line
|
|
const themedButton: any = engine.getComponent<typeof Button>('Button')
|
|
|
|
expect(themedButton.props.injectedClasses).toBeDefined()
|
|
expect(themedButton.props.injectedStyles).toBeDefined()
|
|
expect(themedButton.props.injectedClasses.default).toEqual(['injected-class'])
|
|
expect(themedButton.props.injectedStyles.default).toEqual(['injected-style'])
|
|
})
|
|
|
|
it('applies non-PT class injection correctly', () => {
|
|
const theme: ThemeLibrary = {
|
|
components: {
|
|
Button: {
|
|
component: Button,
|
|
classes: ['my-class'],
|
|
styles: ['my-style'],
|
|
injectionKeys: {
|
|
class: 'class',
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
const engine = new ThemeEngine(theme)
|
|
const themedButton = engine.getComponent<typeof Button>('Button')
|
|
|
|
const result = callSetup(themedButton, {})
|
|
|
|
expect(result.class).toBe('my-class')
|
|
expect(result.style).toBe('my-style')
|
|
})
|
|
|
|
it('handles object-form class values', () => {
|
|
const theme: ThemeLibrary = {
|
|
components: {
|
|
Button: {
|
|
component: Button,
|
|
classes: ['theme-class'],
|
|
injectionKeys: {
|
|
class: 'class',
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
const engine = new ThemeEngine(theme)
|
|
const themedButton = engine.getComponent<typeof Button>('Button')
|
|
|
|
const result = callSetup(themedButton, {
|
|
class: { 'active-class': true, 'inactive-class': false },
|
|
})
|
|
|
|
expect(result.class).toContain('active-class')
|
|
expect(result.class).not.toContain('inactive-class')
|
|
expect(result.class).toContain('theme-class')
|
|
})
|
|
|
|
it('merges array and string classes correctly', () => {
|
|
const theme: ThemeLibrary = {
|
|
components: {
|
|
Button: {
|
|
component: Button,
|
|
classes: ['theme-class'],
|
|
injectionKeys: {
|
|
class: 'class',
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
const engine = new ThemeEngine(theme)
|
|
const themedButton = engine.getComponent<typeof Button>('Button')
|
|
|
|
const result1 = callSetup(themedButton, {
|
|
class: ['user-class-1', 'user-class-2'],
|
|
})
|
|
|
|
expect(result1.class).toContain('user-class-1')
|
|
expect(result1.class).toContain('user-class-2')
|
|
expect(result1.class).toContain('theme-class')
|
|
|
|
const result2 = callSetup(themedButton, {
|
|
class: 'user-string-class',
|
|
})
|
|
|
|
expect(result2.class).toContain('user-string-class')
|
|
expect(result2.class).toContain('theme-class')
|
|
})
|
|
|
|
it('wraps components with custom slots', () => {
|
|
const MockSlot = { name: 'MockSlot', template: '<div>Mock Slot</div>' }
|
|
|
|
const theme: ThemeLibrary = {
|
|
components: {
|
|
Button: {
|
|
component: Button,
|
|
slots: {
|
|
icon: MockSlot,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
const engine = new ThemeEngine(theme)
|
|
//eslint-disable-next-line
|
|
const themedButton: any = engine.getComponent<typeof Button>('Button')
|
|
|
|
expect(themedButton.setup).toBeDefined()
|
|
expect(themedButton.setup({}, {})).toBeDefined()
|
|
})
|
|
|
|
it('slot wrapper returns a render function', () => {
|
|
const MockSlot = { name: 'MockSlot', template: '<div>Mock Slot</div>' }
|
|
|
|
const theme: ThemeLibrary = {
|
|
components: {
|
|
Button: {
|
|
component: Button,
|
|
slots: {
|
|
icon: MockSlot,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
const engine = new ThemeEngine(theme)
|
|
//eslint-disable-next-line
|
|
const themedButton: any = engine.getComponent<typeof Button>('Button')
|
|
|
|
const setupResult = themedButton.setup({}, {})
|
|
|
|
expect(typeof setupResult).toBe('function')
|
|
})
|
|
|
|
it('combines slots with classes and styles', () => {
|
|
const MockSlot = { name: 'MockSlot' }
|
|
|
|
const theme: ThemeLibrary = {
|
|
components: {
|
|
Button: {
|
|
component: Button,
|
|
classes: ['btn-class'],
|
|
styles: ['color: blue'],
|
|
slots: {
|
|
icon: MockSlot,
|
|
},
|
|
injectionKeys: {
|
|
class: 'pt',
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
const engine = new ThemeEngine(theme)
|
|
//eslint-disable-next-line
|
|
const themedButton: any = engine.getComponent<typeof Button>('Button')
|
|
|
|
// With slots, setup returns a render function, not props
|
|
const setupResult = themedButton.setup(
|
|
{
|
|
injectedClasses: ['btn-class'],
|
|
injectedStyles: ['color: blue'],
|
|
pt: {},
|
|
},
|
|
{},
|
|
)
|
|
|
|
expect(typeof setupResult).toBe('function')
|
|
})
|
|
|
|
it('renders original component with modified props via h()', () => {
|
|
const ComponentWithSetup = {
|
|
props: { class: String },
|
|
//eslint-disable-next-line
|
|
setup(props: Record<string, unknown>) {
|
|
return () => h('div', { class: props.class })
|
|
},
|
|
}
|
|
|
|
const theme: ThemeLibrary = {
|
|
components: {
|
|
Custom: {
|
|
component: ComponentWithSetup,
|
|
classes: ['custom-class'],
|
|
},
|
|
},
|
|
}
|
|
|
|
const engine = new ThemeEngine(theme)
|
|
const themedComponent = engine.getComponent('Custom')
|
|
|
|
const result = callSetup(themedComponent, {})
|
|
|
|
// Engine wraps via h() — vnode props contain the theme-injected class
|
|
expect(result.class).toBe('custom-class')
|
|
})
|
|
|
|
it('slot functions are created for each slot component', () => {
|
|
const MockSlot = {
|
|
name: 'MockSlot',
|
|
//eslint-disable-next-line
|
|
setup() {
|
|
return () => h('span', 'slot content')
|
|
},
|
|
}
|
|
|
|
const theme: ThemeLibrary = {
|
|
components: {
|
|
Button: {
|
|
component: Button,
|
|
slots: {
|
|
icon: MockSlot,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
const engine = new ThemeEngine(theme)
|
|
//eslint-disable-next-line
|
|
const themedButton: any = engine.getComponent<typeof Button>('Button')
|
|
|
|
const renderFn = themedButton.setup({}, { slots: {} })
|
|
|
|
expect(typeof renderFn).toBe('function')
|
|
const vnode = renderFn()
|
|
expect(vnode).toBeDefined()
|
|
})
|
|
|
|
it('forwards update:modelValue so v-model works on wrapped components', async () => {
|
|
// Regression: a cloned wrapper inherited the component's emits/extends, so
|
|
// Vue withheld onUpdate:modelValue from ctx.attrs and the engine never
|
|
// forwarded it — breaking two-way v-model on every wrapped component.
|
|
const Emitter = {
|
|
name: 'Emitter',
|
|
props: { modelValue: { type: String, default: '' } },
|
|
emits: ['update:modelValue'],
|
|
//eslint-disable-next-line
|
|
setup(_props: Record<string, unknown>, { emit }: { emit: (e: string, v: string) => void }) {
|
|
return () => h('button', { onClick: () => emit('update:modelValue', 'changed') }, 'x')
|
|
},
|
|
}
|
|
|
|
const theme: ThemeLibrary = { components: { Emitter: { component: Emitter } } }
|
|
const engine = new ThemeEngine(theme)
|
|
const Wrapped = engine.getComponent('Emitter')
|
|
|
|
const Parent = {
|
|
components: { Wrapped },
|
|
setup() {
|
|
const val = ref('initial')
|
|
return { val }
|
|
},
|
|
template: `<div><Wrapped v-model="val" /><span class="out">{{ val }}</span></div>`,
|
|
}
|
|
|
|
const wrapper = mount(Parent)
|
|
expect(wrapper.find('.out').text()).toBe('initial')
|
|
|
|
await wrapper.find('button').trigger('click')
|
|
expect(wrapper.find('.out').text()).toBe('changed')
|
|
})
|
|
|
|
it('merges theme slots with original component slots', () => {
|
|
const ThemeSlot = {
|
|
name: 'ThemeSlot',
|
|
//eslint-disable-next-line
|
|
setup() {
|
|
return () => h('span', 'theme slot')
|
|
},
|
|
}
|
|
|
|
const theme: ThemeLibrary = {
|
|
components: {
|
|
Button: {
|
|
component: Button,
|
|
slots: {
|
|
icon: ThemeSlot,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
const engine = new ThemeEngine(theme)
|
|
//eslint-disable-next-line
|
|
const themedButton: any = engine.getComponent<typeof Button>('Button')
|
|
|
|
const originalSlots = {
|
|
label: () => h('span', 'label slot'),
|
|
}
|
|
|
|
const renderFn = themedButton.setup({}, { slots: originalSlots })
|
|
|
|
expect(typeof renderFn).toBe('function')
|
|
const vnode = renderFn()
|
|
expect(vnode).toBeDefined()
|
|
})
|
|
|
|
it('handles non-string style values by defaulting to empty', () => {
|
|
const theme: ThemeLibrary = {
|
|
components: {
|
|
Button: {
|
|
component: Button,
|
|
styles: ['theme-style'],
|
|
injectionKeys: {
|
|
class: 'pt',
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
const engine = new ThemeEngine(theme)
|
|
const themedButton = engine.getComponent<typeof Button>('Button')
|
|
|
|
const result = callSetup(themedButton, {
|
|
style: { someObject: 'value' },
|
|
pt: {},
|
|
})
|
|
|
|
// When style is not a string, it defaults to empty array, so only theme styles are applied
|
|
expect(result.pt?.root?.style).toBe('theme-style')
|
|
})
|
|
|
|
it('preserves already-prefixed classes', () => {
|
|
const theme: ThemeLibrary = {
|
|
components: {
|
|
Button: {
|
|
component: Button,
|
|
classes: ['my-class', 'custom-already-prefixed'],
|
|
injectionKeys: {
|
|
class: 'pt',
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
const engine = new ThemeEngine(theme)
|
|
const themedButton = engine.getComponent<typeof Button>('Button')
|
|
|
|
const result = callSetup(themedButton, {
|
|
stylePlug: 'custom',
|
|
pt: {},
|
|
})
|
|
|
|
expect(result.pt?.root?.class).toContain('custom-my-class')
|
|
expect(result.pt?.root?.class).toContain('custom-already-prefixed')
|
|
})
|
|
|
|
it('handles styleKey as pt injection key', () => {
|
|
const theme: ThemeLibrary = {
|
|
components: {
|
|
Button: {
|
|
component: Button,
|
|
styles: ['style-value'],
|
|
injectionKeys: {
|
|
class: 'pt',
|
|
style: 'pt',
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
const engine = new ThemeEngine(theme)
|
|
const themedButton = engine.getComponent<typeof Button>('Button')
|
|
|
|
const result = callSetup(themedButton, {
|
|
pt: {},
|
|
})
|
|
|
|
expect(result.pt?.root?.style).toContain('style-value')
|
|
})
|
|
|
|
it('slot wrapper works with non-function modifiedSetup', () => {
|
|
const MockSlot = {
|
|
name: 'MockSlot',
|
|
//eslint-disable-next-line
|
|
setup() {
|
|
return () => h('span', 'slot')
|
|
},
|
|
}
|
|
|
|
const ComponentWithoutSetup = {
|
|
name: 'ComponentWithoutSetup',
|
|
template: '<div></div>',
|
|
}
|
|
|
|
const theme: ThemeLibrary = {
|
|
components: {
|
|
Custom: {
|
|
component: ComponentWithoutSetup,
|
|
slots: {
|
|
icon: MockSlot,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
const engine = new ThemeEngine(theme)
|
|
//eslint-disable-next-line
|
|
const themedComponent: any = engine.getComponent('Custom')
|
|
|
|
const renderFn = themedComponent.setup({}, { slots: {} })
|
|
|
|
expect(typeof renderFn).toBe('function')
|
|
const vnode = renderFn()
|
|
expect(vnode).toBeDefined()
|
|
})
|
|
|
|
it('does not pass internal engine props to the wrapped component', () => {
|
|
const theme = getSimpleTheme()
|
|
const engine = new ThemeEngine(theme)
|
|
const themedButton = engine.getComponent<typeof Button>('Button')
|
|
|
|
const result = callSetup(themedButton, { pt: {}, stylePlug: 'custom' })
|
|
|
|
expect(result).not.toHaveProperty('injectedClasses')
|
|
expect(result).not.toHaveProperty('injectedStyles')
|
|
expect(result).not.toHaveProperty('stylePlug')
|
|
})
|
|
|
|
it('handles classKey that is not pt', () => {
|
|
const theme: ThemeLibrary = {
|
|
components: {
|
|
Button: {
|
|
component: Button,
|
|
classes: ['my-class'],
|
|
styles: ['my-style'],
|
|
injectionKeys: {
|
|
class: 'customClass',
|
|
style: 'customStyle',
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
const engine = new ThemeEngine(theme)
|
|
const themedButton = engine.getComponent<typeof Button>('Button')
|
|
|
|
const result = callSetup(themedButton, {})
|
|
|
|
expect(result.customClass).toBe('my-class')
|
|
expect(result.customStyle).toBe('my-style')
|
|
})
|
|
|
|
it('spreads existing pt object when merging theme props', () => {
|
|
const theme: ThemeLibrary = {
|
|
components: {
|
|
Button: {
|
|
component: Button,
|
|
classes: ['theme-class'],
|
|
injectionKeys: {
|
|
class: 'pt',
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
const engine = new ThemeEngine(theme)
|
|
const themedButton = engine.getComponent<typeof Button>('Button')
|
|
|
|
const result = callSetup(themedButton, {
|
|
pt: {
|
|
label: { class: 'label-class', style: 'color: red' },
|
|
icon: { class: 'icon-class' },
|
|
tooltip: { content: 'help text' },
|
|
},
|
|
})
|
|
|
|
expect(result.pt?.root?.class).toContain('theme-class')
|
|
expect(result.pt?.label?.class).toBe('label-class')
|
|
expect(result.pt?.label?.style).toBe('color: red')
|
|
expect(result.pt?.icon?.class).toBe('icon-class')
|
|
expect(result.pt?.tooltip?.content).toBe('help text')
|
|
})
|
|
|
|
it('handles style as string and splits it correctly', () => {
|
|
const theme: ThemeLibrary = {
|
|
components: {
|
|
Button: {
|
|
component: Button,
|
|
styles: ['margin: 10px'],
|
|
injectionKeys: {
|
|
class: 'pt',
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
const engine = new ThemeEngine(theme)
|
|
const themedButton = engine.getComponent<typeof Button>('Button')
|
|
|
|
const result = callSetup(themedButton, {
|
|
style: 'padding: 5px; color: blue',
|
|
pt: {},
|
|
})
|
|
|
|
expect(result.pt?.root?.style).toContain('padding: 5px')
|
|
expect(result.pt?.root?.style).toContain('color: blue')
|
|
expect(result.pt?.root?.style).toContain('margin: 10px')
|
|
})
|
|
|
|
it('creates pt structure when pt is not provided', () => {
|
|
const theme: ThemeLibrary = {
|
|
components: {
|
|
Button: {
|
|
component: Button,
|
|
classes: ['btn-class'],
|
|
styles: ['btn-style'],
|
|
injectionKeys: {
|
|
class: 'pt',
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
const engine = new ThemeEngine(theme)
|
|
const themedButton = engine.getComponent<typeof Button>('Button')
|
|
|
|
const result = callSetup(themedButton, {})
|
|
|
|
expect(result.pt).toBeDefined()
|
|
expect(result.pt?.root?.class).toBe('btn-class')
|
|
expect(result.pt?.root?.style).toBe('btn-style')
|
|
})
|
|
|
|
it('slot functions created with proper scope handling', () => {
|
|
const mockSlotComponent = {
|
|
name: 'IconSlot',
|
|
//eslint-disable-next-line
|
|
setup(props: Record<string, unknown>) {
|
|
return () => h('i', `Icon: ${props.name}`)
|
|
},
|
|
}
|
|
|
|
const theme: ThemeLibrary = {
|
|
components: {
|
|
Button: {
|
|
component: Button,
|
|
slots: {
|
|
icon: mockSlotComponent,
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
const engine = new ThemeEngine(theme)
|
|
//eslint-disable-next-line
|
|
const themedButton: any = engine.getComponent<typeof Button>('Button')
|
|
|
|
const renderFn = themedButton.setup({}, { slots: {} })
|
|
|
|
expect(typeof renderFn).toBe('function')
|
|
const vnode = renderFn()
|
|
expect(vnode).toBeDefined()
|
|
})
|
|
})
|