Skip to main content

epic-testing

Guide on testing with Vitest and Playwright for Epic Stack

Source facts

Repository
epicweb-dev/epic-stack
Last source activity
January 30, 2026 at 00:59
Detected SKILL.md language
English
Stars
5,538
Forks
463

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
epic-testing
description
Guide on testing with Vitest and Playwright for Epic Stack
categories
["testing","vitest","playwright","e2e"]
# Epic Stack: Testing ## When to use this skill Use this skill when you need to: - Write unit tests for utilities and components - Create E2E tests with Playwright - Test forms and validation - Test routes and loaders - Mock external services with MSW - Test authentication and permissions - Configure test database ## Patterns and conventions ### Testing Philosophy Following Epic Web principles: **Tests should resemble users** - Write tests that mirror how real users interact with your application. Test user workflows, not implementation details. If a user would click a button, your test should click that button. If a user would see an error message, your test should check for that specific message. **Make assertions specific** - Be explicit about what you're testing. Instead of vague assertions, use specific, meaningful checks that clearly communicate the expected behavior. This makes tests easier to understand and debug when they fail. **Example - Tests that resemble users:** ```typescript // ✅ Good - Tests user workflow test('User can sign up and create their first note', async ({ page, navigate }) => { // User visits signup page await navigate('/signup') // User fills out form like a real person would await page.getByRole('textbox', { name: /email/i }).fill('newuser@example.com') await page.getByRole('textbox', { name: /username/i }).fill('newuser') await page.getByRole('textbox', { name: /^password$/i }).fill('securepassword123') await page.getByRole('textbox', { name: /confirm/i }).fill('securepassword123') // User submits form await page.getByRole('button', { name: /sign up/i }).click() // User is redirected to onboarding await expect(page).toHaveURL(/\/onboarding/) // User creates their first note await navigate('/notes/new') await page.getByRole('textbox', { name: /title/i }).fill('My First Note') await page.getByRole('textbox', { name: /content/i }).fill('This is my first note!') await page.getByRole('button', { name: /create/i }).click() // User sees their note await expect(page.getByRole('heading', { name: 'My First Note' })).toBeVisible() await expect(page.getByText('This is my first note!')).toBeVisible() }) // ❌ Avoid - Testing implementation details test('Signup form calls API endpoint', async ({ page }) => { // This tests implementation, not user experience const response = await page.request.post('/signup', { data: {...} }) expect(response.status()).toBe(200) }) ``` **Example - Specific assertions:** ```typescript // ✅ Good - Specific assertions test('Form shows specific validation errors', async ({ page, navigate }) => { await navigate('/signup') await page.getByRole('button', { name: /sign up/i }).click() // Specific error messages that users would see await expect(page.getByText(/email is required/i)).toBeVisible() await expect( page.getByText(/username must be at least 3 characters/i), ).toBeVisible() await expect( page.getByText(/password must be at least 6 characters/i), ).toBeVisible() }) // ❌ Avoid - Vague assertions test('Form shows errors', async ({ page, navigate }) => { await navigate('/signup') await page.getByRole('button', { name: /sign up/i }).click() // Too vague - what errors? where? expect(page.locator('.error')).toBeVisible() }) ``` ### Two Types of Tests Epic Stack uses two types of tests: 1. **Unit Tests with Vitest** - Tests for individual components and utilities 2. **E2E Tests with Playwright** - End-to-end tests of the complete flow ### Unit Tests with Vitest **Basic setup:** ```typescript // app/utils/my-util.test.ts import { describe, expect, it } from 'vitest' import { myUtil } from './my-util.ts' describe('myUtil', () => { it('should do something', () => { expect(myUtil('input')).toBe('expected') }) }) ``` **Testing con DOM:** ```typescript import { describe, expect, it } from 'vitest' import { render, screen } from '@testing-library/react' import { MyComponent } from './my-component.tsx' describe('MyComponent', () => { it('should render correctly', () => { render(<MyComponent />) expect(screen.getByText('Hello')).toBeInTheDocument() }) }) ``` ### E2E Tests with Playwright **Basic setup:** ```typescript // tests/e2e/my-feature.test.ts import { expect, test } from '#tests/playwright-utils.ts' test('Users can do something', async ({ page, navigate, login }) => { const user = await login() await navigate('/my-page') // Interact with the page await page.getByRole('button', { name: /Submit/i }).click() // Verificar resultado await expect(page).toHaveURL('/success') }) ``` ### Login Fixture Epic Stack provides a `login` fixture for authenticated tests. **Use login fixture:** ```typescript test('Protected route', async ({ page, navigate, login }) => { const user = await login() // Creates user and session automatically await navigate('/protected') // User is authenticated await expect(page.getByText(`Welcome ${user.username}`)).toBeVisible() }) ``` **Login with options:** ```typescript const user = await login({ username: 'testuser', email: 'test@example.com', password: 'password123', }) ``` **Note:** The user is automatically deleted when the test completes. ### Insert User without Login To create user without authentication: ```typescript test('Public content', async ({ page, navigate, insertNewUser }) => { const user = await insertNewUser({ username: 'publicuser', email: 'public@example.com', }) await navigate(`/users/${user.username}`) await expect(page.getByText(user.username)).toBeVisible() }) ``` ### Navigate Helper Use the `navigate` helper to navigate with type-safety: ```typescript // Type-safe navigation await navigate('/users/:username/notes', { username: user.username }) await navigate('/users/:username/notes/:noteId', { username: user.username, noteId: note.id, }) // Also works with routes without parameters await navigate('/login') ``` ### Test Database Epic Stack uses a separate test database. **Automatic configuration:** - The test database is configured automatically - It's cleaned between tests - Data created in tests is automatically deleted **Create data in tests:** ```typescript import { prisma } from '#app/utils/db.server.ts' test('User can see notes', async ({ page, navigate, login }) => { const user = await login() // Create note in database const note = await prisma.note.create({ data: { title: 'Test Note', content: 'Test Content', ownerId: user.id, }, }) await navigate('/users/:username/notes/:noteId', { username: user.username, noteId: note.id, }) await expect(page.getByText('Test Note')).toBeVisible() }) ``` ### MSW (Mock Service Worker) Epic Stack uses MSW to mock external services. **Mock example:** ```typescript // tests/mocks/github.ts import { http, HttpResponse } from 'msw' export const handlers = [ http.get('https://api.github.com/user', () => { return HttpResponse.json({ id: '123', login: 'testuser', email: 'test@example.com', }) }), ] ``` **Use in tests:** Mocks are automatically applied when `MOCKS=true` is configured. ### Testing Forms **Test form:** ```typescript test('User can submit form', async ({ page, navigate, login }) => { const user = await login() await navigate('/notes/new') // Fill form await page.getByRole('textbox', { name: /title/i }).fill('New Note') await page.getByRole('textbox', { name: /content/i }).fill('Note content') // Submit await page.getByRole('button', { name: /submit/i }).click() // Verificar redirect await expect(page).toHaveURL(new RegExp('/users/.*/notes/.*')) }) ``` **Test validation:** ```typescript test('Form shows validation errors', async ({ page, navigate }) => { await navigate('/signup') // Submit sin llenar await page.getByRole('button', { name: /submit/i }).click() // Verificar errores await expect(page.getByText(/email is required/i)).toBeVisible() }) ``` ### Testing Loaders **Test loader:** ```typescript // app/utils/my-util.test.ts import { describe, expect, it } from 'vitest' import { loader } from '../routes/my-route.ts' import { prisma } from '../utils/db.server.ts' describe('loader', () => { it('should load data', async () => { // Create data const user = await prisma.user.create({ data: { email: 'test@example.com', username: 'testuser', roles: { connect: { name: 'user' } }, }, }) // Mock request const request = new Request('http://localhost/my-route') // Execute loader const result = await loader({ request, params: {}, context: {} }) // Verify result expect(result.data).toBeDefined() }) }) ``` ### Testing Actions **Test action:** ```typescript // tests/e2e/notes.test.ts test('User can create note', async ({ page, navigate, login }) => { const user = await login() await navigate('/users/:username/notes', { username: user.username }) await page.getByRole('link', { name: /new note/i }).click() const formData = new FormData() formData.set('title', 'Test Note') formData.set('content', 'Test Content') await page.getByRole('textbox', { name: /title/i }).fill('Test Note') await page.getByRole('textbox', { name: /content/i }).fill('Test Content') await page.getByRole('button', { name: /submit/i }).click() // Verify that note was created await expect(page.getByText('Test Note')).toBeVisible() }) ``` ### Testing Permissions **Test permissions:** ```typescript test('Only owner can delete note', async ({ page, navigate, login, insertNewUser, }) => { const owner = await login() const otherUser = await insertNewUser() const note = await prisma.note.create({ data: { title: 'Test Note', content: 'Test', ownerId: owner.id, }, }) // Login as other user const session = await createSession(otherUser.id) await page.context().addCookies([getCookie(session)]) await navigate('/users/:username/notes/:noteId', { username: owner.username, noteId: note.id, }) // Verify that can't delete
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub