Skip to content

Commit ba6aca1

Browse files
authored
[personal-wp] Add Playwright e2e tests (#3405)
## Summary Adds Playwright E2E tests for the `personal-wp` package, focused on code paths and UI that are unique to personal-wp. personal-wp forks several core modules — `resolve-blueprint-from-url.ts`, `boot-site-client.ts`, and `router.ts` — rather than importing from `playground-website`. Blueprint *step execution* is shared via `@wp-playground/blueprints`, so we don't re-test individual step types (writeFile, wp-cli, etc.) that core already covers extensively. Instead, these tests verify personal-wp's own parsing → boot chain and its unique UI. **9 tests** across 2 spec files: - **Smoke** (4): default blueprint welcome page landing, completion of welcome flow, blueprint-from-URL-hash (exercises forked parsing/boot path), toolbar display - **UI** (5): Site Tools panel, menu overlay (open/close/escape), page title, address bar navigation ## Implementation details - `playwright.config.ts` — targets port 5401 (personal-wp dev server), Chromium-only (Firefox/WebKit can't boot via Vite dev server due to missing COEP/COOP headers before service worker claims the page) - `playground-fixtures.ts` — extends Playwright `test` with `wordpress` (iframe chain) and `website` (PersonalWPPage helper) fixtures - `personal-wp-page.ts` — page helper with `goto()` that waits for nested iframes, plus site tools, menu overlay, and address bar methods - CI runs as a dedicated `test-e2e-personal-wp` job on Chromium ## Test plan - [x] Run locally `npx playwright test --config=packages/playground/personal-wp/playwright/playwright.config.ts --project=chromium` - [x] CI passes all 9 tests on Chromium Made with [Cursor](https://cursor.com)
1 parent 35c4979 commit ba6aca1

File tree

7 files changed

+281
-0
lines changed

7 files changed

+281
-0
lines changed

.github/workflows/ci.yml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,24 @@ jobs:
271271
path: packages/playground/website/playwright/e2e/deployment.spec.ts-snapshots/
272272
if-no-files-found: ignore
273273

274+
test-e2e-personal-wp:
275+
runs-on: ubuntu-latest
276+
steps:
277+
- uses: actions/checkout@v4
278+
with:
279+
submodules: true
280+
- uses: ./.github/actions/prepare-playground
281+
- name: Install Playwright Browser
282+
run: npx playwright install chromium --with-deps
283+
- name: Run personal-wp Playwright e2e tests
284+
run: CI=true npx playwright test --config=packages/playground/personal-wp/playwright/playwright.config.ts --project=chromium
285+
- uses: actions/upload-artifact@v4
286+
if: ${{ !cancelled() }}
287+
with:
288+
name: playwright-personal-wp-report
289+
path: packages/playground/personal-wp/playwright-report/
290+
if-no-files-found: ignore
291+
274292
test-e2e-components:
275293
runs-on: ubuntu-latest
276294
steps:
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { test, expect } from '../playground-fixtures';
2+
import type { Blueprint } from '@wp-playground/blueprints';
3+
4+
// personal-wp forks several core modules — blueprint URL parsing
5+
// (resolve-blueprint-from-url.ts), the boot sequence
6+
// (boot-site-client.ts), and the URL router (router.ts). Blueprint
7+
// *step execution* is shared via @wp-playground/blueprints, so we
8+
// don't re-test individual step types (writeFile, wp-cli, etc.).
9+
// These smoke tests verify personal-wp's own parsing → boot chain.
10+
11+
test('should land on the welcome page on first visit', async ({ website }) => {
12+
await website.goto('./');
13+
await expect(website.addressBar()).toHaveValue(
14+
/\/wp-admin\/tools\.php\?page=playground-welcome/
15+
);
16+
});
17+
18+
test('should complete welcome flow and update site title', async ({
19+
website,
20+
wordpress,
21+
}) => {
22+
await website.goto('./');
23+
await expect(website.addressBar()).toHaveValue(
24+
/\/wp-admin\/tools\.php\?page=playground-welcome/
25+
);
26+
27+
const nameInput = wordpress.locator('#display_name');
28+
await nameInput.fill('John Doe');
29+
await nameInput.press('Enter');
30+
31+
await expect(website.addressBar()).toHaveValue(/\/$/);
32+
await expect(wordpress.locator('p.wp-block-site-title')).toHaveText(
33+
"John Doe's WordPress"
34+
);
35+
});
36+
37+
test('should apply a blueprint passed via URL hash', async ({ website }) => {
38+
const blueprint: Blueprint = { landingPage: '/sample-page/' };
39+
await website.goto(`./#${JSON.stringify(blueprint)}`);
40+
await expect(website.addressBar()).toHaveValue(/sample-page/);
41+
});
42+
43+
test('should display the toolbar with address bar', async ({ website }) => {
44+
await website.goto('./');
45+
await expect(
46+
website.page.locator('header[aria-label="Playground toolbar"]')
47+
).toBeVisible();
48+
});
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { test, expect } from '../playground-fixtures';
2+
3+
test('should open and close the Site Tools panel', async ({ website }) => {
4+
await website.goto('./');
5+
6+
await website.ensureSiteToolsIsOpen();
7+
await expect(
8+
website.page.getByRole('button', { name: /Close Site Tools/ })
9+
).toBeVisible();
10+
11+
await website.ensureSiteToolsIsClosed();
12+
await expect(
13+
website.page.getByRole('button', { name: /Open Site Tools/ })
14+
).toBeVisible();
15+
});
16+
17+
test('should open the menu overlay', async ({ website }) => {
18+
await website.goto('./');
19+
20+
await website.openMenuOverlay();
21+
22+
await expect(website.page.getByText('Install Apps')).toBeVisible();
23+
await expect(
24+
website.page.getByRole('heading', { name: 'Backup' })
25+
).toBeVisible();
26+
await expect(
27+
website.page.getByRole('heading', { name: 'Start over' })
28+
).toBeVisible();
29+
await expect(
30+
website.page.getByRole('heading', { name: 'Recovery' })
31+
).toBeVisible();
32+
33+
await website.page
34+
.getByRole('button', { name: 'you can reset this WordPress' })
35+
.click();
36+
await expect(
37+
website.page.getByRole('button', { name: 'Delete everything' })
38+
).toBeVisible();
39+
await website.page
40+
.getByRole('button', { name: 'you can troubleshoot' })
41+
.click();
42+
await expect(
43+
website.page.getByRole('link', {
44+
name: 'Install Health Check & Troubleshoot',
45+
})
46+
).toBeVisible();
47+
});
48+
49+
test('should close the menu overlay with Escape', async ({ website }) => {
50+
await website.goto('./');
51+
52+
await website.openMenuOverlay();
53+
await expect(website.page.getByText('Install Apps')).toBeVisible();
54+
55+
await website.page.keyboard.press('Escape');
56+
await expect(website.page.getByText('Install Apps')).not.toBeVisible();
57+
});
58+
59+
test('should display the page title as "My WordPress"', async ({ website }) => {
60+
await website.goto('./');
61+
await expect(website.page).toHaveTitle('My WordPress');
62+
});
63+
64+
test('should navigate within WordPress when address bar URL changes', async ({
65+
website,
66+
wordpress,
67+
}) => {
68+
await website.goto('./');
69+
70+
const addressBar = website.addressBar();
71+
await addressBar.click();
72+
await addressBar.fill('/wp-admin/edit.php');
73+
await addressBar.press('Enter');
74+
75+
await expect(wordpress.locator('h1.wp-heading-inline')).toHaveText('Posts');
76+
});
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import type { Page } from '@playwright/test';
2+
import { expect } from '@playwright/test';
3+
4+
export class PersonalWPPage {
5+
public readonly page: Page;
6+
7+
constructor(page: Page) {
8+
this.page = page;
9+
}
10+
11+
async waitForNestedIframes() {
12+
await expect(this.wordpress().locator('body')).not.toBeEmpty();
13+
}
14+
15+
wordpress() {
16+
return this.page
17+
.frameLocator(
18+
'#playground-viewport:visible,.playground-viewport:visible'
19+
)
20+
.frameLocator('#wp');
21+
}
22+
23+
async goto(url: string, options?: Parameters<Page['goto']>[1]) {
24+
const originalGoto = this.page.goto.bind(this.page);
25+
const response = await originalGoto(url, options);
26+
await this.waitForNestedIframes();
27+
return response;
28+
}
29+
30+
async ensureSiteToolsIsOpen() {
31+
const button = this.page.getByRole('button', {
32+
name: /Open Site Tools/,
33+
});
34+
if (await button.isVisible()) {
35+
await button.click();
36+
}
37+
}
38+
39+
async ensureSiteToolsIsClosed() {
40+
const button = this.page.getByRole('button', {
41+
name: /Close Site Tools/,
42+
});
43+
if (await button.isVisible()) {
44+
await button.click();
45+
}
46+
}
47+
48+
async openMenuOverlay() {
49+
await this.page
50+
.getByRole('button', { name: 'Playground Menu' })
51+
.click();
52+
}
53+
54+
addressBar() {
55+
return this.page
56+
.locator('header[aria-label="Playground toolbar"]')
57+
.locator('input[type="text"]');
58+
}
59+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import type { FrameLocator } from '@playwright/test';
2+
import { test as base } from '@playwright/test';
3+
import { PersonalWPPage } from './personal-wp-page';
4+
5+
type PersonalWPFixtures = {
6+
wordpress: FrameLocator;
7+
website: PersonalWPPage;
8+
};
9+
10+
export const test = base.extend<PersonalWPFixtures>({
11+
wordpress: async ({ website }, use) => {
12+
await use(website.wordpress());
13+
},
14+
website: async ({ page }, use) => {
15+
await use(new PersonalWPPage(page));
16+
},
17+
});
18+
19+
export { expect } from '@playwright/test';
20+
export type { Page } from '@playwright/test';
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import type { PlaywrightTestConfig } from '@playwright/test';
2+
import { defineConfig, devices } from '@playwright/test';
3+
4+
const baseURL =
5+
process.env.PLAYWRIGHT_TEST_BASE_URL ||
6+
'http://127.0.0.1:5401/website-server/';
7+
8+
export const playwrightConfig: PlaywrightTestConfig = {
9+
testDir: './e2e',
10+
fullyParallel: true,
11+
forbidOnly: !!process.env.CI,
12+
retries: 3,
13+
workers: 3,
14+
reporter: [['html'], ['list', { printSteps: true }]],
15+
use: {
16+
baseURL,
17+
trace: 'on-first-retry',
18+
actionTimeout: 120000,
19+
navigationTimeout: 120000,
20+
},
21+
22+
timeout: 300000,
23+
expect: { timeout: 60000 },
24+
25+
// Firefox and WebKit can't run personal-wp via the Vite dev server:
26+
// the WASM PHP runtime requires SharedArrayBuffer, which needs
27+
// cross-origin isolation headers (COEP/COOP). These are provided by
28+
// the service worker after it claims the page, but on first load the
29+
// service worker isn't active yet and the runtime fails to start.
30+
// The core playground-website E2E tests cover Firefox/WebKit via a
31+
// built app served with proper headers.
32+
projects: [
33+
{
34+
name: 'chromium',
35+
use: {
36+
...devices['Desktop Chrome'],
37+
launchOptions: {
38+
args: ['--js-flags=--enable-experimental-webassembly-jspi'],
39+
},
40+
},
41+
},
42+
],
43+
44+
webServer: {
45+
command: 'npx nx run playground-personal-wp:dev',
46+
url: 'http://127.0.0.1:5401/website-server/',
47+
reuseExistingServer: !process.env.CI,
48+
},
49+
};
50+
51+
export default defineConfig(playwrightConfig);

packages/playground/personal-wp/project.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,15 @@
9595
"reportsDirectory": "../../../coverage/packages/playground/personal-wp"
9696
}
9797
},
98+
"e2e:playwright": {
99+
"executor": "nx:run-commands",
100+
"options": {
101+
"commands": [
102+
"npx playwright test --config=packages/playground/personal-wp/playwright/playwright.config.ts"
103+
],
104+
"parallel": false
105+
}
106+
},
98107
"lint": {
99108
"executor": "@nx/eslint:lint",
100109
"outputs": ["{options.outputFile}"],

0 commit comments

Comments
 (0)