Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1166,7 +1166,7 @@ jobs:
env:
GHOST_IMAGE_TAG: ${{ steps.load.outputs.image-tag }}
TEST_WORKERS_COUNT: 1
run: yarn test:e2e --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
run: yarn test:e2e:all --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}

- name: Upload blob report to GitHub Actions Artifacts
if: failure()
Expand Down
1 change: 1 addition & 0 deletions compose.dev.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ services:
ports:
- "1025:1025" # SMTP server
- "8025:8025" # Web interface
- "8026:8025" # Web interface (for e2e tests)
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:8025"]
interval: 1s
Expand Down
3 changes: 3 additions & 0 deletions docker/dev-gateway/Caddyfile
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
handle /ghost/api/* {
reverse_proxy {env.GHOST_BACKEND} {
header_up Host {host}
header_up Origin http://localhost:2368
header_up X-Real-IP {remote_host}
header_up X-Forwarded-For {remote_host}

Expand Down Expand Up @@ -192,6 +193,7 @@
handle {
reverse_proxy {env.GHOST_BACKEND} {
header_up Host {host}
header_up Origin http://localhost:2368
header_up X-Real-IP {remote_host}
header_up X-Forwarded-For {remote_host}

Expand All @@ -209,6 +211,7 @@
rewrite * {http.request.orig_uri.path}
reverse_proxy {env.GHOST_BACKEND} {
header_up Host {host}
header_up Origin http://localhost:2368
header_up X-Forwarded-Proto https
}
}
Expand Down
12 changes: 12 additions & 0 deletions e2e/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,18 @@ yarn test --debug # See browser during execution,
PRESERVE_ENV=true yarn test # Debug failed tests (keeps containers)
```

## Dev Environment Mode (Recommended)

When `yarn dev` is running, e2e tests automatically use a more efficient execution mode:

```bash
# Terminal 1: Start dev environment
yarn dev

# Terminal 2: Run e2e tests (automatically uses dev environment)
cd e2e && yarn test
```

## Test Structure

### Naming Conventions
Expand Down
40 changes: 39 additions & 1 deletion e2e/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,19 @@ yarn
yarn test
```

### Dev Environment Mode (Recommended for Development)

When `yarn dev` is running from the repository root, e2e tests automatically detect it and use a more efficient execution mode:

```bash
# Terminal 1: Start dev environment (from repository root)
yarn dev

# Terminal 2: Run e2e tests (from e2e folder)
yarn test
```


### Running Specific Tests

```bash
Expand Down Expand Up @@ -134,7 +147,11 @@ For example, a `ghostInstance` fixture creates a new Ghost instance with its own

### Test Isolation

Test isolation is extremely important to avoid flaky tests that are hard to debug. For the most part, you shouldn't have to worry about this when writing tests, because each test gets a fresh Ghost instance with its own database:
Test isolation is extremely important to avoid flaky tests that are hard to debug. For the most part, you shouldn't have to worry about this when writing tests, because each test gets a fresh Ghost instance with its own database.

#### Standalone Mode (Default)

When dev environment is not running, tests use full container isolation:

- Global setup (`tests/global.setup.ts`):
- Starts shared services (MySQL, Tinybird, etc.)
Expand All @@ -149,6 +166,27 @@ Test isolation is extremely important to avoid flaky tests that are hard to debu
- Global teardown (`tests/global.teardown.ts`):
- Stops and removes shared services

#### Dev Environment Mode (When `yarn dev` is running)

When dev environment is detected, tests use a more efficient approach:

- Global setup:
- Creates a database snapshot in the existing `ghost-dev-mysql`
- Worker setup (once per Playwright worker):
- Creates a Ghost container for the worker
- Creates a Caddy gateway container for routing
- Before each test:
- Clones database from snapshot
- Restarts Ghost container with new database
- After each test:
- Drops the test database
- Worker teardown:
- Removes worker's Ghost and gateway containers
- Global teardown:
- Cleans up all e2e containers (namespace: `ghost-dev-e2e`)

All e2e containers use the `ghost-dev-e2e` project namespace for easy identification and cleanup.

### Best Practices

1. **Use page object patterns** to separate page elements, actions on the pages, complex logic from tests. They should help you make them more readable and UI elements reusable.
Expand Down
51 changes: 51 additions & 0 deletions e2e/helpers/environment/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,54 @@ export const MAILPIT = {
PORT: 1025
};

/**
* Configuration for dev environment mode.
* Used when yarn dev infrastructure is detected.
*/
export const DEV_ENVIRONMENT = {
projectNamespace: 'ghost-dev',
networkName: 'ghost_dev'
} as const;

