| 1 |
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'; |
| 2 |
import { dirname, join, relative, resolve, sep } from 'node:path'; |
| 3 |
import { defineConfig, devices } from '@playwright/test'; |
| 4 |
|
| 5 |
const ROOT = resolve(__dirname); |
| 6 |
const TEST_DIR = resolve(ROOT, 'tests/playwright'); |
| 7 |
const BASE_PORT = Number(process.env.BASE_PORT ?? 9400); |
| 8 |
const WP_VERSION = process.env.WP_VERSION ?? 'latest'; |
| 9 |
const RUN_PROJECT = process.env.RUN_PROJECT; |
| 10 |
|
| 11 |
const walkSpecs = (dir: string): string[] => { |
| 12 |
const out: string[] = []; |
| 13 |
for (const entry of readdirSync(dir)) { |
| 14 |
const full = join(dir, entry); |
| 15 |
const stat = statSync(full); |
| 16 |
if (stat.isDirectory()) { |
| 17 |
out.push(...walkSpecs(full)); |
| 18 |
continue; |
| 19 |
} |
| 20 |
if (entry.endsWith('.spec.ts')) out.push(full); |
| 21 |
} |
| 22 |
return out; |
| 23 |
}; |
| 24 |
|
| 25 |
const closestBlueprint = (specPath: string): string | null => { |
| 26 |
let dir = dirname(specPath); |
| 27 |
while (dir.startsWith(TEST_DIR) || dir === TEST_DIR) { |
| 28 |
const candidate = join(dir, 'blueprint.json'); |
| 29 |
if (existsSync(candidate)) return candidate; |
| 30 |
if (dir === TEST_DIR) return null; |
| 31 |
dir = dirname(dir); |
| 32 |
} |
| 33 |
return null; |
| 34 |
}; |
| 35 |
|
| 36 |
const projectName = (specPath: string): string => |
| 37 |
relative(TEST_DIR, specPath) |
| 38 |
.replace(/\.spec\.ts$/, '') |
| 39 |
.split(sep) |
| 40 |
.join('/'); |
| 41 |
|
| 42 |
// First-line `// e2e:disabled` marker — see tests/playwright/scripts/discover-playwright-projects.mjs. |
| 43 |
// RUN_PROJECT explicitly opting in to a disabled spec still wins (for local debug). |
| 44 |
const isDisabled = (spec: string): boolean => |
| 45 |
readFileSync(spec, 'utf8').split('\n', 1)[0].includes('e2e:disabled'); |
| 46 |
|
| 47 |
const specs = existsSync(TEST_DIR) |
| 48 |
? walkSpecs(TEST_DIR).filter( |
| 49 |
(s) => !isDisabled(s) || projectName(s) === RUN_PROJECT, |
| 50 |
) |
| 51 |
: []; |
| 52 |
|
| 53 |
const blueprintPort = new Map<string, number>(); |
| 54 |
for (const spec of specs) { |
| 55 |
const blueprint = closestBlueprint(spec); |
| 56 |
if (!blueprint) continue; |
| 57 |
if (!blueprintPort.has(blueprint)) { |
| 58 |
blueprintPort.set(blueprint, BASE_PORT + blueprintPort.size); |
| 59 |
} |
| 60 |
} |
| 61 |
|
| 62 |
const projectBlueprint = new Map<string, string>(); |
| 63 |
|
| 64 |
const allProjects = specs |
| 65 |
.map((spec) => { |
| 66 |
const blueprint = closestBlueprint(spec); |
| 67 |
const port = blueprint ? blueprintPort.get(blueprint) : undefined; |
| 68 |
if (!port || !blueprint) return null; |
| 69 |
const name = projectName(spec); |
| 70 |
projectBlueprint.set(name, blueprint); |
| 71 |
return { |
| 72 |
name, |
| 73 |
testMatch: relative(ROOT, spec), |
| 74 |
use: { |
| 75 |
...devices['Desktop Chrome'], |
| 76 |
baseURL: `http://127.0.0.1:${port}`, |
| 77 |
}, |
| 78 |
}; |
| 79 |
}) |
| 80 |
.filter((p): p is NonNullable<typeof p> => p !== null); |
| 81 |
|
| 82 |
const projects = RUN_PROJECT |
| 83 |
? allProjects.filter((p) => p.name === RUN_PROJECT) |
| 84 |
: allProjects; |
| 85 |
|
| 86 |
// Only spin up the webServer(s) the selected projects actually need. Spawning |
| 87 |
// every blueprint's playground when only one project will run wastes boot time |
| 88 |
// and burns ports. |
| 89 |
const activeBlueprints = new Set( |
| 90 |
projects.map((p) => projectBlueprint.get(p.name)).filter(Boolean) as string[], |
| 91 |
); |
| 92 |
const webServers = [...blueprintPort.entries()] |
| 93 |
.filter(([blueprint]) => activeBlueprints.has(blueprint)) |
| 94 |
.map(([blueprint, port]) => ({ |
| 95 |
// PHP version is pinned per-blueprint via `preferredVersions.php` — the |
| 96 |
// CLI `--php` flag is silently ignored by wp-playground-cli. Pinned to |
| 97 |
// 8.3 to dodge a PHP 8.5 deprecation in vendor/react/promise that |
| 98 |
// breaks ?action=rest-nonce by printing inline when WP_DEBUG_DISPLAY is |
| 99 |
// on. Drop the pin once react/promise ships an 8.5-compatible release. |
| 100 |
command: `npx wp-playground-cli server --auto-mount --blueprint=${relative(ROOT, blueprint)} --wp=${WP_VERSION} --port=${port} --internal-cookie-store=true --login=false`, |
| 101 |
url: `http://127.0.0.1:${port}`, |
| 102 |
reuseExistingServer: !process.env.CI, |
| 103 |
timeout: 120_000, |
| 104 |
stdout: 'pipe' as const, |
| 105 |
stderr: 'pipe' as const, |
| 106 |
})); |
| 107 |
|
| 108 |
// RequestUtils caches the discovered REST root to this file and reuses it |
| 109 |
// whenever it is already populated, skipping rediscovery. The default path is |
| 110 |
// one shared file and playground ports shift between runs, so a later run can |
| 111 |
// POST to whichever server held that port before. Scope it to the process. |
| 112 |
if (!process.env.STORAGE_STATE_PATH) { |
| 113 |
process.env.STORAGE_STATE_PATH = resolve( |
| 114 |
ROOT, |
| 115 |
`artifacts/storage-states/admin-${process.pid}.json`, |
| 116 |
); |
| 117 |
} |
| 118 |
|
| 119 |
// @wordpress/e2e-test-utils-playwright's RequestUtils HEADs process.env.WP_BASE_URL |
| 120 |
// (default http://localhost:8889 — wp-env's default port) to find the REST root, |
| 121 |
// regardless of each request context's configured baseURL. Pin it to the selected |
| 122 |
// project's playground port so requestUtils.login() reaches the right server. |
| 123 |
if (projects.length > 0 && !process.env.WP_BASE_URL) { |
| 124 |
const firstPort = new URL(projects[0].use.baseURL).port; |
| 125 |
process.env.WP_BASE_URL = `http://127.0.0.1:${firstPort}`; |
| 126 |
} |
| 127 |
|
| 128 |
export default defineConfig({ |
| 129 |
testDir: TEST_DIR, |
| 130 |
globalSetup: './tests/playwright/global-setup.ts', |
| 131 |
forbidOnly: !!process.env.CI, |
| 132 |
retries: process.env.CI ? 1 : 0, |
| 133 |
workers: 1, |
| 134 |
// 30s default is too tight for this wp-playground suite on cold CI — the |
| 135 |
// reload-based save specs run ~25-30s even locally. |
| 136 |
timeout: 120_000, |
| 137 |
// Raise the per-assertion floor from 5s to the 15s specs already hand-patch. |
| 138 |
expect: { timeout: 15_000 }, |
| 139 |
reporter: process.env.CI ? [['github'], ['html', { open: 'never' }]] : 'list', |
| 140 |
use: { |
| 141 |
trace: 'on-first-retry', |
| 142 |
video: 'retain-on-failure', |
| 143 |
screenshot: 'only-on-failure', |
| 144 |
testIdAttribute: 'data-test', |
| 145 |
}, |
| 146 |
webServer: webServers.length > 0 ? webServers : undefined, |
| 147 |
projects, |
| 148 |
}); |
| 149 |
|