mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-15 00:41:09 +02:00
355 lines
7.8 KiB
TypeScript
355 lines
7.8 KiB
TypeScript
/**
|
|
* Example Svelte 5 Component Test
|
|
*
|
|
* This demonstrates best practices for testing Svelte 5 components:
|
|
* - Test component rendering with runes
|
|
* - Test user interactions
|
|
* - Test reactive state ($state, $derived, $effect)
|
|
* - Test events
|
|
* - Test props
|
|
*/
|
|
|
|
import { render, screen, fireEvent } from '@testing-library/svelte';
|
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
import Button from '../Button.svelte';
|
|
import userEvent from '@testing-library/user-event';
|
|
|
|
describe('Button (Svelte 5)', () => {
|
|
const user = userEvent.setup();
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
});
|
|
|
|
describe('Rendering', () => {
|
|
it('should render with text content', () => {
|
|
render(Button, { props: { children: 'Click Me' } });
|
|
|
|
expect(screen.getByText('Click Me')).toBeTruthy();
|
|
});
|
|
|
|
it('should render with variant classes', () => {
|
|
const { container } = render(Button, {
|
|
props: {
|
|
variant: 'primary',
|
|
children: 'Primary Button',
|
|
},
|
|
});
|
|
|
|
const button = container.querySelector('button');
|
|
expect(button?.className).toContain('btn-primary');
|
|
});
|
|
|
|
it('should render with custom class', () => {
|
|
const { container } = render(Button, {
|
|
props: {
|
|
class: 'custom-class',
|
|
children: 'Button',
|
|
},
|
|
});
|
|
|
|
const button = container.querySelector('button');
|
|
expect(button?.className).toContain('custom-class');
|
|
});
|
|
|
|
it('should render loading state', () => {
|
|
render(Button, {
|
|
props: {
|
|
loading: true,
|
|
children: 'Submit',
|
|
},
|
|
});
|
|
|
|
expect(screen.getByTestId('loading-spinner')).toBeTruthy();
|
|
});
|
|
|
|
it('should render disabled state', () => {
|
|
const { container } = render(Button, {
|
|
props: {
|
|
disabled: true,
|
|
children: 'Disabled',
|
|
},
|
|
});
|
|
|
|
const button = container.querySelector('button');
|
|
expect(button?.disabled).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('User Interactions', () => {
|
|
it('should call onclick when clicked', async () => {
|
|
const onclick = vi.fn();
|
|
|
|
render(Button, {
|
|
props: {
|
|
onclick,
|
|
children: 'Click Me',
|
|
},
|
|
});
|
|
|
|
await user.click(screen.getByText('Click Me'));
|
|
|
|
expect(onclick).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it('should not call onclick when disabled', async () => {
|
|
const onclick = vi.fn();
|
|
|
|
render(Button, {
|
|
props: {
|
|
onclick,
|
|
disabled: true,
|
|
children: 'Disabled',
|
|
},
|
|
});
|
|
|
|
await user.click(screen.getByText('Disabled'));
|
|
|
|
expect(onclick).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should not call onclick when loading', async () => {
|
|
const onclick = vi.fn();
|
|
|
|
render(Button, {
|
|
props: {
|
|
onclick,
|
|
loading: true,
|
|
children: 'Loading',
|
|
},
|
|
});
|
|
|
|
const button = screen.getByRole('button');
|
|
await user.click(button);
|
|
|
|
expect(onclick).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('should handle keyboard events', async () => {
|
|
const onclick = vi.fn();
|
|
|
|
render(Button, {
|
|
props: {
|
|
onclick,
|
|
children: 'Press Enter',
|
|
},
|
|
});
|
|
|
|
const button = screen.getByRole('button');
|
|
button.focus();
|
|
await user.keyboard('{Enter}');
|
|
|
|
expect(onclick).toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe('Reactive State (Svelte 5 Runes)', () => {
|
|
it('should react to prop changes', async () => {
|
|
const { component, rerender } = render(Button, {
|
|
props: {
|
|
loading: false,
|
|
children: 'Submit',
|
|
},
|
|
});
|
|
|
|
expect(screen.queryByTestId('loading-spinner')).toBeNull();
|
|
|
|
// Update props
|
|
await rerender({ loading: true });
|
|
|
|
expect(screen.getByTestId('loading-spinner')).toBeTruthy();
|
|
});
|
|
|
|
it('should derive styles based on variant', () => {
|
|
const { container, rerender } = render(Button, {
|
|
props: {
|
|
variant: 'primary',
|
|
children: 'Button',
|
|
},
|
|
});
|
|
|
|
let button = container.querySelector('button');
|
|
expect(button?.className).toContain('btn-primary');
|
|
|
|
rerender({ variant: 'secondary' });
|
|
|
|
button = container.querySelector('button');
|
|
expect(button?.className).toContain('btn-secondary');
|
|
expect(button?.className).not.toContain('btn-primary');
|
|
});
|
|
});
|
|
|
|
describe('Accessibility', () => {
|
|
it('should have button role', () => {
|
|
render(Button, { props: { children: 'Button' } });
|
|
|
|
expect(screen.getByRole('button')).toBeTruthy();
|
|
});
|
|
|
|
it('should support aria-label', () => {
|
|
render(Button, {
|
|
props: {
|
|
'aria-label': 'Close dialog',
|
|
children: 'X',
|
|
},
|
|
});
|
|
|
|
expect(screen.getByLabelText('Close dialog')).toBeTruthy();
|
|
});
|
|
|
|
it('should indicate disabled state to screen readers', () => {
|
|
render(Button, {
|
|
props: {
|
|
disabled: true,
|
|
children: 'Disabled',
|
|
},
|
|
});
|
|
|
|
const button = screen.getByRole('button');
|
|
expect(button.getAttribute('aria-disabled')).toBe('true');
|
|
});
|
|
|
|
it('should indicate loading state to screen readers', () => {
|
|
render(Button, {
|
|
props: {
|
|
loading: true,
|
|
children: 'Loading',
|
|
},
|
|
});
|
|
|
|
const button = screen.getByRole('button');
|
|
expect(button.getAttribute('aria-busy')).toBe('true');
|
|
});
|
|
});
|
|
|
|
describe('Variants', () => {
|
|
it.each([
|
|
['primary', 'btn-primary'],
|
|
['secondary', 'btn-secondary'],
|
|
['danger', 'btn-danger'],
|
|
['ghost', 'btn-ghost'],
|
|
])('should render %s variant with %s class', (variant, expectedClass) => {
|
|
const { container } = render(Button, {
|
|
props: {
|
|
variant,
|
|
children: 'Button',
|
|
},
|
|
});
|
|
|
|
const button = container.querySelector('button');
|
|
expect(button?.className).toContain(expectedClass);
|
|
});
|
|
});
|
|
|
|
describe('Sizes', () => {
|
|
it.each([
|
|
['sm', 'btn-sm'],
|
|
['md', 'btn-md'],
|
|
['lg', 'btn-lg'],
|
|
])('should render %s size with %s class', (size, expectedClass) => {
|
|
const { container } = render(Button, {
|
|
props: {
|
|
size,
|
|
children: 'Button',
|
|
},
|
|
});
|
|
|
|
const button = container.querySelector('button');
|
|
expect(button?.className).toContain(expectedClass);
|
|
});
|
|
});
|
|
|
|
describe('Edge Cases', () => {
|
|
it('should handle rapid clicks (debouncing)', async () => {
|
|
vi.useFakeTimers();
|
|
const onclick = vi.fn();
|
|
|
|
render(Button, {
|
|
props: {
|
|
onclick,
|
|
debounce: 500,
|
|
children: 'Click',
|
|
},
|
|
});
|
|
|
|
const button = screen.getByRole('button');
|
|
|
|
// Rapid clicks
|
|
await user.click(button);
|
|
await user.click(button);
|
|
await user.click(button);
|
|
|
|
// Should only call once
|
|
expect(onclick).toHaveBeenCalledTimes(1);
|
|
|
|
// Wait for debounce
|
|
vi.advanceTimersByTime(500);
|
|
|
|
// Click again
|
|
await user.click(button);
|
|
|
|
expect(onclick).toHaveBeenCalledTimes(2);
|
|
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it('should handle async onclick handlers', async () => {
|
|
const asyncOnclick = vi.fn(async () => {
|
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
});
|
|
|
|
render(Button, {
|
|
props: {
|
|
onclick: asyncOnclick,
|
|
children: 'Async Click',
|
|
},
|
|
});
|
|
|
|
await user.click(screen.getByText('Async Click'));
|
|
|
|
expect(asyncOnclick).toHaveBeenCalled();
|
|
|
|
// Wait for async handler to complete
|
|
await vi.waitFor(() => {
|
|
expect(asyncOnclick).toHaveReturnedWith(expect.any(Promise));
|
|
});
|
|
});
|
|
|
|
it('should handle null children gracefully', () => {
|
|
render(Button, { props: {} });
|
|
|
|
expect(screen.getByRole('button')).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
describe('Slots', () => {
|
|
it('should render icon slot', () => {
|
|
render(Button, {
|
|
props: {
|
|
children: 'With Icon',
|
|
},
|
|
// Note: Testing slots in Vitest requires different approach
|
|
// This is a simplified example
|
|
});
|
|
|
|
expect(screen.getByText('With Icon')).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
describe('Events', () => {
|
|
it('should dispatch custom event on click', async () => {
|
|
const { component } = render(Button, {
|
|
props: {
|
|
children: 'Custom Event',
|
|
},
|
|
});
|
|
|
|
const customEventHandler = vi.fn();
|
|
component.$on('customClick', customEventHandler);
|
|
|
|
await user.click(screen.getByText('Custom Event'));
|
|
|
|
expect(customEventHandler).toHaveBeenCalled();
|
|
});
|
|
});
|
|
});
|