Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ const esModules = [
'@codemirror',
'@jupyter',
'@microsoft',
'@deepnote',
'exenv-es6',
'lib0',
'nanoid',
'vscode-ws-jsonrpc',
'y-protocols',
'y-websocket',
'yjs'
'yjs',
'yaml'
].join('|');

const baseConfig = jestJupyterLab(__dirname);
Expand Down
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -161,9 +161,12 @@
"args": "none"
}
],
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/no-namespace": "off",
"@typescript-eslint/no-use-before-define": "off",
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/no-non-null-assertion": "error",
"@typescript-eslint/prefer-nullish-coalescing": "error",
"@typescript-eslint/quotes": [
"error",
"single",
Expand Down
89 changes: 89 additions & 0 deletions src/__tests__/NotebookPicker.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { framePromise } from '@jupyterlab/testing';
import { NotebookPanel } from '@jupyterlab/notebook';
import { INotebookModel } from '@jupyterlab/notebook';
import { Widget } from '@lumino/widgets';
import { Message } from '@lumino/messaging';
import { simulate } from 'simulate-event';

describe('NotebookPicker', () => {
Expand Down Expand Up @@ -108,4 +109,92 @@ describe('NotebookPicker', () => {
expect(select.options.length).toBeGreaterThanOrEqual(1);
expect(select.options[0] && select.options[0].value).toBe('-');
});

it('should handle context.ready rejection gracefully', async () => {
const consoleErrorSpy = jest
.spyOn(console, 'error')
.mockImplementation(() => {});
const errorPanel = {
context: {
ready: Promise.reject(new Error('Failed to initialize')),
model: {
getMetadata: jest.fn()
}
},
model
} as any;

document.body.innerHTML = '';
const widget = new NotebookPicker(errorPanel);
(widget as any).onAfterAttach = jest.fn();
Widget.attach(widget, document.body);
await framePromise();

await new Promise(resolve => setTimeout(resolve, 10));

expect(consoleErrorSpy).toHaveBeenCalledWith(
'Failed to initialize NotebookPicker:',
expect.any(Error)
);

consoleErrorSpy.mockRestore();
});

it('should handle null model in handleChange', async () => {
const nullModelPanel = {
context: {
ready: Promise.resolve(),
model: {
getMetadata: jest.fn().mockReturnValue({
notebooks: {
nb1: { id: 'nb1', name: 'nb1', cells: [] }
},
notebook_names: ['nb1']
})
}
},
model: null
} as any;

document.body.innerHTML = '';
const widget = new NotebookPicker(nullModelPanel);
(widget as any).onAfterAttach = jest.fn();
Widget.attach(widget, document.body);
await framePromise();

const select = document.querySelector('select') as HTMLSelectElement;
simulate(select, 'change', { target: { value: 'nb1' } });
await framePromise();
});

it('should handle invalid metadata in handleChange', async () => {
const consoleErrorSpy = jest
.spyOn(console, 'error')
.mockImplementation(() => {});
const getMetadata = panel.context.model.getMetadata as jest.Mock;
getMetadata.mockReturnValue({ invalid: 'metadata' });

const select = document.querySelector('select') as HTMLSelectElement;
simulate(select, 'change', { target: { value: 'nb1' } });
await framePromise();

expect(consoleErrorSpy).toHaveBeenCalledWith(
'Invalid deepnote metadata:',
expect.anything()
);
expect(model.fromJSON).not.toHaveBeenCalled();

consoleErrorSpy.mockRestore();
});

it('should handle onAfterAttach without parent', async () => {
document.body.innerHTML = '';
const widget = new NotebookPicker(panel);
Widget.attach(widget, document.body);
await framePromise();

const onAfterAttachMethod = (widget as any).onAfterAttach.bind(widget);
onAfterAttachMethod({} as Message);
await new Promise(resolve => requestAnimationFrame(resolve));
});
});
249 changes: 249 additions & 0 deletions src/__tests__/convert-deepnote-block-to-jupyter-cell.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
// Copyright (c) Deepnote
// Distributed under the terms of the Modified BSD License.

import type { DeepnoteBlock } from '@deepnote/blocks';
import { convertDeepnoteBlockToJupyterCell } from '../convert-deepnote-block-to-jupyter-cell';

describe('convertDeepnoteBlockToJupyterCell', () => {
describe('code cells', () => {
it('should convert a basic code block to a Jupyter code cell', () => {
const block: DeepnoteBlock = {
id: 'block-1',
type: 'code',
content: 'print("hello")',
metadata: { foo: 'bar' },
sortingKey: '1'
};

const result = convertDeepnoteBlockToJupyterCell(block);

expect(result.cell_type).toBe('code');
expect(result.metadata).toEqual({ foo: 'bar', cell_id: 'block-1' });
expect(result.source).toBe('print("hello")');
expect(result.execution_count).toBeNull();
expect(result.outputs).toEqual([]);
});

it('should include execution count if present', () => {
const block: DeepnoteBlock = {
id: 'block-2',
type: 'code',
content: 'x = 1',
metadata: {},
executionCount: 5,
sortingKey: '1'
};

const result = convertDeepnoteBlockToJupyterCell(block);

expect(result.cell_type).toBe('code');
expect(result.execution_count).toBe(5);
});

it('should include outputs if present', () => {
const blockOutputs = [
{
output_type: 'stream',
name: 'stdout',
text: 'hello\n'
}
];

const block: DeepnoteBlock = {
id: 'block-3',
type: 'code',
content: 'print("hello")',
metadata: {},
outputs: blockOutputs,
sortingKey: '1'
};

const result = convertDeepnoteBlockToJupyterCell(block);

expect(result.cell_type).toBe('code');
expect(result.outputs).toEqual(blockOutputs);
});

it('should remove truncated property from outputs', () => {
const blockOutputs = [
{
output_type: 'stream',
name: 'stdout',
text: 'hello\n',
truncated: true
}
];

const block: DeepnoteBlock = {
id: 'block-4',
type: 'code',
content: 'print("hello")',
metadata: {},
outputs: blockOutputs,
sortingKey: '1'
};

const result = convertDeepnoteBlockToJupyterCell(block);

expect(result.cell_type).toBe('code');
expect(result.outputs).toHaveLength(1);
const resultOutputs = result.outputs as Array<{ output_type: string; name: string; text: string }>;
expect(resultOutputs[0]).not.toHaveProperty('truncated');
expect(resultOutputs[0]).toEqual({
output_type: 'stream',
name: 'stdout',
text: 'hello\n'
});
});

it('should handle multiple outputs with truncated properties', () => {
const blockOutputs = [
{
output_type: 'stream',
name: 'stdout',
text: 'line1\n',
truncated: true
},
{
output_type: 'stream',
name: 'stdout',
text: 'line2\n',
truncated: false
}
];

const block: DeepnoteBlock = {
id: 'block-5',
type: 'code',
content: 'print("test")',
metadata: {},
outputs: blockOutputs,
sortingKey: '1'
};

const result = convertDeepnoteBlockToJupyterCell(block);

expect(result.cell_type).toBe('code');
expect(result.outputs).toHaveLength(2);
const resultOutputs = result.outputs as Array<{ output_type: string; name: string; text: string }>;
expect(resultOutputs[0]).not.toHaveProperty('truncated');
expect(resultOutputs[1]).not.toHaveProperty('truncated');
});

it('should not mutate the original block', () => {
const blockOutputs = [
{
output_type: 'stream',
name: 'stdout',
text: 'hello\n',
truncated: true
}
];

const block: DeepnoteBlock = {
id: 'block-6',
type: 'code',
content: 'print("hello")',
metadata: { test: 'value' },
outputs: blockOutputs,
sortingKey: '1'
};

convertDeepnoteBlockToJupyterCell(block);

expect(block.outputs?.[0]).toHaveProperty('truncated');
expect(block.metadata).toEqual({ test: 'value' });
});
});

describe('markdown cells', () => {
it('should convert a basic markdown block to a Jupyter markdown cell', () => {
const block: DeepnoteBlock = {
id: 'block-7',
type: 'markdown',
content: '# Hello',
metadata: { foo: 'bar' },
sortingKey: '1'
};

const result = convertDeepnoteBlockToJupyterCell(block);

expect(result.cell_type).toBe('markdown');
expect(result.metadata).toEqual({});
expect(result.source).toBe('# Hello');
});

it('should convert text-cell-h1 to markdown cell', () => {
const block: DeepnoteBlock = {
id: 'block-8',
type: 'text-cell-h1',
content: 'Heading 1',
metadata: {},
sortingKey: '1'
};

const result = convertDeepnoteBlockToJupyterCell(block);

expect(result.cell_type).toBe('markdown');
});

it('should convert image block to markdown cell', () => {
const block: DeepnoteBlock = {
id: 'block-9',
type: 'image',
content: '![alt](url)',
metadata: {},
sortingKey: '1'
};

const result = convertDeepnoteBlockToJupyterCell(block);

expect(result.cell_type).toBe('markdown');
});

it('should not include metadata from Deepnote block in markdown cells', () => {
const block: DeepnoteBlock = {
id: 'block-10',
type: 'markdown',
content: 'Text',
metadata: { deepnoteMetadata: 'should not appear' },
sortingKey: '1'
};

const result = convertDeepnoteBlockToJupyterCell(block);

expect(result.cell_type).toBe('markdown');
expect(result.metadata).toEqual({});
});
});

describe('special block types', () => {
it('should convert sql block to code cell', () => {
const block: DeepnoteBlock = {
id: 'block-11',
type: 'sql',
content: 'SELECT * FROM table',
metadata: {},
sortingKey: '1'
};

const result = convertDeepnoteBlockToJupyterCell(block);

expect(result.cell_type).toBe('code');
});

it('should convert visualization block to code cell', () => {
const block: DeepnoteBlock = {
id: 'block-12',
type: 'visualization',
content: 'chart_data',
metadata: {},
sortingKey: '1'
};

const result = convertDeepnoteBlockToJupyterCell(block);

expect(result.cell_type).toBe('code');
});
});
});
Loading
Loading