refactor: register AI actions in native Cell toolbar; remove cell footer chain

This commit is contained in:
tao.chen
2026-07-23 10:30:15 +08:00
parent bb846d64fe
commit 0a05424b29
5 changed files with 26 additions and 415 deletions
-156
View File
@@ -1,156 +0,0 @@
/**
* Unit tests for OpenCodeCellFooter 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).
*/
// Mock the heavy JupyterLab modules BEFORE importing the component.
// (jest.mock is hoisted by Jest, but listing it before imports is clearer.)
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 addClass(cls: string): void {
this.node.classList.add(cls);
}
public id: string = '';
public parent: Widget | null = null;
protected onAfterAttach(_msg: unknown): void {
/* overridden by subclass */
}
protected onBeforeDetach(_msg: unknown): void {
/* overridden by subclass */
}
}
return { Widget };
});
import { OpenCodeCellFooter } from '../components/opencode_cell_footer';
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),
// Signal stubs so the component can connect/disconnect without crashing.
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;
// Build a fake parent chain: cell -> notebook -> panel.
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);
}
// The footer's helper walks `cell.parent` looking for a widget with
// `context` + `content.widgets`. The cell's grandparent (notebook) is what
// matches in real JupyterLab. We mirror that here.
Object.defineProperty(cell, 'parent', { value: notebook, configurable: true });
Object.defineProperty(notebook, 'parent', { value: panel, configurable: true });
return cell as CodeCell;
}
describe('OpenCodeCellFooter', () => {
it('renders 3 buttons', () => {
const footer = new OpenCodeCellFooter();
const btns = footer.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 no cell is attached', () => {
const footer = new OpenCodeCellFooter();
const btns = footer.node.querySelectorAll('button');
btns.forEach((b: HTMLButtonElement) => {
expect(b.disabled).toBe(true);
});
});
it('enables optimize and edit when a cell is attached; fix stays disabled without an error', () => {
const cell = makeFakeCell('x = 1', [], 'foo.ipynb', 0, 1);
const footer = new OpenCodeCellFooter();
Object.defineProperty(footer, 'parent', { value: cell, configurable: true });
(footer as any).onAfterAttach({} as any);
const btns = footer.node.querySelectorAll('button');
const optimize = btns[0] as HTMLButtonElement;
const fix = btns[1] as HTMLButtonElement;
const edit = btns[2] as HTMLButtonElement;
expect(optimize.disabled).toBe(false);
expect(fix.disabled).toBe(true);
expect(edit.disabled).toBe(false);
});
it('enables the fix button when the attached cell has an error output', () => {
const cell = makeFakeCell(
'1/0',
[
{
type: 'error',
ename: 'ZeroDivisionError',
evalue: 'div by zero',
traceback: []
}
],
'foo.ipynb',
0,
1
);
const footer = new OpenCodeCellFooter();
Object.defineProperty(footer, 'parent', { value: cell, configurable: true });
(footer as any).onAfterAttach({} as any);
const btns = footer.node.querySelectorAll('button');
const fix = btns[1] as HTMLButtonElement;
expect(fix.disabled).toBe(false);
});
});