Skip to content

Commit 5d0e9b0

Browse files
fix(ui): fix toolbar layout on file load (#31)
* fix(ui): fix toolbar layout on file load Signed-off-by: Andy Jakubowski <hello@andyjakubowski.com> * Add tests to NotebookPicker Signed-off-by: Andy Jakubowski <hello@andyjakubowski.com> * Update cspell, formatting Signed-off-by: Andy Jakubowski <hello@andyjakubowski.com> * Fix test Signed-off-by: Andy Jakubowski <hello@andyjakubowski.com> --------- Signed-off-by: Andy Jakubowski <hello@andyjakubowski.com>
1 parent 2ff5a4c commit 5d0e9b0

File tree

4 files changed

+125
-3
lines changed

4 files changed

+125
-3
lines changed

cspell.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
"coreutils",
4040
"csstree",
4141
"deepnote",
42+
"exenv",
4243
"ipynb",
4344
"Jakubowski",
4445
"jlpm",

jest.config.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ const jestJupyterLab = require('@jupyterlab/testutils/lib/jest-config');
22

33
const esModules = [
44
'@codemirror',
5-
'@jupyter/ydoc',
6-
'@jupyterlab/',
5+
'@jupyter',
6+
'@microsoft',
7+
'exenv-es6',
78
'lib0',
89
'nanoid',
910
'vscode-ws-jsonrpc',
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
// Copyright (c) Deepnote
2+
// Distributed under the terms of the Modified BSD License.
3+
4+
import { NotebookPicker } from '../../src/components/NotebookPicker';
5+
import { framePromise } from '@jupyterlab/testing';
6+
import { NotebookPanel } from '@jupyterlab/notebook';
7+
import { INotebookModel } from '@jupyterlab/notebook';
8+
import { Widget } from '@lumino/widgets';
9+
import { simulate } from 'simulate-event';
10+
11+
describe('NotebookPicker', () => {
12+
let panel: NotebookPanel;
13+
let model: INotebookModel;
14+
15+
beforeEach(async () => {
16+
// Mock model + metadata
17+
model = {
18+
fromJSON: jest.fn(),
19+
get cells() {
20+
return [];
21+
},
22+
dirty: true
23+
} as any;
24+
25+
panel = {
26+
context: {
27+
ready: Promise.resolve(),
28+
model: {
29+
getMetadata: jest.fn().mockReturnValue({
30+
notebooks: {
31+
nb1: { id: 'nb1', name: 'nb1', cells: [{ source: 'code' }] },
32+
nb2: { id: 'nb2', name: 'nb2', cells: [] }
33+
},
34+
notebook_names: ['nb1', 'nb2']
35+
})
36+
}
37+
},
38+
model
39+
} as any;
40+
41+
// Attach to DOM
42+
const widget = new NotebookPicker(panel);
43+
// Override onAfterAttach to avoid errors from this.parent being null
44+
(widget as any).onAfterAttach = jest.fn();
45+
Widget.attach(widget, document.body);
46+
await framePromise();
47+
});
48+
49+
afterEach(() => {
50+
document.body.innerHTML = '';
51+
jest.restoreAllMocks();
52+
});
53+
54+
it('should render a select element', async () => {
55+
await framePromise(); // wait for rendering
56+
const select = document.querySelector('select') as HTMLSelectElement;
57+
expect(select).not.toBeNull();
58+
expect(select.options.length).toBe(2);
59+
expect(select.options[0] && select.options[0].value).toBe('nb1');
60+
});
61+
62+
it('should call fromJSON when selecting a notebook', async () => {
63+
const select = document.querySelector('select') as HTMLSelectElement;
64+
simulate(select, 'change', { target: { value: 'nb2' } });
65+
await framePromise();
66+
expect(model.fromJSON).toHaveBeenCalledWith(
67+
expect.objectContaining({
68+
cells: expect.any(Array),
69+
metadata: expect.objectContaining({
70+
deepnote: expect.objectContaining({
71+
notebooks: expect.any(Object)
72+
})
73+
})
74+
})
75+
);
76+
});
77+
78+
it('should not call fromJSON if selected notebook is invalid', async () => {
79+
const getMetadata = panel.context.model.getMetadata as jest.Mock;
80+
getMetadata.mockReturnValue({ notebooks: {}, notebook_names: [] });
81+
82+
const select = document.querySelector('select') as HTMLSelectElement;
83+
simulate(select, 'change', { target: { value: 'nonexistent' } });
84+
await framePromise();
85+
expect(model.fromJSON).not.toHaveBeenCalled();
86+
});
87+
88+
it('should update UI after selection', async () => {
89+
const select = document.querySelector('select') as HTMLSelectElement;
90+
select.value = 'nb2';
91+
simulate(select, 'change');
92+
await framePromise();
93+
expect(select.value).toBe('nb2');
94+
});
95+
96+
it('should handle empty metadata gracefully', async () => {
97+
const getMetadata = panel.context.model.getMetadata as jest.Mock;
98+
getMetadata.mockReturnValue({ notebooks: {}, notebook_names: [] });
99+
100+
document.body.innerHTML = '';
101+
const widget = new NotebookPicker(panel);
102+
// Override onAfterAttach to avoid errors from this.parent being null
103+
(widget as any).onAfterAttach = jest.fn();
104+
Widget.attach(widget, document.body);
105+
await framePromise();
106+
107+
const select = document.querySelector('select') as HTMLSelectElement;
108+
expect(select.options.length).toBeGreaterThanOrEqual(1);
109+
expect(select.options[0] && select.options[0].value).toBe('-');
110+
});
111+
});

src/components/NotebookPicker.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { ReactWidget } from '@jupyterlab/apputils';
33
import { NotebookPanel } from '@jupyterlab/notebook';
44
import { HTMLSelect } from '@jupyterlab/ui-components';
55
import { deepnoteMetadataSchema } from '../types';
6+
import { Widget } from '@lumino/widgets';
7+
import { Message, MessageLoop } from '@lumino/messaging';
68

79
export class NotebookPicker extends ReactWidget {
810
private selected: string | null = null;
@@ -63,6 +65,13 @@ export class NotebookPicker extends ReactWidget {
6365
this.update();
6466
};
6567

68+
protected onAfterAttach(msg: Message): void {
69+
super.onAfterAttach(msg);
70+
requestAnimationFrame(() => {
71+
MessageLoop.sendMessage(this.parent!, Widget.ResizeMessage.UnknownSize);
72+
});
73+
}
74+
6675
render(): JSX.Element {
6776
const deepnoteMetadata = this.panel.context.model.getMetadata('deepnote');
6877

@@ -81,7 +90,7 @@ export class NotebookPicker extends ReactWidget {
8190
aria-label="Select active notebook"
8291
title="Select active notebook"
8392
style={{
84-
maxWidth: '120px',
93+
width: '120px',
8594
textOverflow: 'ellipsis',
8695
whiteSpace: 'nowrap',
8796
overflow: 'hidden'

0 commit comments

Comments
 (0)