Skip to content
Merged
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
75 changes: 0 additions & 75 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -341,80 +341,6 @@ jobs:
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}

job_browser-tests:
name: Browser tests
timeout-minutes: 60
runs-on:
labels: ubuntu-latest
needs: [job_setup]
# Skip on forked PRs - Stripe secrets are not available
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
steps:
- uses: actions/checkout@v6
with:
submodules: true
- uses: actions/setup-node@v4
env:
FORCE_COLOR: 0
with:
node-version: ${{ env.NODE_VERSION }}
cache: yarn

- name: Install Stripe-CLI
run: |
export VERSION=1.13.5
wget "https://github.com/stripe/stripe-cli/releases/download/v$VERSION/stripe_${VERSION}_linux_x86_64.tar.gz"
tar -zxvf "stripe_${VERSION}_linux_x86_64.tar.gz"
mv stripe /usr/local/bin
stripe -v

- name: Restore caches
uses: ./.github/actions/restore-cache
env:
DEPENDENCY_CACHE_KEY: ${{ needs.job_setup.outputs.dependency_cache_key }}

- name: Run migrations
working-directory: ghost/core
run: yarn knex-migrator init
env:
NODE_OPTIONS: "--import=tsx"

- name: Setup Playwright
uses: ./.github/actions/setup-playwright

- name: Build TS packages
run: yarn nx run-many -t build --exclude=ghost-admin

- name: Build Admin
run: yarn nx run ghost-admin:build:dev

- name: Run Playwright tests locally
run: yarn test:browser
env:
CI: true
STRIPE_PUBLISHABLE_KEY: ${{ secrets.STRIPE_PUBLISHABLE_KEY }}
STRIPE_SECRET_KEY: ${{ secrets.STRIPE_SECRET_KEY }}

- uses: tryghost/actions/actions/slack-build@main
if: failure() && github.event_name == 'push' && github.ref == 'refs/heads/main'
with:
status: ${{ job.status }}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}

- uses: actions/upload-artifact@v4
if: always()
with:
name: browser-tests-playwright-report
path: ghost/core/playwright-report
retention-days: 30

- name: View Test Report command
if: failure()
run: |
echo -e "::notice::To view the Playwright report locally, run:\n\nREPORT_DIR=\$(mktemp -d) && gh run download ${{ github.run_id }} -n browser-tests-playwright-report -D \"\$REPORT_DIR\" && npx playwright show-report \"\$REPORT_DIR\""


job_unit-tests:
runs-on: ubuntu-latest
needs: [job_setup]
Expand Down Expand Up @@ -1347,7 +1273,6 @@ jobs:
job_unit-tests,
job_acceptance-tests,
job_legacy-tests,
job_browser-tests,
job_admin_x_settings,
job_activitypub,
job_comments_ui,
Expand Down
1 change: 0 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,6 @@ cd ghost/core
yarn test:unit # Unit tests only
yarn test:integration # Integration tests
yarn test:e2e # E2E API tests (not browser)
yarn test:browser # Playwright browser tests for core
yarn test:all # All test types

# E2E browser tests (from root)
Expand Down
1 change: 1 addition & 0 deletions e2e/data-factory/factories/post-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export interface Post {
newsletter_id: string | null;
show_title_and_feature_image: boolean;
tags?: Array<{id: string}>;
tiers?: Array<{id: string}>;
}

