From 6cd60e86e8a97ce63fe4c9b4fcab30f9cf8a36ee Mon Sep 17 00:00:00 2001 From: saravanakumardb1 Date: Thu, 28 May 2026 17:19:04 -0700 Subject: [PATCH] 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). --- .../src/__tests__/loading-primitives.test.tsx | 128 ++++++++++++++++++ packages/ui/src/components/Skeleton.tsx | 2 - 2 files changed, 128 insertions(+), 2 deletions(-) create mode 100644 packages/ui/src/__tests__/loading-primitives.test.tsx diff --git a/packages/ui/src/__tests__/loading-primitives.test.tsx b/packages/ui/src/__tests__/loading-primitives.test.tsx new file mode 100644 index 00000000..64c4d37f --- /dev/null +++ b/packages/ui/src/__tests__/loading-primitives.test.tsx @@ -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(); + 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(); + expect((text.firstChild as HTMLElement).className).toContain('rounded-full'); + + const { container: circle } = render(); + expect((circle.firstChild as HTMLElement).className).toContain('h-10'); + + const { container: card } = render(); + expect((card.firstChild as HTMLElement).className).toContain('rounded-2xl'); + + const { container: block } = render(); + expect((block.firstChild as HTMLElement).className).toContain('min-h-20'); + }); + + it('TableSkeleton renders one row per `rows`', () => { + const { container } = render(); + expect(container.querySelectorAll('[aria-hidden="true"]').length).toBe(4); + }); +}); + +describe('SkeletonGroup', () => { + it('renders the fallback (not children) while loading', () => { + render( + placeholder}> + real content + + ); + expect(screen.getByText('placeholder')).toBeTruthy(); + expect(screen.queryByText('real content')).toBeNull(); + }); + + it('renders children once loading flips false', () => { + render( + placeholder}> + real content + + ); + 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( + placeholder}> + real content + + ); + 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 ; + } + + it('exposes a searchbox role with an accessible label', () => { + render(); + const input = screen.getByRole('searchbox', { name: 'Search items' }); + expect(input).toBeTruthy(); + }); + + it('calls onChange with each keystroke value', () => { + const onChange = vi.fn(); + render(); + 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(); + 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( + {}} ariaLabel="Search" suggestions={
hint
} /> + ); + expect(screen.getByTestId('bl-search-suggestions').textContent).toContain('hint'); + }); +}); + +describe('LoadingDots', () => { + it('is a status region with an accessible label', () => { + render(); + const status = screen.getByRole('status', { name: 'Thinking' }); + expect(status.getAttribute('aria-busy')).toBe('true'); + }); + + it('renders exactly three animated dots', () => { + const { container } = render(); + const dots = container.querySelectorAll('[aria-hidden="true"]'); + expect(dots.length).toBe(3); + }); + + it('honours a custom dot colour', () => { + const { container } = render(); + const firstDot = container.querySelector('[aria-hidden="true"]') as HTMLElement; + expect(firstDot.style.background).toBe('rgb(255, 0, 0)'); + }); +}); diff --git a/packages/ui/src/components/Skeleton.tsx b/packages/ui/src/components/Skeleton.tsx index 1baca0d3..c21f36c4 100644 --- a/packages/ui/src/components/Skeleton.tsx +++ b/packages/ui/src/components/Skeleton.tsx @@ -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';