Skip to content

Commit 507efb4

Browse files
committed
test(e2e): run a notebook cell through the labview, isolate the work dir
Adds an env-backed e2e that drives the labview WebContentsView: open a notebook, run 1 + 1, assert the output renders. The point is the Electron glue, not JupyterLab, so it stays at 1 + 1 (a kernel running over the websocket Electron's net stack carries) rather than testing Python packages. The cell handling mirrors what Galata does (textbox role + pressSequentially, the status-bar kernel-ready check) instead of guessing selectors, and the kernel picker is accepted only if it appears. Also closes a gap the env-backed tests already meant to cover: new sessions now get defaultWorkingDirectory set to the per-launch temp dir, so notebooks the test creates land there and are cleaned up with it rather than in the real home (on macOS HOME alone doesn't move the session cwd). I validated this locally against a clean jupyterlab venv: the cell test passes repeatedly (~22s) and the home directory stays empty before and after. jupyterlab already pulls ipykernel, so the venv has a python3 kernel without installing one separately, which I confirmed with a jupyterlab-only venv (kernelspec lists python3).
1 parent 661c576 commit 507efb4

3 files changed

Lines changed: 66 additions & 1 deletion

File tree

.github/workflows/e2e.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ jobs:
8181
ver=$(awk '$1=="-" && $2=="jupyterlab" {print $3}' env_installer/jlab_server.yaml)
8282
[[ "$ver" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "unexpected jupyterlab pin '$ver' in jlab_server.yaml (want exact x.y.z)"; exit 1; }
8383
python3 -m venv "$RUNNER_TEMP/jlab-venv"
84+
# jupyterlab pulls in ipykernel, so the venv already has a python3
85+
# kernel for the cell-run test; no need to install it separately.
8486
"$RUNNER_TEMP/jlab-venv/bin/pip" install --quiet "jupyterlab==$ver"
8587
echo "JLAB_TEST_PYTHON_PATH=$RUNNER_TEMP/jlab-venv/bin/python" >> "$GITHUB_ENV"
8688
# xvfb is Linux-only; on macOS the runner has a real display, so run the

test/e2e/helpers.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,16 @@ export async function launchApp(opts?: {
6262
]
6363
})
6464
);
65+
// Point new sessions at the temp jupyter dir so notebooks the test creates
66+
// (New notebook) land there and get removed with it, instead of the real
67+
// home. On macOS the spawned server's cwd follows this setting; HOME alone
68+
// does not move it because Electron resolves home via NSHomeDirectory.
6569
writeFileSync(
6670
join(userDataDir, 'settings.json'),
67-
JSON.stringify({ pythonPath: opts.pythonPath })
71+
JSON.stringify({
72+
pythonPath: opts.pythonPath,
73+
defaultWorkingDirectory: jupyterDir
74+
})
6875
);
6976
}
7077
try {

test/e2e/python-env.test.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,59 @@ test('with a seeded Python env, New notebook opens the labview', async () => {
3434
cleanup(userDataDir, jupyterDir);
3535
}
3636
});
37+
38+
// The point of this one is the Electron glue, not JupyterLab: a notebook cell
39+
// evaluated through the labview WebContentsView exercises the whole embedded
40+
// chain (server boots, the kernel connects over the websocket Electron's net
41+
// stack carries, output renders back in the view). 1 + 1 is enough; testing
42+
// numpy here would be testing Python, not Electron.
43+
test('a notebook cell runs through the Electron labview', async () => {
44+
test.skip(!pythonPath, NEEDS_PYTHON);
45+
test.setTimeout(180000);
46+
const { app, userDataDir, jupyterDir } = await launchApp({ pythonPath });
47+
try {
48+
const welcome = await pageByTitle(app, /welcome/i);
49+
const newNotebook = welcome.locator('#new-notebook-link');
50+
await expect(newNotebook).not.toHaveClass(/disabled/, { timeout: 20000 });
51+
await newNotebook.click();
52+
53+
const lab = await pageByUrl(app, LAB_URL);
54+
// With the seeded env the notebook usually picks the kernel on its own, but
55+
// a kernel picker can still appear; accept it if it does, don't block if not.
56+
await lab
57+
.locator('.jp-Dialog button.jp-mod-accept')
58+
.click({ timeout: 15000 })
59+
.catch(() => undefined);
60+
61+
// Wait for the kernel to finish connecting before running anything, using
62+
// Galata's readiness check (the status bar stops showing connecting states)
63+
// rather than a fixed delay.
64+
await lab.waitForFunction(
65+
() => {
66+
const text =
67+
document.querySelector('#jp-main-statusbar')?.textContent ?? '';
68+
return !['Connecting', 'Initializing', 'Starting'].some(s =>
69+
text.includes(s)
70+
);
71+
},
72+
{ timeout: 90000 }
73+
);
74+
75+
// Drive the active notebook's first cell the way Galata does: target the
76+
// editor by its textbox role, replace its content, then run with Shift+Enter.
77+
const cell = lab
78+
.locator('.jp-NotebookPanel:not(.lm-mod-hidden) .jp-Notebook .jp-Cell')
79+
.first();
80+
const editor = cell.getByRole('textbox');
81+
await editor.click();
82+
await editor.press('Control+A');
83+
await editor.pressSequentially('1 + 1');
84+
await lab.keyboard.press('Shift+Enter');
85+
86+
const output = cell.locator('.jp-OutputArea-output').first();
87+
await expect(output).toContainText('2', { timeout: 90000 });
88+
} finally {
89+
await app.close();
90+
cleanup(userDataDir, jupyterDir);
91+
}
92+
});

0 commit comments

Comments
 (0)