@furystack/shades-common-components
Version:
Common UI components for FuryStack Shades
306 lines • 15.8 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 { LayoutService, createLayoutService } from '../../services/layout-service.js';
import { DrawerToggleButton } from './drawer-toggle-button.js';
/**
* Creates a mock element for LayoutService
*/
const createMockElement = () => ({
style: {
setProperty: vi.fn(),
},
});
describe('DrawerToggleButton component', () => {
beforeEach(() => {
document.body.innerHTML = '<div id="root"></div>';
});
afterEach(() => {
document.body.innerHTML = '';
});
describe('rendering', () => {
it('should render the shade-drawer-toggle-button custom element', async () => {
await usingAsync(createInjector(), async (injector) => {
const layoutService = createLayoutService(createMockElement());
injector.bind(LayoutService, () => layoutService);
const rootElement = document.getElementById('root');
initializeShadeRoot({
injector,
rootElement,
jsxElement: createComponent(DrawerToggleButton, { position: "left" }),
});
await flushUpdates();
const element = document.querySelector('shade-drawer-toggle-button');
expect(element).not.toBeNull();
expect(element?.tagName.toLowerCase()).toBe('shade-drawer-toggle-button');
});
});
it('should render a button element', async () => {
await usingAsync(createInjector(), async (injector) => {
const layoutService = createLayoutService(createMockElement());
injector.bind(LayoutService, () => layoutService);
const rootElement = document.getElementById('root');
initializeShadeRoot({
injector,
rootElement,
jsxElement: createComponent(DrawerToggleButton, { position: "left" }),
});
await flushUpdates();
const button = document.querySelector('button');
expect(button).not.toBeNull();
});
});
it('should render hamburger icon', async () => {
await usingAsync(createInjector(), async (injector) => {
const layoutService = createLayoutService(createMockElement());
injector.bind(LayoutService, () => layoutService);
const rootElement = document.getElementById('root');
initializeShadeRoot({
injector,
rootElement,
jsxElement: createComponent(DrawerToggleButton, { position: "left" }),
});
await flushUpdates();
const hamburger = document.querySelector('.hamburger');
expect(hamburger).not.toBeNull();
// Should have 3 lines
const spans = hamburger?.querySelectorAll('span');
expect(spans?.length).toBe(3);
});
});
});
describe('accessibility', () => {
// Note: aria-* attributes in Shades JSX don't propagate in JSDOM test environment.
// These tests verify the component accepts the props correctly.
// The aria attributes are set in JSX and work correctly in browser environments.
it('should accept ariaLabel prop with default value', async () => {
await usingAsync(createInjector(), async (injector) => {
const layoutService = createLayoutService(createMockElement());
injector.bind(LayoutService, () => layoutService);
const rootElement = document.getElementById('root');
initializeShadeRoot({
injector,
rootElement,
jsxElement: createComponent(DrawerToggleButton, { position: "left" }),
});
await flushUpdates();
const button = document.querySelector('[data-testid="drawer-toggle-left"]');
expect(button).not.toBeNull();
// Button is rendered, aria-label is set in JSX (may not be visible in JSDOM)
});
});
it('should accept custom ariaLabel prop', async () => {
await usingAsync(createInjector(), async (injector) => {
const layoutService = createLayoutService(createMockElement());
injector.bind(LayoutService, () => layoutService);
const rootElement = document.getElementById('root');
initializeShadeRoot({
injector,
rootElement,
jsxElement: createComponent(DrawerToggleButton, { position: "left", ariaLabel: "Toggle navigation menu" }),
});
await flushUpdates();
const button = document.querySelector('[data-testid="drawer-toggle-left"]');
expect(button).not.toBeNull();
// Button is rendered with custom ariaLabel prop (may not be visible in JSDOM)
});
});
it('should reflect drawer state in visual appearance', async () => {
await usingAsync(createInjector(), async (injector) => {
const layoutService = createLayoutService(createMockElement());
injector.bind(LayoutService, () => layoutService);
const rootElement = document.getElementById('root');
// Initialize drawer as open
layoutService.initDrawer('left', { open: true, width: '240px', variant: 'collapsible' });
initializeShadeRoot({
injector,
rootElement,
jsxElement: createComponent(DrawerToggleButton, { position: "left" }),
});
await flushUpdates();
const button = document.querySelector('[data-testid="drawer-toggle-left"]');
expect(button).not.toBeNull();
// Verify hamburger has open class when drawer is open
let hamburger = document.querySelector('.hamburger');
expect(hamburger?.classList.contains('open')).toBe(true);
// Close drawer
layoutService.setDrawerOpen('left', false);
await flushUpdates();
// Verify hamburger doesn't have open class when drawer is closed
hamburger = document.querySelector('.hamburger');
expect(hamburger?.classList.contains('open')).toBe(false);
});
});
it('should have type="button" to prevent form submission', async () => {
await usingAsync(createInjector(), async (injector) => {
const layoutService = createLayoutService(createMockElement());
injector.bind(LayoutService, () => layoutService);
const rootElement = document.getElementById('root');
initializeShadeRoot({
injector,
rootElement,
jsxElement: createComponent(DrawerToggleButton, { position: "left" }),
});
await flushUpdates();
const button = document.querySelector('button');
expect(button?.getAttribute('type')).toBe('button');
});
});
});
describe('toggling', () => {
it('should toggle left drawer when clicked', async () => {
await usingAsync(createInjector(), async (injector) => {
const layoutService = createLayoutService(createMockElement());
injector.bind(LayoutService, () => layoutService);
const rootElement = document.getElementById('root');
// Initialize drawer as open
layoutService.initDrawer('left', { open: true, width: '240px', variant: 'collapsible' });
initializeShadeRoot({
injector,
rootElement,
jsxElement: createComponent(DrawerToggleButton, { position: "left" }),
});
await flushUpdates();
expect(layoutService.drawerState.getValue().left?.open).toBe(true);
// Click the button
const button = document.querySelector('button');
button.click();
await flushUpdates();
expect(layoutService.drawerState.getValue().left?.open).toBe(false);
// Click again
button.click();
await flushUpdates();
expect(layoutService.drawerState.getValue().left?.open).toBe(true);
});
});
it('should toggle right drawer when clicked', async () => {
await usingAsync(createInjector(), async (injector) => {
const layoutService = createLayoutService(createMockElement());
injector.bind(LayoutService, () => layoutService);
const rootElement = document.getElementById('root');
// Initialize drawer as open
layoutService.initDrawer('right', { open: true, width: '200px', variant: 'collapsible' });
initializeShadeRoot({
injector,
rootElement,
jsxElement: createComponent(DrawerToggleButton, { position: "right" }),
});
await flushUpdates();
expect(layoutService.drawerState.getValue().right?.open).toBe(true);
// Click the button
const button = document.querySelector('button');
button.click();
await flushUpdates();
expect(layoutService.drawerState.getValue().right?.open).toBe(false);
});
});
it('should not throw if drawer is not initialized', async () => {
await usingAsync(createInjector(), async (injector) => {
const layoutService = createLayoutService(createMockElement());
injector.bind(LayoutService, () => layoutService);
const rootElement = document.getElementById('root');
initializeShadeRoot({
injector,
rootElement,
jsxElement: createComponent(DrawerToggleButton, { position: "left" }),
});
await flushUpdates();
// Should not throw when clicking even though drawer isn't initialized
const button = document.querySelector('button');
expect(() => button.click()).not.toThrow();
});
});
});
describe('visual state', () => {
it('should not have open class when drawer is closed', async () => {
await usingAsync(createInjector(), async (injector) => {
const layoutService = createLayoutService(createMockElement());
injector.bind(LayoutService, () => layoutService);
const rootElement = document.getElementById('root');
// Initialize drawer as closed
layoutService.initDrawer('left', { open: false, width: '240px', variant: 'collapsible' });
initializeShadeRoot({
injector,
rootElement,
jsxElement: createComponent(DrawerToggleButton, { position: "left" }),
});
await flushUpdates();
const hamburger = document.querySelector('.hamburger');
expect(hamburger?.classList.contains('open')).toBe(false);
});
});
it('should have open class when drawer is open', async () => {
await usingAsync(createInjector(), async (injector) => {
const layoutService = createLayoutService(createMockElement());
injector.bind(LayoutService, () => layoutService);
const rootElement = document.getElementById('root');
// Initialize drawer as open
layoutService.initDrawer('left', { open: true, width: '240px', variant: 'collapsible' });
initializeShadeRoot({
injector,
rootElement,
jsxElement: createComponent(DrawerToggleButton, { position: "left" }),
});
await flushUpdates();
const hamburger = document.querySelector('.hamburger');
expect(hamburger?.classList.contains('open')).toBe(true);
});
});
it('should update visual state when drawer state changes', async () => {
await usingAsync(createInjector(), async (injector) => {
const layoutService = createLayoutService(createMockElement());
injector.bind(LayoutService, () => layoutService);
const rootElement = document.getElementById('root');
// Initialize drawer as open
layoutService.initDrawer('left', { open: true, width: '240px', variant: 'collapsible' });
initializeShadeRoot({
injector,
rootElement,
jsxElement: createComponent(DrawerToggleButton, { position: "left" }),
});
await flushUpdates();
let hamburger = document.querySelector('.hamburger');
expect(hamburger?.classList.contains('open')).toBe(true);
// Close drawer via LayoutService
layoutService.setDrawerOpen('left', false);
await flushUpdates();
hamburger = document.querySelector('.hamburger');
expect(hamburger?.classList.contains('open')).toBe(false);
});
});
});
describe('data-testid', () => {
it('should have data-testid for left position', async () => {
await usingAsync(createInjector(), async (injector) => {
const layoutService = createLayoutService(createMockElement());
injector.bind(LayoutService, () => layoutService);
const rootElement = document.getElementById('root');
initializeShadeRoot({
injector,
rootElement,
jsxElement: createComponent(DrawerToggleButton, { position: "left" }),
});
await flushUpdates();
const button = document.querySelector('[data-testid="drawer-toggle-left"]');
expect(button).not.toBeNull();
});
});
it('should have data-testid for right position', async () => {
await usingAsync(createInjector(), async (injector) => {
const layoutService = createLayoutService(createMockElement());
injector.bind(LayoutService, () => layoutService);
const rootElement = document.getElementById('root');
initializeShadeRoot({
injector,
rootElement,
jsxElement: createComponent(DrawerToggleButton, { position: "right" }),
});
await flushUpdates();
const button = document.querySelector('[data-testid="drawer-toggle-right"]');
expect(button).not.toBeNull();
});
});
});
});
//# sourceMappingURL=drawer-toggle-button.spec.js.map