
playwright-e2e-spellbook
by runecalico-ai
SKILL.md
name: playwright-e2e-spellbook description: Playwright E2E testing for the Spellbook Tauri desktop application with shared fixtures, page objects, and test utilities. Use when writing or modifying E2E tests for the Spellbook app, debugging test failures, or setting up new test infrastructure. Includes automatic app lifecycle management, file tracking, and common test patterns.
Playwright E2E Testing for Spellbook
Specialized guidance for writing E2E tests for the Spellbook Tauri desktop application using Playwright with custom fixtures and utilities.
Migrating Existing Tests
Updating old tests to use modern infrastructure? See references/MIGRATION.md for:
- Step-by-step migration checklist
- Quick wins (low effort, high value changes)
- Complete before/after examples
- PR review checklist
- Common migration pitfalls
Build Requirements Before Testing
[!CRITICAL] ALWAYS rebuild the application before running tests after code changes. Playwright tests launch the compiled application, not source code.
Backend Changes (Rust)
If you modified files in src-tauri/src/ or Cargo.toml:
cd spellbook/apps/desktop
pnpm tauri:build --debug
This rebuilds the Rust backend and bundles the frontend (30-90 seconds full, 5-15 incremental).
Frontend Changes (TypeScript/React)
If you only modified files in src/:
cd spellbook/apps/desktop
pnpm build
This creates the production bundle in dist/ (5-20 seconds).
Both or Unsure
When in doubt, perform a full rebuild:
cd spellbook/apps/desktop
pnpm tauri:build --debug
See /test-workflow for the complete rebuild and test workflow.
Quick Start Template
import { expect, test } from "./fixtures/test-fixtures";
import { TIMEOUTS } from "./fixtures/constants";
import { createTmpFilePath, generateRunId, getTestDirname } from "./fixtures/test-utils";
import { SpellbookApp } from "./page-objects/SpellbookApp";
const __dirname = getTestDirname(import.meta.url);
test.describe("My Feature", () => {
test("my test case", async ({ appContext, fileTracker }) => {
const { page } = appContext;
const app = new SpellbookApp(page);
const runId = generateRunId();
// Your test code here
});
});
Core Infrastructure
Playwright Fixtures (Automatic Lifecycle)
Import custom fixtures instead of base Playwright test:
import { expect, test } from "./fixtures/test-fixtures";
Available fixtures:
appContext: Tauri app with browser, page, and process handles (auto cleanup)fileTracker: File tracker for temporary files (auto cleanup)
Benefits:
- No manual
beforeAll/afterAllhooks - Automatic cleanup even on test failure
- Type-safe fixture access
Test Utilities
import { createTmpFilePath, generateRunId, getTestDirname } from "./fixtures/test-utils";
// Get __dirname
const __dirname = getTestDirname(import.meta.url);
// Generate unique IDs
const runId = generateRunId();
const name = `Test Item ${runId}`;
// Create temp files with auto-tracking
const filePath = createTmpFilePath(__dirname, "backup.zip", fileTracker);
Standard Timeouts
import { TIMEOUTS } from "./fixtures/constants";
await expect(element).toBeVisible({ timeout: TIMEOUTS.medium });
| Constant | Value | Use Case |
|---|---|---|
TIMEOUTS.short | 5000ms | Quick UI updates |
TIMEOUTS.medium | 15000ms | Form submissions, navigation |
TIMEOUTS.long | 30000ms | App startup, complex operations |
TIMEOUTS.batch | 120000ms | Batch imports, file processing |
SpellbookApp Page Object
Always use the page object for common interactions:
const app = new SpellbookApp(page);
// Navigation
await app.navigate("Library" | "Characters" | "Import" | "Add Spell");
// Spell management
await app.createSpell({ name, level, description, ... });
await app.openSpell(name);
await app.importFile(path, allowOverwrite?);
// Character management
await app.createCharacter(name);
await app.selectCharacter(name);
await app.openCharacterEditor(name);
await app.addClass(className);
await app.addSpellToClass(className, spellName, "KNOWN" | "PREPARED");
Dialog Handling
Custom Modals (React-based)
import { handleCustomModal } from "./utils/dialog-handler";
await page.getByRole("button", { name: "Save" }).click();
await handleCustomModal(page, "OK");
await page.getByRole("button", { name: "Delete" }).click();
await handleCustomModal(page, "Confirm");
Native Browser Dialogs
import { setupDialogHandler } from "./utils/dialog-handler";
const cleanup = setupDialogHandler(page, {
acceptDelete: true,
dismissValidation: true,
debug: true,
});
// Trigger action that shows dialog
await page.getByRole("button", { name: "Reset" }).click();
cleanup(); // CRITICAL: Stop listening after action
Test Organization
Use test.step() for logical phases:
test("Feature Test", async ({ appContext }) => {
await test.step("Setup: Create test data", async () => {
// setup
});
await test.step("Action: Perform operation", async () => {
// main test action
});
await test.step("Verify: Check results", async () => {
// assertions
});
});
Locator Strategy
Use the most user-facing, semantic locators available. Follow this priority hierarchy:
| Priority | Locator Method | Use Case | Example |
|---|---|---|---|
| 1 | getByTestId() | Interactive elements, dynamic content | page.getByTestId('save-button') |
| 2 | getByRole() | Semantic HTML elements | page.getByRole('button', { name: 'Save' }) |
| 3 | getByLabel() | Form fields with labels | page.getByLabel('Character Name') |
| 4 | getByPlaceholder() | Inputs with placeholders | page.getByPlaceholder('Search spells...') |
| 5 | getByText() | Static text content | page.getByText('Fireball') |
| 6 | locator() with CSS | Last resort only | page.locator('.btn-primary') |
When New UI Elements Are Added
Before writing tests for new UI components:
-
Verify the element has a
data-testid:const count = await page.getByTestId('new-element-id').count(); console.log(`Found ${count} elements`); // Should be 1 -
If
data-testidis missing, check if semantic locators work:await page.getByRole('button', { name: 'New Button' }).click(); -
If neither works, request the UI developer add
data-testidto the component (seesrc/AGENTS.mdfor guidelines).
Debugging Locators
If you can't find an element:
// List all available data-testid attributes
const testIds = await page.locator('[data-testid]').evaluateAll(
nodes => nodes.map(n => n.getAttribute('data-testid'))
);
console.log('Available testids:', testIds);
// Check if element exists anywhere
const exists = await page.locator('text=My Element').count();
console.log(`Element exists: ${exists > 0}`);
Common Patterns
Creating Unique Test Data
const runId = generateRunId();
const spellName = `Test Spell ${runId}`;
const tmpFile = createTmpFilePath(__dirname, "data.json", fileTracker);
Working with Temporary Files
test("backup flow", async ({ fileTracker }) => {
const backupPath = createTmpFilePath(__dirname, "backup.zip", fileTracker);
// Use the file
fs.writeFileSync(backupPath, data);
// Cleanup is automatic!
});
Settlement Waits
After navigation or complex UI switches:
await app.navigate("Library");
await page.waitForTimeout(500); // Standard settlement wait
Running Tests
# Run all tests
npx playwright test
# Run specific test file
npx playwright test vault.spec.ts
# Run with UI mode
npx playwright test --ui
# Show HTML report
npx playwright show-report
Debugging Test Failures
When tests fail, follow this systematic approach to gather useful debug information:
1. View the HTML Report
npx playwright show-report
The report provides:
- Failed test details with stack traces
- Screenshots at the point of failure
- Full execution traces with DOM snapshots
- Network activity and console logs
2. Analyze the Trace
In the HTML report:
- Click on the failed test
- Select the Trace tab
- Review:
- DOM snapshots at each test step
- Network requests (look for failed API calls)
- Console logs (check for JavaScript errors)
- The exact line where the test failed
3. Capture Debug Screenshots
Add a screenshot immediately before the failing assertion:
// Add this before the assertion that's failing
await page.screenshot({ path: 'tests/screenshots/debug.png', fullPage: true });
// Then the assertion
await expect(element).toBeVisible();
The screenshot will be saved to spellbook/apps/desktop/tests/screenshots/debug.png.
4. Extract Error Context
When reporting or analyzing failures, include:
- The assertion that failed (exact line and message)
- DOM state at failure (from trace or screenshot)
- Console errors (check browser console in trace)
- Network failures (failed requests in trace)
- Recent code changes that might have affected the test
5. Common Failure Patterns
| Error Pattern | Likely Cause | Solution |
|---|---|---|
Timeout: element not found | Missing data-testid or element doesn't exist | Check UI code, add data-testid, or use semantic locators |
Strict mode violation | Multiple elements match locator | Make locator more specific with filters |
Navigation timeout | App startup too slow or crashed | Check app logs, increase timeout, verify build |
Database is locked | Previous test didn't clean up | Kill orphaned processes, check data isolation |
6. Use Playwright Inspector
For interactive debugging:
npx playwright test --debug
This opens the Playwright Inspector where you can:
- Step through the test line-by-line
- Inspect the DOM at each step
- Try different locators interactively
- See live screenshots
7. Verify Element Exists
Before writing assertions, verify the element can be found:
// Debug: Check element count
const count = await page.getByTestId('new-element').count();
console.log(`Found ${count} elements with testid 'new-element'`);
// Debug: List all testids
const allTestIds = await page.locator('[data-testid]').allTextContents();
console.log('Available testids:', allTestIds);
Troubleshooting
Port Collisions
Tests use isolated ports per worker. If "Address already in use" errors occur:
- Ensure no other app instances are running
- Check for orphaned processes:
Get-Process | Where-Object {$_.ProcessName -match "spellbook"} | Stop-Process
WebView2 State Pollution
Tests use isolated data directories. If state leaks between runs:
- Check
tests/tmp/data-w*directories - Cleanup happens automatically but may fail if processes don't terminate cleanly
Blank Screen on Launch
Debug builds expect Vite dev server at http://127.0.0.1:5173. The test infrastructure handles this automatically.
Advanced: Manual Lifecycle
For test suites sharing app state across multiple tests:
import { type TauriAppContext, cleanupTauriApp, createFileTracker, launchTauriApp } from "./fixtures/tauri-fixture";
let appContext: TauriAppContext | null = null;
const fileTracker = createFileTracker();
test.beforeAll(async () => {
appContext = await launchTauriApp();
});
test.afterAll(async () => {
if (appContext) {
await cleanupTauriApp(appContext);
}
fileTracker.cleanup();
});
test("my test", async () => {
if (!appContext) throw new Error("App context not initialized");
const { page } = appContext;
// test code
});
Use manual lifecycle when:
- Sharing app state across multiple tests
- Running large test suites (minimize app launches)
- Need fine-grained control over lifecycle
Key Constraints
- Windows only: Tests require WebView2 (Windows-only)
- Workers: Configured for
workers: 1to avoid port collisions - Timeout: Default test timeout is 120000ms (2 minutes)
- Data isolation: Each test run uses unique data directory
Score
Total Score
Based on repository quality metrics
SKILL.mdファイルが含まれている
ライセンスが設定されている
100文字以上の説明がある
GitHub Stars 100以上
3ヶ月以内に更新がある
10回以上フォークされている
オープンIssueが50未満
プログラミング言語が設定されている
1つ以上のタグが設定されている
Reviews
Reviews coming soon