test(web): add Vitest+RTL tests for 15 design-system presentational components

Covers Badge, Divider, EmptyState, Numeric, PriceDelta, Signal, Skeleton,
StatusChip, Surface, StatCard, KpiCard, DensityToggle, Footer, MarketIndex,
CompactHeader — rendering, variants, props, a11y attributes, className merging.

All 1139 web tests pass. Zustand persist store mocked for DensityToggle to
avoid jsdom localStorage incompatibility.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Ho Ngoc Hai
2026-04-23 20:33:17 +07:00
parent 7d26436461
commit 5a119df806
15 changed files with 766 additions and 0 deletions

View File

@@ -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(<Badge>Hoạt đng</Badge>);
expect(screen.getByText('Hoạt động')).toBeInTheDocument();
});
it('renders as a span', () => {
render(<Badge data-testid="b">Test</Badge>);
expect(screen.getByTestId('b').tagName).toBe('SPAN');
});
it('applies default variant classes', () => {
render(<Badge data-testid="b">Default</Badge>);
expect(screen.getByTestId('b')).toHaveClass('bg-muted');
});
it('applies primary variant', () => {
render(<Badge data-testid="b" variant="primary">Primary</Badge>);
expect(screen.getByTestId('b')).toHaveClass('bg-primary/10');
});
it('applies warning variant', () => {
render(<Badge data-testid="b" variant="warning">Warning</Badge>);
expect(screen.getByTestId('b')).toHaveClass('bg-warning/10');
});
it('applies destructive variant', () => {
render(<Badge data-testid="b" variant="destructive">Error</Badge>);
expect(screen.getByTestId('b')).toHaveClass('bg-destructive/10');
});
it('applies outline variant', () => {
render(<Badge data-testid="b" variant="outline">Outline</Badge>);
expect(screen.getByTestId('b')).toHaveClass('bg-transparent');
});
it('merges custom className', () => {
render(<Badge data-testid="b" className="custom-class">Custom</Badge>);
expect(screen.getByTestId('b')).toHaveClass('custom-class');
});
});

View File

@@ -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(<CompactHeader />);
expect(screen.getByRole('banner')).toBeInTheDocument();
});
it('renders logo when provided', () => {
render(<CompactHeader logo={<span data-testid="logo" />} />);
expect(screen.getByTestId('logo')).toBeInTheDocument();
});
it('renders breadcrumb when provided', () => {
render(<CompactHeader breadcrumb={<span>Trang chủ</span>} />);
expect(screen.getByText('Trang chủ')).toBeInTheDocument();
});
it('renders search slot when provided', () => {
render(<CompactHeader search={<input data-testid="search" />} />);
expect(screen.getByTestId('search')).toBeInTheDocument();
});
it('renders actions when provided', () => {
render(<CompactHeader actions={<button data-testid="act">Đăng nhập</button>} />);
expect(screen.getByTestId('act')).toBeInTheDocument();
});
it('does not render logo slot when omitted', () => {
const { container } = render(<CompactHeader />);
// 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(<CompactHeader data-testid="ch" />);
expect(screen.getByRole('banner')).toHaveClass('sticky', 'border-b');
});
it('merges custom className', () => {
render(<CompactHeader className="custom-header" />);
expect(screen.getByRole('banner')).toHaveClass('custom-header');
});
});

View File

@@ -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(<DensityToggle />);
expect(screen.getByRole('switch')).toBeInTheDocument();
});
it('has aria-checked="false" when density is regular', () => {
render(<DensityToggle />);
expect(screen.getByRole('switch')).toHaveAttribute('aria-checked', 'false');
});
it('has aria-checked="true" when density is compact', () => {
mockDensity = 'compact';
render(<DensityToggle />);
expect(screen.getByRole('switch')).toHaveAttribute('aria-checked', 'true');
});
it('calls toggleDensity on click', () => {
render(<DensityToggle />);
fireEvent.click(screen.getByRole('switch'));
expect(mockToggleDensity).toHaveBeenCalledOnce();
});
it('applies custom aria-label via label prop', () => {
render(<DensityToggle label="Toggle view" />);
expect(screen.getByRole('switch')).toHaveAttribute('aria-label', 'Toggle view');
});
it('has a default aria-label', () => {
render(<DensityToggle />);
expect(screen.getByRole('switch')).toHaveAttribute('aria-label');
});
it('merges custom className', () => {
render(<DensityToggle className="extra" />);
expect(screen.getByRole('switch')).toHaveClass('extra');
});
});

