playwright.config.ts
Rama main · réplica de lectura del repositorio original.
/// <reference types="node" />
import { defineConfig, devices } from "@playwright/test";
import { resolve } from "node:path";
import { capturePerformanceEnvironment, performanceSampleCount } from "./scripts/browser-performance";
// 这个根层配置经常被编辑器作为独立文件打开;文件级 Node types 避免 TS Server
// 还没关联 tsconfig.playwright.json 时误报 process/node 内置类型缺失。
const env = process.env;
const performanceMode = env.RENEWLET_E2E_PERFORMANCE === "1";
const profilingMode = env.RENEWLET_E2E_PROFILE === "1";
const previousDist = env.RENEWLET_E2E_PREVIOUS_DIST;
if (previousDist && performanceMode) throw new Error("Deployment upgrade journeys must run separately from performance samples");
if (profilingMode && !performanceMode) throw new Error("RENEWLET_E2E_PROFILE requires the isolated performance fixture");
// 根包由 Playwright 按 CommonJS 加载;指纹根目录跟随配置文件,不依赖调用者 cwd。
const performanceEnvironment = performanceMode ? capturePerformanceEnvironment(__dirname) : undefined;
const productionClientBuild = profilingMode
? "pnpm --filter @renewlet/client exec vite build --sourcemap"
: performanceMode ? "pnpm exec tsx scripts/build-performance-client.ts" : "pnpm --filter @renewlet/client build";
// Playwright 会为 reporter 设置 FORCE_COLOR;继承 NO_COLOR 会让 Node 在每个 webServer 子进程重复打印冲突告警。
delete env.NO_COLOR;
delete env.no_color;
// 本地 E2E 依赖 127.0.0.1 上的 Go server 和 Vite。继承用户代理配置时,
// localhost 请求可能被转发到外部代理,导致 healthcheck 或 API 等待随机失败。
for (const key of ["NO_PROXY", "no_proxy"]) {
const values = new Set(
(env[key] ?? "")
.split(",")
.map((value) => value.trim())
.filter(Boolean),
);
values.add("127.0.0.1");
values.add("localhost");
values.add("::1");
env[key] = Array.from(values).join(",");
}
const proxyEnv = {
NO_PROXY: env.NO_PROXY ?? "",
no_proxy: env.no_proxy ?? "",
};
// 避开 Linux 默认临时端口段 32768–60999,避免服务监听前被出站连接占用;仍拒绝复用已有服务。
const e2eServerPort = 19090;
const e2eClientPort = 15173;
const e2eServerURL = `http://127.0.0.1:${e2eServerPort}`;
const e2eClientURL = `http://127.0.0.1:${e2eClientPort}`;
const adminStorageState = "e2e/.auth/admin.json";
const browserExecutablePath = env.RENEWLET_E2E_BROWSER_EXECUTABLE?.trim();
// 本地可能遇到 Playwright CDN/TLS 被代理拦截;只在显式传入时用系统浏览器,CI 仍使用 hermetic 浏览器。
const localBrowserFallback = browserExecutablePath ? { launchOptions: { executablePath: browserExecutablePath } } : {};
// 端口必须保持拆分:后端只给 PocketBase/Go API,浏览器页面只从前端的 Vite 入口进入。
// 如果把 baseURL 指到后端端口,headed 调试会看到 PocketBase UI 而不是 Renewlet 前端。
export default defineConfig({
testDir: "./e2e",
fullyParallel: false,
workers: 1,
// 性能样本不能自动重试到变绿;原始失败与不足十次的分组必须保留。
retries: performanceMode ? 0 : env.CI ? 1 : 0,
metadata: performanceEnvironment ? { performance: performanceEnvironment } : {},
// CPU/commit 诊断不能生成可被比较器接受的生产耗时报告。
reporter: performanceMode && !profilingMode ? [["list"], ["./e2e/support/performance-reporter.ts"]] : [
["list"], ["html", { open: "never" }],
["./e2e/support/server-diagnostics-reporter.ts", { outputFile: resolve(__dirname, "test-results/server-diagnostics.json") }],
],
timeout: 90_000,
expect: {
timeout: 10_000,
},
use: {
baseURL: e2eClientURL,
locale: "zh-CN",
timezoneId: "Asia/Shanghai",
screenshot: performanceMode ? "off" : "only-on-failure",
trace: performanceMode ? "off" : "retain-on-failure",
video: performanceMode ? "off" : "retain-on-failure",
...localBrowserFallback,
},
webServer: [
{
// 每次清空 e2e 数据目录,让 setup project 拥有唯一的初始化状态;不复用旧 server
// 可以避免 storage state 与 PocketBase SQLite 数据跨测试轮次串味。
command: `rm -rf ./pb_data_e2e && go run ./cmd/renewlet serve --http=127.0.0.1:${e2eServerPort} --dir=./pb_data_e2e`,
cwd: "./apps/docker-server",
env: {
...proxyEnv,
SETUP_ENABLED: "true",
GOMEMLIMIT: "128MiB",
},
url: `${e2eServerURL}/api/app/health`,
reuseExistingServer: false,
timeout: 120_000,
// Go 与 Vite 都可能在 stdout 输出诊断;两条流都交给 reporter,不能仅依赖页面 console 事件。
stdout: "pipe",
stderr: "pipe",
},
{
// 性能模式重建生产产物;普通 E2E 仍重建 optimizer,不复用开发机残留缓存。
command: performanceMode || previousDist
? `${productionClientBuild} && pnpm --dir apps/web exec vite preview --host 127.0.0.1 --port ${e2eClientPort} --strictPort`
: `pnpm --dir apps/web exec vite --force --host 127.0.0.1 --port ${e2eClientPort} --strictPort`,
env: {
...proxyEnv,
VITE_DEV_PROXY_TARGET: e2eServerURL,
...(performanceMode || previousDist ? { VITE_RENEWLET_RUNTIME: "docker" } : {}),
},
url: e2eClientURL,
reuseExistingServer: false,
timeout: performanceMode || previousDist ? 300_000 : 120_000,
stdout: "pipe",
stderr: "pipe",
},
],
projects: [
{
name: "setup",
testMatch: /.*\.setup\.ts/,
testIgnore: ["**/cloudflare-check/**", "**/performance.setup.ts"],
use: { ...devices["Desktop Chrome"] },
},
...(performanceMode ? [{
name: "performance-probe",
testMatch: ["**/performance-probe.spec.ts", ...(profilingMode ? [] : ["**/performance-vitals-probe.spec.ts"])],
use: { ...devices["Desktop Chrome"] },
}, {
name: "performance-seed",
dependencies: ["setup", "performance-probe"],
testMatch: "**/performance.setup.ts",
use: { ...devices["Desktop Chrome"] },
}] : []),
// setup 通过真实 Renewlet UI 生成登录态;业务项目只消费 storage state,
// 这样每个 spec 独立验证旅程,又不重复穿过首次安装流程。
{
name: performanceMode ? "performance-desktop" : "desktop",
dependencies: [performanceMode ? "performance-seed" : "setup"],
repeatEach: performanceMode && !profilingMode ? performanceSampleCount : 1,
testMatch: previousDist ? ["**/version-upgrade.spec.ts"] : performanceMode ? ["**/performance.spec.ts", ...(profilingMode ? [] : ["**/performance-vitals.spec.ts"])] : [
"**/calendar-feed-management.spec.ts",
"**/subscriptions.spec.ts",
"**/subscription-renewal.spec.ts",
"**/subscription-renewal-members.spec.ts",
"**/settings.spec.ts",
"**/lunar-calendar.spec.ts",
"**/statistics.spec.ts",
"**/release-smoke.spec.ts",
"**/route-progress.spec.ts",
"**/report-exchange-rates.spec.ts",
],
use: {
...devices["Desktop Chrome"],
storageState: adminStorageState,
},
},
{
name: performanceMode ? "performance-mobile" : "mobile",
dependencies: [performanceMode ? "performance-seed" : "setup"],
repeatEach: performanceMode && !profilingMode ? performanceSampleCount : 1,
testMatch: previousDist ? ["**/version-upgrade.spec.ts"] : performanceMode ? ["**/performance.spec.ts", ...(profilingMode ? [] : ["**/performance-vitals.spec.ts"])] : ["**/mobile-*.spec.ts", "**/lunar-calendar.spec.ts", "**/route-progress.spec.ts", "**/report-exchange-rates.spec.ts"],
use: {
...devices["Pixel 5"],
storageState: adminStorageState,
},
},
],
});