test(09-02): add browser-mode component tests for Svelte 5 components

- CompletedToggle: 5 tests for checkbox rendering, state, and interaction
- SearchBar: 7 tests for input, placeholder, recent searches dropdown
- TagInput: 6 tests for rendering with various tag configurations
- Update vitest-setup-client.ts with $app/state, preferences, recentSearches mocks
- All component tests run in real Chromium browser via Playwright
This commit is contained in:
Thomas Richter
2026-02-03 23:36:19 +01:00
parent 283a9214ad
commit 43446b807d
4 changed files with 262 additions and 0 deletions

View File

@@ -0,0 +1,54 @@
import { render } from 'vitest-browser-svelte';
import { page } from 'vitest/browser';
import { describe, expect, it, vi, beforeEach } from 'vitest';
import CompletedToggle from './CompletedToggle.svelte';
describe('CompletedToggle', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('renders the toggle checkbox', async () => {
render(CompletedToggle);
const checkbox = page.getByRole('checkbox');
await expect.element(checkbox).toBeInTheDocument();
});
it('renders "Show completed" label text', async () => {
render(CompletedToggle);
const label = page.getByText('Show completed');
await expect.element(label).toBeInTheDocument();
});
it('renders checkbox in unchecked state by default', async () => {
render(CompletedToggle);
const checkbox = page.getByRole('checkbox');
await expect.element(checkbox).not.toBeChecked();
});
it('checkbox becomes checked when clicked', async () => {
render(CompletedToggle);
const checkbox = page.getByRole('checkbox');
await expect.element(checkbox).not.toBeChecked();
await checkbox.click();
await expect.element(checkbox).toBeChecked();
});
it('has accessible label with correct text', async () => {
render(CompletedToggle);
// Verify the label has the correct text and is associated with the checkbox
const label = page.getByText('Show completed');
await expect.element(label).toBeInTheDocument();
// The label should be a <label> element with a checkbox inside
const checkbox = page.getByRole('checkbox');
await expect.element(checkbox).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,82 @@
import { render } from 'vitest-browser-svelte';
import { page } from 'vitest/browser';
import { describe, expect, it, vi, beforeEach } from 'vitest';
import SearchBar from './SearchBar.svelte';
describe('SearchBar', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('renders an input element', async () => {
render(SearchBar, { props: { value: '' } });
const input = page.getByRole('textbox');
await expect.element(input).toBeInTheDocument();
});
it('displays placeholder text', async () => {
render(SearchBar, { props: { value: '' } });
const input = page.getByPlaceholder('Search entries... (press "/")');
await expect.element(input).toBeInTheDocument();
});
it('displays the initial value', async () => {
render(SearchBar, { props: { value: 'initial search' } });
const input = page.getByRole('textbox');
await expect.element(input).toHaveValue('initial search');
});
it('shows recent searches dropdown when focused with empty input', async () => {
render(SearchBar, {
props: {
value: '',
recentSearches: ['previous search', 'another search']
}
});
const input = page.getByRole('textbox');
await input.click();
// Should show the "Recent searches" header
const recentHeader = page.getByText('Recent searches');
await expect.element(recentHeader).toBeInTheDocument();
// Should show the recent search items
const recentItem = page.getByText('previous search');
await expect.element(recentItem).toBeInTheDocument();
});
it('hides recent searches dropdown when no recent searches', async () => {
render(SearchBar, {
props: {
value: '',
recentSearches: []
}
});
const input = page.getByRole('textbox');
await input.click();
// Recent searches header should not be visible when empty
const recentHeader = page.getByText('Recent searches');
await expect.element(recentHeader).not.toBeInTheDocument();
});
it('applies correct styling classes to input', async () => {
render(SearchBar, { props: { value: '' } });
const input = page.getByRole('textbox');
await expect.element(input).toHaveClass('w-full');
await expect.element(input).toHaveClass('rounded-lg');
});
it('input has correct type attribute', async () => {
render(SearchBar, { props: { value: '' } });
const input = page.getByRole('textbox');
await expect.element(input).toHaveAttribute('type', 'text');
});
});

View File

@@ -0,0 +1,102 @@
import { render } from 'vitest-browser-svelte';
import { page } from 'vitest/browser';
import { describe, expect, it, vi, beforeEach } from 'vitest';
import TagInput from './TagInput.svelte';
import type { Tag } from '$lib/server/db/schema';
// Sample test data
const mockTags: Tag[] = [
{ id: 'tag-1', name: 'work', createdAt: '2026-01-15T10:00:00Z' },
{ id: 'tag-2', name: 'personal', createdAt: '2026-01-15T10:00:00Z' },
{ id: 'tag-3', name: 'urgent', createdAt: '2026-01-15T10:00:00Z' }
];
describe('TagInput', () => {
let onchangeMock: ReturnType<typeof vi.fn>;
beforeEach(() => {
vi.clearAllMocks();
onchangeMock = vi.fn();
});
it('renders the component', async () => {
const { container } = render(TagInput, {
props: {
availableTags: mockTags,
selectedTags: [],
onchange: onchangeMock
}
});
// Component renders - Svelecte creates its own DOM structure
expect(container).toBeTruthy();
});
it('renders with available tags passed as options', async () => {
const { container } = render(TagInput, {
props: {
availableTags: mockTags,
selectedTags: [],
onchange: onchangeMock
}
});
// Component renders successfully with available tags
expect(container).toBeTruthy();
});
it('renders with pre-selected tags', async () => {
const selectedTags = [mockTags[0]]; // 'work' tag selected
const { container } = render(TagInput, {
props: {
availableTags: mockTags,
selectedTags,
onchange: onchangeMock
}
});
// Component renders with selected tags
expect(container).toBeTruthy();
});
it('renders with multiple selected tags', async () => {
const selectedTags = [mockTags[0], mockTags[2]]; // 'work' and 'urgent'
const { container } = render(TagInput, {
props: {
availableTags: mockTags,
selectedTags,
onchange: onchangeMock
}
});
expect(container).toBeTruthy();
});
it('accepts empty available tags array', async () => {
const { container } = render(TagInput, {
props: {
availableTags: [],
selectedTags: [],
onchange: onchangeMock
}
});
expect(container).toBeTruthy();
});
it('renders placeholder text', async () => {
render(TagInput, {
props: {
availableTags: mockTags,
selectedTags: [],
onchange: onchangeMock
}
});
// Svelecte renders with placeholder
const placeholder = page.getByPlaceholder('Add tags...');
await expect.element(placeholder).toBeInTheDocument();
});
});

View File

@@ -34,3 +34,27 @@ vi.mock('$app/environment', () => ({
dev: true,
building: false
}));
// Mock $app/state (Svelte 5 runes-based state)
vi.mock('$app/state', () => ({
page: {
url: new URL('http://localhost'),
params: {},
route: { id: null },
status: 200,
error: null,
data: {},
form: null
}
}));
// Mock preferences store
vi.mock('$lib/stores/preferences.svelte', () => ({
preferences: writable({ showCompleted: false, lastEntryType: 'thought' })
}));
// Mock recent searches store
vi.mock('$lib/stores/recentSearches', () => ({
addRecentSearch: vi.fn(),
recentSearches: writable([])
}));