View File

@@ -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(<Divider />);
expect(screen.getByRole('separator')).toBeInTheDocument();
});
it('defaults to horizontal orientation', () => {
render(<Divider data-testid="d" />);
expect(screen.getByTestId('d')).toHaveAttribute('aria-orientation', 'horizontal');
});
it('renders vertical orientation', () => {
render(<Divider data-testid="d" orientation="vertical" />);
expect(screen.getByTestId('d')).toHaveAttribute('aria-orientation', 'vertical');
expect(screen.getByTestId('d')).toHaveClass('h-full', 'w-px');
});
it('applies strong variant class', () => {
render(<Divider data-testid="d" strong />);
expect(screen.getByTestId('d')).toHaveClass('bg-border-strong');
});
it('applies default border class without strong', () => {
render(<Divider data-testid="d" />);
expect(screen.getByTestId('d')).toHaveClass('bg-border');
});
it('horizontal adds h-px and w-full', () => {
render(<Divider data-testid="d" orientation="horizontal" />);
expect(screen.getByTestId('d')).toHaveClass('h-px', 'w-full');
});
it('merges custom className', () => {
render(<Divider data-testid="d" className="my-custom" />);
expect(screen.getByTestId('d')).toHaveClass('my-custom');
});
});

View File

@@ -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(<EmptyState title="Không có dữ liệu" />);
expect(screen.getByText('Không có dữ liệu')).toBeInTheDocument();
});
it('renders description when provided', () => {
render(<EmptyState title="Title" description="Mô tả phụ" />);
expect(screen.getByText('Mô tả phụ')).toBeInTheDocument();
});
it('does not render description when omitted', () => {
render(<EmptyState title="Title" />);
expect(screen.queryByText('Mô tả phụ')).not.toBeInTheDocument();
});
it('renders icon node', () => {
render(<EmptyState title="Title" icon={<span data-testid="icon" />} />);
expect(screen.getByTestId('icon')).toBeInTheDocument();
});
it('renders action node', () => {
render(
<EmptyState
title="Title"
action={<button data-testid="action">Thêm mới</button>}
/>,
);
expect(screen.getByTestId('action')).toBeInTheDocument();
});
it('merges custom className', () => {
render(<EmptyState data-testid="es" title="T" className="custom" />);
expect(screen.getByTestId('es')).toHaveClass('custom');
});
});

View File

@@ -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 }) => (
<a href={href} className={className}>{children}</a>
);
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(<Footer {...defaultProps} />);
expect(screen.getByText('GoodGo')).toBeInTheDocument();
});
it('renders description', () => {
render(<Footer {...defaultProps} />);
expect(screen.getByText('Nền tảng bất động sản')).toBeInTheDocument();
});
it('renders copyright text', () => {
render(<Footer {...defaultProps} />);
expect(screen.getByText('© 2024 GoodGo')).toBeInTheDocument();
});
it('renders link group title', () => {
render(<Footer {...defaultProps} />);
expect(screen.getByText('Sản phẩm')).toBeInTheDocument();
});
it('renders link group links', () => {
render(<Footer {...defaultProps} />);
expect(screen.getByText('Bán')).toBeInTheDocument();
expect(screen.getByText('Thuê')).toBeInTheDocument();
});
it('renders contact address when provided', () => {
render(
<Footer
{...defaultProps}
contact={{ address: '123 Nguyễn Huệ, TP.HCM' }}
/>,
);
expect(screen.getByText('123 Nguyễn Huệ, TP.HCM')).toBeInTheDocument();
});
it('renders contact phone with tel link', () => {
render(<Footer {...defaultProps} contact={{ phone: '0901234567' }} />);
expect(screen.getByRole('link', { name: '0901234567' })).toHaveAttribute(
'href',
'tel:0901234567',
);
});
it('renders contact email with mailto link', () => {
render(<Footer {...defaultProps} contact={{ email: 'hello@goodgo.vn' }} />);
expect(screen.getByRole('link', { name: 'hello@goodgo.vn' })).toHaveAttribute(
'href',
'mailto:hello@goodgo.vn',
);
});
it('renders footer element with role="contentinfo"', () => {
render(<Footer {...defaultProps} />);
expect(screen.getByRole('contentinfo')).toBeInTheDocument();
});
it('does not render contact section when omitted', () => {
render(<Footer {...defaultProps} />);
expect(screen.queryByText(/Nguyễn Huệ/)).toBeNull();
});
});

