@furystack/shades-common-components
Version:
Common UI components for FuryStack Shades
423 lines • 22.5 kB
JavaScript
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;
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;
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;
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;
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)');
});
});
});
//# sourceMappingURL=css-variable-theme.spec.js.map