export class PostFactory extends Factory<Partial<Post>, Post> {
Expand Down
33 changes: 28 additions & 5 deletions e2e/helpers/environment/service-managers/ghost-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,9 @@ export class GhostManager {
const gatewayName = `ghost-e2e-gateway-${this.config.workerIndex}`;

// Try to reuse existing containers (handles process restarts after test failures)
this.ghostContainer = await this.getOrCreateContainer(ghostName, () => this.createGhostContainer(ghostName, database));
this.gatewayContainer = await this.getOrCreateContainer(gatewayName, () => this.createGatewayContainer(gatewayName, ghostName));
const schedulerUrl = await this.getGatewaySchedulerUrl();
this.ghostContainer = await this.getOrCreateContainer(ghostName, () => this.createGhostContainer(ghostName, database, undefined, schedulerUrl));

debug(`Worker ${this.config.workerIndex} containers ready`);
}
Expand Down Expand Up @@ -201,11 +202,16 @@ export class GhostManager {
await this.waitForHealthy(this.ghostContainer, timeoutMs);
}

private async buildEnv(database: string = 'ghost_testing', extraConfig?: GhostEnvOverrides): Promise<string[]> {
private async buildEnvWithSchedulerUrl(
database: string = 'ghost_testing',
extraConfig?: GhostEnvOverrides,
schedulerUrl?: string
): Promise<string[]> {
const env = [
...BASE_GHOST_ENV,
`database__connection__database=${database}`,
`url=http://localhost:${this.getGatewayPort()}`
`url=http://localhost:${this.getGatewayPort()}`,
`scheduling__schedulerUrl=${schedulerUrl || `http://localhost:${this.getGatewayPort()}/ghost/api/admin`}`
];

// Add Tinybird config if available
Expand Down Expand Up @@ -266,7 +272,8 @@ export class GhostManager {
private async createGhostContainer(
name: string,
database: string = 'ghost_testing',
extraConfig?: GhostEnvOverrides
extraConfig?: GhostEnvOverrides,
schedulerUrl?: string
): Promise<Container> {
const mode = this.config.mode;
debug(`Creating Ghost container for mode: ${mode}`);
Expand All @@ -278,11 +285,12 @@ export class GhostManager {

// Build volume mounts based on mode
const binds = this.getGhostBinds();
const resolvedSchedulerUrl = schedulerUrl || (this.gatewayContainer ? await this.getGatewaySchedulerUrl() : undefined);

const config: ContainerCreateOptions = {
name,
Image: image,
Env: await this.buildEnv(database, extraConfig),
Env: await this.buildEnvWithSchedulerUrl(database, extraConfig, resolvedSchedulerUrl),
ExposedPorts: {[`${TEST_ENVIRONMENT.ghost.port}/tcp`]: {}},
Healthcheck: {
// Same health check as compose.dev.yaml - Ghost is ready when it responds
Expand Down Expand Up @@ -310,6 +318,21 @@ export class GhostManager {
return this.docker.createContainer(config);
}

private async getGatewaySchedulerUrl(): Promise<string> {
if (!this.gatewayContainer) {
throw new Error('Gateway container not initialized');
}

const gatewayInfo = await this.gatewayContainer.inspect();
const gatewayIp = gatewayInfo.NetworkSettings?.Networks?.[DEV_ENVIRONMENT.networkName]?.IPAddress;

if (!gatewayIp) {
throw new Error(`Gateway container is missing an IP on network ${DEV_ENVIRONMENT.networkName}`);
}

return `http://${gatewayIp}/ghost/api/admin`;
}

/**
* Get volume binds for Ghost container based on mode.
* - dev: Mount ghost directory for source code (hot reload)
Expand Down
35 changes: 0 additions & 35 deletions e2e/helpers/pages/admin/posts/post/post-editor-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,6 @@ class SettingsMenu extends BasePage {
readonly postUrlInput: Locator;
readonly publishDateInput: Locator;
readonly publishTimeInput: Locator;
readonly visibilitySelect: Locator;
readonly visibilitySegmentSelect: Locator;
readonly visibilitySegmentInput: Locator;
readonly visibilitySelectedTokens: Locator;
readonly customExcerptInput: Locator;
readonly deletePostButton: Locator;
readonly deletePostConfirmButton: Locator;
Expand All @@ -21,42 +17,11 @@ class SettingsMenu extends BasePage {
this.postUrlInput = page.getByRole('textbox', {name: 'Post URL'});
this.publishDateInput = page.getByLabel('Date Picker');
this.publishTimeInput = page.getByLabel('Time Picker');
this.visibilitySelect = page.locator('[data-test-select="post-visibility"]');
this.visibilitySegmentSelect = page.locator('[data-test-visibility-segment-select]');
this.visibilitySegmentInput = this.visibilitySegmentSelect.getByTestId('token-input-search');
this.visibilitySelectedTokens = this.visibilitySegmentSelect.locator('[data-test-selected-token]');
this.customExcerptInput = page.locator('[data-test-field="custom-excerpt"]');
this.deletePostButton = page.locator('[data-test-button="delete-post"]');
this.deletePostConfirmButton = page.locator('[data-test-button="delete-post-confirm"]');
}

async setVisibility(visibility: 'public' | 'members' | 'paid' | 'tiers'): Promise<void> {
await this.visibilitySelect.selectOption(visibility);

if (visibility === 'tiers') {
await this.visibilitySegmentSelect.waitFor({state: 'visible'});
}
}

async clearVisibilityTiers(): Promise<void> {
await this.visibilitySegmentInput.click();

let selectedCount = await this.visibilitySelectedTokens.count();

while (selectedCount > 0) {
const token = this.visibilitySelectedTokens.nth(selectedCount - 1);
await this.page.keyboard.press('Backspace');
await token.waitFor({state: 'detached'});
selectedCount -= 1;
}
}

async selectVisibilityTier(name: string): Promise<void> {
await this.visibilitySegmentInput.click();
await this.page.keyboard.type(name);
await this.page.locator(`[data-test-visibility-segment-option="${name}"]`).click();
}

async deletePost(): Promise<void> {
await this.deletePostButton.click();
await this.deletePostConfirmButton.click();
Expand Down
Loading
Loading