Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 148 additions & 0 deletions frontend/src/components/ui/__tests__/Button.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import '@testing-library/jest-dom';
import { Button } from '../Button';

describe('Button', () => {
// -------------------------------------------------------------------------
// Rendering
// -------------------------------------------------------------------------

it('renders its children', () => {
render(<Button>Save</Button>);
expect(screen.getByRole('button', { name: /save/i })).toBeInTheDocument();
});

it('defaults to type="button"', () => {
render(<Button>Click</Button>);
expect(screen.getByRole('button')).toHaveAttribute('type', 'button');
});

it('respects an explicit type="submit"', () => {
render(<Button type="submit">Submit</Button>);
expect(screen.getByRole('button')).toHaveAttribute('type', 'submit');
});

it('applies the primary variant class by default', () => {
render(<Button>Default</Button>);
expect(screen.getByRole('button')).toHaveClass('ui-btn--primary');
});

it.each(['primary', 'secondary', 'danger', 'ghost'] as const)(
'applies the %s variant class',
(variant) => {
render(<Button variant={variant}>Btn</Button>);
expect(screen.getByRole('button')).toHaveClass(`ui-btn--${variant}`);
}
);

it('merges a custom className', () => {
render(<Button className="my-class">Btn</Button>);
const btn = screen.getByRole('button');
expect(btn).toHaveClass('ui-btn');
expect(btn).toHaveClass('my-class');
});

// -------------------------------------------------------------------------
// Disabled / loading states
// -------------------------------------------------------------------------

it('is not disabled by default', () => {
render(<Button>Btn</Button>);
expect(screen.getByRole('button')).not.toBeDisabled();
});

it('is disabled when disabled prop is set', () => {
render(<Button disabled>Btn</Button>);
const btn = screen.getByRole('button');
expect(btn).toBeDisabled();
expect(btn).toHaveAttribute('aria-disabled', 'true');
});

it('is disabled when isLoading=true', () => {
render(<Button isLoading>Btn</Button>);
const btn = screen.getByRole('button');
expect(btn).toBeDisabled();
expect(btn).toHaveAttribute('aria-disabled', 'true');
});

it('sets aria-busy="true" when isLoading=true', () => {
render(<Button isLoading>Btn</Button>);
expect(screen.getByRole('button')).toHaveAttribute('aria-busy', 'true');
});

it('does not set aria-busy when not loading', () => {
render(<Button>Btn</Button>);
expect(screen.getByRole('button')).not.toHaveAttribute('aria-busy');
});

it('renders a spinner element when isLoading=true', () => {
const { container } = render(<Button isLoading>Btn</Button>);
expect(container.querySelector('.ui-btn__spinner')).toBeInTheDocument();
});

it('does not render a spinner when not loading', () => {
const { container } = render(<Button>Btn</Button>);
expect(container.querySelector('.ui-btn__spinner')).not.toBeInTheDocument();
});

// -------------------------------------------------------------------------
// Icons
// -------------------------------------------------------------------------

it('renders leftIcon when not loading', () => {
render(<Button leftIcon={<span data-testid="left" />}>Btn</Button>);
expect(screen.getByTestId('left')).toBeInTheDocument();
});

it('renders rightIcon when not loading', () => {
render(<Button rightIcon={<span data-testid="right" />}>Btn</Button>);
expect(screen.getByTestId('right')).toBeInTheDocument();
});

it('hides leftIcon when isLoading=true', () => {
render(<Button isLoading leftIcon={<span data-testid="left" />}>Btn</Button>);
expect(screen.queryByTestId('left')).not.toBeInTheDocument();
});

it('hides rightIcon when isLoading=true', () => {
render(<Button isLoading rightIcon={<span data-testid="right" />}>Btn</Button>);
expect(screen.queryByTestId('right')).not.toBeInTheDocument();
});

// -------------------------------------------------------------------------
// Interaction
// -------------------------------------------------------------------------

it('calls onClick when clicked', async () => {
const onClick = jest.fn();
render(<Button onClick={onClick}>Click me</Button>);
await userEvent.click(screen.getByRole('button'));
expect(onClick).toHaveBeenCalledTimes(1);
});

it('does not fire onClick when disabled', async () => {
const onClick = jest.fn();
render(<Button disabled onClick={onClick}>Btn</Button>);
await userEvent.click(screen.getByRole('button'));
expect(onClick).not.toHaveBeenCalled();
});

it('does not fire onClick when isLoading', async () => {
const onClick = jest.fn();
render(<Button isLoading onClick={onClick}>Btn</Button>);
await userEvent.click(screen.getByRole('button'));
expect(onClick).not.toHaveBeenCalled();
});

// -------------------------------------------------------------------------
// Ref forwarding
// -------------------------------------------------------------------------

it('forwards ref to the underlying <button>', () => {
const ref = React.createRef<HTMLButtonElement>();
render(<Button ref={ref}>Btn</Button>);
expect(ref.current).toBeInstanceOf(HTMLButtonElement);
});
});
138 changes: 138 additions & 0 deletions frontend/src/components/ui/__tests__/Card.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import { Card, CardHeader, CardTitle, CardBody, CardFooter } from '../Card';

