component-testing-patterns
Vitest browser mode component testing. Use for testing Svelte 5 components with real browsers, locators, accessibility patterns, and reactive state.
Packaged view
This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.
Install command
npx @skill-hub/cli install spences10-devhub-crm-component-testing-patterns
Repository
Skill path: .claude/skills/component-testing-patterns
Vitest browser mode component testing. Use for testing Svelte 5 components with real browsers, locators, accessibility patterns, and reactive state.
Open repositoryBest for
Primary workflow: Ship Full Stack.
Technical facets: Full Stack, Testing.
Target audience: everyone.
License: Unknown.
Original source
Catalog source: SkillHub Club.
Repository owner: spences10.
This is still a mirrored public skill entry. Review the repository before installing into production workflows.
What it helps with
- Install component-testing-patterns into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
- Review https://github.com/spences10/devhub-crm before adding component-testing-patterns to shared team environments
- Use component-testing-patterns for development workflows
Works across
Favorites: 0.
Sub-skills: 0.
Aggregator: No.
Original source / Raw SKILL.md
---
name: component-testing-patterns
# prettier-ignore
description: Vitest browser mode component testing. Use for testing Svelte 5 components with real browsers, locators, accessibility patterns, and reactive state.
---
# Component Testing Patterns
## Quick Start
```typescript
import { page } from 'vitest/browser';
import { render } from 'vitest-browser-svelte';
render(Button, { label: 'Click' });
await page.getByRole('button', { name: 'Click' }).click();
await expect.element(page.getByRole('button')).toBeInTheDocument();
```
## Core Principles
- **Locators, never containers**: `page.getByRole()` auto-retries
- **Semantic queries**: `getByRole()`, `getByLabelText()` for
accessibility
- **Await assertions**: `await expect.element(el).toBeInTheDocument()`
- **Real browsers**: Tests run in Playwright, not jsdom
## Common Patterns
- **Locators**: `page.getByRole('button')`, `.first()`, `.nth(0)`,
`.last()`
- **Interactions**: `await input.fill('text')`, `await button.click()`
- **Runes**: Use `.test.svelte.ts` files, `flushSync()`, `untrack()`
- **Files**: `*.svelte.test.ts` (browser), `*.ssr.test.ts` (SSR),
`*.test.ts` (server)
## References
- [setup-configuration.md](references/setup-configuration.md) -
Complete Vitest browser setup
- [testing-patterns.md](references/testing-patterns.md) -
Comprehensive testing patterns
- [locator-strategies.md](references/locator-strategies.md) - Semantic
locator guide
- [troubleshooting.md](references/troubleshooting.md) - Common issues
and fixes
<!--
PROGRESSIVE DISCLOSURE GUIDELINES:
- Keep this file ~50 lines total (max ~150 lines)
- Use 1-2 code blocks only (recommend 1)
- Keep description <200 chars for Level 1 efficiency
- Move detailed docs to references/ for Level 3 loading
- This is Level 2 - quick reference ONLY, not a manual
-->
---
## Referenced Files
> The following files are referenced in this skill and included for context.
### references/setup-configuration.md
```markdown
# Setup & Configuration
## Installation
### Create New SvelteKit Project
```bash
pnpm dlx sv@latest create my-testing-app
```
Select options:
- Template: SvelteKit minimal
- TypeScript: Yes, using TypeScript syntax
- Additions: Include vitest for unit testing
### Install Browser Testing Dependencies
```bash
cd my-testing-app
pnpm install -D @vitest/browser-playwright vitest-browser-svelte playwright
pnpm un @testing-library/jest-dom @testing-library/svelte jsdom
```
## Vite Configuration
### Multi-Project Setup
Update `vite.config.ts` with the "Client-Server Alignment Strategy":
```typescript
import tailwindcss from '@tailwindcss/vite';
import { playwright } from '@vitest/browser-playwright';
import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vitest/config';
export default defineConfig({
plugins: [tailwindcss(), sveltekit()],
test: {
projects: [
{
extends: './vite.config.ts',
test: {
name: 'client',
browser: {
enabled: true,
provider: playwright(),
instances: [{ browser: 'chromium' }],
},
include: ['src/**/*.svelte.{test,spec}.{js,ts}'],
setupFiles: ['./vitest-setup-client.ts'],
},
},
{
extends: './vite.config.ts',
test: {
name: 'ssr',
environment: 'node',
include: ['src/**/*.ssr.{test,spec}.{js,ts}'],
},
},
{
extends: './vite.config.ts',
test: {
name: 'server',
environment: 'node',
include: ['src/**/*.{test,spec}.{js,ts}'],
exclude: ['src/**/*.svelte.{test,spec}.{js,ts}'],
},
},
],
},
});
```
### Vitest Setup File
Create `vitest-setup-client.ts`:
```typescript
/// <reference types="vitest/browser" />
/// <reference types="@vitest/browser-playwright" />
```
## File Naming Conventions
- `*.svelte.test.ts` - Client-side component tests (runs in browser)
- `*.ssr.test.ts` - Server-side rendering tests (runs in Node)
- `*.test.ts` - Server/utility tests (runs in Node)
## Running Tests
```bash
# Run all tests once
pnpm run test:unit
# Run specific component in watch mode
pnpm vitest src/lib/components/my-button.svelte
# Run only client tests
pnpm vitest --project client
# Run with UI
pnpm vitest --ui
```
## Package.json Scripts
```json
{
"scripts": {
"test:unit": "vitest",
"test:unit:ui": "vitest --ui",
"test:unit:watch": "vitest --watch"
}
}
```
```
### references/testing-patterns.md
```markdown
# Testing Patterns
## Basic Component Test
```typescript
import { describe, expect, it } from 'vitest';
import { render } from 'vitest-browser-svelte';
import { page } from 'vitest/browser';
import MyButton from './my-button.svelte';
describe('MyButton', () => {
it('should render with correct text', async () => {
render(MyButton, { text: 'Click me' });
const button = page.getByRole('button', { name: 'Click me' });
await expect.element(button).toBeInTheDocument();
});
});
```
## Testing with Children (Snippets)
For Svelte 5 components that accept children, use `createRawSnippet`:
```typescript
import { createRawSnippet } from 'svelte';
it('should render with children', async () => {
const children = createRawSnippet(() => ({
render: () => `<span>Button Text</span>`,
}));
render(MyButton, { children });
const button = page.getByRole('button', { name: 'Button Text' });
await expect.element(button).toBeInTheDocument();
});
```
## Testing Click Events
```typescript
import { vi } from 'vitest';
it('should handle click events', async () => {
const click_handler = vi.fn();
render(MyButton, { onclick: click_handler, text: 'Click me' });
const button = page.getByRole('button', { name: 'Click me' });
await button.click();
expect(click_handler).toHaveBeenCalledOnce();
});
```
## Testing Form Inputs
```typescript
it('should handle form input', async () => {
render(MyForm);
const input = page.getByLabel('Email address');
await input.fill('[email protected]');
const value = await input.evaluate(
(el) => (el as HTMLInputElement).value,
);
expect(value).toBe('[email protected]');
});
```
## Testing Conditional Rendering
```typescript
it('should show error message on invalid input', async () => {
render(MyForm);
const input = page.getByLabel('Email');
await input.fill('invalid-email');
const submitButton = page.getByRole('button', { name: 'Submit' });
await submitButton.click();
await expect
.element(page.getByText('Invalid email address'))
.toBeInTheDocument();
});
```
## Testing Props Changes
```typescript
it('should update when props change', async () => {
const { rerender } = render(MyComponent, { count: 0 });
await expect
.element(page.getByText('Count: 0'))
.toBeInTheDocument();
rerender({ count: 5 });
await expect
.element(page.getByText('Count: 5'))
.toBeInTheDocument();
});
```
## Testing Async Operations
```typescript
it('should handle async data loading', async () => {
render(MyComponent);
// Initially shows loading state
await expect
.element(page.getByText('Loading...'))
.toBeInTheDocument();
// Wait for data to load
await expect
.element(page.getByText('Data loaded'))
.toBeInTheDocument();
});
```
## Testing Multiple Elements
When multiple elements match, use selectors:
```typescript
it('should handle multiple buttons', async () => {
render(MyComponent);
const buttons = page.getByRole('button', { name: 'Action' });
// Select specific instance
await buttons.first().click();
await buttons.nth(1).click();
await buttons.last().click();
});
```
## Testing Disabled States
```typescript
it('should disable button when loading', async () => {
render(MyButton, { disabled: true });
const button = page.getByRole('button');
await expect.element(button).toBeDisabled();
});
```
## Testing CSS Classes
```typescript
it('should apply correct variant class', async () => {
render(MyButton, { variant: 'primary' });
const button = page.getByRole('button');
const className = await button.evaluate((el) => el.className);
expect(className).toContain('btn-primary');
});
```
## Testing Event Modifiers
```typescript
it('should prevent default on form submit', async () => {
const submit_handler = vi.fn();
render(MyForm, { onsubmit: submit_handler });
const form = page.getByRole('form');
await form.evaluate((el) => {
el.dispatchEvent(new Event('submit', { cancelable: true }));
});
expect(submit_handler).toHaveBeenCalled();
});
```
## Testing Accessibility
```typescript
it('should have accessible name', async () => {
render(MyButton, { 'aria-label': 'Close dialog' });
const button = page.getByRole('button', { name: 'Close dialog' });
await expect.element(button).toBeInTheDocument();
});
```
## Mocking Modules
```typescript
import { vi } from 'vitest';
vi.mock('$app/navigation', () => ({
goto: vi.fn(),
}));
it('should navigate on click', async () => {
const { goto } = await import('$app/navigation');
render(MyLink);
const link = page.getByRole('link', { name: 'Home' });
await link.click();
expect(goto).toHaveBeenCalledWith('/home');
});
```
```
### references/locator-strategies.md
```markdown
# Locator Strategies
## Priority Order
Use locators in this order of preference:
1. **Semantic roles** - `getByRole()`
2. **Labels** - `getByLabel()`
3. **Text content** - `getByText()`
4. **Test IDs** - `getByTestId()`
## Semantic Roles (Recommended)
### Buttons
```typescript
page.getByRole('button', { name: 'Submit' });
page.getByRole('button', { name: /submit/i }); // Case insensitive
```
### Links
```typescript
page.getByRole('link', { name: 'Home' });
page.getByRole('link', { name: 'Contact Us' });
```
### Headings
```typescript
page.getByRole('heading', { name: 'Welcome', level: 1 });
page.getByRole('heading', { level: 2 });
```
### Form Elements
```typescript
page.getByRole('textbox', { name: 'Email' });
page.getByRole('checkbox', { name: 'Accept terms' });
page.getByRole('radio', { name: 'Option 1' });
page.getByRole('combobox', { name: 'Country' });
```
### Other Common Roles
```typescript
page.getByRole('navigation');
page.getByRole('main');
page.getByRole('banner');
page.getByRole('contentinfo');
page.getByRole('dialog');
page.getByRole('alert');
page.getByRole('status');
page.getByRole('list');
page.getByRole('listitem');
page.getByRole('table');
page.getByRole('row');
page.getByRole('cell');
```
## By Label (Form Elements)
Best for form inputs with associated labels:
```typescript
page.getByLabel('Email address');
page.getByLabel('Password');
page.getByLabel('Remember me');
```
Component example:
```svelte
<label>
Email address
<input type="email" name="email" />
</label>
```
Or with `for` attribute:
```svelte
<label for="email">Email address</label>
<input id="email" type="email" name="email" />
```
## By Text
For elements containing specific text:
```typescript
page.getByText('Welcome back');
page.getByText(/welcome/i); // Case insensitive
page.getByText('Error:', { exact: false }); // Partial match
```
## By Test ID
Last resort when semantic locators aren't available:
```typescript
page.getByTestId('submit-button');
page.getByTestId('user-profile');
```
Component example:
```svelte
<button data-testid="submit-button">Submit</button>
```
## By Placeholder
For inputs with placeholder text:
```typescript
page.getByPlaceholder('Enter your email');
page.getByPlaceholder(/search/i);
```
## By Alt Text
For images:
```typescript
page.getByAltText('Company logo');
page.getByAltText(/profile/i);
```
## By Title
For elements with title attributes:
```typescript
page.getByTitle('Close');
page.getByTitle('More information');
```
## Combining Locators
### Filtering
```typescript
page.getByRole('button').filter({ hasText: 'Delete' });
page.getByRole('listitem').filter({ has: page.getByRole('button') });
```
### Chaining
```typescript
page.getByRole('navigation').getByRole('link', { name: 'Home' });
page.getByTestId('user-card').getByRole('button', { name: 'Edit' });
```
## Handling Multiple Matches
### Selectors
```typescript
page.getByRole('button').first();
page.getByRole('button').last();
page.getByRole('button').nth(1); // Zero-indexed
```
### Getting All
```typescript
const buttons = page.getByRole('button').all();
expect(buttons).toHaveLength(3);
```
## State-Based Locators
### Disabled
```typescript
page.getByRole('button', { disabled: true });
page.getByRole('button', { disabled: false });
```
### Checked
```typescript
page.getByRole('checkbox', { checked: true });
page.getByRole('radio', { checked: false });
```
### Expanded
```typescript
page.getByRole('button', { expanded: true });
```
### Pressed
```typescript
page.getByRole('button', { pressed: true });
```
## Advanced Patterns
### Within Specific Container
```typescript
const dialog = page.getByRole('dialog');
dialog.getByRole('button', { name: 'Confirm' });
```
### By CSS Selector (Avoid)
Only use as last resort:
```typescript
page.locator('.my-custom-class');
page.locator('#unique-id');
page.locator('[data-custom-attr="value"]');
```
### By XPath (Avoid)
```typescript
page.locator('xpath=//button[contains(text(), "Submit")]');
```
## Best Practices
1. **Always prefer semantic roles** - They match how users and
assistive technologies interact
2. **Use exact names when possible** - More explicit and less fragile
3. **Avoid CSS classes** - They're implementation details that change
4. **Avoid XPath** - Hard to read and maintain
5. **Use test IDs sparingly** - Only when no semantic option exists
6. **Test accessibility** - If you can't find it with semantic
locators, users with screen readers can't either
## Common ARIA Roles Reference
| Role | HTML Element | Example |
| ------------- | ----------------------------------- | ---------------------- |
| `button` | `<button>`, `<input type="button">` | Clickable buttons |
| `link` | `<a href>` | Navigation links |
| `heading` | `<h1>` to `<h6>` | Section headings |
| `textbox` | `<input type="text">`, `<textarea>` | Text inputs |
| `checkbox` | `<input type="checkbox">` | Checkboxes |
| `radio` | `<input type="radio">` | Radio buttons |
| `combobox` | `<select>`, custom dropdowns | Dropdowns |
| `navigation` | `<nav>` | Navigation areas |
| `main` | `<main>` | Main content |
| `banner` | `<header>` | Page header |
| `contentinfo` | `<footer>` | Page footer |
| `dialog` | Custom dialogs with `role="dialog"` | Modal dialogs |
| `alert` | Elements with `role="alert"` | Error/warning messages |
```
### references/troubleshooting.md
```markdown
# Troubleshooting
## Common Issues and Solutions
### Strict Mode Violation Error
**Error:**
```
Error: locator.click: strict mode violation: getByRole('button') resolved to 2 elements
```
**Cause:** Multiple elements match your locator in strict mode.
**Solutions:**
1. Make the locator more specific:
```typescript
// Instead of:
page.getByRole('button');
// Use:
page.getByRole('button', { name: 'Submit' });
```
2. Use selectors for expected multiple matches:
```typescript
page.getByRole('button').first();
page.getByRole('button').nth(1);
page.getByRole('button').last();
```
3. Filter by container:
```typescript
page.getByTestId('modal').getByRole('button', { name: 'Close' });
```
### Test Hangs or Times Out
**Symptoms:** Test runs indefinitely or hits timeout.
**Common Causes:**
1. **SvelteKit form actions** - Don't test actual form submissions:
```typescript
// Don't do this:
await page.getByRole('button', { name: 'Submit' }).click();
// Do this instead - test state changes:
const { component } = render(MyForm);
// Test component state directly
```
2. **Missing await** - Always await async operations:
```typescript
// Wrong:
button.click();
// Correct:
await button.click();
```
3. **Waiting for elements that never appear:**
```typescript
// Add timeout or check existence first:
const element = page.getByText('Success');
await expect.element(element).toBeInTheDocument({ timeout: 5000 });
```
### Mock Function Signature Mismatch
**Error:**
```
Type '() => void' is not assignable to type '(event: CustomEvent) => void'
```
**Solution:** Match the expected function signature:
```typescript
// Wrong:
const handler = vi.fn();
// Correct:
const handler = vi.fn((event: CustomEvent) => {});
// Or use type assertion:
const handler = vi.fn() as (event: CustomEvent) => void;
```
### Element Not Found
**Error:**
```
Error: locator.click: getByRole('button') resolved to 0 elements
```
**Debugging Steps:**
1. Check if element is rendered:
```typescript
// Log the page content:
const content = await page.locator('body').innerHTML();
console.log(content);
```
2. Try different locator strategies:
```typescript
// Try by text:
page.getByText('Submit');
// Try by test ID:
page.getByTestId('submit-button');
// Try by CSS:
page.locator('button[type="submit"]');
```
3. Wait for element to appear:
```typescript
await expect
.element(page.getByRole('button'))
.toBeInTheDocument({ timeout: 5000 });
```
### Cannot Access `.current` Property
**Error:**
```
Cannot read property 'current' of undefined
```
**Cause:** Trying to access remote function state incorrectly.
**Solution:**
```typescript
// Wrong:
expect(data.current).toBe('value');
// Correct - check if state exists first:
if (data.current) {
expect(data.current.value).toBe('expected');
}
```
### Snippets Not Rendering
**Error:** Children don't render or show empty content.
**Solution:** Use `createRawSnippet` correctly:
```typescript
import { createRawSnippet } from 'svelte';
const children = createRawSnippet(() => ({
render: () => `<span>Content</span>`,
}));
render(MyComponent, { children });
```
### Module Mock Not Working
**Issue:** Mocked module still uses real implementation.
**Solution:** Ensure mock is defined before import:
```typescript
import { vi } from 'vitest';
// Mock BEFORE importing the component:
vi.mock('$app/navigation', () => ({
goto: vi.fn(),
}));
// Now import:
import MyComponent from './my-component.svelte';
```
### Playwright Browser Not Starting
**Error:**
```
browserType.launch: Executable doesn't exist
```
**Solution:** Install Playwright browsers:
```bash
pnpm exec playwright install chromium
# Or for all browsers:
pnpm exec playwright install
```
### Test File Not Detected
**Issue:** Vitest doesn't run your test file.
**Check:**
1. File naming matches config:
- Client: `*.svelte.test.ts`
- SSR: `*.ssr.test.ts`
- Server: `*.test.ts`
2. File location matches include pattern in `vite.config.ts`:
```typescript
include: ['src/**/*.svelte.{test,spec}.{js,ts}'];
```
### Type Errors with Vitest Browser
**Error:**
```
Cannot find name 'page' or module '@vitest/browser/context'
```
**Solution:** Add type references to `vitest-setup-client.ts`:
```typescript
/// <reference types="@vitest/browser/matchers" />
/// <reference types="@vitest/browser/providers/playwright" />
```
### Assertion Not Awaited
**Error:**
```
Promise returned from expect was not awaited
```
**Solution:** Always await browser assertions:
```typescript
// Wrong:
expect.element(button).toBeInTheDocument();
// Correct:
await expect.element(button).toBeInTheDocument();
```
### Form Input Not Updating
**Issue:** Input value doesn't change when using `.fill()`.
**Solution:**
1. Ensure element is an input:
```typescript
const input = page.getByLabel('Email');
await expect.element(input).toBeInTheDocument();
await input.fill('[email protected]');
```
2. Try dispatching input event:
```typescript
await input.evaluate((el, value) => {
(el as HTMLInputElement).value = value;
el.dispatchEvent(new Event('input', { bubbles: true }));
}, '[email protected]');
```
### Component State Not Updating
**Issue:** Props change but UI doesn't update.
**Solution:** Use `rerender` and wait for updates:
```typescript
const { rerender } = render(MyComponent, { count: 0 });
await expect.element(page.getByText('0')).toBeInTheDocument();
rerender({ count: 1 });
// Wait for update:
await expect.element(page.getByText('1')).toBeInTheDocument();
```
## Debugging Tips
### 1. Use Vitest UI
```bash
pnpm vitest --ui
```
Provides visual test runner with debugging tools.
### 2. Add Screenshots
```typescript
import { page } from '@vitest/browser/context';
it('should work', async () => {
render(MyComponent);
await page.screenshot({ path: 'debug.png' });
});
```
### 3. Pause Test Execution
```typescript
import { page } from '@vitest/browser/context';
it('should work', async () => {
render(MyComponent);
await page.pause(); // Opens browser inspector
});
```
### 4. Console Logs
```typescript
// Log component HTML:
const html = await page.locator('body').innerHTML();
console.log(html);
// Log element properties:
const button = page.getByRole('button');
const text = await button.textContent();
console.log('Button text:', text);
```
### 5. Increase Timeout
```typescript
// Per test:
it('slow test', { timeout: 10000 }, async () => {
// test code
});
// Per assertion:
await expect.element(button).toBeInTheDocument({ timeout: 5000 });
```
## Performance Issues
### Tests Running Slowly
**Solutions:**
1. Reduce `testTimeout` in config:
```typescript
browser: {
testTimeout: 2000, // Default is 5000
}
```
2. Run tests in parallel (default in Vitest)
3. Use `.skip()` or `.only()` during development:
```typescript
it.only('this test only', async () => {});
it.skip('skip this', async () => {});
```
### Too Many Browser Instances
**Solution:** Limit instances in config:
```typescript
browser: {
instances: [{ browser: 'chromium' }],
headless: true,
}
```
```