View File

@@ -0,0 +1,46 @@
import { render, screen } from '@testing-library/react';
import { describe, expect, it } from 'vitest';
import { KpiCard } from '../kpi-card';
describe('KpiCard', () => {
it('renders label', () => {
render(<KpiCard label="Giá TB" value="2.5 tỷ" />);
expect(screen.getByText('Giá TB')).toBeInTheDocument();
});
it('renders value', () => {
render(<KpiCard label="Label" value="100" />);
expect(screen.getByText('100')).toBeInTheDocument();
});
it('renders footnote', () => {
render(<KpiCard label="Label" value="100" footnote="Hôm nay" />);
expect(screen.getByText('Hôm nay')).toBeInTheDocument();
});
it('renders PriceDelta when delta provided', () => {
render(<KpiCard label="Label" value="100" delta={1.5} />);
expect(screen.getByText('+1.50%')).toBeInTheDocument();
});
it('renders icon', () => {
render(<KpiCard label="L" value="V" icon={<span data-testid="icon" />} />);
expect(screen.getByTestId('icon')).toBeInTheDocument();
});
it('renders loading skeleton when loading=true', () => {
const { container } = render(<KpiCard label="L" value="V" loading />);
expect(container.querySelector('[aria-busy]')).toBeInTheDocument();
expect(container.querySelector('.animate-pulse')).toBeInTheDocument();
});
it('does not show value when loading', () => {
render(<KpiCard label="Label" value="100" loading />);
expect(screen.queryByText('100')).toBeNull();
});
it('merges custom className', () => {
render(<KpiCard data-testid="kc" label="L" value="V" className="extra" />);
expect(screen.getByTestId('kc')).toHaveClass('extra');
});
});

View File

@@ -0,0 +1,57 @@
import { render, screen } from '@testing-library/react';
import { describe, expect, it } from 'vitest';
import { MarketIndex } from '../market-index';
describe('MarketIndex', () => {
it('renders index name', () => {
render(<MarketIndex name="GGX Market" value="1,234" changePercent={2.5} />);
expect(screen.getByText('GGX Market')).toBeInTheDocument();
});
it('renders value', () => {
render(<MarketIndex name="GGX" value="1,234" changePercent={2.5} />);
expect(screen.getByText('1,234')).toBeInTheDocument();
});
it('renders PriceDelta for positive changePercent', () => {
render(<MarketIndex name="GGX" value="1,234" changePercent={2.5} />);
expect(screen.getByText('+2.50%')).toBeInTheDocument();
});
it('renders PriceDelta for negative changePercent', () => {
render(<MarketIndex name="GGX" value="1,234" changePercent={-1.2} />);
expect(screen.getByText('-1.20%')).toBeInTheDocument();
});
it('renders default window "24h" when change is provided', () => {
render(<MarketIndex name="GGX" value="1,234" changePercent={1} change="+10" />);
expect(screen.getByText(/24h/)).toBeInTheDocument();
});
it('renders custom window when specified', () => {
render(
<MarketIndex name="GGX" value="1,234" changePercent={1} window="7 ngày" />,
);
expect(screen.getByText('7 ngày')).toBeInTheDocument();
});
it('renders change value with window in parentheses', () => {
render(
<MarketIndex name="GGX" value="1,234" changePercent={1} change="+50" window="24h" />,
);
expect(screen.getByText('+50 (24h)')).toBeInTheDocument();
});
it('renders value with data-numeric', () => {
render(<MarketIndex name="GGX" value="1,234" changePercent={1} />);
const elements = document.querySelectorAll('[data-numeric]');
expect(elements.length).toBeGreaterThan(0);
});
it('merges custom className', () => {
render(
<MarketIndex data-testid="mi" name="GGX" value="1,234" changePercent={1} className="extra" />,
);
expect(screen.getByTestId('mi')).toHaveClass('extra');
});
});

