/** * Unit tests for OpenCodeCellActions DOM rendering and button disabled states. * * Mocks @jupyterlab/cells, @jupyterlab/apputils, and @lumino/widgets at the * test boundary so the real ESM packages are not loaded (Jest + pnpm can't * transform the hoisted @jupyterlab/* packages out of the box). */ jest.mock('@jupyterlab/cells', () => ({ CodeCell: class CodeCell {} })); jest.mock('@jupyterlab/apputils', () => ({ Notification: { info: jest.fn(), error: jest.fn(), success: jest.fn(), warning: jest.fn() } })); jest.mock('@lumino/widgets', () => { class Widget { public node: HTMLElement = document.createElement('div'); public id = ''; public parent: Widget | null = null; private _isDisposed = false; public get isDisposed(): boolean { return this._isDisposed; } public addClass(cls: string): void { this.node.classList.add(cls); } public dispose(): void { this._isDisposed = true; } } return { Widget }; }); import { OpenCodeCellActions } from '../components/opencode_cell_actions'; import { CodeCell } from '@jupyterlab/cells'; function makeFakeOutputs(items: any[]): any { return { get length() { return items.length; }, get(i: number) { return items[i]; } }; } function makeFakeCell( source: string, errorOutputs: any[] = [], notebookPath = 'foo.ipynb', cellIndex = 0, totalCells = 1 ): CodeCell { const model: any = { id: 'cell-test', type: 'code', sharedModel: { getSource: () => source }, outputs: makeFakeOutputs(errorOutputs), contentChanged: { connect: jest.fn(), disconnect: jest.fn() }, stateChanged: { connect: jest.fn(), disconnect: jest.fn() } }; const cell = new (CodeCell as unknown as new () => CodeCell)(); (cell as any).model = model; // Parent chain: cell -> notebook -> panel (mirrors real JupyterLab, where // the cell's grandparent Notebook matches the duck-typed panel lookup). const notebook: any = { widgets: [] as CodeCell[] }; const panel: any = { context: { path: notebookPath }, content: notebook }; for (let i = 0; i < cellIndex; i++) { notebook.widgets.push({ model: { sharedModel: { getSource: () => '' } } } as any); } notebook.widgets.push(cell); for (let i = cellIndex + 1; i < totalCells; i++) { notebook.widgets.push({ model: { sharedModel: { getSource: () => '' } } } as any); } Object.defineProperty(cell, 'parent', { value: notebook, configurable: true }); Object.defineProperty(notebook, 'parent', { value: panel, configurable: true }); return cell as CodeCell; } describe('OpenCodeCellActions', () => { it('renders 3 buttons with the expected labels', () => { const cell = makeFakeCell('x = 1'); const actions = new OpenCodeCellActions(cell); const btns = actions.node.querySelectorAll('button'); expect(btns.length).toBe(3); expect(btns[0].textContent).toBe('✨ δΌ˜εŒ–'); expect(btns[1].textContent).toBe('πŸ› ζŽ’ι”™'); expect(btns[2].textContent).toBe('πŸͺ„ ηΌ–θΎ‘'); }); it('disables all buttons when the notebook panel cannot be resolved', () => { const cell = new (CodeCell as unknown as new () => CodeCell)(); (cell as any).model = { id: 'orphan', type: 'code', sharedModel: { getSource: () => 'x = 1' }, outputs: makeFakeOutputs([]), contentChanged: { connect: jest.fn(), disconnect: jest.fn() }, stateChanged: { connect: jest.fn(), disconnect: jest.fn() } }; // No parent chain: extractCellContextFromCell must return null. const actions = new OpenCodeCellActions(cell); const btns = actions.node.querySelectorAll('button'); btns.forEach((b: HTMLButtonElement) => { expect(b.disabled).toBe(true); }); }); it('enables optimize and edit; fix stays disabled without an error', () => { const cell = makeFakeCell('x = 1', [], 'foo.ipynb', 0, 1); const actions = new OpenCodeCellActions(cell); const btns = actions.node.querySelectorAll('button'); expect((btns[0] as HTMLButtonElement).disabled).toBe(false); expect((btns[1] as HTMLButtonElement).disabled).toBe(true); expect((btns[2] as HTMLButtonElement).disabled).toBe(false); }); it('enables the fix button when the cell has an error output', () => { const cell = makeFakeCell( '1/0', [ { type: 'error', ename: 'ZeroDivisionError', evalue: 'div by zero', traceback: [] } ], 'foo.ipynb', 0, 1 ); const actions = new OpenCodeCellActions(cell); const btns = actions.node.querySelectorAll('button'); expect((btns[1] as HTMLButtonElement).disabled).toBe(false); }); it('disconnects model signals on dispose', () => { const cell = makeFakeCell('x = 1'); const actions = new OpenCodeCellActions(cell); actions.dispose(); const model = (cell as any).model; expect(model.contentChanged.disconnect).toHaveBeenCalled(); expect(model.stateChanged.disconnect).toHaveBeenCalled(); }); });