Skip to content

Commit 518131c

Browse files
gjmooneyjtpiogithub-actions[bot]
authored
Tweaks (#46)
* Change listCommands return to a promise * Send message when ready * Enable returns from execute * Add a `ready` promise (#47) * ready promise * custom proxy * update demo * lint * Update Playwright Snapshots * Save video * Update snapshot names * lint * Add structuredClone check * Change viewport size for tests * Update tests --------- Co-authored-by: Greg <[email protected]> Co-authored-by: Jeremy Tuloup <[email protected]> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent 32e5270 commit 518131c

File tree

10 files changed

+141
-38
lines changed

10 files changed

+141
-38
lines changed

demo/index.html

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,15 @@ <h1>%VITE_TITLE% Demo</h1>
5353
</div>
5454
</div>
5555

56-
<iframe
57-
id="jupyterlab"
58-
src="%VITE_DEMO_SRC%"
59-
sandbox="allow-scripts allow-same-origin"
60-
title="JupyterLab Instance"
61-
loading="lazy"
62-
></iframe>
56+
<div class="iframe-container">
57+
<div id="bridge-status" class="status-indicator">Connecting to JupyterLab...</div>
58+
<iframe
59+
id="jupyterlab"
60+
src="%VITE_DEMO_SRC%"
61+
sandbox="allow-scripts allow-same-origin"
62+
title="JupyterLab Instance"
63+
loading="lazy"
64+
></iframe>
65+
</div>
6366
</body>
6467
</html>

demo/src/main.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,20 @@ import { createBridge } from 'jupyter-iframe-commands-host';
44

55
const commandBridge = createBridge({ iframeId: 'jupyterlab' });
66

7+
const statusIndicator = document.getElementById('bridge-status');
8+
statusIndicator.style.backgroundColor = '#ffa500'; // Orange for connecting
9+
10+
let bridgeReady = false;
11+
712
const submitCommand = async (command, args) => {
13+
// Don't allow command execution until bridge is ready
14+
if (!bridgeReady) {
15+
document.getElementById('error-dialog').innerHTML =
16+
'<code>Command bridge is not ready yet. Please wait.</code>';
17+
errorDialog.showModal();
18+
return;
19+
}
20+
821
try {
922
await commandBridge.execute(command, args ? JSON.parse(args) : {});
1023
} catch (e) {
@@ -157,3 +170,10 @@ modeRadios.forEach(radio => {
157170
iframe.src = currentUrl.toString();
158171
});
159172
});
173+
174+
// Wait for the command bridge to be ready
175+
commandBridge.ready.then(() => {
176+
bridgeReady = true;
177+
statusIndicator.textContent = 'Connected to JupyterLab';
178+
statusIndicator.style.backgroundColor = '#32CD32'; // Green for connected
179+
});

demo/src/style.css

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,3 +258,22 @@ dialog button[value='close']:hover {
258258
font-weight: 500;
259259
user-select: none;
260260
}
261+
262+
.status-indicator {
263+
padding: 5px 10px;
264+
border-radius: 4px;
265+
color: #fff;
266+
font-size: 14px;
267+
font-weight: bold;
268+
margin-bottom: 10px;
269+
display: inline-block;
270+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
271+
}
272+
273+
.iframe-container {
274+
position: relative;
275+
width: 100%;
276+
display: flex;
277+
flex-direction: column;
278+
align-items: center;
279+
}

packages/extension/src/index.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,15 +45,29 @@ const plugin: JupyterFrontEndPlugin<void> = {
4545

4646
const api: ICommandBridgeRemote = {
4747
async execute(command: string, args: ReadonlyPartialJSONObject) {
48-
await commands.execute(command, args);
48+
const result = await commands.execute(command, args);
49+
try {
50+
// Attempt to clone result to check if it's serializable
51+
const clone = structuredClone(result);
52+
return clone;
53+
} catch (error) {
54+
// Non-serializable values can't be sent over Comlink
55+
void error;
56+
}
4957
},
50-
listCommands() {
58+
async listCommands() {
5159
return commands.listCommands();
60+
},
61+
get ready() {
62+
return app.started;
5263
}
5364
};
5465

55-
const endpoint = windowEndpoint(self.parent);
56-
expose(api, endpoint);
66+
app.started.then(() => {
67+
const endpoint = windowEndpoint(self.parent);
68+
expose(api, endpoint);
69+
window.parent?.postMessage('extension-loaded', '*');
70+
});
5771
}
5872
};
5973

packages/extension/src/interface.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,17 @@ export interface ICommandBridgeRemote {
1212
* @param command - The name of the command to execute.
1313
* @param args - An object containing arguments for the command.
1414
*/
15-
execute(command: string, args: ReadonlyPartialJSONObject): void;
15+
execute(command: string, args: ReadonlyPartialJSONObject): Promise<unknown>;
1616

1717
/**
1818
* Lists all available commands.
1919
*
2020
* @returns An array of strings representing the names of all available commands.
2121
*/
22-
listCommands(): string[];
22+
listCommands(): Promise<string[]>;
23+
24+
/**
25+
* Waits for the JupyterLab environment to be ready.
26+
*/
27+
ready: Promise<void>;
2328
}

packages/host/src/index.ts

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,15 @@
33
import { windowEndpoint, wrap, proxy, ProxyOrClone } from 'comlink';
44

55
import { ICommandBridgeRemote } from 'jupyter-iframe-commands';
6+
67
/**
78
* A bridge to expose actions on JupyterLab commands.
89
*/
9-
export function createBridge({ iframeId }: { iframeId: string }) {
10+
export function createBridge({
11+
iframeId
12+
}: {
13+
iframeId: string;
14+
}): ICommandBridgeRemote {
1015
const iframe = document.getElementById(iframeId) as HTMLIFrameElement;
1116

1217
if (!iframe) {
@@ -21,7 +26,46 @@ export function createBridge({ iframeId }: { iframeId: string }) {
2126
);
2227
}
2328

24-
return wrap<ICommandBridgeRemote>(windowEndpoint(iframe.contentWindow));
29+
const wrappedBridge = wrap<ICommandBridgeRemote>(
30+
windowEndpoint(iframe.contentWindow)
31+
);
32+
33+
// Create a promise that resolves when the iframe signals it's ready
34+
const readyPromise = new Promise<void>(resolve => {
35+
const controller = new AbortController();
36+
const signal = controller.signal;
37+
38+
const messageHandler = (event: MessageEvent) => {
39+
if (event.data === 'extension-loaded') {
40+
controller.abort();
41+
resolve();
42+
}
43+
};
44+
45+
window.addEventListener('message', messageHandler, { signal });
46+
47+
wrappedBridge.ready
48+
.then(() => {
49+
controller.abort();
50+
resolve();
51+
})
52+
.catch(() => {
53+
// If ready() fails, we'll still wait for the message
54+
// No need to do anything, the message listener is still active
55+
});
56+
});
57+
58+
// Create a proxy that intercepts property access
59+
return new Proxy(wrappedBridge, {
60+
get(target, prop, receiver) {
61+
// If the property is 'ready', return the ready promise
62+
if (prop === 'ready') {
63+
return readyPromise;
64+
}
65+
// Otherwise delegate to the comlink wrapped bridge
66+
return Reflect.get(target, prop, receiver);
67+
}
68+
});
2569
}
2670

2771
/**

ui-tests/playwright.config.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ module.exports = {
77
...baseConfig,
88
retries: process.env.CI ? 2 : 0,
99
use: {
10-
trace: 'on-first-retry'
10+
trace: 'on-first-retry',
11+
video: 'retain-on-failure'
1112
},
1213
webServer: [
1314
{

ui-tests/tests/hostpage-tests.spec.ts

Lines changed: 19 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { test, expect, Page } from '@playwright/test';
1+
import { expect, Page, test } from '@playwright/test';
22

33
const waitForApp = async (page: Page) => {
44
const iframe = page.locator('#jupyterlab').contentFrame();
@@ -11,8 +11,8 @@ const waitForApp = async (page: Page) => {
1111
test.use({
1212
baseURL: 'http://localhost:8080',
1313
viewport: {
14-
width: 1024,
15-
height: 768
14+
width: 1280,
15+
height: 1024
1616
}
1717
});
1818
/**
@@ -22,36 +22,29 @@ test.describe('Commands from host should affect lab in iframe', () => {
2222
test.beforeEach(async ({ page }) => {
2323
await page.goto('index.html');
2424

25-
const iframe = page.locator('#jupyterlab').contentFrame();
25+
const iframe = page
26+
.locator('iframe[title="JupyterLab Instance"]')
27+
.contentFrame();
2628

2729
await iframe.locator('.jp-LauncherCard-icon').first().waitFor();
2830

29-
// Make sure left sidebar is hidden
30-
await iframe.getByText('View', { exact: true }).click();
31-
await iframe.getByText('Appearance').hover();
32-
await iframe
33-
.locator('#jp-mainmenu-view-appearance')
34-
.getByText('Show Left Sidebar')
35-
.waitFor();
36-
3731
const leftSidebarOpen = await iframe
38-
.getByRole('menuitem', { name: 'Show Left Sidebar Ctrl+B' })
39-
.getByRole('img')
32+
.getByRole('button', { name: 'New Folder' })
4033
.isVisible();
4134

4235
if (leftSidebarOpen) {
43-
await iframe
44-
.locator('#jp-mainmenu-view-appearance')
45-
.getByText('Show Left Sidebar')
46-
.click();
36+
await iframe.getByText('View', { exact: true }).click();
37+
await iframe.getByText('Appearance').hover();
38+
await iframe.getByText('Show Left Sidebar').waitFor();
39+
await iframe.getByText('Show Left Sidebar').click();
4740
}
4841

4942
await iframe.locator('#jp-MainLogo').click();
5043

5144
await waitForApp(page);
5245
});
5346

54-
test('Swich to light theme', async ({ page }) => {
47+
test('Switch to light theme', async ({ page }) => {
5548
await page
5649
.getByPlaceholder('Enter a command')
5750
.fill('apputils:change-theme');
@@ -62,10 +55,12 @@ test.describe('Commands from host should affect lab in iframe', () => {
6255

6356
await waitForApp(page);
6457

65-
await expect(page).toHaveScreenshot('light-theme.png', { timeout: 1500 });
58+
await expect(page).toHaveScreenshot('light-theme.png', {
59+
timeout: 1500
60+
});
6661
});
6762

68-
test('Swich to dark theme', async ({ page }) => {
63+
test('Switch to dark theme', async ({ page }) => {
6964
await page
7065
.getByPlaceholder('Enter a command')
7166
.fill('apputils:change-theme');
@@ -76,7 +71,9 @@ test.describe('Commands from host should affect lab in iframe', () => {
7671

7772
await waitForApp(page);
7873

79-
await expect(page).toHaveScreenshot('dark-theme.png', { timeout: 1500 });
74+
await expect(page).toHaveScreenshot('dark-theme.png', {
75+
timeout: 1500
76+
});
8077
});
8178

8279
test('Open a new notebook', async ({ page }) => {
Loading
Loading

0 commit comments

Comments
 (0)