Skip to content

Commit ef2efce

Browse files
authored
feat(e2e): add UI badge assertions + 5 new parallel CI matrix entries (#1041)
Add data-testid and data-env-manager/data-kernel-status attributes to NotebookToolbar for E2E assertions. Each fixture spec now verifies: - Runtime badge (Python/Deno) appears in toolbar - Env manager badge (UV/conda) appears after kernel launch - Trust dialog elements are present before approval New CI matrix entries (parallel runners): - UV Inline Deps (uv:inline + trust dialog) - Conda Inline Deps (conda:inline + trust dialog) - Trust Dialog (dismiss behavior) - UV Pyproject (project file detection) All 5 fixture specs updated to use setCellSource + button click pattern for WebDriver plugin compatibility. Note: prewarmed-uv badge assertion uses waitUntil polling since env-manager syncs from RuntimeStateDoc after kernel launch.
1 parent f8c75af commit ef2efce

File tree

10 files changed

+374
-88
lines changed

10 files changed

+374
-88
lines changed

.github/workflows/build.yml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,30 @@ jobs:
453453
uv_pool: 0
454454
conda_pool: 0
455455
timeout: 8
456+
- name: UV Inline Deps
457+
spec: "e2e/specs/uv-inline.spec.js"
458+
notebook: "crates/notebook/fixtures/audit-test/2-uv-inline.ipynb"
459+
uv_pool: 3
460+
conda_pool: 0
461+
timeout: 12
462+
- name: Conda Inline Deps
463+
spec: "e2e/specs/conda-inline.spec.js"
464+
notebook: "crates/notebook/fixtures/audit-test/3-conda-inline.ipynb"
465+
uv_pool: 0
466+
conda_pool: 3
467+
timeout: 15
468+
- name: Trust Dialog
469+
spec: "e2e/specs/trust-dialog-dismiss.spec.js"
470+
notebook: "crates/notebook/fixtures/audit-test/2-uv-inline.ipynb"
471+
uv_pool: 3
472+
conda_pool: 0
473+
timeout: 12
474+
- name: UV Pyproject
475+
spec: "e2e/specs/uv-pyproject.spec.js"
476+
notebook: "crates/notebook/fixtures/audit-test/pyproject-project/5-pyproject.ipynb"
477+
uv_pool: 3
478+
conda_pool: 0
479+
timeout: 12
456480
steps:
457481
- uses: actions/checkout@v6
458482

apps/notebook/src/components/NotebookToolbar.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,7 @@ export function NotebookToolbar({
261261
onClick={onToggleDependencies}
262262
data-testid="deps-toggle"
263263
data-runtime={runtime}
264+
data-env-manager={envManager || undefined}
264265
className={cn(
265266
"flex items-center gap-1 rounded px-1.5 py-0.5 text-[10px] font-medium transition-colors",
266267
runtime === "deno"
@@ -311,6 +312,8 @@ export function NotebookToolbar({
311312
role="status"
312313
aria-label={`Kernel: ${kernelStatusDescription}`}
313314
title={envErrorMessage ? undefined : kernelStatusTooltip}
315+
data-testid="kernel-status"
316+
data-kernel-status={kernelStatus}
314317
>
315318
<div
316319
className={cn(
Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,35 @@
11
{
2-
"metadata": {},
32
"nbformat": 4,
43
"nbformat_minor": 5,
4+
"metadata": {
5+
"runt": {
6+
"schema_version": "1"
7+
}
8+
},
59
"cells": [
610
{
7-
"cell_type": "code",
811
"id": "8b5de5f8-9d57-4008-9c3e-349dc0904a90",
9-
"metadata": {},
10-
"execution_count": null,
12+
"cell_type": "code",
1113
"source": [
12-
"import sys\n",
13-
"\n",
14-
"print(sys.executable) # edited!"
14+
"import sys; print(sys.executable)"
15+
],
16+
"metadata": {},
17+
"outputs": [
18+
{
19+
"output_type": "stream",
20+
"name": "stdout",
21+
"text": "/Users/kylekelley/.cache/uv/builds-v0/.tmprecQOX/bin/python\n"
22+
}
1523
],
16-
"outputs": []
24+
"execution_count": 1
1725
},
1826
{
19-
"cell_type": "code",
2027
"id": "d8535959-759a-4c53-af14-bfcb0e4a3345",
21-
"metadata": {},
22-
"execution_count": null,
28+
"cell_type": "code",
2329
"source": [],
24-
"outputs": []
30+
"metadata": {},
31+
"outputs": [],
32+
"execution_count": null
2533
}
2634
]
2735
}

e2e/specs/conda-inline.spec.js

Lines changed: 80 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,62 +5,121 @@
55
* environment with those deps installed (via rattler, not the prewarmed pool).
66
*
77
* Fixture: 3-conda-inline.ipynb (has markupsafe dependency via conda)
8+
*
9+
* Updated to use setCellSource + explicit button clicks for compatibility
10+
* with tauri-plugin-webdriver (synthetic keyboard events don't work).
811
*/
912

1013
import { browser } from "@wdio/globals";
1114
import {
1215
approveTrustDialog,
13-
executeFirstCell,
14-
typeSlowly,
16+
setCellSource,
1517
waitForCellOutput,
1618
waitForKernelReady,
19+
waitForNotebookSynced,
1720
} from "../helpers.js";
1821

1922
describe("Conda Inline Dependencies", () => {
2023
it("should auto-launch kernel (may need trust approval)", async () => {
21-
// Wait for kernel or trust dialog (120s for first startup + conda env creation)
22-
await waitForKernelReady(180000);
24+
console.log("[conda-inline] Waiting for kernel ready (up to 300s)...");
25+
// Wait for kernel or trust dialog (300s for first startup + conda env creation)
26+
await waitForKernelReady(300000);
27+
console.log("[conda-inline] Kernel is ready");
28+
});
29+
30+
it("should show conda badge in toolbar", async () => {
31+
const depsToggle = await $('[data-testid="deps-toggle"]');
32+
await depsToggle.waitForExist({ timeout: 10000 });
33+
34+
// env-manager syncs from RuntimeStateDoc after kernel launch — poll for it
35+
await browser.waitUntil(
36+
async () => {
37+
const mgr = await depsToggle.getAttribute("data-env-manager");
38+
return mgr === "conda";
39+
},
40+
{
41+
timeout: 30000,
42+
interval: 500,
43+
timeoutMsg: "Expected conda badge never appeared",
44+
},
45+
);
46+
47+
expect(await depsToggle.getAttribute("data-env-manager")).toBe("conda");
48+
expect(await depsToggle.getAttribute("data-runtime")).toBe("python");
49+
console.log("[conda-inline] Conda badge verified in toolbar");
2350
});
2451

2552
it("should have inline deps available after trust", async () => {
26-
// Execute the first cell (prints sys.executable)
27-
const cell = await executeFirstCell();
53+
console.log("[conda-inline] Waiting for notebook to sync...");
54+
await waitForNotebookSynced();
55+
56+
// Find the first code cell
57+
const codeCell = await $('[data-cell-type="code"]');
58+
await codeCell.waitForExist({ timeout: 10000 });
59+
console.log("[conda-inline] Found first code cell");
60+
61+
// Set the cell source via CodeMirror dispatch (bypasses keyboard events)
62+
await setCellSource(codeCell, "import sys; print(sys.executable)");
63+
console.log("[conda-inline] Set cell source via setCellSource");
64+
65+
// Click the execute button
66+
const executeButton = await codeCell.$('[data-testid="execute-button"]');
67+
await executeButton.waitForClickable({ timeout: 5000 });
68+
await executeButton.click();
69+
console.log("[conda-inline] Clicked execute button");
2870

2971
// May need to approve trust dialog for inline deps
3072
const approved = await approveTrustDialog(15000);
3173
if (approved) {
74+
console.log(
75+
"[conda-inline] Trust dialog approved, waiting for kernel restart...",
76+
);
3277
// If trust dialog appeared, wait for kernel to restart with deps
33-
await waitForKernelReady(180000);
34-
// Re-execute after kernel restart
35-
await browser.keys(["Shift", "Enter"]);
78+
await waitForKernelReady(300000);
79+
console.log("[conda-inline] Kernel restarted after trust approval");
80+
81+
// Re-execute after kernel restart by clicking the button again
82+
const reExecuteButton = await codeCell.$(
83+
'[data-testid="execute-button"]',
84+
);
85+
await reExecuteButton.waitForClickable({ timeout: 5000 });
86+
await reExecuteButton.click();
87+
console.log("[conda-inline] Re-executed cell after kernel restart");
3688
}
3789

3890
// Wait for output
39-
const output = await waitForCellOutput(cell, 120000);
91+
const output = await waitForCellOutput(codeCell, 120000);
92+
console.log(`[conda-inline] Cell output: ${output}`);
4093

4194
// Should be a cached conda inline env (conda-inline-* path)
4295
expect(output).toContain("conda-inline-");
4396
});
4497

4598
it("should be able to import inline dependency", async () => {
46-
// Find a cell and type import test
99+
// Find the cells — use a second cell if available, otherwise the first
47100
const cells = await $$('[data-cell-type="code"]');
48101
const cell = cells.length > 1 ? cells[1] : cells[0];
102+
console.log(
103+
`[conda-inline] Using cell index ${cells.length > 1 ? 1 : 0} for import test`,
104+
);
49105

50-
const editor = await cell.$('.cm-content[contenteditable="true"]');
51-
await editor.click();
52-
await browser.pause(200);
106+
// Set the cell source directly via CodeMirror dispatch
107+
await setCellSource(
108+
cell,
109+
"import markupsafe; print(markupsafe.__version__)",
110+
);
111+
console.log("[conda-inline] Set import test source via setCellSource");
53112

54-
// Select all and type import
55-
const modKey = process.platform === "darwin" ? "Meta" : "Control";
56-
await browser.keys([modKey, "a"]);
57-
await browser.pause(100);
58-
59-
await typeSlowly("import markupsafe; print(markupsafe.__version__)");
60-
await browser.keys(["Shift", "Enter"]);
113+
// Click the execute button
114+
const executeButton = await cell.$('[data-testid="execute-button"]');
115+
await executeButton.waitForClickable({ timeout: 5000 });
116+
await executeButton.click();
117+
console.log("[conda-inline] Clicked execute button for import test");
61118

62119
// Wait for version output
63120
const output = await waitForCellOutput(cell, 30000);
121+
console.log(`[conda-inline] Import test output: ${output}`);
122+
64123
// Should show a version number (e.g., "1.26.4")
65124
expect(output).toMatch(/^\d+\.\d+/);
66125
});

e2e/specs/deno.spec.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@ describe("Deno Kernel", () => {
2424
await waitForKernelReady(300000);
2525
});
2626

27+
it("should show Deno runtime in toolbar", async () => {
28+
const depsToggle = await $('[data-testid="deps-toggle"]');
29+
await depsToggle.waitForExist({ timeout: 10000 });
30+
expect(await depsToggle.getAttribute("data-runtime")).toBe("deno");
31+
});
32+
2733
it("should execute TypeScript and show output", async () => {
2834
await waitForNotebookSynced();
2935

e2e/specs/prewarmed-uv.spec.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,28 @@ describe("Prewarmed Environment Pool", () => {
2626
await waitForKernelReady(300000);
2727
});
2828

29+
it("should show Python runtime with UV badge", async () => {
30+
const depsToggle = await $('[data-testid="deps-toggle"]');
31+
await depsToggle.waitForExist({ timeout: 10000 });
32+
expect(await depsToggle.getAttribute("data-runtime")).toBe("python");
33+
34+
// env-manager syncs from RuntimeStateDoc after kernel launch — poll for it
35+
await browser.waitUntil(
36+
async () => {
37+
const mgr = await depsToggle.getAttribute("data-env-manager");
38+
return mgr === "uv";
39+
},
40+
{
41+
timeout: 30000,
42+
interval: 500,
43+
timeoutMsg: "UV badge never appeared on deps toggle",
44+
},
45+
);
46+
const envManager = await depsToggle.getAttribute("data-env-manager");
47+
console.log(`[prewarmed-uv] env-manager: ${envManager}`);
48+
expect(envManager).toBe("uv");
49+
});
50+
2951
it("should execute code and show managed env path", async () => {
3052
await waitForNotebookSynced();
3153

e2e/specs/trust-dialog-dismiss.spec.js

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,37 @@
1111

1212
import { browser, expect } from "@wdio/globals";
1313
import {
14-
executeFirstCell,
1514
getKernelStatus,
15+
setCellSource,
1616
waitForAppReady,
17+
waitForNotebookSynced,
1718
} from "../helpers.js";
1819

1920
describe("Trust Dialog Dismiss", () => {
2021
before(async () => {
2122
await waitForAppReady();
23+
console.log("[trust-dialog-dismiss] App ready");
2224
});
2325

2426
it("should close trust dialog on single click without waiting for kernel", async () => {
25-
// Execute a cell to trigger kernel start (which requires trust approval)
26-
// This is how other trust-related specs trigger the dialog
27-
await executeFirstCell();
27+
// Wait for the notebook to sync and render cells
28+
await waitForNotebookSynced();
29+
console.log("[trust-dialog-dismiss] Notebook synced");
30+
31+
// Find the first code cell
32+
const codeCell = await $('[data-cell-type="code"]');
33+
await codeCell.waitForExist({ timeout: 10000 });
34+
console.log("[trust-dialog-dismiss] Found code cell");
35+
36+
// Set cell source via CodeMirror dispatch (bypasses synthetic keyboard events)
37+
await setCellSource(codeCell, "import sys; print(sys.executable)");
38+
console.log("[trust-dialog-dismiss] Set cell source");
39+
40+
// Click the execute button to trigger kernel start (which requires trust approval)
41+
const executeButton = await codeCell.$('[data-testid="execute-button"]');
42+
await executeButton.waitForClickable({ timeout: 5000 });
43+
await executeButton.click();
44+
console.log("[trust-dialog-dismiss] Clicked execute button");
2845

2946
// Wait for the trust dialog to appear (notebook has untrusted deps)
3047
const dialog = await $('[data-testid="trust-dialog"]');
@@ -35,17 +52,28 @@ describe("Trust Dialog Dismiss", () => {
3552
timeoutMsg:
3653
"Trust dialog did not appear - fixture notebook should have untrusted deps",
3754
});
55+
// The trust dialog should be visible before approval
56+
expect(await dialog.isExisting()).toBe(true);
57+
console.log("[trust-dialog-dismiss] Trust dialog appeared");
3858

3959
// Record current kernel status before clicking
4060
const statusBefore = await getKernelStatus();
41-
console.log(`Kernel status before trust approval: ${statusBefore}`);
61+
console.log(
62+
`[trust-dialog-dismiss] Kernel status before trust approval: ${statusBefore}`,
63+
);
4264

43-
// Find and click the approve button
65+
// Approve and decline buttons should be present
4466
const approveButton = await $('[data-testid="trust-approve-button"]');
67+
expect(await approveButton.isExisting()).toBe(true);
68+
const declineButton = await $('[data-testid="trust-decline-button"]');
69+
expect(await declineButton.isExisting()).toBe(true);
70+
71+
// Click the approve button
4572
await approveButton.waitForClickable({ timeout: 5000 });
4673

4774
const clickTime = Date.now();
4875
await approveButton.click();
76+
console.log("[trust-dialog-dismiss] Clicked approve button");
4977

5078
// Dialog should close QUICKLY (within 3 seconds) - this is the key assertion
5179
// If it waited for kernel launch, this would timeout
@@ -58,11 +86,13 @@ describe("Trust Dialog Dismiss", () => {
5886

5987
const closeTime = Date.now();
6088
const dismissTime = closeTime - clickTime;
61-
console.log(`Dialog dismissed in ${dismissTime}ms`);
89+
console.log(`[trust-dialog-dismiss] Dialog dismissed in ${dismissTime}ms`);
6290

6391
// Check kernel status - should be starting or have quickly reached idle
6492
const statusAfter = await getKernelStatus();
65-
console.log(`Kernel status after dialog closed: ${statusAfter}`);
93+
console.log(
94+
`[trust-dialog-dismiss] Kernel status after dialog closed: ${statusAfter}`,
95+
);
6696
expect(["starting", "idle", "busy"]).toContain(statusAfter);
6797
});
6898
});

0 commit comments

Comments
 (0)