export const TEST_ENVIRONMENT = {
projectNamespace: 'ghost-dev-e2e',
gateway: {
image: 'ghost-dev-ghost-dev-gateway'
},
ghost: {
image: 'ghost-dev-ghost-dev',
workdir: '/home/ghost/ghost/core',
port: 2368,
env: [
// Environment configuration
'NODE_ENV=development',
'server__host=0.0.0.0',
`server__port=2368`,

// Database configuration (database name is set per container)
'database__client=mysql2',
`database__connection__host=ghost-dev-mysql`,
`database__connection__port=3306`,
`database__connection__user=root`,
`database__connection__password=root`,

// Redis configuration
'adapters__cache__Redis__host=ghost-dev-redis',
'adapters__cache__Redis__port=6379',

// Email configuration
'mail__transport=SMTP',
'mail__options__host=ghost-dev-mailpit',
'mail__options__port=1025',

// Public assets via gateway (same as compose.dev.yaml)
'portal__url=/ghost/assets/portal/portal.min.js',
'comments__url=/ghost/assets/comments-ui/comments-ui.min.js',
'sodoSearch__url=/ghost/assets/sodo-search/sodo-search.min.js',
'sodoSearch__styles=/ghost/assets/sodo-search/main.css',
'signupForm__url=/ghost/assets/signup-form/signup-form.min.js',
'announcementBar__url=/ghost/assets/announcement-bar/announcement-bar.min.js'
]
}
} as const;

153 changes: 153 additions & 0 deletions e2e/helpers/environment/dev-environment-manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import Docker from 'dockerode';
import baseDebug from '@tryghost/debug';
import logging from '@tryghost/logging';
import {DevGhostManager} from './service-managers/dev-ghost-manager';
import {DockerCompose} from './docker-compose';
import {GhostInstance, MySQLManager} from './service-managers';
import {randomUUID} from 'crypto';

const debug = baseDebug('e2e:DevEnvironmentManager');

