5.7 KiB
AGENTS.md
E2E testing guidance for AI assistants (Claude, Codex, etc.) working with Ghost tests.
IMPORTANT: When creating or modifying E2E tests, always refer to .claude/E2E_TEST_WRITING_GUIDE.md for comprehensive testing guidelines and patterns.
Critical Rules
- Always follow ADRs in
../adr/folder (ADR-0001: AAA pattern, ADR-0002: Page Objects) - Always use pnpm, never npm
- Always run after changes:
pnpm lintandpnpm test:types - Never use CSS/XPath selectors - only semantic locators or data-testid
- Prefer less comments and giving things clear names
Running E2E Tests
pnpm dev must be running before you run E2E tests. The E2E test runner auto-detects
whether the admin dev server is reachable at http://127.0.0.1:5174. If it is, tests run
in dev mode (fast, no pre-built Docker image required). If not, tests fall back to
build mode which requires a ghost-e2e:local Docker image that is only built in CI.
If you see the error Build image not found: ghost-e2e:local, it means pnpm dev is
not running. Start it first, wait for the admin dev server to be ready, then re-run tests.
# Terminal 1 (or background): Start dev environment from the repo root
pnpm dev
# Wait for the admin dev server to be reachable (http://127.0.0.1:5174)
# Terminal 2: Run e2e tests from the e2e/ directory
pnpm test # Run all tests
pnpm test tests/path/to/test.ts # Run specific test
pnpm lint # Required after writing tests
pnpm test:types # Check TypeScript errors
pnpm build # Required after factory changes
pnpm test --debug # See browser during execution, for debugging
PRESERVE_ENV=true pnpm test # Debug failed tests (keeps containers)
Test Structure
Naming Conventions
- Test suites:
Ghost Admin - FeatureorGhost Public - Feature - Test names:
what is tested - expected outcome(lowercase) - One test = one scenario (never mix multiple scenarios)
AAA Pattern
test('action performed - expected result', async ({page}) => {
const analyticsPage = new AnalyticsGrowthPage(page);
const postFactory = createPostFactory(page.request);
const post = await postFactory.create({status: 'published'});
await analyticsPage.goto();
await analyticsPage.topContent.postsButton.click();
await expect(analyticsPage.topContent.contentCard).toContainText('No conversions');
});
Page Objects
Structure
export class AnalyticsPage extends AdminPage {
// Public readonly locators only
public readonly saveButton = this.page.getByRole('button', {name: 'Save'});
public readonly emailInput = this.page.getByLabel('Email');
// Semantic action methods
async saveSettings() {
await this.saveButton.click();
}
}
Rules
- Page Objects are located in
helpers/pages/ - Expose locators as
public readonlywhen used with assertions - Methods use semantic names (
login()notclickLoginButton()) - Use
waitFor()for guards, neverexpect()in page objects - Keep all assertions in test files
Locators (Strict Priority)
-
Semantic (always prefer):
getByRole('button', {name: 'Save'})getByLabel('Email')getByText('Success')
-
Test IDs (when semantic unavailable):
getByTestId('analytics-card')- Suggest adding
data-testidto Ghost codebase when needed
-
Never use: CSS selectors, XPath, nth-child, class names
Playwright MCP Usage
- Use
mcp__playwright__browser_snapshotto find elements - Use
mcp__playwright__browser_clickwith semantic descriptions - If no good locator exists, suggest
data-testidaddition to Ghost
Test Data
Factory Pattern (Required)
import {PostFactory, UserFactory} from '../data-factory';
const postFactory = createPostFactory(page.request);
const post = await postFactory.create({userId: user.id});
Best Practices
DO ✅
- Use
usePerTestIsolation()from@/helpers/playwright/isolationif a file needs per-test isolation - Treat
configandlabsas environment-identity inputs: changing them should be an intentional part of test setup - Use
resetEnvironment()only inbeforeEachhooks when you need a forced recycle inside per-file mode - Keep
stripeEnabledtests in per-test mode; the fixture forces this automatically - Use factories for all test data
- Use Playwright's auto-waiting
- Run tests multiple times to ensure stability
- Use
test.only()for debugging single tests
DON'T ❌
- Use
test.describe.parallel(...)ortest.describe.serial(...)in e2e tests - Use nested
test.describe.configure({mode: ...})(mode toggles are root-level only) - Call
resetEnvironment()after resolvingbaseURL,page,pageWithAuthenticatedUser, orghostAccountOwner - Hard-coded waits (
waitForTimeout) - networkidle in waits (
networkidle) - Test dependencies (Test B needs Test A)
- Direct database manipulation
- Multiple scenarios in one test
- Assertions in page objects
- Manual login (auto-authenticated via fixture)
Project Structure
tests/admin/- Admin area teststests/public/- Public site testshelpers/pages/- Page objectshelpers/environment/- Container managementdata-factory/- Test data factories
Validation Checklist
After writing tests, verify:
- Test passes:
pnpm test path/to/test.ts - Linting passes:
pnpm lint - Types check:
pnpm test:types - Follows AAA pattern with clear sections
- Uses page objects appropriately
- Uses semantic locators or data-testid only
- No hard-coded waits or CSS selectors