describe('Card', () => {
// -------------------------------------------------------------------------
// Rendering
// -------------------------------------------------------------------------

it('renders children', () => {
render(<Card>Card content</Card>);
expect(screen.getByText('Card content')).toBeInTheDocument();
});

it('applies the base ui-card class', () => {
const { container } = render(<Card />);
expect(container.firstChild).toHaveClass('ui-card');
});

it('merges a custom className', () => {
const { container } = render(<Card className="my-card" />);
expect(container.firstChild).toHaveClass('ui-card');
expect(container.firstChild).toHaveClass('my-card');
});

it('forwards extra HTML attributes', () => {
render(<Card data-testid="card-root" role="region" aria-label="Market" />);
const el = screen.getByTestId('card-root');
expect(el).toHaveAttribute('role', 'region');
expect(el).toHaveAttribute('aria-label', 'Market');
});

// -------------------------------------------------------------------------
// Variants
// -------------------------------------------------------------------------

it('does not apply interactive class by default', () => {
const { container } = render(<Card />);
expect(container.firstChild).not.toHaveClass('ui-card--interactive');
});

it('applies interactive class when interactive=true', () => {
const { container } = render(<Card interactive />);
expect(container.firstChild).toHaveClass('ui-card--interactive');
});

it('does not apply no-padding class by default', () => {
const { container } = render(<Card />);
expect(container.firstChild).not.toHaveClass('ui-card--no-padding');
});

it('applies no-padding class when noPadding=true', () => {
const { container } = render(<Card noPadding />);
expect(container.firstChild).toHaveClass('ui-card--no-padding');
});

it('can combine interactive and noPadding', () => {
const { container } = render(<Card interactive noPadding />);
expect(container.firstChild).toHaveClass('ui-card--interactive');
expect(container.firstChild).toHaveClass('ui-card--no-padding');
});

// -------------------------------------------------------------------------
// Ref forwarding
// -------------------------------------------------------------------------

it('forwards ref to the underlying <div>', () => {
const ref = React.createRef<HTMLDivElement>();
render(<Card ref={ref} />);
expect(ref.current).toBeInstanceOf(HTMLDivElement);
});
});

describe('CardHeader', () => {
it('renders children and applies the header class', () => {
const { container } = render(<CardHeader>Header</CardHeader>);
expect(screen.getByText('Header')).toBeInTheDocument();
expect(container.firstChild).toHaveClass('ui-card__header');
});

it('merges a custom className', () => {
const { container } = render(<CardHeader className="extra">H</CardHeader>);
expect(container.firstChild).toHaveClass('ui-card__header');
expect(container.firstChild).toHaveClass('extra');
});
});

describe('CardTitle', () => {
it('renders as <h3> by default', () => {
render(<CardTitle>My title</CardTitle>);
expect(screen.getByRole('heading', { level: 3, name: 'My title' })).toBeInTheDocument();
});

it.each(['h2', 'h3', 'h4'] as const)('renders as <%s> when as="%s"', (tag) => {
render(<CardTitle as={tag}>Title</CardTitle>);
const level = parseInt(tag[1]);
expect(screen.getByRole('heading', { level, name: 'Title' })).toBeInTheDocument();
});

it('applies the title class', () => {
const { container } = render(<CardTitle>T</CardTitle>);
expect(container.firstChild).toHaveClass('ui-card__title');
});
});

describe('CardBody', () => {
it('renders children and applies the body class', () => {
const { container } = render(<CardBody>Body content</CardBody>);
expect(screen.getByText('Body content')).toBeInTheDocument();
expect(container.firstChild).toHaveClass('ui-card__body');
});
});

describe('CardFooter', () => {
it('renders children and applies the footer class', () => {
const { container } = render(<CardFooter>Footer content</CardFooter>);
expect(screen.getByText('Footer content')).toBeInTheDocument();
expect(container.firstChild).toHaveClass('ui-card__footer');
});
});

describe('Card composition', () => {
it('renders a full Card with all sub-components', () => {
render(
<Card>
<CardHeader>
<CardTitle>Market Title</CardTitle>
</CardHeader>
<CardBody>Some body text.</CardBody>
<CardFooter>Actions here</CardFooter>
</Card>
);
expect(screen.getByRole('heading', { name: 'Market Title' })).toBeInTheDocument();
expect(screen.getByText('Some body text.')).toBeInTheDocument();
expect(screen.getByText('Actions here')).toBeInTheDocument();
});
});
Loading
Loading