View File

@@ -0,0 +1,45 @@
import { render, screen } from '@testing-library/react';
import { describe, expect, it } from 'vitest';
import { Numeric } from '../numeric';
describe('Numeric', () => {
it('formats VND by default', () => {
render(<Numeric value={1000000} />);
// Intl.NumberFormat vi-VN with currency VND — should contain digits
expect(screen.getByText(/1\.000\.000/)).toBeInTheDocument();
});
it('formats percent', () => {
render(<Numeric value={5.5} format="percent" />);
expect(screen.getByText('+5.5%')).toBeInTheDocument();
});
it('formats negative percent without plus sign', () => {
render(<Numeric value={-3.2} format="percent" />);
expect(screen.getByText('-3.2%')).toBeInTheDocument();
});
it('formats decimal', () => {
render(<Numeric value={1234.5} format="decimal" fractionDigits={2} />);
// vi-VN decimal uses comma as decimal separator
expect(screen.getByText(/1\.234/)).toBeInTheDocument();
});
it('formats compact', () => {
render(<Numeric value={2000000} format="compact" />);
expect(screen.getByText(/\d/)).toBeInTheDocument();
});
it('renders as span with data-numeric attribute', () => {
render(<Numeric value={100} data-testid="num" />);
const el = screen.getByTestId('num');
expect(el.tagName).toBe('SPAN');
expect(el).toHaveAttribute('data-numeric');
});
it('has tabular-nums and font-mono classes', () => {
render(<Numeric value={100} data-testid="num" />);
const el = screen.getByTestId('num');
expect(el).toHaveClass('tabular-nums', 'font-mono');
});
});

View File

