@@ -22,7 +22,27 @@ interface ElectronFixtures {
2222
2323const appRoot = path . resolve ( __dirname , ".." , ".." ) ;
2424const 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
2747async 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 ) => {
0 commit comments