Skip to content

Commit 1ea9924

Browse files
authored
test: allow e2e suite to run against dist bundle (#164)
Run make build before playwright to provide dist artifacts when needed. Gate dist mode behind CMUX_E2E_LOAD_DIST and assert required files. Skip the dev server and launch electron with production env when set. Enable parallel playwright workers to keep runtimes acceptable.
1 parent 1bb8c0a commit 1ea9924

File tree

4 files changed

+113
-55
lines changed

4 files changed

+113
-55
lines changed

Makefile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,8 @@ test-coverage: ## Run tests with coverage
133133
@./scripts/test.sh --coverage
134134

135135
test-e2e: ## Run end-to-end tests
136-
@PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 bun x playwright test --project=electron
136+
@$(MAKE) build
137+
@CMUX_E2E_LOAD_DIST=1 CMUX_E2E_SKIP_BUILD=1 PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 bun x playwright test --project=electron
137138

138139
## Distribution
139140
dist: build ## Build distributable packages

playwright.config.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,13 @@ export default defineConfig({
88
expect: {
99
timeout: 5_000,
1010
},
11-
fullyParallel: false,
11+
fullyParallel: true,
1212
forbidOnly: isCI,
1313
retries: isCI ? 1 : 0,
1414
reporter: [
1515
["list"],
1616
["html", { outputFolder: "artifacts/playwright-report", open: "never" }],
1717
],
18-
workers: 1,
1918
use: {
2019
trace: isCI ? "on-first-retry" : "retain-on-failure",
2120
screenshot: "only-on-failure",

tests/e2e/electronTest.ts

Lines changed: 106 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,27 @@ interface ElectronFixtures {
2222

2323
const appRoot = path.resolve(__dirname, "..", "..");
2424
const defaultTestRoot = path.join(appRoot, "tests", "e2e", "tmp", "cmux-root");
25-
const DEV_SERVER_PORT = 5173;
25+
const BASE_DEV_SERVER_PORT = Number(process.env.CMUX_E2E_DEVSERVER_PORT_BASE ?? "5173");
26+
const shouldLoadDist = process.env.CMUX_E2E_LOAD_DIST === "1";
27+
28+
const REQUIRED_DIST_FILES = [
29+
path.join(appRoot, "dist", "index.html"),
30+
path.join(appRoot, "dist", "main.js"),
31+
path.join(appRoot, "dist", "preload.js"),
32+
] as const;
33+
34+
function assertDistBundleReady(): void {
35+
if (!shouldLoadDist) {
36+
return;
37+
}
38+
for (const filePath of REQUIRED_DIST_FILES) {
39+
if (!fs.existsSync(filePath)) {
40+
throw new Error(
41+
`Missing build artifact at ${filePath}. Run "make build" before executing dist-mode e2e tests.`
42+
);
43+
}
44+
}
45+
}
2646

2747
async function waitForServerReady(url: string, timeoutMs = 20_000): Promise<void> {
2848
const start = Date.now();
@@ -70,9 +90,8 @@ export const electronTest = base.extend<ElectronFixtures>({
7090
workspace: async ({}, use, testInfo) => {
7191
const envRoot = process.env.CMUX_TEST_ROOT ?? "";
7292
const baseRoot = envRoot || defaultTestRoot;
73-
const testRoot = envRoot
74-
? baseRoot
75-
: path.join(baseRoot, sanitizeForPath(testInfo.title ?? testInfo.testId));
93+
const uniqueTestId = testInfo.testId || testInfo.title || `test-${Date.now()}`;
94+
const testRoot = envRoot ? baseRoot : path.join(baseRoot, sanitizeForPath(uniqueTestId));
7695

7796
const shouldCleanup = !envRoot;
7897

@@ -95,34 +114,55 @@ export const electronTest = base.extend<ElectronFixtures>({
95114
},
96115
app: async ({ workspace }, use, testInfo) => {
97116
const { configRoot } = workspace;
98-
buildTarget("build-main");
99-
buildTarget("build-preload");
100-
101-
const devServer = spawn("make", ["dev"], {
102-
cwd: appRoot,
103-
stdio: ["ignore", "ignore", "inherit"],
104-
env: {
105-
...process.env,
106-
NODE_ENV: "development",
107-
VITE_DISABLE_MERMAID: "1",
108-
},
109-
});
117+
const devServerPort = BASE_DEV_SERVER_PORT + testInfo.workerIndex;
118+
119+
if (shouldLoadDist) {
120+
assertDistBundleReady();
121+
} else {
122+
buildTarget("build-main");
123+
buildTarget("build-preload");
124+
}
110125

126+
const shouldStartDevServer = !shouldLoadDist;
127+
let devServer: ReturnType<typeof spawn> | undefined;
111128
let devServerExited = false;
112-
const devServerExitPromise = new Promise<void>((resolve) => {
113-
const handleExit = () => {
114-
devServerExited = true;
115-
resolve();
116-
};
117-
118-
if (devServer.exitCode !== null) {
119-
handleExit();
120-
} else {
121-
devServer.once("exit", handleExit);
129+
let devServerExitPromise: Promise<void> | undefined;
130+
131+
if (shouldStartDevServer) {
132+
devServer = spawn("make", ["dev"], {
133+
cwd: appRoot,
134+
stdio: ["ignore", "ignore", "inherit"],
135+
env: {
136+
...process.env,
137+
NODE_ENV: "development",
138+
VITE_DISABLE_MERMAID: "1",
139+
CMUX_VITE_PORT: String(devServerPort),
140+
},
141+
});
142+
143+
const activeDevServer = devServer;
144+
if (!activeDevServer) {
145+
throw new Error("Failed to spawn dev server process");
122146
}
123-
});
147+
148+
devServerExitPromise = new Promise<void>((resolve) => {
149+
const handleExit = () => {
150+
devServerExited = true;
151+
resolve();
152+
};
153+
154+
if (activeDevServer.exitCode !== null) {
155+
handleExit();
156+
} else {
157+
activeDevServer.once("exit", handleExit);
158+
}
159+
});
160+
}
124161

125162
const stopDevServer = async () => {
163+
if (!devServer || !devServerExitPromise) {
164+
return;
165+
}
126166
if (!devServerExited && devServer.exitCode === null) {
127167
devServer.kill("SIGTERM");
128168
}
@@ -134,29 +174,44 @@ export const electronTest = base.extend<ElectronFixtures>({
134174
let electronApp: ElectronApplication | undefined;
135175

136176
try {
137-
await waitForServerReady(`http://127.0.0.1:${DEV_SERVER_PORT}`);
138-
if (devServer.exitCode !== null) {
139-
throw new Error(`Vite dev server exited early (code ${devServer.exitCode})`);
177+
let devHost = "127.0.0.1";
178+
if (shouldStartDevServer) {
179+
devHost = process.env.CMUX_DEVSERVER_HOST ?? "127.0.0.1";
180+
await waitForServerReady(`http://${devHost}:${devServerPort}`);
181+
const exitCode = devServer?.exitCode;
182+
if (exitCode !== null && exitCode !== undefined) {
183+
throw new Error(`Vite dev server exited early (code ${exitCode})`);
184+
}
140185
}
141186

142187
recordVideoDir = testInfo.outputPath("electron-video");
143188
fs.mkdirSync(recordVideoDir, { recursive: true });
144189

145-
const devHost = process.env.CMUX_DEVSERVER_HOST ?? "127.0.0.1";
190+
const electronEnv: Record<string, string> = {};
191+
for (const [key, value] of Object.entries(process.env)) {
192+
if (typeof value === "string") {
193+
electronEnv[key] = value;
194+
}
195+
}
196+
electronEnv.ELECTRON_DISABLE_SECURITY_WARNINGS = "true";
197+
electronEnv.CMUX_MOCK_AI = electronEnv.CMUX_MOCK_AI ?? "1";
198+
electronEnv.CMUX_TEST_ROOT = configRoot;
199+
electronEnv.CMUX_E2E = "1";
200+
electronEnv.CMUX_E2E_LOAD_DIST = shouldLoadDist ? "1" : "0";
201+
electronEnv.VITE_DISABLE_MERMAID = "1";
202+
203+
if (shouldStartDevServer) {
204+
electronEnv.CMUX_DEVSERVER_PORT = String(devServerPort);
205+
electronEnv.CMUX_DEVSERVER_HOST = devHost;
206+
electronEnv.NODE_ENV = electronEnv.NODE_ENV ?? "development";
207+
} else {
208+
electronEnv.NODE_ENV = electronEnv.NODE_ENV ?? "production";
209+
}
210+
146211
electronApp = await electron.launch({
147212
args: ["."],
148213
cwd: appRoot,
149-
env: {
150-
...process.env,
151-
ELECTRON_DISABLE_SECURITY_WARNINGS: "true",
152-
CMUX_MOCK_AI: process.env.CMUX_MOCK_AI ?? "1",
153-
CMUX_TEST_ROOT: configRoot,
154-
CMUX_E2E: "1",
155-
CMUX_E2E_LOAD_DIST: "0",
156-
CMUX_DEVSERVER_PORT: String(DEV_SERVER_PORT),
157-
CMUX_DEVSERVER_HOST: devHost,
158-
VITE_DISABLE_MERMAID: "1",
159-
},
214+
env: electronEnv,
160215
recordVideo: {
161216
dir: recordVideoDir,
162217
size: { width: 1280, height: 720 },
@@ -170,14 +225,17 @@ export const electronTest = base.extend<ElectronFixtures>({
170225
await electronApp.close();
171226
}
172227

228+
const displayName = testInfo.title ?? testInfo.testId;
173229
if (recordVideoDir) {
174230
try {
175231
const videoFiles = await fsPromises.readdir(recordVideoDir);
176232
if (electronApp && videoFiles.length) {
177233
const videosDir = path.join(appRoot, "artifacts", "videos");
178234
await fsPromises.mkdir(videosDir, { recursive: true });
179235
const orderedFiles = [...videoFiles].sort();
180-
const baseName = testInfo.title.replace(/\s+/g, "-").toLowerCase();
236+
const baseName = sanitizeForPath(
237+
testInfo.testId || testInfo.title || "cmux-e2e-video"
238+
);
181239
for (const [index, file] of orderedFiles.entries()) {
182240
const ext = path.extname(file) || ".webm";
183241
const suffix = orderedFiles.length > 1 ? `-${index}` : "";
@@ -187,19 +245,19 @@ export const electronTest = base.extend<ElectronFixtures>({
187245
console.log(`[video] saved to ${destination}`); // eslint-disable-line no-console
188246
}
189247
} else if (electronApp) {
190-
console.warn(
191-
`[video] no video captured for "${testInfo.title}" at ${recordVideoDir}`
192-
); // eslint-disable-line no-console
248+
console.warn(`[video] no video captured for "${displayName}" at ${recordVideoDir}`); // eslint-disable-line no-console
193249
}
194250
} catch (error) {
195-
console.error(`[video] failed to process video for "${testInfo.title}":`, error); // eslint-disable-line no-console
251+
console.error(`[video] failed to process video for "${displayName}":`, error); // eslint-disable-line no-console
196252
} finally {
197253
await fsPromises.rm(recordVideoDir, { recursive: true, force: true });
198254
}
199255
}
200256
}
201257
} finally {
202-
await stopDevServer();
258+
if (shouldStartDevServer) {
259+
await stopDevServer();
260+
}
203261
}
204262
},
205263
page: async ({ app }, use) => {

vite.config.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import { fileURLToPath } from "url";
66

77
const __dirname = path.dirname(fileURLToPath(import.meta.url));
88
const disableMermaid = process.env.VITE_DISABLE_MERMAID === "1";
9+
const devServerPort = Number(process.env.CMUX_VITE_PORT ?? "5173");
10+
const previewPort = Number(process.env.CMUX_VITE_PREVIEW_PORT ?? "4173");
911

1012
const alias: Record<string, string> = {
1113
"@": path.resolve(__dirname, "./src"),
@@ -29,8 +31,6 @@ export default defineConfig(({ mode }) => ({
2931
sourcemap: true,
3032
minify: "esbuild",
3133
rollupOptions: {
32-
// Exclude ai-tokenizer from renderer bundle - it's never used there (only in main process)
33-
external: ["ai-tokenizer"],
3434
output: {
3535
format: "es",
3636
inlineDynamicImports: false,
@@ -46,13 +46,13 @@ export default defineConfig(({ mode }) => ({
4646
},
4747
server: {
4848
host: "127.0.0.1",
49-
port: 5173,
49+
port: devServerPort,
5050
strictPort: true,
5151
allowedHosts: ["localhost", "127.0.0.1"],
5252
},
5353
preview: {
5454
host: "127.0.0.1",
55-
port: 4173,
55+
port: previewPort,
5656
strictPort: true,
5757
allowedHosts: ["localhost", "127.0.0.1"],
5858
},

0 commit comments

Comments
 (0)