179 lines
4.8 KiB
Markdown
179 lines
4.8 KiB
Markdown
|
|
# End-to-End Testing with Playwright
|
||
|
|
|
||
|
|
Hemmelig uses [Playwright](https://playwright.dev/) for end-to-end integration testing.
|
||
|
|
|
||
|
|
## Setup
|
||
|
|
|
||
|
|
Playwright and browsers are installed as dev dependencies. If you need to install browser dependencies on your system:
|
||
|
|
|
||
|
|
```bash
|
||
|
|
sudo npx playwright install-deps
|
||
|
|
```
|
||
|
|
|
||
|
|
### Test Database
|
||
|
|
|
||
|
|
The e2e tests automatically use a **separate test database** (`database/hemmelig-test.db`) that is:
|
||
|
|
|
||
|
|
1. Created fresh before each test run
|
||
|
|
2. Migrated with the latest schema
|
||
|
|
3. Seeded with a test user
|
||
|
|
4. Deleted after tests complete
|
||
|
|
|
||
|
|
This ensures tests don't affect your development database.
|
||
|
|
|
||
|
|
**Test User Credentials** (created automatically):
|
||
|
|
|
||
|
|
- Email: `e2e-test@hemmelig.local`
|
||
|
|
- Username: `e2etestuser`
|
||
|
|
- Password: `E2ETestPassword123!`
|
||
|
|
|
||
|
|
## Running Tests
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Run all e2e tests
|
||
|
|
npm run test:e2e
|
||
|
|
|
||
|
|
# Run tests with interactive UI
|
||
|
|
npm run test:e2e:ui
|
||
|
|
|
||
|
|
# Run tests in debug mode
|
||
|
|
npm run test:e2e:debug
|
||
|
|
|
||
|
|
# Run a specific test file
|
||
|
|
npx playwright test tests/e2e/secret.spec.ts
|
||
|
|
|
||
|
|
# Run tests in headed mode (see the browser)
|
||
|
|
npx playwright test --headed
|
||
|
|
```
|
||
|
|
|
||
|
|
## Test Structure
|
||
|
|
|
||
|
|
Tests are located in `tests/e2e/`:
|
||
|
|
|
||
|
|
| File | Description |
|
||
|
|
| -------------------- | ------------------------------------------------- |
|
||
|
|
| `auth.spec.ts` | Authentication tests (setup, login, registration) |
|
||
|
|
| `home.spec.ts` | Homepage and secret form tests |
|
||
|
|
| `secret.spec.ts` | Secret creation, viewing, and deletion flows |
|
||
|
|
| `navigation.spec.ts` | Navigation and routing tests |
|
||
|
|
| `fixtures.ts` | Shared test fixtures (`authenticatedPage`) |
|
||
|
|
| `global-setup.ts` | Creates test database and user before tests |
|
||
|
|
| `global-teardown.ts` | Cleans up test database after tests |
|
||
|
|
|
||
|
|
## Configuration
|
||
|
|
|
||
|
|
The Playwright configuration is in `playwright.config.ts`:
|
||
|
|
|
||
|
|
- **Test directory**: `tests/e2e/`
|
||
|
|
- **Base URL**: `http://localhost:5173`
|
||
|
|
- **Browser**: Chromium (Desktop Chrome)
|
||
|
|
- **Web server**: Automatically starts Vite with test database
|
||
|
|
- **Global setup**: Creates fresh test database and test user
|
||
|
|
- **Global teardown**: Deletes test database
|
||
|
|
|
||
|
|
## Writing Tests
|
||
|
|
|
||
|
|
### Basic Test Structure
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
import { expect, test } from '@playwright/test';
|
||
|
|
|
||
|
|
test.describe('Feature Name', () => {
|
||
|
|
test('should do something', async ({ page }) => {
|
||
|
|
await page.goto('/');
|
||
|
|
|
||
|
|
// Interact with elements
|
||
|
|
await page.locator('.ProseMirror').fill('My secret');
|
||
|
|
await page.getByRole('button', { name: /create/i }).click();
|
||
|
|
|
||
|
|
// Assert results
|
||
|
|
await expect(page.getByText(/success/i)).toBeVisible();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
### Using Authenticated Page Fixture
|
||
|
|
|
||
|
|
For tests that require authentication:
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
import { expect, test } from './fixtures';
|
||
|
|
|
||
|
|
test('should create a secret when logged in', async ({ authenticatedPage }) => {
|
||
|
|
await authenticatedPage.goto('/');
|
||
|
|
// authenticatedPage is already logged in with test user
|
||
|
|
await expect(authenticatedPage.locator('.ProseMirror')).toBeVisible();
|
||
|
|
});
|
||
|
|
```
|
||
|
|
|
||
|
|
### Common Patterns
|
||
|
|
|
||
|
|
**Interacting with the secret editor:**
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
const editor = page.locator('.ProseMirror');
|
||
|
|
await editor.click();
|
||
|
|
await editor.fill('Secret content');
|
||
|
|
```
|
||
|
|
|
||
|
|
**Creating and viewing a secret:**
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// Create
|
||
|
|
await page.goto('/');
|
||
|
|
await page.locator('.ProseMirror').fill('My secret');
|
||
|
|
await page
|
||
|
|
.getByRole('button', { name: /create/i })
|
||
|
|
.first()
|
||
|
|
.click();
|
||
|
|
|
||
|
|
// Get the URL
|
||
|
|
const urlInput = page.locator('input[readonly]').first();
|
||
|
|
const secretUrl = await urlInput.inputValue();
|
||
|
|
|
||
|
|
// View
|
||
|
|
await page.goto(secretUrl);
|
||
|
|
await page.getByRole('button', { name: /unlock/i }).click();
|
||
|
|
```
|
||
|
|
|
||
|
|
## CI Integration
|
||
|
|
|
||
|
|
Tests run in CI with these settings (from `playwright.config.ts`):
|
||
|
|
|
||
|
|
- `forbidOnly: true` - Fails if `.only` is left in tests
|
||
|
|
- `retries: 2` - Retries failed tests twice
|
||
|
|
- `workers: 1` - Single worker to prevent conflicts
|
||
|
|
|
||
|
|
### GitHub Actions Example
|
||
|
|
|
||
|
|
```yaml
|
||
|
|
- name: Install Playwright Browsers
|
||
|
|
run: npx playwright install --with-deps chromium
|
||
|
|
|
||
|
|
- name: Run E2E Tests
|
||
|
|
run: npm run test:e2e
|
||
|
|
```
|
||
|
|
|
||
|
|
## Viewing Test Reports
|
||
|
|
|
||
|
|
After running tests, view the HTML report:
|
||
|
|
|
||
|
|
```bash
|
||
|
|
npx playwright show-report
|
||
|
|
```
|
||
|
|
|
||
|
|
## Debugging Failed Tests
|
||
|
|
|
||
|
|
1. **Run in debug mode**: `npm run test:e2e:debug`
|
||
|
|
2. **Run with UI**: `npm run test:e2e:ui`
|
||
|
|
3. **View traces**: Failed tests generate traces in `test-results/`
|
||
|
|
4. **Screenshots**: Failed tests save screenshots automatically
|
||
|
|
|
||
|
|
## Best Practices
|
||
|
|
|
||
|
|
1. **Use data-testid for stable selectors** when possible
|
||
|
|
2. **Prefer user-facing selectors** like `getByRole`, `getByText`, `getByPlaceholder`
|
||
|
|
3. **Add appropriate timeouts** for async operations
|
||
|
|
4. **Keep tests independent** - each test should work in isolation
|
||
|
|
5. **Use `.first()` when multiple elements match** to avoid strict mode violations
|