UNPKG

@furystack/shades-common-components

Version:

Common UI components for FuryStack Shades

527 lines (447 loc) 19.9 kB
import { afterEach, beforeEach, describe, expect, it } from 'vitest' import { buildTransition, cssVariableTheme, getCssVariable, removeCssVariable, setCssVariable, useThemeCssVariables, } from './css-variable-theme.js' describe('css-variable-theme', () => { describe('cssVariableTheme', () => { it('should have a name property', () => { expect(cssVariableTheme.name).toBe('css-variable-theme') }) it('should have text properties with CSS variable references', () => { expect(cssVariableTheme.text.primary).toBe('var(--shades-theme-text-primary)') expect(cssVariableTheme.text.secondary).toBe('var(--shades-theme-text-secondary)') expect(cssVariableTheme.text.disabled).toBe('var(--shades-theme-text-disabled)') }) it('should have background properties with CSS variable references', () => { expect(cssVariableTheme.background.default).toBe('var(--shades-theme-background-default)') expect(cssVariableTheme.background.paper).toBe('var(--shades-theme-background-paper)') }) it('should have palette with color variants', () => { expect(cssVariableTheme.palette.primary.main).toBe('var(--shades-theme-palette-primary-main)') expect(cssVariableTheme.palette.error.main).toBe('var(--shades-theme-palette-error-main)') }) it('should have action properties with CSS variable references', () => { expect(cssVariableTheme.action.hoverBackground).toBe('var(--shades-theme-action-hover-background)') expect(cssVariableTheme.action.selectedBackground).toBe('var(--shades-theme-action-selected-background)') expect(cssVariableTheme.action.activeBackground).toBe('var(--shades-theme-action-active-background)') expect(cssVariableTheme.action.focusRing).toBe('var(--shades-theme-action-focus-ring)') expect(cssVariableTheme.action.focusOutline).toBe('var(--shades-theme-action-focus-outline)') expect(cssVariableTheme.action.disabledOpacity).toBe('var(--shades-theme-action-disabled-opacity)') expect(cssVariableTheme.action.backdrop).toBe('var(--shades-theme-action-backdrop)') expect(cssVariableTheme.action.subtleBorder).toBe('var(--shades-theme-action-subtle-border)') }) it('should have shape properties with CSS variable references', () => { expect(cssVariableTheme.shape.borderRadius.xs).toBe('var(--shades-theme-shape-border-radius-xs)') expect(cssVariableTheme.shape.borderRadius.sm).toBe('var(--shades-theme-shape-border-radius-sm)') expect(cssVariableTheme.shape.borderRadius.md).toBe('var(--shades-theme-shape-border-radius-md)') expect(cssVariableTheme.shape.borderRadius.lg).toBe('var(--shades-theme-shape-border-radius-lg)') expect(cssVariableTheme.shape.borderRadius.full).toBe('var(--shades-theme-shape-border-radius-full)') }) it('should have shadow properties with CSS variable references', () => { expect(cssVariableTheme.shadows.none).toBe('var(--shades-theme-shadows-none)') expect(cssVariableTheme.shadows.sm).toBe('var(--shades-theme-shadows-sm)') expect(cssVariableTheme.shadows.md).toBe('var(--shades-theme-shadows-md)') expect(cssVariableTheme.shadows.lg).toBe('var(--shades-theme-shadows-lg)') expect(cssVariableTheme.shadows.xl).toBe('var(--shades-theme-shadows-xl)') }) it('should have typography properties with CSS variable references', () => { expect(cssVariableTheme.typography.fontFamily).toBe('var(--shades-theme-typography-font-family)') expect(cssVariableTheme.typography.fontSize.xs).toBe('var(--shades-theme-typography-font-size-xs)') expect(cssVariableTheme.typography.fontSize.md).toBe('var(--shades-theme-typography-font-size-md)') expect(cssVariableTheme.typography.fontSize.xxl).toBe('var(--shades-theme-typography-font-size-xxl)') expect(cssVariableTheme.typography.fontSize.xxxl).toBe('var(--shades-theme-typography-font-size-xxxl)') expect(cssVariableTheme.typography.fontSize.xxxxl).toBe('var(--shades-theme-typography-font-size-xxxxl)') expect(cssVariableTheme.typography.fontWeight.normal).toBe('var(--shades-theme-typography-font-weight-normal)') expect(cssVariableTheme.typography.fontWeight.bold).toBe('var(--shades-theme-typography-font-weight-bold)') expect(cssVariableTheme.typography.lineHeight.tight).toBe('var(--shades-theme-typography-line-height-tight)') expect(cssVariableTheme.typography.lineHeight.normal).toBe('var(--shades-theme-typography-line-height-normal)') expect(cssVariableTheme.typography.textShadow).toBe('var(--shades-theme-typography-text-shadow)') }) it('should have transition properties with CSS variable references', () => { expect(cssVariableTheme.transitions.duration.fast).toBe('var(--shades-theme-transitions-duration-fast)') expect(cssVariableTheme.transitions.duration.normal).toBe('var(--shades-theme-transitions-duration-normal)') expect(cssVariableTheme.transitions.duration.slow).toBe('var(--shades-theme-transitions-duration-slow)') expect(cssVariableTheme.transitions.easing.default).toBe('var(--shades-theme-transitions-easing-default)') expect(cssVariableTheme.transitions.easing.easeOut).toBe('var(--shades-theme-transitions-easing-ease-out)') expect(cssVariableTheme.transitions.easing.easeInOut).toBe('var(--shades-theme-transitions-easing-ease-in-out)') }) it('should have spacing properties with CSS variable references', () => { expect(cssVariableTheme.spacing.xs).toBe('var(--shades-theme-spacing-xs)') expect(cssVariableTheme.spacing.sm).toBe('var(--shades-theme-spacing-sm)') expect(cssVariableTheme.spacing.md).toBe('var(--shades-theme-spacing-md)') expect(cssVariableTheme.spacing.lg).toBe('var(--shades-theme-spacing-lg)') expect(cssVariableTheme.spacing.xl).toBe('var(--shades-theme-spacing-xl)') }) }) describe('setCssVariable', () => { let testElement: HTMLElement beforeEach(() => { testElement = document.createElement('div') document.body.appendChild(testElement) }) afterEach(() => { testElement.remove() }) it('should set CSS variable on element', () => { setCssVariable('--test-color', 'red', testElement) expect(testElement.style.getPropertyValue('--test-color')).toBe('red') }) it('should handle var() wrapper in key name', () => { setCssVariable('var(--test-padding)', '10px', testElement) expect(testElement.style.getPropertyValue('--test-padding')).toBe('10px') }) it('should set multiple CSS variables on same element', () => { setCssVariable('--color-a', 'blue', testElement) setCssVariable('--color-b', 'green', testElement) expect(testElement.style.getPropertyValue('--color-a')).toBe('blue') expect(testElement.style.getPropertyValue('--color-b')).toBe('green') }) it('should override existing CSS variable', () => { setCssVariable('--test-value', 'first', testElement) setCssVariable('--test-value', 'second', testElement) expect(testElement.style.getPropertyValue('--test-value')).toBe('second') }) }) describe('removeCssVariable', () => { let testElement: HTMLElement beforeEach(() => { testElement = document.createElement('div') document.body.appendChild(testElement) }) afterEach(() => { testElement.remove() }) it('should remove a CSS variable from element', () => { testElement.style.setProperty('--test-color', 'red') expect(testElement.style.getPropertyValue('--test-color')).toBe('red') removeCssVariable('--test-color', testElement) expect(testElement.style.getPropertyValue('--test-color')).toBe('') }) it('should handle var() wrapper in key name', () => { testElement.style.setProperty('--test-padding', '10px') removeCssVariable('var(--test-padding)', testElement) expect(testElement.style.getPropertyValue('--test-padding')).toBe('') }) it('should not throw when removing a non-existent variable', () => { expect(() => removeCssVariable('--non-existent', testElement)).not.toThrow() }) }) describe('getCssVariable', () => { let testElement: HTMLElement beforeEach(() => { testElement = document.createElement('div') document.body.appendChild(testElement) }) afterEach(() => { testElement.remove() }) it('should get CSS variable from element', () => { testElement.style.setProperty('--test-color', 'red') const result = getCssVariable('--test-color', testElement) expect(result).toBe('red') }) it('should handle var() wrapper in key name', () => { testElement.style.setProperty('--test-padding', '20px') const result = getCssVariable('var(--test-padding)', testElement) expect(result).toBe('20px') }) it('should return empty string for non-existent variable', () => { const result = getCssVariable('--non-existent', testElement) expect(result).toBe('') }) }) describe('useThemeCssVariables', () => { let root: HTMLElement beforeEach(() => { root = document.documentElement }) afterEach(() => { root.style.cssText = '' }) it('should set text color CSS variables from theme', () => { useThemeCssVariables({ text: { primary: '#ffffff', secondary: '#cccccc', }, }) expect(root.style.getPropertyValue('--shades-theme-text-primary')).toBe('#ffffff') expect(root.style.getPropertyValue('--shades-theme-text-secondary')).toBe('#cccccc') }) it('should set background CSS variables from theme', () => { useThemeCssVariables({ background: { default: '#000000', paper: '#111111', }, }) expect(root.style.getPropertyValue('--shades-theme-background-default')).toBe('#000000') expect(root.style.getPropertyValue('--shades-theme-background-paper')).toBe('#111111') }) it('should set button CSS variables from theme', () => { useThemeCssVariables({ button: { active: '#ff0000', hover: '#00ff00', }, }) expect(root.style.getPropertyValue('--shades-theme-button-active')).toBe('#ff0000') expect(root.style.getPropertyValue('--shades-theme-button-hover')).toBe('#00ff00') }) it('should set deeply nested palette CSS variables from theme', () => { useThemeCssVariables({ palette: { primary: { main: '#1976d2', light: '#42a5f5', dark: '#1565c0', }, error: { main: '#d32f2f', }, }, }) expect(root.style.getPropertyValue('--shades-theme-palette-primary-main')).toBe('#1976d2') expect(root.style.getPropertyValue('--shades-theme-palette-primary-light')).toBe('#42a5f5') expect(root.style.getPropertyValue('--shades-theme-palette-primary-dark')).toBe('#1565c0') expect(root.style.getPropertyValue('--shades-theme-palette-error-main')).toBe('#d32f2f') }) it('should set divider CSS variable from theme', () => { useThemeCssVariables({ divider: 'rgba(255, 255, 255, 0.12)', }) expect(root.style.getPropertyValue('--shades-theme-divider')).toBe('rgba(255, 255, 255, 0.12)') }) it('should handle partial theme with mixed nesting levels', () => { useThemeCssVariables({ text: { primary: '#fff', }, divider: '#333', palette: { success: { main: '#2e7d32', }, }, }) expect(root.style.getPropertyValue('--shades-theme-text-primary')).toBe('#fff') expect(root.style.getPropertyValue('--shades-theme-divider')).toBe('#333') expect(root.style.getPropertyValue('--shades-theme-palette-success-main')).toBe('#2e7d32') }) it('should allow overriding previously set CSS variables', () => { useThemeCssVariables({ text: { primary: '#aaa', }, }) expect(root.style.getPropertyValue('--shades-theme-text-primary')).toBe('#aaa') useThemeCssVariables({ text: { primary: '#bbb', }, }) expect(root.style.getPropertyValue('--shades-theme-text-primary')).toBe('#bbb') }) it('should remove stale CSS variables not present in the new theme', () => { useThemeCssVariables({ text: { primary: '#fff', secondary: '#ccc', disabled: '#999', }, background: { default: '#000', paper: '#111', paperImage: 'url(texture.png)', }, }) expect(root.style.getPropertyValue('--shades-theme-text-primary')).toBe('#fff') expect(root.style.getPropertyValue('--shades-theme-text-secondary')).toBe('#ccc') expect(root.style.getPropertyValue('--shades-theme-background-paper-image')).toBe('url(texture.png)') useThemeCssVariables({ text: { primary: '#eee', }, background: { default: '#000', paper: '#222', }, }) expect(root.style.getPropertyValue('--shades-theme-text-primary')).toBe('#eee') expect(root.style.getPropertyValue('--shades-theme-text-secondary')).toBe('') expect(root.style.getPropertyValue('--shades-theme-text-disabled')).toBe('') expect(root.style.getPropertyValue('--shades-theme-background-default')).toBe('#000') expect(root.style.getPropertyValue('--shades-theme-background-paper')).toBe('#222') expect(root.style.getPropertyValue('--shades-theme-background-paper-image')).toBe('') }) it('should remove all nested CSS variables when a whole section is omitted', () => { useThemeCssVariables({ palette: { primary: { light: '#aaa', lightContrast: '#000', main: '#bbb', mainContrast: '#000', dark: '#ccc', darkContrast: '#fff', }, }, spacing: { xs: '4px', sm: '8px', md: '16px', lg: '24px', xl: '32px', }, }) expect(root.style.getPropertyValue('--shades-theme-palette-primary-main')).toBe('#bbb') expect(root.style.getPropertyValue('--shades-theme-spacing-md')).toBe('16px') useThemeCssVariables({ spacing: { xs: '2px', sm: '4px', md: '8px', lg: '16px', xl: '24px', }, }) expect(root.style.getPropertyValue('--shades-theme-palette-primary-main')).toBe('') expect(root.style.getPropertyValue('--shades-theme-palette-primary-light')).toBe('') expect(root.style.getPropertyValue('--shades-theme-spacing-md')).toBe('8px') }) it('should set action CSS variables from theme', () => { useThemeCssVariables({ action: { hoverBackground: 'rgba(255, 255, 255, 0.08)', focusRing: '0 0 0 3px rgba(255, 255, 255, 0.15)', }, }) expect(root.style.getPropertyValue('--shades-theme-action-hover-background')).toBe('rgba(255, 255, 255, 0.08)') expect(root.style.getPropertyValue('--shades-theme-action-focus-ring')).toBe( '0 0 0 3px rgba(255, 255, 255, 0.15)', ) }) it('should set shape CSS variables from theme', () => { useThemeCssVariables({ shape: { borderRadius: { md: '8px', full: '50%', }, }, }) expect(root.style.getPropertyValue('--shades-theme-shape-border-radius-md')).toBe('8px') expect(root.style.getPropertyValue('--shades-theme-shape-border-radius-full')).toBe('50%') }) it('should set typography CSS variables from theme', () => { useThemeCssVariables({ typography: { fontFamily: 'monospace', fontSize: { md: '14px', }, fontWeight: { bold: '700', }, }, }) expect(root.style.getPropertyValue('--shades-theme-typography-font-family')).toBe('monospace') expect(root.style.getPropertyValue('--shades-theme-typography-font-size-md')).toBe('14px') expect(root.style.getPropertyValue('--shades-theme-typography-font-weight-bold')).toBe('700') }) it('should set transition CSS variables from theme', () => { useThemeCssVariables({ transitions: { duration: { fast: '0.15s', }, easing: { default: 'ease', }, }, }) expect(root.style.getPropertyValue('--shades-theme-transitions-duration-fast')).toBe('0.15s') expect(root.style.getPropertyValue('--shades-theme-transitions-easing-default')).toBe('ease') }) it('should set spacing CSS variables from theme', () => { useThemeCssVariables({ spacing: { xs: '4px', md: '16px', xl: '32px', }, }) expect(root.style.getPropertyValue('--shades-theme-spacing-xs')).toBe('4px') expect(root.style.getPropertyValue('--shades-theme-spacing-md')).toBe('16px') expect(root.style.getPropertyValue('--shades-theme-spacing-xl')).toBe('32px') }) it('should set CSS variables on a custom root element instead of :root', () => { const customRoot = document.createElement('div') document.body.appendChild(customRoot) useThemeCssVariables( { text: { primary: '#ff0000' }, divider: '#00ff00', }, customRoot, ) expect(customRoot.style.getPropertyValue('--shades-theme-text-primary')).toBe('#ff0000') expect(customRoot.style.getPropertyValue('--shades-theme-divider')).toBe('#00ff00') expect(root.style.getPropertyValue('--shades-theme-text-primary')).toBe('') customRoot.remove() }) it('should scope nested palette variables to custom root element', () => { const customRoot = document.createElement('div') document.body.appendChild(customRoot) useThemeCssVariables( { palette: { primary: { main: '#1976d2', mainContrast: '#ffffff', }, }, }, customRoot, ) expect(customRoot.style.getPropertyValue('--shades-theme-palette-primary-main')).toBe('#1976d2') expect(customRoot.style.getPropertyValue('--shades-theme-palette-primary-main-contrast')).toBe('#ffffff') expect(root.style.getPropertyValue('--shades-theme-palette-primary-main')).toBe('') customRoot.remove() }) }) describe('buildTransition', () => { it('should build a single transition string', () => { expect(buildTransition(['background', '0.2s', 'ease'])).toBe('background 0.2s ease') }) it('should join multiple transitions with commas', () => { expect(buildTransition(['background', '0.2s', 'ease'], ['opacity', '0.15s', 'ease-out'])).toBe( 'background 0.2s ease, opacity 0.15s ease-out', ) }) it('should handle three or more transitions', () => { const result = buildTransition( ['background', '0.2s', 'ease'], ['color', '0.3s', 'linear'], ['transform', '0.1s', 'ease-in-out'], ) expect(result).toBe('background 0.2s ease, color 0.3s linear, transform 0.1s ease-in-out') }) it('should work with CSS variable references', () => { expect( buildTransition([ 'background', cssVariableTheme.transitions.duration.normal, cssVariableTheme.transitions.easing.default, ]), ).toBe( 'background var(--shades-theme-transitions-duration-normal) var(--shades-theme-transitions-easing-default)', ) }) }) })