@furystack/shades-common-components
Version:
Common UI components for FuryStack Shades
387 lines • 18.9 kB
JavaScript
import { createInjector } from '@furystack/inject';
import { createComponent, flushUpdates, initializeShadeRoot } from '@furystack/shades';
import { usingAsync } from '@furystack/utils';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { ThemeProviderService } from '../../services/theme-provider-service.js';
import { Form, FormContextToken } from '../form.js';
import { Switch } from './switch.js';
describe('Switch', () => {
beforeEach(() => {
document.body.innerHTML = '<div id="root"></div>';
});
afterEach(() => {
document.body.innerHTML = '';
vi.restoreAllMocks();
});
it('should render as custom element', async () => {
await usingAsync(createInjector(), async (injector) => {
const rootElement = document.getElementById('root');
initializeShadeRoot({
injector,
rootElement,
jsxElement: createComponent(Switch, null),
});
await flushUpdates();
const switchEl = document.querySelector('shade-switch');
expect(switchEl).not.toBeNull();
});
});
it('should render the hidden checkbox input', async () => {
await usingAsync(createInjector(), async (injector) => {
const rootElement = document.getElementById('root');
initializeShadeRoot({
injector,
rootElement,
jsxElement: createComponent(Switch, { name: "testSwitch" }),
});
await flushUpdates();
const input = document.querySelector('shade-switch input[type="checkbox"]');
expect(input).not.toBeNull();
expect(input.name).toBe('testSwitch');
expect(input.getAttribute('role')).toBe('switch');
});
});
it('should render the switch track and thumb', async () => {
await usingAsync(createInjector(), async (injector) => {
const rootElement = document.getElementById('root');
initializeShadeRoot({
injector,
rootElement,
jsxElement: createComponent(Switch, null),
});
await flushUpdates();
const track = document.querySelector('shade-switch .switch-track');
const thumb = document.querySelector('shade-switch .switch-thumb');
expect(track).not.toBeNull();
expect(thumb).not.toBeNull();
});
});
it('should render the label title', async () => {
await usingAsync(createInjector(), async (injector) => {
const rootElement = document.getElementById('root');
initializeShadeRoot({
injector,
rootElement,
jsxElement: createComponent(Switch, { labelTitle: "Enable notifications" }),
});
await flushUpdates();
const label = document.querySelector('shade-switch label');
expect(label).not.toBeNull();
expect(label.textContent).toContain('Enable notifications');
});
});
it('should not render label span when labelTitle is not provided', async () => {
await usingAsync(createInjector(), async (injector) => {
const rootElement = document.getElementById('root');
initializeShadeRoot({
injector,
rootElement,
jsxElement: createComponent(Switch, null),
});
await flushUpdates();
const labelSpan = document.querySelector('shade-switch .switch-label');
expect(labelSpan).toBeNull();
});
});
describe('checked state', () => {
it('should render as checked when checked prop is true', async () => {
await usingAsync(createInjector(), async (injector) => {
const rootElement = document.getElementById('root');
initializeShadeRoot({
injector,
rootElement,
jsxElement: createComponent(Switch, { checked: true }),
});
await flushUpdates();
const input = document.querySelector('shade-switch input[type="checkbox"]');
expect(input.checked).toBe(true);
});
});
it('should render as unchecked when checked prop is false', async () => {
await usingAsync(createInjector(), async (injector) => {
const rootElement = document.getElementById('root');
initializeShadeRoot({
injector,
rootElement,
jsxElement: createComponent(Switch, { checked: false }),
});
await flushUpdates();
const input = document.querySelector('shade-switch input[type="checkbox"]');
expect(input.checked).toBe(false);
});
});
});
describe('disabled state', () => {
it('should set data-disabled attribute when disabled', async () => {
await usingAsync(createInjector(), async (injector) => {
const rootElement = document.getElementById('root');
initializeShadeRoot({
injector,
rootElement,
jsxElement: createComponent(Switch, { disabled: true }),
});
await flushUpdates();
const switchEl = document.querySelector('shade-switch');
expect(switchEl.hasAttribute('data-disabled')).toBe(true);
});
});
it('should not have data-disabled attribute when not disabled', async () => {
await usingAsync(createInjector(), async (injector) => {
const rootElement = document.getElementById('root');
initializeShadeRoot({
injector,
rootElement,
jsxElement: createComponent(Switch, { disabled: false }),
});
await flushUpdates();
const switchEl = document.querySelector('shade-switch');
expect(switchEl.hasAttribute('data-disabled')).toBe(false);
});
});
it('should set the disabled attribute on the input element', async () => {
await usingAsync(createInjector(), async (injector) => {
const rootElement = document.getElementById('root');
initializeShadeRoot({
injector,
rootElement,
jsxElement: createComponent(Switch, { disabled: true }),
});
await flushUpdates();
const input = document.querySelector('shade-switch input[type="checkbox"]');
expect(input.disabled).toBe(true);
});
});
});
describe('size', () => {
it('should set data-size="small" when size is small', async () => {
await usingAsync(createInjector(), async (injector) => {
const rootElement = document.getElementById('root');
initializeShadeRoot({
injector,
rootElement,
jsxElement: createComponent(Switch, { size: "small" }),
});
await flushUpdates();
const switchEl = document.querySelector('shade-switch');
expect(switchEl.getAttribute('data-size')).toBe('small');
});
});
it('should not have data-size attribute when size is medium (default)', async () => {
await usingAsync(createInjector(), async (injector) => {
const rootElement = document.getElementById('root');
initializeShadeRoot({
injector,
rootElement,
jsxElement: createComponent(Switch, { size: "medium" }),
});
await flushUpdates();
const switchEl = document.querySelector('shade-switch');
expect(switchEl.hasAttribute('data-size')).toBe(false);
});
});
});
describe('callbacks', () => {
it('should call onchange when switch is toggled', async () => {
await usingAsync(createInjector(), async (injector) => {
const rootElement = document.getElementById('root');
const onchange = vi.fn();
initializeShadeRoot({
injector,
rootElement,
jsxElement: createComponent(Switch, { name: "toggle", onchange: onchange }),
});
await flushUpdates();
const input = document.querySelector('shade-switch input[type="checkbox"]');
input.dispatchEvent(new Event('change', { bubbles: true }));
await flushUpdates();
expect(onchange).toHaveBeenCalledOnce();
});
});
});
describe('theme integration', () => {
it('should set CSS color variable from theme', async () => {
await usingAsync(createInjector(), async (injector) => {
const rootElement = document.getElementById('root');
initializeShadeRoot({
injector,
rootElement,
jsxElement: createComponent(Switch, null),
});
await flushUpdates();
const switchEl = document.querySelector('shade-switch');
const themeService = injector.get(ThemeProviderService);
expect(switchEl.style.getPropertyValue('--switch-color')).toBe(themeService.theme.palette.primary.main);
});
});
it('should use custom color from color prop', async () => {
await usingAsync(createInjector(), async (injector) => {
const rootElement = document.getElementById('root');
initializeShadeRoot({
injector,
rootElement,
jsxElement: createComponent(Switch, { color: "secondary" }),
});
await flushUpdates();
const switchEl = document.querySelector('shade-switch');
const themeService = injector.get(ThemeProviderService);
expect(switchEl.style.getPropertyValue('--switch-color')).toBe(themeService.theme.palette.secondary.main);
});
});
});
describe('FormService integration', () => {
it('should register input with FormService when inside a Form', async () => {
await usingAsync(createInjector(), async (injector) => {
const rootElement = document.getElementById('root');
initializeShadeRoot({
injector,
rootElement,
jsxElement: (createComponent(Form, { onSubmit: () => { }, validate: (_data) => true },
createComponent(Switch, { name: "notifications", labelTitle: "Enable notifications" }))),
});
await flushUpdates();
const form = document.querySelector('form[is="shade-form"]');
const formInjector = form.injector;
const formService = formInjector.get(FormContextToken);
expect(formService.inputs.size).toBe(1);
});
});
it('should unregister input from FormService on cleanup', async () => {
await usingAsync(createInjector(), async (injector) => {
const rootElement = document.getElementById('root');
initializeShadeRoot({
injector,
rootElement,
jsxElement: (createComponent(Form, { onSubmit: () => { }, validate: (_data) => true },
createComponent(Switch, { name: "notifications", labelTitle: "Enable notifications" }))),
});
await flushUpdates();
const form = document.querySelector('form[is="shade-form"]');
const formInjector = form.injector;
const formService = formInjector.get(FormContextToken);
expect(formService.inputs.size).toBe(1);
rootElement.innerHTML = '';
await flushUpdates();
});
});
});
describe('labelProps', () => {
it('should pass labelProps to the label element', async () => {
await usingAsync(createInjector(), async (injector) => {
const rootElement = document.getElementById('root');
initializeShadeRoot({
injector,
rootElement,
jsxElement: createComponent(Switch, { labelProps: { className: 'custom-label' } }),
});
await flushUpdates();
const label = document.querySelector('shade-switch label');
expect(label.className).toContain('custom-label');
});
});
});
describe('required', () => {
it('should set the required attribute on the input element', async () => {
await usingAsync(createInjector(), async (injector) => {
const rootElement = document.getElementById('root');
initializeShadeRoot({
injector,
rootElement,
jsxElement: createComponent(Switch, { required: true }),
});
await flushUpdates();
const input = document.querySelector('shade-switch input[type="checkbox"]');
expect(input.required).toBe(true);
});
});
});
describe('value', () => {
it('should set the value attribute on the input element', async () => {
await usingAsync(createInjector(), async (injector) => {
const rootElement = document.getElementById('root');
initializeShadeRoot({
injector,
rootElement,
jsxElement: createComponent(Switch, { value: "yes" }),
});
await flushUpdates();
const input = document.querySelector('shade-switch input[type="checkbox"]');
expect(input.value).toBe('yes');
});
});
it('should fall back to the native "on" default when value prop is omitted', async () => {
await usingAsync(createInjector(), async (injector) => {
const rootElement = document.getElementById('root');
initializeShadeRoot({
injector,
rootElement,
jsxElement: (createComponent(Form, { onSubmit: () => { }, validate: (_data) => true },
createComponent(Switch, { name: "notifications", labelTitle: "Enable notifications", checked: true }))),
});
await flushUpdates();
const form = document.querySelector('form[is="shade-form"]');
const input = form.querySelector('input[type="checkbox"]');
expect(input.hasAttribute('value')).toBe(false);
expect(Object.fromEntries(new FormData(form).entries())).toEqual({ notifications: 'on' });
});
});
it('should propagate change events to the parent Form so rawFormData updates', async () => {
await usingAsync(createInjector(), async (injector) => {
const rootElement = document.getElementById('root');
initializeShadeRoot({
injector,
rootElement,
jsxElement: (createComponent(Form, { onSubmit: () => { }, validate: (_data) => true },
createComponent(Switch, { name: "notifications", value: "yes", labelTitle: "Enable notifications" }))),
});
await flushUpdates();
const form = document.querySelector('form[is="shade-form"]');
const input = form.querySelector('input[type="checkbox"]');
const formInjector = form.injector;
const formService = formInjector.get(FormContextToken);
input.checked = true;
input.dispatchEvent(new Event('change', { bubbles: true }));
await flushUpdates();
expect(formService.rawFormData.getValue()).toEqual({ notifications: 'yes' });
});
});
});
describe('size', () => {
it('should not set data-size when size is not specified', async () => {
await usingAsync(createInjector(), async (injector) => {
const rootElement = document.getElementById('root');
initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(Switch, null) });
await flushUpdates();
const el = document.querySelector('shade-switch');
expect(el.getAttribute('data-size')).toBeNull();
});
});
it('should not set data-size for medium size (default)', async () => {
await usingAsync(createInjector(), async (injector) => {
const rootElement = document.getElementById('root');
initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(Switch, { size: "medium" }) });
await flushUpdates();
const el = document.querySelector('shade-switch');
expect(el.getAttribute('data-size')).toBeNull();
});
});
it('should set data-size="small" for small size', async () => {
await usingAsync(createInjector(), async (injector) => {
const rootElement = document.getElementById('root');
initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(Switch, { size: "small" }) });
await flushUpdates();
const el = document.querySelector('shade-switch');
expect(el.getAttribute('data-size')).toBe('small');
});
});
it('should set data-size="large" for large size', async () => {
await usingAsync(createInjector(), async (injector) => {
const rootElement = document.getElementById('root');
initializeShadeRoot({ injector, rootElement, jsxElement: createComponent(Switch, { size: "large" }) });
await flushUpdates();
const el = document.querySelector('shade-switch');
expect(el.getAttribute('data-size')).toBe('large');
});
});
});
});
//# sourceMappingURL=switch.spec.js.map