diff --git a/apps/web/components/design-system/__tests__/badge.spec.tsx b/apps/web/components/design-system/__tests__/badge.spec.tsx new file mode 100644 index 0000000..3f05793 --- /dev/null +++ b/apps/web/components/design-system/__tests__/badge.spec.tsx @@ -0,0 +1,45 @@ +import { render, screen } from '@testing-library/react'; +import { describe, expect, it } from 'vitest'; +import { Badge } from '../badge'; + +describe('Badge (design-system)', () => { + it('renders children', () => { + render(Hoạt động); + expect(screen.getByText('Hoạt động')).toBeInTheDocument(); + }); + + it('renders as a span', () => { + render(Test); + expect(screen.getByTestId('b').tagName).toBe('SPAN'); + }); + + it('applies default variant classes', () => { + render(Default); + expect(screen.getByTestId('b')).toHaveClass('bg-muted'); + }); + + it('applies primary variant', () => { + render(Primary); + expect(screen.getByTestId('b')).toHaveClass('bg-primary/10'); + }); + + it('applies warning variant', () => { + render(Warning); + expect(screen.getByTestId('b')).toHaveClass('bg-warning/10'); + }); + + it('applies destructive variant', () => { + render(Error); + expect(screen.getByTestId('b')).toHaveClass('bg-destructive/10'); + }); + + it('applies outline variant', () => { + render(Outline); + expect(screen.getByTestId('b')).toHaveClass('bg-transparent'); + }); + + it('merges custom className', () => { + render(Custom); + expect(screen.getByTestId('b')).toHaveClass('custom-class'); + }); +}); diff --git a/apps/web/components/design-system/__tests__/compact-header.spec.tsx b/apps/web/components/design-system/__tests__/compact-header.spec.tsx new file mode 100644 index 0000000..78b52c0 --- /dev/null +++ b/apps/web/components/design-system/__tests__/compact-header.spec.tsx @@ -0,0 +1,48 @@ +import { render, screen } from '@testing-library/react'; +import { describe, expect, it } from 'vitest'; +import { CompactHeader } from '../compact-header'; + +describe('CompactHeader', () => { + it('renders a header element', () => { + render(); + expect(screen.getByRole('banner')).toBeInTheDocument(); + }); + + it('renders logo when provided', () => { + render(} />); + expect(screen.getByTestId('logo')).toBeInTheDocument(); + }); + + it('renders breadcrumb when provided', () => { + render(Trang chủ} />); + expect(screen.getByText('Trang chủ')).toBeInTheDocument(); + }); + + it('renders search slot when provided', () => { + render(} />); + expect(screen.getByTestId('search')).toBeInTheDocument(); + }); + + it('renders actions when provided', () => { + render(Đăng nhập} />); + expect(screen.getByTestId('act')).toBeInTheDocument(); + }); + + it('does not render logo slot when omitted', () => { + const { container } = render(); + // no extra wrapping div for logo + const header = container.querySelector('header') as HTMLElement; + // Slot divs: just the ml-auto actions div should be present + expect(header.children.length).toBe(1); + }); + + it('is sticky and has border-b class', () => { + render(); + expect(screen.getByRole('banner')).toHaveClass('sticky', 'border-b'); + }); + + it('merges custom className', () => { + render(); + expect(screen.getByRole('banner')).toHaveClass('custom-header'); + }); +}); diff --git a/apps/web/components/design-system/__tests__/density-toggle.spec.tsx b/apps/web/components/design-system/__tests__/density-toggle.spec.tsx new file mode 100644 index 0000000..ee3ed8e --- /dev/null +++ b/apps/web/components/design-system/__tests__/density-toggle.spec.tsx @@ -0,0 +1,60 @@ +import { render, screen, fireEvent } from '@testing-library/react'; +import { describe, expect, it, vi, beforeEach } from 'vitest'; +import { DensityToggle } from '../density-toggle'; + +// Mock the zustand store to avoid localStorage/persist issues in jsdom +const mockToggleDensity = vi.fn(); +let mockDensity = 'regular'; + +vi.mock('@/lib/preferences-store', () => ({ + usePreferencesStore: () => ({ + get density() { + return mockDensity; + }, + toggleDensity: mockToggleDensity, + }), +})); + +describe('DensityToggle', () => { + beforeEach(() => { + mockDensity = 'regular'; + mockToggleDensity.mockClear(); + }); + + it('renders a button with role="switch"', () => { + render(); + expect(screen.getByRole('switch')).toBeInTheDocument(); + }); + + it('has aria-checked="false" when density is regular', () => { + render(); + expect(screen.getByRole('switch')).toHaveAttribute('aria-checked', 'false'); + }); + + it('has aria-checked="true" when density is compact', () => { + mockDensity = 'compact'; + render(); + expect(screen.getByRole('switch')).toHaveAttribute('aria-checked', 'true'); + }); + + it('calls toggleDensity on click', () => { + render(); + fireEvent.click(screen.getByRole('switch')); + expect(mockToggleDensity).toHaveBeenCalledOnce(); + }); + + it('applies custom aria-label via label prop', () => { + render(); + expect(screen.getByRole('switch')).toHaveAttribute('aria-label', 'Toggle view'); + }); + + it('has a default aria-label', () => { + render(); + expect(screen.getByRole('switch')).toHaveAttribute('aria-label'); + }); + + it('merges custom className', () => { + render(); + expect(screen.getByRole('switch')).toHaveClass('extra'); + }); +}); diff --git a/apps/web/components/design-system/__tests__/divider.spec.tsx b/apps/web/components/design-system/__tests__/divider.spec.tsx new file mode 100644 index 0000000..77c2bcf --- /dev/null +++ b/apps/web/components/design-system/__tests__/divider.spec.tsx @@ -0,0 +1,41 @@ +import { render, screen } from '@testing-library/react'; +import { describe, expect, it } from 'vitest'; +import { Divider } from '../divider'; + +describe('Divider', () => { + it('renders with role="separator"', () => { + render(); + expect(screen.getByRole('separator')).toBeInTheDocument(); + }); + + it('defaults to horizontal orientation', () => { + render(); + expect(screen.getByTestId('d')).toHaveAttribute('aria-orientation', 'horizontal'); + }); + + it('renders vertical orientation', () => { + render(); + expect(screen.getByTestId('d')).toHaveAttribute('aria-orientation', 'vertical'); + expect(screen.getByTestId('d')).toHaveClass('h-full', 'w-px'); + }); + + it('applies strong variant class', () => { + render(); + expect(screen.getByTestId('d')).toHaveClass('bg-border-strong'); + }); + + it('applies default border class without strong', () => { + render(); + expect(screen.getByTestId('d')).toHaveClass('bg-border'); + }); + + it('horizontal adds h-px and w-full', () => { + render(); + expect(screen.getByTestId('d')).toHaveClass('h-px', 'w-full'); + }); + + it('merges custom className', () => { + render(); + expect(screen.getByTestId('d')).toHaveClass('my-custom'); + }); +}); diff --git a/apps/web/components/design-system/__tests__/empty-state.spec.tsx b/apps/web/components/design-system/__tests__/empty-state.spec.tsx new file mode 100644 index 0000000..c100606 --- /dev/null +++ b/apps/web/components/design-system/__tests__/empty-state.spec.tsx @@ -0,0 +1,40 @@ +import { render, screen } from '@testing-library/react'; +import { describe, expect, it } from 'vitest'; +import { EmptyState } from '../empty-state'; + +describe('EmptyState', () => { + it('renders title', () => { + render(); + expect(screen.getByText('Không có dữ liệu')).toBeInTheDocument(); + }); + + it('renders description when provided', () => { + render(); + expect(screen.getByText('Mô tả phụ')).toBeInTheDocument(); + }); + + it('does not render description when omitted', () => { + render(); + expect(screen.queryByText('Mô tả phụ')).not.toBeInTheDocument(); + }); + + it('renders icon node', () => { + render(} />); + expect(screen.getByTestId('icon')).toBeInTheDocument(); + }); + + it('renders action node', () => { + render( + Thêm mới} + />, + ); + expect(screen.getByTestId('action')).toBeInTheDocument(); + }); + + it('merges custom className', () => { + render(); + expect(screen.getByTestId('es')).toHaveClass('custom'); + }); +}); diff --git a/apps/web/components/design-system/__tests__/footer.spec.tsx b/apps/web/components/design-system/__tests__/footer.spec.tsx new file mode 100644 index 0000000..264b3bd --- /dev/null +++ b/apps/web/components/design-system/__tests__/footer.spec.tsx @@ -0,0 +1,87 @@ +import { render, screen } from '@testing-library/react'; +import { describe, expect, it } from 'vitest'; +import { Footer, type FooterProps } from '../footer'; + +const renderLink: FooterProps['renderLink'] = ({ href, children, className }) => ( + {children} +); + +const defaultProps: FooterProps = { + brand: 'GoodGo', + description: 'Nền tảng bất động sản', + copyright: '© 2024 GoodGo', + linkGroups: [ + { + title: 'Sản phẩm', + links: [ + { label: 'Bán', href: '/ban' }, + { label: 'Thuê', href: '/thue' }, + ], + }, + ], + renderLink, +}; + +describe('Footer', () => { + it('renders brand name', () => { + render(