Testing Guide
PiSovereign maintains a comprehensive testing strategy across multiple layers to ensure reliability, correctness, and accessibility.
Test Architecture
┌──────────────────────────────────────────────┐
│ Playwright E2E Journeys │ Real browser, live server
├──────────────────────────────────────────────┤
│ Vitest Component / Hook Tests │ jsdom, mocked APIs
├──────────────────────────────────────────────┤
│ Vitest API / Store Tests │ MSW-intercepted fetch
├──────────────────────────────────────────────┤
│ Rust Unit & Integration Tests │ mockall, wiremock, axum-test
├──────────────────────────────────────────────┤
│ Rust Property Tests │ proptest
└──────────────────────────────────────────────┘
Frontend Tests (Vitest)
Running Tests
# Run all unit / component / store tests
just web-test # or: cd crates/presentation_web/frontend && npx vitest run
# Watch mode during development
npx vitest # re-runs on file changes
# Coverage report
npx vitest run --coverage
Test File Conventions
| Category | Location | Naming |
|---|---|---|
| Component tests | Next to component source | *.test.tsx |
| Hook tests | src/hooks/ | *.test.ts |
| API module tests | src/api/ | *.test.ts |
| Store tests | src/stores/ | *.test.ts |
| Utility tests | src/utils/ | *.test.ts |
Render Helpers
All components that use @solidjs/router primitives (useNavigate, useLocation,
<A>) must be wrapped with the router-aware render helper:
import { renderWithProviders } from '@/test/render';
it('renders correctly', () => {
renderWithProviders(() => <MyComponent />);
expect(screen.getByText('Hello')).toBeInTheDocument();
});
renderWithProviders wraps the component in both <Router> with a catch-all
<Route> (required by @solidjs/router v0.15+) and the <Toaster> provider.
Components that don’t use router primitives can use plain render() from
@solidjs/testing-library.
MSW (Mock Service Worker)
API calls are intercepted during tests via MSW. Default handlers live in
src/test/mocks/handlers.ts and cover all API endpoints. Per-test overrides use
server.use():
import { server } from '@/test/mocks/server';
import { http, HttpResponse } from 'msw';
it('handles error', async () => {
server.use(
http.get('/v1/my-endpoint', () =>
HttpResponse.json({ error: 'fail' }, { status: 500 }),
),
);
// ... test error handling
});
jsdom Limitations
Some browser APIs are unavailable in jsdom and need polyfills:
IntersectionObserver— Stub viavi.stubGlobal:vi.stubGlobal('IntersectionObserver', class { observe() {} unobserve() {} disconnect() {} });matchMedia— Already handled by the global test setup.crypto.randomUUID— Already handled by the global test setup.
Known Patterns
- The
Buttoncomponent usesaria-disabledinstead of the nativedisabledattribute. UsetoHaveAttribute('aria-disabled', 'true')instead oftoBeDisabled(). - Images with
alt=""haverole="presentation", notrole="img". Query them withcontainer.querySelector('img')instead ofgetByRole('img').
E2E Tests (Playwright)
Running Tests
# Run all E2E journeys (requires running backend on localhost:3000)
npx playwright test
# Run a specific journey
npx playwright test journeys/reminders.journey.spec.ts
# Show the test report
npx playwright show-report
# Debug mode (headed browser with inspector)
npx playwright test --debug
# List discovered tests
npx playwright test --list --reporter=list
Prerequisites
E2E tests require the full application stack running locally:
just docker-up # Start all services
# Wait for the application to be healthy, then:
npx playwright test
Project Configuration
| Project | Purpose | Auth |
|---|---|---|
setup | Authenticates and saves session | Creates |
chromium | Main test suite (Desktop Chrome) | Reuses |
llm | LLM-dependent tests (@llm tag) | Reuses |
Tests tagged @llm are excluded by default. Run them explicitly:
npx playwright test --project=llm
Journey Test Structure
Each journey spec follows a consistent pattern:
import { test, expect } from '../fixtures/auth.fixture';
test.describe('Feature Journey', () => {
test('scenario name', async ({ page }) => {
await test.step('navigate to page', async () => {
await page.goto('/feature');
await page.waitForLoadState('domcontentloaded');
});
await test.step('interact with UI', async () => {
// Use role-based locators for accessibility
await page.getByRole('button', { name: 'Action' }).click();
});
await test.step('verify result', async () => {
await expect(page.getByText('Success')).toBeVisible();
});
});
});
Helper Utilities
e2e/helpers/form.helper.ts—fillField(),clickButton(),expectToast(),expectModal(),closeModal(),testId()e2e/helpers/navigation.helper.ts—navigateVia(),expectHeading(),waitForContentLoaded(),expectNoFatalError()
Bug Report Generation
Failed E2E tests automatically generate Markdown bug reports in bugreports/.
Each report includes: test metadata, steps to reproduce, error details, and
screenshots. This is powered by the custom BugreportReporter.
Locator Best Practices
- Prefer role-based locators:
getByRole('button', { name: 'Save' }) - Use
exact: truewhen button text is a substring of another (e.g., “Large” vs “X-Large”) - Use regex for buttons with descriptions: Theme buttons include description
text in their accessible name (e.g., “Dark Always dark theme”), so use
getByRole('button', { name: /^Dark/ })instead of{ name: 'Dark', exact: true } - Scope to sections when button names collide across different UI areas
Rust Tests
Running Tests
# All workspace tests
cargo test --workspace
# Specific crate
cargo test -p domain
cargo test -p application
# With output
cargo test --workspace -- --nocapture
Test Categories
| Type | Location | Tools |
|---|---|---|
| Unit tests | #[cfg(test)] mod tests inline | assert!, mockall |
| Integration | crates/*/tests/ | wiremock, axum-test |
| Property tests | crates/*/tests/proptest_*.rs | proptest |
Mock Generation
Port traits are annotated for automatic mock generation:
#[cfg_attr(test, automock)]
#[async_trait]
pub trait MyPort: Send + Sync {
async fn do_thing(&self, id: &str) -> Result<Thing, DomainError>;
}
In tests, create mocks via MockMyPort::new() and set expectations.
Coverage Targets
| Layer | Target |
|---|---|
| Domain crate | 95–100% |
| Application crate | 90–100% |
| Integration crates | 85–95% |
| Frontend (Vitest) | 85–95% |
| E2E journeys | All pages |
CI Integration
The full test suite runs as part of the mandatory workflow:
just fmt+just web-fmt— Format checkjust lint— Clippy + ESLintcargo test --workspace— Rust testsnpx vitest run— Frontend unit + component testsnpx playwright test— E2E journeyscargo doc --workspace --no-deps— Doc generation
All steps must pass before committing.