@@ -4,6 +4,16 @@ import { mkdtempSync, rmSync, writeFileSync } from 'fs';
44import { join } from 'path' ;
55import { tmpdir } from 'os' ;
66
7+ // A booted JupyterLab server URL: the labview navigates here once a session is
8+ // up. Shared so every server-backed test waits on the same pattern.
9+ export const LAB_URL = / h t t p s ? : \/ \/ ( 1 2 7 \. 0 \. 0 \. 1 | l o c a l h o s t ) : \d + / ;
10+
11+ // The env-backed tests need a real Python with jupyterlab; CI provisions one and
12+ // points JLAB_TEST_PYTHON_PATH at it. Shared so the skip reason reads the same
13+ // everywhere.
14+ export const NEEDS_PYTHON =
15+ 'set JLAB_TEST_PYTHON_PATH to a python with jupyterlab' ;
16+
717// A fresh userData dir per launch keeps tests independent. Electron's native
818// --user-data-dir flag is honored because the app reads app.getPath('userData')
919// (see getUserDataDir in utils) and only overrides it on Snap. Returns the app
@@ -17,10 +27,26 @@ import { tmpdir } from 'os';
1727// actions; `userSetPythonEnvs` registers the env so the session registry
1828// resolves it) and `settings.json` (`pythonPath` is what a new session reads
1929// via workspace settings to boot its server).
30+ //
31+ // The spawned Jupyter server inherits the launch env via `{ ...process.env }`
32+ // in src/main/server.ts. Redirecting HOME alone isolates it: jupyter derives its
33+ // config, data and runtime dirs from HOME (~/.jupyter, ~/Library/Jupyter, etc.),
34+ // so with HOME pointed at a temp the server and its kernels/terminal never read
35+ // or write the runner's real Jupyter dirs. Setting JUPYTER_CONFIG_DIR/DATA_DIR/
36+ // RUNTIME_DIR explicitly would be redundant. On macOS, HOME does not move
37+ // Electron's own app.getPath('home') (it uses NSHomeDirectory), but the app's
38+ // own state is already isolated by --user-data-dir; HOME only needs to reach the
39+ // python server, which it does through `...process.env`. The jupyter temp dir is
40+ // returned so the caller can remove it alongside userDataDir.
2041export async function launchApp ( opts ?: {
2142 pythonPath ?: string ;
22- } ) : Promise < { app : ElectronApplication ; userDataDir : string } > {
43+ } ) : Promise < {
44+ app : ElectronApplication ;
45+ userDataDir : string ;
46+ jupyterDir : string ;
47+ } > {
2348 const userDataDir = mkdtempSync ( join ( tmpdir ( ) , 'jlab-e2e-' ) ) ;
49+ const jupyterDir = mkdtempSync ( join ( tmpdir ( ) , 'jlab-e2e-jupyter-' ) ) ;
2450 if ( opts ?. pythonPath ) {
2551 writeFileSync (
2652 join ( userDataDir , 'app-data.json' ) ,
@@ -43,18 +69,25 @@ export async function launchApp(opts?: {
4369 }
4470 try {
4571 const app = await electron . launch ( {
46- args : [ '.' , `--user-data-dir=${ userDataDir } ` ]
72+ args : [ '.' , `--user-data-dir=${ userDataDir } ` ] ,
73+ env : {
74+ ...process . env ,
75+ HOME : jupyterDir
76+ }
4777 } ) ;
4878 await stubAllDialogs ( app ) ;
49- return { app, userDataDir } ;
79+ return { app, userDataDir, jupyterDir } ;
5080 } catch ( error ) {
51- cleanup ( userDataDir ) ;
81+ cleanup ( userDataDir , jupyterDir ) ;
5282 throw error ;
5383 }
5484}
5585
56- export function cleanup ( userDataDir : string ) : void {
86+ export function cleanup ( userDataDir : string , jupyterDir ?: string ) : void {
5787 rmSync ( userDataDir , { recursive : true , force : true } ) ;
88+ if ( jupyterDir ) {
89+ rmSync ( jupyterDir , { recursive : true , force : true } ) ;
90+ }
5891}
5992
6093// The app opens several windows (titlebar, welcome, session, manager) and most
@@ -81,3 +114,29 @@ export async function pageByTitle(
81114 }
82115 throw new Error ( `no window title matched ${ pattern } ` ) ;
83116}
117+
118+ // Wait for any open page whose URL matches `pattern`. Unlike
119+ // electron-playwright-helpers' waitForWindowByUrl, which only resolves on a new
120+ // "window" event, this also catches an in-place navigation of an existing page
121+ // (e.g. the session content view swapping to a remote server URL), so it is the
122+ // reliable wait for "a view reached this URL" regardless of how it got there.
123+ export async function pageByUrl (
124+ app : ElectronApplication ,
125+ pattern : RegExp ,
126+ timeout = 90000
127+ ) {
128+ const deadline = Date . now ( ) + timeout ;
129+ while ( Date . now ( ) < deadline ) {
130+ for ( const page of app . windows ( ) ) {
131+ try {
132+ if ( pattern . test ( page . url ( ) ) ) {
133+ return page ;
134+ }
135+ } catch {
136+ // page may be closing; retry on the next pass
137+ }
138+ }
139+ await new Promise ( resolve => setTimeout ( resolve , 150 ) ) ;
140+ }
141+ throw new Error ( `no window URL matched ${ pattern } ` ) ;
142+ }
0 commit comments