test(ui): cover Skeleton, SkeletonGroup, SearchInput, LoadingDots (TODO #2)
Adds 13 vitest cases for the Wave 9.D loading primitives; ui suite now 19/19. Removes the resolved ROADMAP-EXEC-TODO #2 marker from Skeleton.tsx. Verified: npx vitest run --pool forks (19 passed); npx tsc --noEmit (clean).
This commit is contained in:
parent
e5061350a5
commit
6cd60e86e8
128
packages/ui/src/__tests__/loading-primitives.test.tsx
Normal file
128
packages/ui/src/__tests__/loading-primitives.test.tsx
Normal file
@ -0,0 +1,128 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { cleanup, fireEvent, render, screen } from '@testing-library/react';
|
||||
import * as React from 'react';
|
||||
|
||||
import { Skeleton, SkeletonGroup, TableSkeleton } from '../components/Skeleton.js';
|
||||
import { SearchInput } from '../components/SearchInput.js';
|
||||
import { LoadingDots } from '../components/LoadingDots.js';
|
||||
|
||||
beforeEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
describe('Skeleton', () => {
|
||||
it('is aria-hidden so screen readers skip the placeholder', () => {
|
||||
const { container } = render(<Skeleton />);
|
||||
const node = container.firstChild as HTMLElement;
|
||||
expect(node.getAttribute('aria-hidden')).toBe('true');
|
||||
});
|
||||
|
||||
it('applies shape-specific classes for each of the 4 variants', () => {
|
||||
const { container: text } = render(<Skeleton shape="text" />);
|
||||
expect((text.firstChild as HTMLElement).className).toContain('rounded-full');
|
||||
|
||||
const { container: circle } = render(<Skeleton shape="circle" />);
|
||||
expect((circle.firstChild as HTMLElement).className).toContain('h-10');
|
||||
|
||||
const { container: card } = render(<Skeleton shape="card" />);
|
||||
expect((card.firstChild as HTMLElement).className).toContain('rounded-2xl');
|
||||
|
||||
const { container: block } = render(<Skeleton shape="block" />);
|
||||
expect((block.firstChild as HTMLElement).className).toContain('min-h-20');
|
||||
});
|
||||
|
||||
it('TableSkeleton renders one row per `rows`', () => {
|
||||
const { container } = render(<TableSkeleton rows={4} />);
|
||||
expect(container.querySelectorAll('[aria-hidden="true"]').length).toBe(4);
|
||||
});
|
||||
});
|
||||
|
||||
describe('SkeletonGroup', () => {
|
||||
it('renders the fallback (not children) while loading', () => {
|
||||
render(
|
||||
<SkeletonGroup loading fallback={<span>placeholder</span>}>
|
||||
<span>real content</span>
|
||||
</SkeletonGroup>
|
||||
);
|
||||
expect(screen.getByText('placeholder')).toBeTruthy();
|
||||
expect(screen.queryByText('real content')).toBeNull();
|
||||
});
|
||||
|
||||
it('renders children once loading flips false', () => {
|
||||
render(
|
||||
<SkeletonGroup loading={false} fallback={<span>placeholder</span>}>
|
||||
<span>real content</span>
|
||||
</SkeletonGroup>
|
||||
);
|
||||
expect(screen.getByText('real content')).toBeTruthy();
|
||||
expect(screen.queryByText('placeholder')).toBeNull();
|
||||
});
|
||||
|
||||
it('keeps children mounted (hidden) while loading when keepContent is set', () => {
|
||||
const { container } = render(
|
||||
<SkeletonGroup loading keepContent fallback={<span>placeholder</span>}>
|
||||
<span>real content</span>
|
||||
</SkeletonGroup>
|
||||
);
|
||||
expect(container.querySelector('[data-bl-skeleton-group="loading"]')).toBeTruthy();
|
||||
expect(screen.getByText('real content')).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('SearchInput', () => {
|
||||
function Harness({ clearable }: { clearable?: boolean } = {}) {
|
||||
const [v, setV] = React.useState('');
|
||||
return <SearchInput value={v} onChange={setV} clearable={clearable} ariaLabel="Search items" />;
|
||||
}
|
||||
|
||||
it('exposes a searchbox role with an accessible label', () => {
|
||||
render(<Harness />);
|
||||
const input = screen.getByRole('searchbox', { name: 'Search items' });
|
||||
expect(input).toBeTruthy();
|
||||
});
|
||||
|
||||
it('calls onChange with each keystroke value', () => {
|
||||
const onChange = vi.fn();
|
||||
render(<SearchInput value="" onChange={onChange} ariaLabel="Search" />);
|
||||
fireEvent.change(screen.getByRole('searchbox'), { target: { value: 'hello' } });
|
||||
expect(onChange).toHaveBeenCalledWith('hello');
|
||||
});
|
||||
|
||||
it('shows a clear button only when value is non-empty and clears on click', () => {
|
||||
render(<Harness />);
|
||||
const input = screen.getByRole('searchbox');
|
||||
expect(screen.queryByTestId('bl-search-clear')).toBeNull();
|
||||
fireEvent.change(input, { target: { value: 'abc' } });
|
||||
const clear = screen.getByTestId('bl-search-clear');
|
||||
expect(clear).toBeTruthy();
|
||||
fireEvent.click(clear);
|
||||
expect((input as HTMLInputElement).value).toBe('');
|
||||
});
|
||||
|
||||
it('renders a suggestions slot when provided', () => {
|
||||
render(
|
||||
<SearchInput value="a" onChange={() => {}} ariaLabel="Search" suggestions={<div>hint</div>} />
|
||||
);
|
||||
expect(screen.getByTestId('bl-search-suggestions').textContent).toContain('hint');
|
||||
});
|
||||
});
|
||||
|
||||
describe('LoadingDots', () => {
|
||||
it('is a status region with an accessible label', () => {
|
||||
render(<LoadingDots label="Thinking" />);
|
||||
const status = screen.getByRole('status', { name: 'Thinking' });
|
||||
expect(status.getAttribute('aria-busy')).toBe('true');
|
||||
});
|
||||
|
||||
it('renders exactly three animated dots', () => {
|
||||
const { container } = render(<LoadingDots />);
|
||||
const dots = container.querySelectorAll('[aria-hidden="true"]');
|
||||
expect(dots.length).toBe(3);
|
||||
});
|
||||
|
||||
it('honours a custom dot colour', () => {
|
||||
const { container } = render(<LoadingDots color="rgb(255, 0, 0)" />);
|
||||
const firstDot = container.querySelector('[aria-hidden="true"]') as HTMLElement;
|
||||
expect(firstDot.style.background).toBe('rgb(255, 0, 0)');
|
||||
});
|
||||
});
|
||||
@ -1,5 +1,3 @@
|
||||
// ROADMAP-EXEC-TODO #2 — vitest setup pending in @bytelyst/ui; add Skeleton +
|
||||
// SkeletonGroup tests once happy-dom + @testing-library/react devDeps land.
|
||||
import * as React from 'react';
|
||||
import { clsx } from 'clsx';
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user