UNPKG

@furystack/shades-common-components

Version:

Common UI components for FuryStack Shades

310 lines 16.9 kB
import { createInjector } from '@furystack/inject'; import { createComponent, flushUpdates, initializeShadeRoot, Shade } from '@furystack/shades'; import { usingAsync } from '@furystack/utils'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { Button } from './button.js'; describe('Button', () => { beforeEach(() => { document.body.innerHTML = '<div id="root"></div>'; }); afterEach(() => { document.body.innerHTML = ''; document.querySelector('style[data-shades-button-spinner]')?.remove(); }); const renderButton = async (props = {}, children) => { const injector = createInjector(); const root = document.getElementById('root'); initializeShadeRoot({ injector, rootElement: root, jsxElement: createComponent(Button, { ...props }, children), }); await flushUpdates(); return { injector, button: root.querySelector('button[is="shade-button"]'), [Symbol.asyncDispose]: () => injector[Symbol.asyncDispose](), }; }; describe('rendering', () => { it('should render a button element', async () => { await usingAsync(await renderButton(), async ({ button }) => { expect(button).toBeTruthy(); expect(button.tagName.toLowerCase()).toBe('button'); expect(button.getAttribute('is')).toBe('shade-button'); }); }); it('should render children', async () => { await usingAsync(await renderButton({}, ['Click me']), async ({ button }) => { expect(button.textContent).toContain('Click me'); }); }); }); describe('variants', () => { it('should have no data-variant attribute when variant is not specified (flat default)', async () => { await usingAsync(await renderButton(), async ({ button }) => { expect(button.getAttribute('data-variant')).toBeNull(); }); }); it('should set data-variant="contained" for contained variant', async () => { await usingAsync(await renderButton({ variant: 'contained' }), async ({ button }) => { expect(button.getAttribute('data-variant')).toBe('contained'); }); }); it('should set data-variant="outlined" for outlined variant', async () => { await usingAsync(await renderButton({ variant: 'outlined' }), async ({ button }) => { expect(button.getAttribute('data-variant')).toBe('outlined'); }); }); it('should have no data-variant for text variant (same as default)', async () => { await usingAsync(await renderButton({ variant: 'text' }), async ({ button }) => { expect(button.getAttribute('data-variant')).toBeNull(); }); }); it('should remove data-variant attribute when variant changes to undefined', async () => { await usingAsync(createInjector(), async (injector) => { const root = document.getElementById('root'); const TestComponent = Shade({ customElementName: 'button-test-variant-wrapper', render: ({ props }) => createComponent(Button, { variant: props.variant }, "Test"), }); initializeShadeRoot({ injector, rootElement: root, jsxElement: createComponent(TestComponent, { variant: "contained" }), }); await flushUpdates(); const button = root.querySelector('button[is="shade-button"]'); expect(button.getAttribute('data-variant')).toBe('contained'); }); }); }); describe('colors', () => { const colors = ['primary', 'secondary', 'error', 'warning', 'success', 'info']; colors.forEach((color) => { it(`should set CSS custom properties for ${color} color`, async () => { await usingAsync(await renderButton({ color }), async ({ button }) => { expect(button.style.getPropertyValue('--btn-color-main')).toBe(`var(--shades-theme-palette-${color}-main)`); expect(button.style.getPropertyValue('--btn-color-main-contrast')).toBe(`var(--shades-theme-palette-${color}-main-contrast)`); expect(button.style.getPropertyValue('--btn-color-light')).toBe(`var(--shades-theme-palette-${color}-light)`); expect(button.style.getPropertyValue('--btn-color-dark')).toBe(`var(--shades-theme-palette-${color}-dark)`); expect(button.style.getPropertyValue('--btn-color-dark-contrast')).toBe(`var(--shades-theme-palette-${color}-dark-contrast)`); }); }); }); it('should set default color CSS custom properties when no color is specified', async () => { await usingAsync(await renderButton(), async ({ button }) => { expect(button.style.getPropertyValue('--btn-color-main')).toBe('var(--shades-theme-text-secondary)'); expect(button.style.getPropertyValue('--btn-color-main-contrast')).toBe('var(--shades-theme-background-default)'); expect(button.style.getPropertyValue('--btn-color-light')).toBe('var(--shades-theme-text-primary)'); expect(button.style.getPropertyValue('--btn-color-dark')).toBe('var(--shades-theme-button-disabled-background)'); expect(button.style.getPropertyValue('--btn-color-dark-contrast')).toBe('var(--shades-theme-text-primary)'); }); }); }); describe('variant and color combinations', () => { it('should apply both contained variant and primary color', async () => { await usingAsync(await renderButton({ variant: 'contained', color: 'primary' }), async ({ button }) => { expect(button.getAttribute('data-variant')).toBe('contained'); expect(button.style.getPropertyValue('--btn-color-main')).toBe('var(--shades-theme-palette-primary-main)'); }); }); it('should apply both outlined variant and error color', async () => { await usingAsync(await renderButton({ variant: 'outlined', color: 'error' }), async ({ button }) => { expect(button.getAttribute('data-variant')).toBe('outlined'); expect(button.style.getPropertyValue('--btn-color-main')).toBe('var(--shades-theme-palette-error-main)'); }); }); }); describe('disabled state', () => { it('should be enabled by default', async () => { await usingAsync(await renderButton(), async ({ button }) => { expect(button.disabled).toBe(false); }); }); it('should be disabled when disabled prop is set', async () => { await usingAsync(await renderButton({ disabled: true }), async ({ button }) => { expect(button.disabled).toBe(true); }); }); }); describe('custom styles', () => { it('should apply custom styles from style prop', async () => { await usingAsync(await renderButton({ style: { margin: '20px', padding: '15px' }, }), async ({ button }) => { expect(button.style.margin).toBe('20px'); expect(button.style.padding).toBe('15px'); }); }); it('should merge custom styles with component defaults', async () => { await usingAsync(await renderButton({ color: 'primary', style: { fontSize: '18px' }, }), async ({ button }) => { expect(button.style.fontSize).toBe('18px'); expect(button.style.getPropertyValue('--btn-color-main')).toBe('var(--shades-theme-palette-primary-main)'); }); }); }); describe('event handling', () => { it('should trigger onclick handler when clicked', async () => { const handleClick = vi.fn(); await usingAsync(await renderButton({ onclick: handleClick }), async ({ button }) => { button.click(); expect(handleClick).toHaveBeenCalledTimes(1); }); }); it('should not trigger onclick when disabled', async () => { const handleClick = vi.fn(); await usingAsync(await renderButton({ onclick: handleClick, disabled: true }), async ({ button }) => { button.click(); expect(handleClick).not.toHaveBeenCalled(); }); }); }); describe('HTML button attributes', () => { it('should support type attribute', async () => { await usingAsync(await renderButton({ type: 'submit' }), async ({ button }) => { expect(button.type).toBe('submit'); }); }); it('should support name attribute', async () => { await usingAsync(await renderButton({ name: 'my-button' }), async ({ button }) => { expect(button.name).toBe('my-button'); }); }); }); describe('size', () => { it('should not set data-size when size is not specified', async () => { await usingAsync(await renderButton(), async ({ button }) => { expect(button.getAttribute('data-size')).toBeNull(); }); }); it('should not set data-size for medium size (default)', async () => { await usingAsync(await renderButton({ size: 'medium' }), async ({ button }) => { expect(button.getAttribute('data-size')).toBeNull(); }); }); it('should set data-size="small" for small size', async () => { await usingAsync(await renderButton({ size: 'small' }), async ({ button }) => { expect(button.getAttribute('data-size')).toBe('small'); }); }); it('should set data-size="large" for large size', async () => { await usingAsync(await renderButton({ size: 'large' }), async ({ button }) => { expect(button.getAttribute('data-size')).toBe('large'); }); }); }); describe('danger', () => { it('should use error palette colors when danger is true', async () => { await usingAsync(await renderButton({ danger: true }), async ({ button }) => { expect(button.style.getPropertyValue('--btn-color-main')).toBe('var(--shades-theme-palette-error-main)'); expect(button.style.getPropertyValue('--btn-color-main-contrast')).toBe('var(--shades-theme-palette-error-main-contrast)'); }); }); it('should override color prop when danger is true', async () => { await usingAsync(await renderButton({ danger: true, color: 'primary' }), async ({ button }) => { expect(button.style.getPropertyValue('--btn-color-main')).toBe('var(--shades-theme-palette-error-main)'); }); }); it('should use specified color when danger is false', async () => { await usingAsync(await renderButton({ danger: false, color: 'primary' }), async ({ button }) => { expect(button.style.getPropertyValue('--btn-color-main')).toBe('var(--shades-theme-palette-primary-main)'); }); }); }); describe('loading', () => { it('should set data-loading attribute when loading is true', async () => { await usingAsync(await renderButton({ loading: true }), async ({ button }) => { expect(button.hasAttribute('data-loading')).toBe(true); }); }); it('should disable the button when loading is true', async () => { await usingAsync(await renderButton({ loading: true }), async ({ button }) => { expect(button.disabled).toBe(true); }); }); it('should render a spinner element when loading', async () => { await usingAsync(await renderButton({ loading: true }), async ({ button }) => { const spinner = button.querySelector('.shade-btn-spinner'); expect(spinner).toBeTruthy(); }); }); it('should not render a spinner when not loading', async () => { await usingAsync(await renderButton({ loading: false }), async ({ button }) => { const spinner = button.querySelector('.shade-btn-spinner'); expect(spinner).toBeNull(); }); }); it('should inject spinner keyframes stylesheet', async () => { await usingAsync(await renderButton({ loading: true }), async () => { const style = document.querySelector('style[data-shades-button-spinner]'); expect(style).toBeTruthy(); expect(style?.textContent).toContain('shade-btn-spin'); }); }); it('should not set data-loading when loading is false', async () => { await usingAsync(await renderButton({ loading: false }), async ({ button }) => { expect(button.hasAttribute('data-loading')).toBe(false); }); }); }); describe('startIcon and endIcon', () => { it('should render startIcon before children', async () => { const icon = (createComponent("span", { className: "test-start-icon" }, "\u2605")); await usingAsync(await renderButton({ startIcon: icon }, ['Label']), async ({ button }) => { const startIcon = button.querySelector('.shade-btn-start-icon'); expect(startIcon).toBeTruthy(); expect(startIcon?.querySelector('.test-start-icon')).toBeTruthy(); expect(button.textContent).toContain('Label'); }); }); it('should render endIcon after children', async () => { const icon = (createComponent("span", { className: "test-end-icon" }, "\u2192")); await usingAsync(await renderButton({ endIcon: icon }, ['Label']), async ({ button }) => { const endIcon = button.querySelector('.shade-btn-end-icon'); expect(endIcon).toBeTruthy(); expect(endIcon?.querySelector('.test-end-icon')).toBeTruthy(); }); }); it('should not render startIcon when loading', async () => { const icon = (createComponent("span", { className: "test-start-icon" }, "\u2605")); await usingAsync(await renderButton({ startIcon: icon, loading: true }), async ({ button }) => { expect(button.querySelector('.shade-btn-start-icon')).toBeNull(); expect(button.querySelector('.shade-btn-spinner')).toBeTruthy(); }); }); it('should not render endIcon when loading', async () => { const icon = (createComponent("span", { className: "test-end-icon" }, "\u2192")); await usingAsync(await renderButton({ endIcon: icon, loading: true }), async ({ button }) => { expect(button.querySelector('.shade-btn-end-icon')).toBeNull(); }); }); }); describe('combined features', () => { it('should support size with variant and color', async () => { await usingAsync(await renderButton({ size: 'small', variant: 'contained', color: 'primary' }), async ({ button }) => { expect(button.getAttribute('data-size')).toBe('small'); expect(button.getAttribute('data-variant')).toBe('contained'); expect(button.style.getPropertyValue('--btn-color-main')).toBe('var(--shades-theme-palette-primary-main)'); }); }); it('should support danger with contained variant', async () => { await usingAsync(await renderButton({ danger: true, variant: 'contained' }), async ({ button }) => { expect(button.getAttribute('data-variant')).toBe('contained'); expect(button.style.getPropertyValue('--btn-color-main')).toBe('var(--shades-theme-palette-error-main)'); }); }); it('should support loading with variant and size', async () => { await usingAsync(await renderButton({ loading: true, variant: 'outlined', size: 'large' }), async ({ button }) => { expect(button.hasAttribute('data-loading')).toBe(true); expect(button.getAttribute('data-variant')).toBe('outlined'); expect(button.getAttribute('data-size')).toBe('large'); expect(button.disabled).toBe(true); }); }); }); }); //# sourceMappingURL=button.spec.js.map