/**
* Orchestrates e2e test environment when dev infrastructure is available.
*
* Uses:
* - MySQLManager with DockerCompose pointing to ghost-dev project
* - DevGhostManager for Ghost/Gateway container lifecycle
*
* All e2e containers use the 'ghost-dev-e2e' project namespace for easy cleanup.
*/
export class DevEnvironmentManager {
private readonly workerIndex: number;
private readonly dockerCompose: DockerCompose;
private readonly mysql: MySQLManager;
private readonly ghost: DevGhostManager;
private initialized = false;

constructor() {
this.workerIndex = parseInt(process.env.TEST_PARALLEL_INDEX || '0', 10);

// Use DockerCompose pointing to ghost-dev project to find MySQL container
this.dockerCompose = new DockerCompose({
composeFilePath: '', // Not needed for container lookup
projectName: 'ghost-dev',
docker: new Docker()
});
this.mysql = new MySQLManager(this.dockerCompose);
this.ghost = new DevGhostManager({
workerIndex: this.workerIndex
});
}

/**
* Global setup - creates database snapshot for test isolation.
* 1. Create base database
* 2. Initialize Ghost containers
* 3. Start Ghost instance (migrations run automatically on startup)
* 4. Create snapshot of database
* 5. Keep instance running for reuse in per-test setup
*
* Note: User onboarding happens in global.setup.ts using parameterized tests
*/
async globalSetup(): Promise<{baseUrl: string}> {
logging.info('Starting dev environment global setup...');

await this.cleanupResources();

// Create base database
await this.mysql.recreateBaseDatabase('ghost_e2e_base');

// Initialize Ghost containers (will be reused by perTestSetup)
debug('Initializing Ghost containers for global setup');
await this.ghost.setup();
this.initialized = true;

// Start Ghost instance connected to base database (migrations run automatically)
const baseInstanceId = 'ghost_e2e_base';
await this.ghost.restartWithDatabase(baseInstanceId);
await this.ghost.waitForReady();

const port = this.ghost.getGatewayPort();
const baseUrl = `http://localhost:${port}`;

logging.info('Ghost instance ready');

return {baseUrl};
}

/**
* Create snapshot after user onboarding is complete
*/
async createSnapshot(): Promise<void> {
logging.info('Creating database snapshot...');
await this.mysql.createSnapshot('ghost_e2e_base');
logging.info('Database snapshot created');
}

/**
* Global teardown - cleanup resources.
*/
async globalTeardown(): Promise<void> {
if (this.shouldPreserveEnvironment()) {
logging.info('PRESERVE_ENV is set - skipping teardown');
return;
}

logging.info('Starting dev environment global teardown...');
await this.cleanupResources();
logging.info('Dev environment global teardown complete');
}

/**
* Per-test setup - creates containers on first call, then clones database and restarts Ghost.
*/
async perTestSetup(options: {config?: unknown} = {}): Promise<GhostInstance> {
// Lazy initialization of Ghost containers (once per worker)
if (!this.initialized) {
debug('Initializing Ghost containers for worker', this.workerIndex);
await this.ghost.setup();
this.initialized = true;
}

const siteUuid = randomUUID();
const instanceId = `ghost_e2e_${siteUuid.replace(/-/g, '_')}`;

// Setup database
await this.mysql.setupTestDatabase(instanceId, siteUuid);

// Restart Ghost with new database
const extraConfig = options.config as Record<string, string> | undefined;
await this.ghost.restartWithDatabase(instanceId, extraConfig);
await this.ghost.waitForReady();

const port = this.ghost.getGatewayPort();

return {
containerId: this.ghost.ghostContainerId!,
instanceId,
database: instanceId,
port,
baseUrl: `http://localhost:${port}`,
siteUuid
};
}

/**
* Per-test teardown - drops test database.
*/
async perTestTeardown(instance: GhostInstance): Promise<void> {
await this.mysql.cleanupTestDatabase(instance.database);
}

private async cleanupResources(): Promise<void> {
logging.info('Cleaning up e2e resources...');
await this.ghost.cleanupAllContainers();
await this.mysql.dropAllTestDatabases();
await this.mysql.deleteSnapshot();
logging.info('E2E resources cleaned up');
}

private shouldPreserveEnvironment(): boolean {
return process.env.PRESERVE_ENV === 'true';
}
}
19 changes: 19 additions & 0 deletions e2e/helpers/environment/environment-factory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import {DevEnvironmentManager} from './dev-environment-manager';
import {EnvironmentManager} from './environment-manager';
import {isDevEnvironmentAvailable} from './service-availability';

// Cached manager instance (one per worker process)
let cachedManager: EnvironmentManager | DevEnvironmentManager | null = null;

/**
* Get the environment manager for this worker.
* Creates and caches a manager on first call, returns cached instance thereafter.
*/
export async function getEnvironmentManager(): Promise<EnvironmentManager | DevEnvironmentManager> {
if (!cachedManager) {
const useDevEnv = await isDevEnvironmentAvailable();
cachedManager = useDevEnv ? new DevEnvironmentManager() : new EnvironmentManager();
}
return cachedManager;
}

3 changes: 3 additions & 0 deletions e2e/helpers/environment/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
export * from './service-managers';
export * from './environment-manager';
export * from './dev-environment-manager';
export * from './environment-factory';
export * from './service-availability';

Loading
Loading