@furystack/shades-common-components
Version:
Common UI components for FuryStack Shades
325 lines • 15.6 kB
JavaScript
import { createComponent, flushUpdates } from '@furystack/shades';
import { describe, expect, it, vi } from 'vitest';
import { AccordionItem } from './accordion-item.js';
import { Accordion } from './accordion.js';
describe('Accordion', () => {
it('should be defined', () => {
expect(Accordion).toBeDefined();
expect(typeof Accordion).toBe('function');
});
it('should create an accordion element', () => {
const el = (createComponent(Accordion, null));
expect(el).toBeDefined();
expect(el.tagName?.toLowerCase()).toBe('shade-accordion');
});
it('should render children', async () => {
const el = (createComponent("div", null,
createComponent(Accordion, null,
createComponent("span", null, "child content"))));
const accordion = el.firstElementChild;
accordion.updateComponent();
await flushUpdates();
expect(accordion.querySelector('span')).not.toBeNull();
});
it('should set data-variant attribute when variant is provided', async () => {
const el = (createComponent("div", null,
createComponent(Accordion, { variant: "elevation" })));
const accordion = el.firstElementChild;
accordion.updateComponent();
await flushUpdates();
expect(accordion.getAttribute('data-variant')).toBe('elevation');
});
it('should not set data-variant attribute for outlined (default)', async () => {
const el = (createComponent("div", null,
createComponent(Accordion, null)));
const accordion = el.firstElementChild;
accordion.updateComponent();
await flushUpdates();
expect(accordion.hasAttribute('data-variant')).toBe(false);
});
describe('spatial navigation', () => {
it('should set data-nav-section with auto-generated id', async () => {
const el = (createComponent("div", null,
createComponent(Accordion, null)));
const accordion = el.firstElementChild;
accordion.updateComponent();
await flushUpdates();
const navSection = accordion.getAttribute('data-nav-section');
expect(navSection).toBeTruthy();
expect(navSection).toMatch(/^accordion-\d+$/);
});
it('should use custom navSection when provided', async () => {
const el = (createComponent("div", null,
createComponent(Accordion, { navSection: "my-accordion" })));
const accordion = el.firstElementChild;
accordion.updateComponent();
await flushUpdates();
expect(accordion.getAttribute('data-nav-section')).toBe('my-accordion');
});
});
});
describe('AccordionItem', () => {
it('should be defined', () => {
expect(AccordionItem).toBeDefined();
expect(typeof AccordionItem).toBe('function');
});
it('should create an accordion-item element', () => {
const el = (createComponent(AccordionItem, { title: "Test" }));
expect(el).toBeDefined();
expect(el.tagName?.toLowerCase()).toBe('shade-accordion-item');
});
it('should render the title text in the header', async () => {
const el = (createComponent("div", null,
createComponent(AccordionItem, { title: "My Section" },
createComponent("p", null, "Content"))));
const item = el.firstElementChild;
item.updateComponent();
await flushUpdates();
const title = item.querySelector('.accordion-title');
expect(title).not.toBeNull();
expect(title.textContent).toBe('My Section');
});
it('should render children in the content area', async () => {
const el = (createComponent("div", null,
createComponent(AccordionItem, { title: "Section" },
createComponent("p", null, "Inner content"))));
const item = el.firstElementChild;
item.updateComponent();
await flushUpdates();
const inner = item.querySelector('.accordion-content-inner');
expect(inner).not.toBeNull();
expect(inner.querySelector('p')).not.toBeNull();
});
it('should not set data-expanded when defaultExpanded is not set', async () => {
const el = (createComponent("div", null,
createComponent(AccordionItem, { title: "Collapsed" })));
const item = el.firstElementChild;
item.updateComponent();
await flushUpdates();
expect(item.hasAttribute('data-expanded')).toBe(false);
});
it('should set data-expanded when defaultExpanded is true', async () => {
const el = (createComponent("div", null,
createComponent(AccordionItem, { title: "Expanded", defaultExpanded: true })));
const item = el.firstElementChild;
item.updateComponent();
await flushUpdates();
expect(item.hasAttribute('data-expanded')).toBe(true);
});
it('should set data-disabled when disabled is true', async () => {
const el = (createComponent("div", null,
createComponent(AccordionItem, { title: "Disabled", disabled: true })));
const item = el.firstElementChild;
item.updateComponent();
await flushUpdates();
expect(item.hasAttribute('data-disabled')).toBe(true);
});
it('should not set data-disabled when disabled is false', async () => {
const el = (createComponent("div", null,
createComponent(AccordionItem, { title: "Enabled" })));
const item = el.firstElementChild;
item.updateComponent();
await flushUpdates();
expect(item.hasAttribute('data-disabled')).toBe(false);
});
it('should render the icon when provided', async () => {
const el = (createComponent("div", null,
createComponent(AccordionItem, { title: "With Icon", icon: "\uD83D\uDD27" })));
const item = el.firstElementChild;
item.updateComponent();
await flushUpdates();
const icon = item.querySelector('.accordion-icon');
expect(icon).not.toBeNull();
expect(icon.textContent).toBe('🔧');
});
it('should not render the icon when not provided', async () => {
const el = (createComponent("div", null,
createComponent(AccordionItem, { title: "No Icon" })));
const item = el.firstElementChild;
item.updateComponent();
await flushUpdates();
expect(item.querySelector('.accordion-icon')).toBeNull();
});
it('should render the chevron', async () => {
const el = (createComponent("div", null,
createComponent(AccordionItem, { title: "Has Chevron" })));
const item = el.firstElementChild;
item.updateComponent();
await flushUpdates();
const chevron = item.querySelector('.accordion-chevron');
expect(chevron).not.toBeNull();
expect(chevron.querySelector('shade-icon')).not.toBeNull();
});
it('should set aria-expanded to false when collapsed', async () => {
const el = (createComponent("div", null,
createComponent(AccordionItem, { title: "Collapsed" })));
const item = el.firstElementChild;
item.updateComponent();
await flushUpdates();
const header = item.querySelector('.accordion-header');
expect(header.getAttribute('aria-expanded')).toBe('false');
});
it('should set aria-expanded to true when expanded', async () => {
const el = (createComponent("div", null,
createComponent(AccordionItem, { title: "Expanded", defaultExpanded: true })));
const item = el.firstElementChild;
item.updateComponent();
await flushUpdates();
const header = item.querySelector('.accordion-header');
expect(header.getAttribute('aria-expanded')).toBe('true');
});
it('should set content height to 0px when collapsed', async () => {
const el = (createComponent("div", null,
createComponent(AccordionItem, { title: "Collapsed" })));
const item = el.firstElementChild;
item.updateComponent();
await flushUpdates();
const content = item.querySelector('.accordion-content');
expect(content.style.height).toBe('0px');
expect(content.style.opacity).toBe('0');
});
it('should not set content height to 0px when expanded', async () => {
const el = (createComponent("div", null,
createComponent(AccordionItem, { title: "Expanded", defaultExpanded: true })));
const item = el.firstElementChild;
item.updateComponent();
await flushUpdates();
const content = item.querySelector('.accordion-content');
expect(content.style.height).not.toBe('0px');
});
it('should render the header as a <button> element', async () => {
const el = (createComponent("div", null,
createComponent(AccordionItem, { title: "Test" })));
const item = el.firstElementChild;
item.updateComponent();
await flushUpdates();
const header = item.querySelector('.accordion-header');
expect(header.tagName).toBe('BUTTON');
expect(header.type).toBe('button');
});
it('should not disable the header button when not disabled', async () => {
const el = (createComponent("div", null,
createComponent(AccordionItem, { title: "Test" })));
const item = el.firstElementChild;
item.updateComponent();
await flushUpdates();
const header = item.querySelector('.accordion-header');
expect(header.disabled).toBe(false);
});
it('should disable the header button when disabled', async () => {
const el = (createComponent("div", null,
createComponent(AccordionItem, { title: "Test", disabled: true })));
const item = el.firstElementChild;
item.updateComponent();
await flushUpdates();
const header = item.querySelector('.accordion-header');
expect(header.disabled).toBe(true);
});
it('should toggle data-expanded on header click', async () => {
const mockAnimation = { finished: Promise.resolve() };
Element.prototype.animate = vi.fn().mockReturnValue(mockAnimation);
const el = (createComponent("div", null,
createComponent(AccordionItem, { title: "Toggleable" },
createComponent("p", null, "Content"))));
const item = el.firstElementChild;
item.updateComponent();
await flushUpdates();
expect(item.hasAttribute('data-expanded')).toBe(false);
const header = item.querySelector('.accordion-header');
header.click();
await flushUpdates();
expect(item.hasAttribute('data-expanded')).toBe(true);
});
it('should toggle data-expanded back on second click', async () => {
const mockAnimation = { finished: Promise.resolve() };
Element.prototype.animate = vi.fn().mockReturnValue(mockAnimation);
const el = (createComponent("div", null,
createComponent(AccordionItem, { title: "Toggleable", defaultExpanded: true },
createComponent("p", null, "Content"))));
const item = el.firstElementChild;
item.updateComponent();
await flushUpdates();
expect(item.hasAttribute('data-expanded')).toBe(true);
const header = item.querySelector('.accordion-header');
header.click();
await flushUpdates();
expect(item.hasAttribute('data-expanded')).toBe(false);
});
it('should not toggle when disabled', async () => {
const el = (createComponent("div", null,
createComponent(AccordionItem, { title: "Disabled", disabled: true },
createComponent("p", null, "Content"))));
const item = el.firstElementChild;
item.updateComponent();
await flushUpdates();
expect(item.hasAttribute('data-expanded')).toBe(false);
const header = item.querySelector('.accordion-header');
header.click();
expect(item.hasAttribute('data-expanded')).toBe(false);
});
describe('spatial navigation integration', () => {
it('should set inert on content when collapsed', async () => {
const el = (createComponent("div", null,
createComponent(AccordionItem, { title: "Collapsed" },
createComponent("p", null, "Content"))));
const item = el.firstElementChild;
item.updateComponent();
await flushUpdates();
const content = item.querySelector('.accordion-content');
expect(content.inert).toBe(true);
});
it('should not set inert on content when expanded', async () => {
const el = (createComponent("div", null,
createComponent(AccordionItem, { title: "Expanded", defaultExpanded: true },
createComponent("p", null, "Content"))));
const item = el.firstElementChild;
item.updateComponent();
await flushUpdates();
const content = item.querySelector('.accordion-content');
expect(content.inert).toBe(false);
});
it('should set inert on content after collapsing', async () => {
const mockAnimation = { finished: Promise.resolve() };
Element.prototype.animate = vi.fn().mockReturnValue(mockAnimation);
const el = (createComponent("div", null,
createComponent(AccordionItem, { title: "Toggleable", defaultExpanded: true },
createComponent("p", null, "Content"))));
const item = el.firstElementChild;
item.updateComponent();
await flushUpdates();
const content = item.querySelector('.accordion-content');
expect(content.inert).toBe(false);
const header = item.querySelector('.accordion-header');
header.click();
await flushUpdates();
expect(content.inert).toBe(true);
});
it('should remove inert on content after expanding', async () => {
const mockAnimation = { finished: Promise.resolve() };
Element.prototype.animate = vi.fn().mockReturnValue(mockAnimation);
const el = (createComponent("div", null,
createComponent(AccordionItem, { title: "Toggleable" },
createComponent("p", null, "Content"))));
const item = el.firstElementChild;
item.updateComponent();
await flushUpdates();
const content = item.querySelector('.accordion-content');
expect(content.inert).toBe(true);
const header = item.querySelector('.accordion-header');
header.click();
await flushUpdates();
expect(content.inert).toBe(false);
});
it('should exclude disabled header from spatial navigation via native disabled attribute', async () => {
const el = (createComponent("div", null,
createComponent(AccordionItem, { title: "Disabled", disabled: true })));
const item = el.firstElementChild;
item.updateComponent();
await flushUpdates();
const header = item.querySelector('.accordion-header');
expect(header.disabled).toBe(true);
expect(header.matches('button:not([disabled])')).toBe(false);
});
});
});
//# sourceMappingURL=accordion.spec.js.map