@@ -0,0 +1,61 @@
import { render, screen } from '@testing-library/react';
import { describe, expect, it } from 'vitest';
import { PriceDelta } from '../price-delta';
describe('PriceDelta', () => {
it('renders formatted positive value with default unit %', () => {
render(<PriceDelta value={3.14} />);
expect(screen.getByText('+3.14%')).toBeInTheDocument();
});
it('renders negative value', () => {
render(<PriceDelta value={-2.5} />);
expect(screen.getByText('-2.50%')).toBeInTheDocument();
});
it('renders zero value as neutral', () => {
render(<PriceDelta value={0} />);
// 0 is not > 0 so no "+" prefix
expect(screen.getByText('0.00%')).toBeInTheDocument();
});
it('renders with custom unit', () => {
render(<PriceDelta value={1.5} unit=" tr" precision={1} />);
expect(screen.getByText('+1.5 tr')).toBeInTheDocument();
});
it('hides icon when hideIcon=true', () => {
const { container } = render(<PriceDelta value={2} hideIcon />);
// With hideIcon, the ArrowUp/Down/Minus svg is not rendered
expect(container.querySelector('svg')).toBeNull();
});
it('renders icon svg by default', () => {
const { container } = render(<PriceDelta value={2} />);
expect(container.querySelector('svg')).toBeInTheDocument();
});
it('has data-numeric attribute on root span', () => {
render(<PriceDelta data-testid="pd" value={1} />);
expect(screen.getByTestId('pd')).toHaveAttribute('data-numeric');
});
it('has font-mono class', () => {
render(<PriceDelta data-testid="pd" value={1} />);
expect(screen.getByTestId('pd')).toHaveClass('font-mono');
});
it('merges custom className', () => {
render(<PriceDelta data-testid="pd" value={1} className="extra" />);
expect(screen.getByTestId('pd')).toHaveClass('extra');
});
it('renders direction override independently of value sign', () => {
// Positive value with forced down direction still shows the down arrow
const { container } = render(<PriceDelta value={5} direction="down" />);
// Should still render an svg (ArrowDown)
expect(container.querySelector('svg')).toBeInTheDocument();
// Value text is formatted from the `value` prop
expect(screen.getByText('+5.00%')).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,45 @@
import { render, screen } from '@testing-library/react';
import { describe, expect, it } from 'vitest';
import { Signal } from '../signal';
describe('Signal', () => {
it('renders without a label', () => {
const { container } = render(<Signal direction="up" />);
expect(container.querySelector('span')).toBeInTheDocument();
});
it('renders label text when provided', () => {
render(<Signal direction="up" label="Tăng" />);
expect(screen.getByText('Tăng')).toBeInTheDocument();
});
it('renders icon with aria-hidden for up direction', () => {
const { container } = render(<Signal direction="up" label="Up" />);
expect(container.querySelector('[aria-hidden="true"]')).toBeInTheDocument();
});
it('renders icon with aria-hidden for down direction', () => {
const { container } = render(<Signal direction="down" label="Down" />);
expect(container.querySelector('[aria-hidden="true"]')).toBeInTheDocument();
});
it('renders icon with aria-hidden for neutral direction', () => {
const { container } = render(<Signal direction="neutral" label="Neutral" />);
expect(container.querySelector('[aria-hidden="true"]')).toBeInTheDocument();
});
it('renders as an inline span', () => {
const { container } = render(<Signal direction="up" label="x" />);
expect(container.firstChild?.nodeName).toBe('SPAN');
});
it('merges custom className onto root span', () => {
const { container } = render(<Signal direction="up" className="extra" label="x" />);
expect(container.firstChild).toHaveClass('extra');
});
it('contains an svg icon', () => {
const { container } = render(<Signal direction="up" />);
expect(container.querySelector('svg')).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,47 @@
import { render, screen } from '@testing-library/react';
import { describe, expect, it } from 'vitest';
import { Skeleton } from '../skeleton';
describe('Skeleton', () => {
it('renders base skeleton with animate-pulse', () => {
const { container } = render(<Skeleton />);
expect(container.firstChild).toHaveClass('animate-pulse');
});
it('base skeleton has aria-hidden', () => {
const { container } = render(<Skeleton />);
expect(container.firstChild).toHaveAttribute('aria-hidden');
});
it('Skeleton.Row renders row placeholder', () => {
const { container } = render(<Skeleton.Row />);
expect(container.firstChild).toHaveAttribute('aria-hidden');
// Should have multiple skeleton blocks
expect(container.querySelectorAll('.animate-pulse').length).toBeGreaterThan(0);
});
it('Skeleton.Card renders card placeholder', () => {
const { container } = render(<Skeleton.Card />);
expect(container.firstChild).toHaveAttribute('aria-hidden');
});
it('Skeleton.Table renders header and default 5 rows', () => {
const { container } = render(<Skeleton.Table />);
expect(container.firstChild).toHaveAttribute('aria-hidden');
// header + 5 rows = 6 row-like blocks; just check children exist
expect(container.children.length).toBeGreaterThan(0);
});
it('Skeleton.Table renders custom row count', () => {
const { container } = render(<Skeleton.Table rows={3} />);
// container.firstChild has header div + 3 SkeletonRow children
const el = container.firstChild as HTMLElement;
// header + 3 rows
expect(el.children.length).toBe(4);
});
it('merges custom className on base', () => {
render(<Skeleton data-testid="sk" className="h-10" />);
expect(screen.getByTestId('sk')).toHaveClass('h-10');
});
});

View File

@@ -0,0 +1,53 @@
import { render, screen } from '@testing-library/react';
import { describe, expect, it } from 'vitest';
import { StatCard } from '../stat-card';
describe('StatCard', () => {
it('renders label', () => {
render(<StatCard label="Giá TB/m²" value="25" />);
expect(screen.getByText('Giá TB/m²')).toBeInTheDocument();
});
it('renders string value', () => {
render(<StatCard label="Label" value="25 tr/m²" />);
expect(screen.getByText('25 tr/m²')).toBeInTheDocument();
});
it('renders number value', () => {
render(<StatCard label="Label" value={1234} />);
expect(screen.getByText('1234')).toBeInTheDocument();
});
it('renders unit when provided', () => {
render(<StatCard label="Label" value="25" unit="tr/m²" />);
expect(screen.getByText('tr/m²')).toBeInTheDocument();
});
it('renders PriceDelta when delta provided', () => {
const { container } = render(<StatCard label="Label" value="25" delta={3.5} />);
expect(container.querySelector('[data-numeric]')).toBeInTheDocument();
});
it('does not render delta section when delta is undefined', () => {
render(<StatCard label="Label" value="25" />);
// no PriceDelta text like +x%
expect(screen.queryByText(/\+\d/)).toBeNull();
});
it('renders sublabel', () => {
render(<StatCard label="Label" value="25" sublabel="7 ngày" />);
expect(screen.getByText('7 ngày')).toBeInTheDocument();
});
it('renders icon node', () => {
render(
<StatCard label="Label" value="25" icon={<span data-testid="icon" />} />,
);
expect(screen.getByTestId('icon')).toBeInTheDocument();
});
it('merges custom className', () => {
render(<StatCard data-testid="sc" label="L" value="V" className="extra" />);
expect(screen.getByTestId('sc')).toHaveClass('extra');
});
});

View File

@@ -0,0 +1,49 @@
import { render, screen } from '@testing-library/react';
import { describe, expect, it } from 'vitest';
import { StatusChip, type PropertyStatus } from '../status-chip';
const statuses: PropertyStatus[] = ['active', 'pending', 'sold', 'rented', 'rejected', 'draft'];
const labels: Record<PropertyStatus, string> = {
active: 'Đang bán',
pending: 'Chờ duyệt',
sold: 'Đã bán',
rented: 'Đã thuê',
rejected: 'Từ chối',
draft: 'Bản nháp',
};
describe('StatusChip', () => {
statuses.forEach((status) => {
it(`renders label for status "${status}"`, () => {
render(<StatusChip status={status} />);
expect(screen.getByText(labels[status])).toBeInTheDocument();
});
});
it('shows dot indicator by default', () => {
const { container } = render(<StatusChip status="active" />);
// dot is a span with aria-hidden
expect(container.querySelector('[aria-hidden]')).toBeInTheDocument();
});
it('hides dot when hideDot=true', () => {
const { container } = render(<StatusChip status="active" hideDot />);
expect(container.querySelector('[aria-hidden]')).toBeNull();
});
it('merges custom className', () => {
render(<StatusChip data-testid="sc" status="active" className="extra" />);
expect(screen.getByTestId('sc')).toHaveClass('extra');
});
it('applies active color classes', () => {
render(<StatusChip data-testid="sc" status="active" />);
expect(screen.getByTestId('sc')).toHaveClass('bg-signal-up-bg');
});
it('applies rejected color classes', () => {
render(<StatusChip data-testid="sc" status="rejected" />);
expect(screen.getByTestId('sc')).toHaveClass('bg-destructive/10');
});
});

View File

@@ -0,0 +1,42 @@
import { render, screen } from '@testing-library/react';
import { describe, expect, it } from 'vitest';
import { Surface, SurfaceElevated } from '../surface';
describe('Surface', () => {
it('renders children', () => {
render(<Surface>Content</Surface>);
expect(screen.getByText('Content')).toBeInTheDocument();
});
it('has bg-background class', () => {
render(<Surface data-testid="s">x</Surface>);
expect(screen.getByTestId('s')).toHaveClass('bg-background');
});
it('has rounded-lg class', () => {
render(<Surface data-testid="s">x</Surface>);
expect(screen.getByTestId('s')).toHaveClass('rounded-lg');
});
it('merges custom className', () => {
render(<Surface data-testid="s" className="p-4">x</Surface>);
expect(screen.getByTestId('s')).toHaveClass('p-4');
});
});
describe('SurfaceElevated', () => {
it('renders children', () => {
render(<SurfaceElevated>Elevated</SurfaceElevated>);
expect(screen.getByText('Elevated')).toBeInTheDocument();
});
it('has bg-background-elevated class', () => {
render(<SurfaceElevated data-testid="se">x</SurfaceElevated>);
expect(screen.getByTestId('se')).toHaveClass('bg-background-elevated');
});
it('has shadow-elevation-1 class', () => {
render(<SurfaceElevated data-testid="se">x</SurfaceElevated>);
expect(screen.getByTestId('se')).toHaveClass('shadow-elevation-1');
});
});