e2e/README.md
This test suite runs automated browser tests against a running Ghost instance to ensure critical user journeys work correctly.
corepack enable pnpm first)To run the test, within this e2e folder run:
# Install dependencies
pnpm
# All tests
pnpm test
If GHOST_E2E_MODE is unset, the e2e shell entrypoints auto-select:
dev when the local admin dev server is reachable on http://127.0.0.1:5174build otherwiseTo use dev mode, start pnpm dev before running tests:
# Terminal 1: Start dev environment (from repository root)
pnpm dev
# Terminal 2: Run e2e tests (from e2e folder)
pnpm test
If infra is already running, pnpm infra:up is safe to run again.
For dev-mode test runs, infra:up also ensures required local Ghost/gateway dev images exist.
If you want to force a mode, set GHOST_E2E_MODE=dev or GHOST_E2E_MODE=build explicitly.
When working on analytics locally, use:
# Terminal 1 (repo root)
pnpm dev:analytics
# Terminal 2
pnpm test:analytics
E2E test scripts automatically sync Tinybird tokens when Tinybird is running.
Use build mode when you don’t want to run dev servers. It uses a prebuilt Ghost image and serves public assets from /content/files.
# From repository root
pnpm build
pnpm --filter @tryghost/e2e build:apps
GHOST_E2E_BASE_IMAGE=<ghost-image> pnpm --filter @tryghost/e2e build:docker
GHOST_E2E_MODE=build pnpm --filter @tryghost/e2e infra:up
# Run tests
GHOST_E2E_MODE=build GHOST_E2E_IMAGE=ghost-e2e:local pnpm --filter @tryghost/e2e test
For a CI-like local preflight (pulls Playwright + gateway images and starts infra), run:
pnpm --filter @tryghost/e2e preflight:build
# Specific test file
pnpm test specific/folder/testfile.spec.ts
# Matching a pattern
pnpm test --grep "homepage"
# With browser visible (for debugging)
pnpm test --debug
The test suite is organized into separate directories for different areas/functions:
tests/public/ - Public-facing site tests (homepage, posts, etc.)tests/admin/ - Ghost admin panel tests (login, content creation, settings)We can decide whether to add additional sub-folders as we add more tests.
Example structure for admin tests:
tests/admin/
├── login.spec.ts
├── posts.spec.ts
└── settings.spec.ts
Project folder structure can be seen below:
e2e/
├── tests/ # All the tests
│ ├── public/ # Public site tests
│ │ └── testname.spec.ts # Test cases
│ ├── admin/ # Admin site tests
│ │ └── testname.spec.ts # Test cases
│ ├── global.setup.ts # Global setup script
│ ├── global.teardown.ts # Global teardown script
│ └── .eslintrc.js # Test-specific ESLint config
├── helpers/ # All helpers that support the tests, utilities, fixtures, page objects etc.
│ ├── playwright/ # Playwright specific helpers
│ │ └── fixture.ts # Playwright fixtures
│ ├── pages/ # Page Object Models
│ │ └── HomePage.ts # Page Object
│ ├── utils/ # Utils
│ │ └── math.ts # Math related utils
│ └── index.ts # Main exports
├── playwright.config.mjs # Playwright configuration
├── package.json # Dependencies and scripts
└── tsconfig.json # TypeScript configuration
Tests use Playwright Test framework with page objects. Aim to format tests in Arrange Act Assert style - it will help you with directions when writing your tests.
test.describe('Ghost Homepage', () => {
test('loads correctly', async ({page}) => {
// ARRANGE - setup fixtures, create helpers, prepare things that helps will need to be executed
const homePage = new HomePage(page);
// ACT - do the actions you need to do, to verify certain behaviour
await homePage.goto();
// ASSERT
await expect(homePage.title).toBeVisible();
});
});
Page objects encapsulate page elements, and interactions. To read more about them, check this link out and this link.
// Create a page object for admin login
export class AdminLoginPage {
private pageUrl:string;
constructor(private page: Page) {
this.pageUrl = '/ghost'
}
async goto(urlToVisit = this.pageUrl) {
await this.page.goto(urlToVisit);
}
async login(email: string, password: string) {
await this.page.fill('[name="identification"]', email);
await this.page.fill('[name="password"]', password);
await this.page.click('button[type="submit"]');
}
}
Tests use Project Dependencies to define special tests as global setup and teardown tests:
tests/global.setup.ts - runs once before all teststests/global.teardown.ts - runs once after all testsPlaywright Fixtures are defined in helpers/playwright/fixture.ts and provide reusable test setup/teardown logic.
The fixture resolves isolation mode per test file:
usePerTestIsolation() from @/helpers/playwright/isolation at the root of the filefullyParallel: trueTest isolation is still automatic, but no longer always per-test.
Infrastructure (MySQL, Redis, Mailpit, Tinybird) must already be running before tests start. Use pnpm dev or pnpm --filter @tryghost/e2e infra:up.
Global setup (tests/global.setup.ts) does:
Per-file mode (helpers/playwright/fixture.ts) does:
Per-test mode (helpers/playwright/fixture.ts) does:
Environment identity for per-file reuse:
config participates in the environment identity.labs participates in the environment identity.stripeEnabled does not participate in per-file reuse. It always forces per-test isolation because Ghost must boot against a per-test fake Stripe server.Fixture option behavior:
config: use for boot-time Ghost config that should get a fresh environment when it changes.labs: use for labs flags that should get a fresh environment when they change.stripeEnabled: use for Stripe-backed tests; this always runs each test with a fully isolated Ghost environment.Escape hatch:
resetEnvironment() is supported only in beforeEach hooks for per-file tests.baseURL, page, pageWithAuthenticatedUser, or ghostAccountOwner.test.beforeEach(async ({resetEnvironment}) => { ... })resetEnvironment() after page or an authenticated session has already been created.Opting into per-test isolation:
usePerTestIsolation() from @/helpers/playwright/isolation at the root of the file.Global teardown (tests/global.teardown.ts) does:
Modes:
/content/filesdata-testid attributes for reliable element selection, in case you cannot locate elements in a simple way. Example: page.getByLabel('User Name'). Avoid, css, xpath locators - they make tests brittle.Tests run automatically in GitHub Actions on every PR and commit to main.
pnpm --filter @tryghost/e2e build:docker (layers public apps into /content/files)pnpm --filter @tryghost/e2e preflight:build)Within the e2e directory:
# Run all tests
pnpm test
# Start/stop test infra (MySQL/Redis/Mailpit/Tinybird)
pnpm infra:up
pnpm infra:down
# CI-like preflight for build mode (pulls images + starts infra)
pnpm preflight:build
# Debug failed tests (keeps containers)
PRESERVE_ENV=true pnpm test
# Run TypeScript type checking
pnpm test:types
# Lint code and tests
pnpm lint
# Build (for utilities)
pnpm build
pnpm dev # Watch mode for TypeScript compilation
test-results/ directorypnpm test --debug or pnpm test --ui to see browser