-
Notifications
You must be signed in to change notification settings - Fork 0
fix: Add ESLint rules for type safety and fix violations #46
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
saltenasl
merged 16 commits into
main
from
devin/1761239803-add-eslint-type-safety-rules
Oct 27, 2025
Merged
Changes from 15 commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
18782e5
Add ESLint rules for type safety and fix violations
devin-ai-integration[bot] 87b67d5
Add test for error handling in NotebookPicker constructor
devin-ai-integration[bot] 9e2eda0
Add comprehensive test coverage for NotebookPicker
devin-ai-integration[bot] 0e32ba8
Merge test coverage from PR #45
devin-ai-integration[bot] 684667c
fix: remove mocks, test real implementation
dinohamzic b7738b4
chore: format
dinohamzic 10562ab
chore: remove out of scope tests
dinohamzic 6659212
chore: remove out of scope test
dinohamzic 8887ef7
refactor: fix test and format
dinohamzic ccc6169
chore: remove out of scope test
dinohamzic c2223c9
fix: address violations
dinohamzic 03da012
refactor: improve test
dinohamzic e4e92ba
refactor: TypeError
dinohamzic e09f7e6
fix: drop unnecessary property
dinohamzic 9849c7b
refactor: simplify promise
dinohamzic edbd6c1
chore: improve "dirty" test
dinohamzic File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,111 +1,269 @@ | ||
| // Copyright (c) Deepnote | ||
| // Distributed under the terms of the Modified BSD License. | ||
|
|
||
| import { NotebookPicker } from '../../src/components/NotebookPicker'; | ||
| import type { INotebookModel, NotebookPanel } from '@jupyterlab/notebook'; | ||
| import { framePromise } from '@jupyterlab/testing'; | ||
| import { NotebookPanel } from '@jupyterlab/notebook'; | ||
| import { INotebookModel } from '@jupyterlab/notebook'; | ||
| import type { PartialJSONObject } from '@lumino/coreutils'; | ||
| import { Widget } from '@lumino/widgets'; | ||
| import { simulate } from 'simulate-event'; | ||
| import { NotebookPicker } from '../components/NotebookPicker'; | ||
|
|
||
| describe('NotebookPicker', () => { | ||
| let panel: NotebookPanel; | ||
| let model: INotebookModel; | ||
| let widget: NotebookPicker; | ||
| let mockNotebookModel: Partial<INotebookModel>; | ||
| let deepnoteMetadata: PartialJSONObject; | ||
| let consoleErrorSpy: jest.SpyInstance | null = null; | ||
|
|
||
| beforeEach(async () => { | ||
| // Mock model + metadata | ||
| model = { | ||
| const createMockPanel = (metadata: PartialJSONObject): NotebookPanel => { | ||
| deepnoteMetadata = metadata; | ||
|
|
||
| mockNotebookModel = { | ||
| fromJSON: jest.fn(), | ||
| get cells() { | ||
| return []; | ||
| }, | ||
| dirty: true | ||
| } as any; | ||
| dirty: false, | ||
| getMetadata: jest.fn((key: string) => { | ||
| if (key === 'deepnote') { | ||
| return deepnoteMetadata; | ||
| } | ||
| return undefined; | ||
| }) | ||
| }; | ||
|
|
||
| panel = { | ||
| return { | ||
| context: { | ||
| ready: Promise.resolve(), | ||
| model: { | ||
| getMetadata: jest.fn().mockReturnValue({ | ||
| notebooks: { | ||
| nb1: { id: 'nb1', name: 'nb1', cells: [{ source: 'code' }] }, | ||
| nb2: { id: 'nb2', name: 'nb2', cells: [] } | ||
| }, | ||
| notebook_names: ['nb1', 'nb2'] | ||
| }) | ||
| } | ||
| model: mockNotebookModel as INotebookModel | ||
| }, | ||
| model | ||
| } as any; | ||
| model: mockNotebookModel as INotebookModel | ||
| } as unknown as NotebookPanel; | ||
| }; | ||
|
|
||
| // Attach to DOM | ||
| const widget = new NotebookPicker(panel); | ||
| // Override onAfterAttach to avoid errors from this.parent being null | ||
| (widget as any).onAfterAttach = jest.fn(); | ||
| const attachWidget = async (panel: NotebookPanel): Promise<void> => { | ||
| widget = new NotebookPicker(panel); | ||
| Widget.attach(widget, document.body); | ||
| // Wait for widget to attach and render | ||
| await framePromise(); | ||
| }); | ||
| // Wait for constructor's async initialization to complete | ||
| await Promise.resolve(); | ||
| await framePromise(); | ||
| }; | ||
|
|
||
| afterEach(() => { | ||
| document.body.innerHTML = ''; | ||
| jest.restoreAllMocks(); | ||
| if (consoleErrorSpy) { | ||
| consoleErrorSpy.mockRestore(); | ||
| consoleErrorSpy = null; | ||
| } | ||
| if (widget && !widget.isDisposed) { | ||
| widget.dispose(); | ||
| } | ||
| // Clean up DOM | ||
| const attached = document.querySelectorAll('.jp-ReactWidget'); | ||
| attached.forEach(node => { | ||
| node.remove(); | ||
| }); | ||
| }); | ||
|
|
||
| it('should render a select element', async () => { | ||
| await framePromise(); // wait for rendering | ||
| const select = document.querySelector('select') as HTMLSelectElement; | ||
| expect(select).not.toBeNull(); | ||
| expect(select.options.length).toBe(2); | ||
| expect(select.options[0] && select.options[0].value).toBe('nb1'); | ||
| describe('rendering', () => { | ||
| it('should render a select element with notebooks', async () => { | ||
| const metadata = { | ||
| notebooks: { | ||
| 'Notebook 1': { id: 'nb1', name: 'Notebook 1', cells: [] }, | ||
| 'Notebook 2': { id: 'nb2', name: 'Notebook 2', cells: [] } | ||
| } | ||
| }; | ||
|
|
||
| const panel = createMockPanel(metadata); | ||
| await attachWidget(panel); | ||
|
|
||
| const select = widget.node.querySelector('select'); | ||
| expect(select).not.toBeNull(); | ||
| expect(select?.options.length).toBe(2); | ||
| expect(select?.options[0]?.value).toBe('Notebook 1'); | ||
| expect(select?.options[1]?.value).toBe('Notebook 2'); | ||
| }); | ||
|
|
||
| it('should render a placeholder when no notebooks are available', async () => { | ||
| const metadata = { | ||
| notebooks: {} | ||
| }; | ||
|
|
||
| const panel = createMockPanel(metadata); | ||
| await attachWidget(panel); | ||
|
|
||
| const select = widget.node.querySelector('select'); | ||
| expect(select).not.toBeNull(); | ||
| expect(select?.options.length).toBe(1); | ||
| expect(select?.options[0]?.value).toBe('-'); | ||
| }); | ||
|
|
||
| it('should handle invalid metadata gracefully', async () => { | ||
| const metadata = { | ||
| notebooks: null | ||
| } as PartialJSONObject; | ||
|
|
||
| const panel = createMockPanel(metadata); | ||
| await attachWidget(panel); | ||
|
|
||
| const select = widget.node.querySelector('select'); | ||
| expect(select).not.toBeNull(); | ||
| expect(select?.options.length).toBe(1); | ||
| expect(select?.options[0]?.value).toBe('-'); | ||
| }); | ||
| }); | ||
|
|
||
| it('should call fromJSON when selecting a notebook', async () => { | ||
| const select = document.querySelector('select') as HTMLSelectElement; | ||
| simulate(select, 'change', { target: { value: 'nb2' } }); | ||
| await framePromise(); | ||
| expect(model.fromJSON).toHaveBeenCalledWith( | ||
| expect.objectContaining({ | ||
| cells: expect.any(Array), | ||
| metadata: expect.objectContaining({ | ||
| deepnote: expect.objectContaining({ | ||
| notebooks: expect.any(Object) | ||
| }) | ||
| describe('notebook selection', () => { | ||
| let panel: NotebookPanel; | ||
|
|
||
| beforeEach(async () => { | ||
| const metadata = { | ||
| notebooks: { | ||
| 'Notebook 1': { | ||
| id: 'nb1', | ||
| name: 'Notebook 1', | ||
| cells: [{ cell_type: 'code', source: 'print(1)' }] | ||
| }, | ||
| 'Notebook 2': { | ||
| id: 'nb2', | ||
| name: 'Notebook 2', | ||
| cells: [{ cell_type: 'code', source: 'print(2)' }] | ||
| } | ||
| } | ||
| }; | ||
|
|
||
| panel = createMockPanel(metadata); | ||
| await attachWidget(panel); | ||
| }); | ||
|
|
||
| it('should call fromJSON when selecting a different notebook', async () => { | ||
| const select = widget.node.querySelector('select') as HTMLSelectElement; | ||
| expect(select).not.toBeNull(); | ||
|
|
||
| select.value = 'Notebook 2'; | ||
| select.dispatchEvent(new Event('change', { bubbles: true })); | ||
| await framePromise(); | ||
|
|
||
| expect(mockNotebookModel.fromJSON).toHaveBeenCalledTimes(1); | ||
| expect(mockNotebookModel.fromJSON).toHaveBeenCalledWith( | ||
| expect.objectContaining({ | ||
| cells: [{ cell_type: 'code', source: 'print(2)' }], | ||
| metadata: { | ||
| deepnote: { | ||
| notebooks: expect.objectContaining({ | ||
| 'Notebook 1': expect.any(Object), | ||
| 'Notebook 2': expect.any(Object) | ||
| }) | ||
| } | ||
| }, | ||
| nbformat: 4, | ||
| nbformat_minor: 0 | ||
| }) | ||
| }) | ||
| ); | ||
| }); | ||
| ); | ||
| }); | ||
|
|
||
| it('should not call fromJSON if selected notebook is invalid', async () => { | ||
| const getMetadata = panel.context.model.getMetadata as jest.Mock; | ||
| getMetadata.mockReturnValue({ notebooks: {}, notebook_names: [] }); | ||
| it('should set model.dirty to false after switching notebooks', async () => { | ||
| const select = widget.node.querySelector('select') as HTMLSelectElement; | ||
| select.value = 'Notebook 2'; | ||
| select.dispatchEvent(new Event('change', { bubbles: true })); | ||
| await framePromise(); | ||
|
|
||
| const select = document.querySelector('select') as HTMLSelectElement; | ||
| simulate(select, 'change', { target: { value: 'nonexistent' } }); | ||
| await framePromise(); | ||
| expect(model.fromJSON).not.toHaveBeenCalled(); | ||
| expect(mockNotebookModel.dirty).toBe(false); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the default is already dirty = false (line 21)
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good eyes 👀 fixed! |
||
| }); | ||
|
|
||
| it('should not call fromJSON when selecting a non-existent notebook', async () => { | ||
| const select = widget.node.querySelector('select') as HTMLSelectElement; | ||
| select.value = 'NonExistent'; | ||
| select.dispatchEvent(new Event('change', { bubbles: true })); | ||
| await framePromise(); | ||
|
|
||
| expect(mockNotebookModel.fromJSON).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it('should not call fromJSON when panel.model is null', async () => { | ||
| widget.dispose(); | ||
|
|
||
| // Create panel with null model | ||
| const nullModelPanel = { | ||
| context: { | ||
| ready: Promise.resolve(), | ||
| model: { | ||
| getMetadata: jest.fn(() => deepnoteMetadata) | ||
| } | ||
| }, | ||
| model: null | ||
| } as unknown as NotebookPanel; | ||
dinohamzic marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| await attachWidget(nullModelPanel); | ||
dinohamzic marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| const select = widget.node.querySelector('select') as HTMLSelectElement; | ||
| select.value = 'Notebook 2'; | ||
| select.dispatchEvent(new Event('change', { bubbles: true })); | ||
| await framePromise(); | ||
|
|
||
| expect(mockNotebookModel.fromJSON).not.toHaveBeenCalled(); | ||
| }); | ||
| }); | ||
|
|
||
| it('should update UI after selection', async () => { | ||
| const select = document.querySelector('select') as HTMLSelectElement; | ||
| select.value = 'nb2'; | ||
| simulate(select, 'change'); | ||
| await framePromise(); | ||
| expect(select.value).toBe('nb2'); | ||
| describe('initialization', () => { | ||
| it('should select first notebook by default when notebooks exist', async () => { | ||
| const metadata = { | ||
| notebooks: { | ||
| First: { id: 'nb1', name: 'First', cells: [] }, | ||
| Second: { id: 'nb2', name: 'Second', cells: [] } | ||
| } | ||
| }; | ||
|
|
||
| const panel = createMockPanel(metadata); | ||
| await attachWidget(panel); | ||
|
|
||
| const select = widget.node.querySelector('select') as HTMLSelectElement; | ||
| expect(select.value).toBe('First'); | ||
| }); | ||
|
|
||
| it('should handle initialization errors gracefully', async () => { | ||
| consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(); | ||
|
|
||
| const failingPanel = { | ||
| context: { | ||
| ready: Promise.reject(new Error('Initialization failed')), | ||
| model: { | ||
| getMetadata: jest.fn(() => ({})) | ||
| } | ||
| }, | ||
| model: mockNotebookModel as INotebookModel | ||
| } as unknown as NotebookPanel; | ||
dinohamzic marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| await attachWidget(failingPanel); | ||
dinohamzic marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| await framePromise(); | ||
|
|
||
| expect(consoleErrorSpy).toHaveBeenCalledWith( | ||
| 'Failed to initialize NotebookPicker:', | ||
| expect.any(Error) | ||
| ); | ||
| }); | ||
| }); | ||
|
|
||
| it('should handle empty metadata gracefully', async () => { | ||
| const getMetadata = panel.context.model.getMetadata as jest.Mock; | ||
| getMetadata.mockReturnValue({ notebooks: {}, notebook_names: [] }); | ||
| describe('metadata validation', () => { | ||
| it('should handle invalid metadata when changing notebooks', async () => { | ||
| consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(); | ||
|
|
||
| document.body.innerHTML = ''; | ||
| const widget = new NotebookPicker(panel); | ||
| // Override onAfterAttach to avoid errors from this.parent being null | ||
| (widget as any).onAfterAttach = jest.fn(); | ||
| Widget.attach(widget, document.body); | ||
| await framePromise(); | ||
| const metadata = { | ||
| notebooks: { | ||
| 'Notebook 1': { id: 'nb1', name: 'Notebook 1', cells: [] } | ||
| } | ||
| }; | ||
|
|
||
| const panel = createMockPanel(metadata); | ||
| await attachWidget(panel); | ||
|
|
||
| // Change the metadata to invalid format | ||
| deepnoteMetadata = { invalid: 'metadata' } as PartialJSONObject; | ||
|
|
||
| const select = widget.node.querySelector('select') as HTMLSelectElement; | ||
| select.value = 'Notebook 1'; | ||
| select.dispatchEvent(new Event('change', { bubbles: true })); | ||
| await framePromise(); | ||
|
|
||
| const select = document.querySelector('select') as HTMLSelectElement; | ||
| expect(select.options.length).toBeGreaterThanOrEqual(1); | ||
| expect(select.options[0] && select.options[0].value).toBe('-'); | ||
| expect(consoleErrorSpy).toHaveBeenCalled(); | ||
| expect(consoleErrorSpy.mock.calls[0]?.[0]).toMatch(/invalid.*metadata/i); | ||
| expect(mockNotebookModel.fromJSON).not.toHaveBeenCalled(); | ||
| }); | ||
| }); | ||
| }); | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.