notebooktools.spec.ts 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import { CodeMirrorEditorFactory } from '@jupyterlab/codemirror';
  4. import { Context } from '@jupyterlab/docregistry';
  5. import { ObservableJSON } from '@jupyterlab/observables';
  6. import { initNotebookContext, sleep } from '@jupyterlab/testutils';
  7. import { JupyterServer } from '@jupyterlab/testutils/lib/start_jupyter_server';
  8. import { Message } from '@lumino/messaging';
  9. import { TabPanel, Widget } from '@lumino/widgets';
  10. import { simulate } from 'simulate-event';
  11. import {
  12. INotebookModel,
  13. NotebookActions,
  14. NotebookPanel,
  15. NotebookTools,
  16. NotebookTracker
  17. } from '..';
  18. import * as utils from './utils';
  19. class LogTool extends NotebookTools.Tool {
  20. methods: string[] = [];
  21. protected onActiveNotebookPanelChanged(msg: Message): void {
  22. super.onActiveNotebookPanelChanged(msg);
  23. this.methods.push('onActiveNotebookPanelChanged');
  24. }
  25. protected onActiveCellChanged(msg: Message): void {
  26. super.onActiveCellChanged(msg);
  27. this.methods.push('onActiveCellChanged');
  28. }
  29. protected onSelectionChanged(msg: Message): void {
  30. super.onSelectionChanged(msg);
  31. this.methods.push('onSelectionChanged');
  32. }
  33. protected onActiveCellMetadataChanged(
  34. msg: ObservableJSON.ChangeMessage
  35. ): void {
  36. super.onActiveCellMetadataChanged(msg);
  37. this.methods.push('onActiveCellMetadataChanged');
  38. }
  39. protected onActiveNotebookPanelMetadataChanged(
  40. msg: ObservableJSON.ChangeMessage
  41. ): void {
  42. super.onActiveNotebookPanelMetadataChanged(msg);
  43. this.methods.push('onActiveNotebookPanelMetadataChanged');
  44. }
  45. }
  46. class LogKeySelector extends NotebookTools.KeySelector {
  47. events: string[] = [];
  48. methods: string[] = [];
  49. handleEvent(event: Event): void {
  50. super.handleEvent(event);
  51. this.events.push(event.type);
  52. }
  53. protected onAfterAttach(message: Message): void {
  54. super.onAfterAttach(message);
  55. this.methods.push('onAfterAttach');
  56. }
  57. protected onBeforeDetach(message: Message): void {
  58. super.onBeforeDetach(message);
  59. this.methods.push('onBeforeDetach');
  60. }
  61. protected onActiveCellChanged(message: Message): void {
  62. super.onActiveCellChanged(message);
  63. this.methods.push('onActiveCellChanged');
  64. }
  65. protected onActiveCellMetadataChanged(
  66. message: ObservableJSON.ChangeMessage
  67. ): void {
  68. super.onActiveCellMetadataChanged(message);
  69. this.methods.push('onActiveCellMetadataChanged');
  70. }
  71. protected onValueChanged(): void {
  72. super.onValueChanged();
  73. this.methods.push('onValueChanged');
  74. }
  75. }
  76. const server = new JupyterServer();
  77. beforeAll(async () => {
  78. jest.setTimeout(20000);
  79. await server.start();
  80. });
  81. afterAll(async () => {
  82. await server.shutdown();
  83. });
  84. describe('@jupyterlab/notebook', () => {
  85. describe('notebooktools', () => {
  86. let notebookTools: NotebookTools;
  87. let tabpanel: TabPanel;
  88. let tracker: NotebookTracker;
  89. let context0: Context<INotebookModel>;
  90. let context1: Context<INotebookModel>;
  91. let panel0: NotebookPanel;
  92. let panel1: NotebookPanel;
  93. beforeEach(async () => {
  94. context0 = await initNotebookContext();
  95. panel0 = utils.createNotebookPanel(context0);
  96. utils.populateNotebook(panel0.content);
  97. context1 = await initNotebookContext();
  98. panel1 = utils.createNotebookPanel(context1);
  99. utils.populateNotebook(panel1.content);
  100. tracker = new NotebookTracker({ namespace: 'notebook' });
  101. await tracker.add(panel0);
  102. await tracker.add(panel1);
  103. notebookTools = new NotebookTools({ tracker });
  104. tabpanel = new TabPanel();
  105. tabpanel.addWidget(panel0);
  106. tabpanel.addWidget(panel1);
  107. tabpanel.addWidget(notebookTools);
  108. tabpanel.node.style.height = '800px';
  109. Widget.attach(tabpanel, document.body);
  110. // Give the posted messages a chance to be handled.
  111. await sleep();
  112. });
  113. afterEach(() => {
  114. tabpanel.dispose();
  115. notebookTools.dispose();
  116. panel1.dispose();
  117. panel0.dispose();
  118. context1.dispose();
  119. context0.dispose();
  120. });
  121. describe('NotebookTools', () => {
  122. describe('#constructor()', () => {
  123. it('should create a notebooktools object', () => {
  124. expect(notebookTools).toBeInstanceOf(NotebookTools);
  125. });
  126. });
  127. describe('#activeNotebookPanel', () => {
  128. it('should be the active notebook', () => {
  129. expect(notebookTools.activeNotebookPanel).toBe(panel1);
  130. tabpanel.currentIndex = 0;
  131. simulate(panel0.node, 'focus');
  132. expect(notebookTools.activeNotebookPanel).toBe(panel0);
  133. });
  134. });
  135. describe('#activeCell', () => {
  136. it('should be the active cell', () => {
  137. expect(notebookTools.activeCell).toBe(panel1.content.activeCell);
  138. tabpanel.currentIndex = 0;
  139. simulate(panel0.node, 'focus');
  140. expect(notebookTools.activeCell).toBe(panel0.content.activeCell);
  141. });
  142. });
  143. describe('#selectedCells', () => {
  144. it('should be the currently selected cells', () => {
  145. expect(notebookTools.selectedCells).toEqual([
  146. panel1.content.activeCell
  147. ]);
  148. tabpanel.currentIndex = 0;
  149. simulate(panel0.node, 'focus');
  150. expect(notebookTools.selectedCells).toEqual([
  151. panel0.content.activeCell
  152. ]);
  153. panel0.content.select(panel0.content.widgets[1]);
  154. expect(notebookTools.selectedCells.length).toBe(2);
  155. });
  156. });
  157. describe('#addItem()', () => {
  158. it('should add a cell tool item', () => {
  159. const tool = new NotebookTools.Tool();
  160. notebookTools.addItem({ tool });
  161. tool.dispose();
  162. });
  163. it('should accept a rank', () => {
  164. const tool = new NotebookTools.Tool();
  165. notebookTools.addItem({ tool, rank: 100 });
  166. tool.dispose();
  167. });
  168. });
  169. });
  170. describe('NotebookTools.Tool', () => {
  171. describe('#constructor', () => {
  172. it('should create a new base tool', () => {
  173. const tool = new NotebookTools.Tool();
  174. expect(tool).toBeInstanceOf(NotebookTools.Tool);
  175. });
  176. });
  177. describe('#parent', () => {
  178. it('should be the notebooktools object used by the tool', () => {
  179. const tool = new NotebookTools.Tool({});
  180. notebookTools.addItem({ tool });
  181. expect(tool.notebookTools).toBe(notebookTools);
  182. });
  183. });
  184. describe('#onActiveNotebookPanelChanged()', () => {
  185. it('should be called when the active notebook panel changes', () => {
  186. const tool = new LogTool({});
  187. notebookTools.addItem({ tool });
  188. tool.methods = [];
  189. simulate(panel0.node, 'focus');
  190. expect(tool.methods).toContain('onActiveNotebookPanelChanged');
  191. });
  192. });
  193. describe('#onActiveCellChanged()', () => {
  194. it('should be called when the active cell changes', () => {
  195. const tool = new LogTool({});
  196. notebookTools.addItem({ tool });
  197. tool.methods = [];
  198. simulate(panel0.node, 'focus');
  199. expect(tool.methods).toContain('onActiveCellChanged');
  200. });
  201. });
  202. describe('#onSelectionChanged()', () => {
  203. it('should be called when the selection changes', () => {
  204. const tool = new LogTool({});
  205. notebookTools.addItem({ tool });
  206. tool.methods = [];
  207. const current = tracker.currentWidget!;
  208. current.content.select(current.content.widgets[1]);
  209. expect(tool.methods).toContain('onSelectionChanged');
  210. });
  211. });
  212. describe('#onActiveCellMetadataChanged()', () => {
  213. it('should be called when the active cell metadata changes', () => {
  214. const tool = new LogTool({});
  215. notebookTools.addItem({ tool });
  216. tool.methods = [];
  217. const metadata = notebookTools.activeCell!.model.metadata;
  218. metadata.set('foo', 1);
  219. metadata.set('foo', 2);
  220. expect(tool.methods).toContain('onActiveCellMetadataChanged');
  221. });
  222. });
  223. describe('#onActiveNotebookPanelMetadataChanged()', () => {
  224. it('should be called when the active notebook panel metadata changes', () => {
  225. const tool = new LogTool({});
  226. notebookTools.addItem({ tool });
  227. tool.methods = [];
  228. const metadata = notebookTools.activeNotebookPanel!.model!.metadata;
  229. metadata.set('foo', 1);
  230. metadata.set('foo', 2);
  231. expect(tool.methods).toContain(
  232. 'onActiveNotebookPanelMetadataChanged'
  233. );
  234. });
  235. });
  236. });
  237. describe('NotebookTools.ActiveCellTool', () => {
  238. it('should create a new active cell tool', () => {
  239. const tool = new NotebookTools.ActiveCellTool();
  240. notebookTools.addItem({ tool });
  241. expect(tool).toBeInstanceOf(NotebookTools.ActiveCellTool);
  242. });
  243. it('should handle a change to the active cell', () => {
  244. const tool = new NotebookTools.ActiveCellTool();
  245. notebookTools.addItem({ tool });
  246. const widget = tracker.currentWidget!;
  247. widget.content.activeCellIndex++;
  248. widget.content.activeCell!.model.metadata.set('bar', 1);
  249. expect(tool.node.querySelector('.jp-InputArea-editor')).toBeTruthy();
  250. });
  251. });
  252. describe('NotebookTools.CellMetadataEditorTool', () => {
  253. const editorServices = new CodeMirrorEditorFactory();
  254. const editorFactory = editorServices.newInlineEditor.bind(editorServices);
  255. it('should create a new metadata editor tool', () => {
  256. const tool = new NotebookTools.CellMetadataEditorTool({
  257. editorFactory
  258. });
  259. expect(tool).toBeInstanceOf(NotebookTools.CellMetadataEditorTool);
  260. });
  261. it('should handle a change to the active cell', () => {
  262. const tool = new NotebookTools.CellMetadataEditorTool({
  263. editorFactory
  264. });
  265. notebookTools.addItem({ tool });
  266. const model = tool.editor.model;
  267. expect(JSON.stringify(model.value.text)).toBeTruthy();
  268. const widget = tracker.currentWidget!;
  269. widget.content.activeCellIndex++;
  270. widget.content.activeCell!.model.metadata.set('bar', 1);
  271. expect(JSON.stringify(model.value.text)).toContain('bar');
  272. });
  273. it('should handle a change to the metadata', () => {
  274. const tool = new NotebookTools.CellMetadataEditorTool({
  275. editorFactory
  276. });
  277. notebookTools.addItem({ tool });
  278. const model = tool.editor.model;
  279. const previous = model.value.text;
  280. const metadata = notebookTools.activeCell!.model.metadata;
  281. metadata.set('foo', 1);
  282. expect(model.value.text).not.toBe(previous);
  283. });
  284. });
  285. describe('NotebookTools.NotebookMetadataEditorTool', () => {
  286. const editorServices = new CodeMirrorEditorFactory();
  287. const editorFactory = editorServices.newInlineEditor.bind(editorServices);
  288. it('should create a new metadata editor tool', () => {
  289. const tool = new NotebookTools.NotebookMetadataEditorTool({
  290. editorFactory
  291. });
  292. expect(tool).toBeInstanceOf(NotebookTools.NotebookMetadataEditorTool);
  293. });
  294. it('should handle a change to the active notebook', () => {
  295. panel0.model!.metadata.set('panel0', 1);
  296. panel1.model!.metadata.set('panel1', 1);
  297. const tool = new NotebookTools.NotebookMetadataEditorTool({
  298. editorFactory
  299. });
  300. notebookTools.addItem({ tool });
  301. const model = tool.editor.model;
  302. expect(JSON.stringify(model.value.text)).toBeTruthy();
  303. simulate(panel0.node, 'focus');
  304. expect(JSON.stringify(model.value.text)).toContain('panel0');
  305. expect(JSON.stringify(model.value.text)).not.toContain('panel1');
  306. simulate(panel1.node, 'focus');
  307. expect(JSON.stringify(model.value.text)).not.toContain('panel0');
  308. expect(JSON.stringify(model.value.text)).toContain('panel1');
  309. });
  310. it('should handle a change to the metadata', () => {
  311. const tool = new NotebookTools.NotebookMetadataEditorTool({
  312. editorFactory
  313. });
  314. notebookTools.addItem({ tool });
  315. const model = tool.editor.model;
  316. const widget = tracker.currentWidget!;
  317. expect(JSON.stringify(model.value.text)).not.toContain('newvalue');
  318. widget.content.model!.metadata.set('newvalue', 1);
  319. expect(JSON.stringify(model.value.text)).toContain('newvalue');
  320. });
  321. });
  322. describe('NotebookTools.KeySelector', () => {
  323. let tool: LogKeySelector;
  324. beforeEach(() => {
  325. tool = new LogKeySelector({
  326. key: 'foo',
  327. title: 'Foo',
  328. optionValueArray: [
  329. ['bar', 1],
  330. ['baz', [1, 2, 'a']]
  331. ]
  332. });
  333. notebookTools.addItem({ tool });
  334. simulate(panel0.node, 'focus');
  335. tabpanel.currentIndex = 2;
  336. });
  337. afterEach(() => {
  338. tool.dispose();
  339. });
  340. describe('#constructor()', () => {
  341. it('should create a new key selector', () => {
  342. expect(tool).toBeInstanceOf(NotebookTools.KeySelector);
  343. });
  344. });
  345. describe('#key', () => {
  346. it('should be the key used by the selector', () => {
  347. expect(tool.key).toBe('foo');
  348. });
  349. });
  350. describe('#selectNode', () => {
  351. it('should be the select node', () => {
  352. expect(tool.selectNode.localName).toBe('select');
  353. });
  354. });
  355. describe('#handleEvent()', () => {
  356. describe('change', () => {
  357. it('should update the metadata', () => {
  358. const select = tool.selectNode;
  359. simulate(select, 'focus');
  360. select.selectedIndex = 1;
  361. simulate(select, 'change');
  362. expect(tool.events).toContain('change');
  363. const metadata = notebookTools.activeCell!.model.metadata;
  364. expect(metadata.get('foo')).toEqual([1, 2, 'a']);
  365. });
  366. });
  367. describe('focus', () => {
  368. it('should add the focused class to the wrapper node', () => {
  369. const select = tool.selectNode;
  370. simulate(select, 'focus');
  371. const selector = '.jp-mod-focused';
  372. expect(tool.node.querySelector(selector)).toBeTruthy();
  373. });
  374. });
  375. describe('blur', () => {
  376. it('should remove the focused class from the wrapper node', () => {
  377. const select = tool.selectNode;
  378. simulate(select, 'focus');
  379. simulate(select, 'blur');
  380. const selector = '.jp-mod-focused';
  381. expect(tool.node.querySelector(selector)).toBeFalsy();
  382. });
  383. });
  384. });
  385. describe('#onAfterAttach()', () => {
  386. it('should add event listeners', () => {
  387. const select = tool.selectNode;
  388. expect(tool.methods).toContain('onAfterAttach');
  389. simulate(select, 'focus');
  390. simulate(select, 'blur');
  391. select.selectedIndex = 0;
  392. simulate(select, 'change');
  393. expect(tool.events).toEqual(['change']);
  394. });
  395. });
  396. describe('#onBeforeDetach()', () => {
  397. it('should remove event listeners', () => {
  398. const select = tool.selectNode;
  399. notebookTools.dispose();
  400. expect(tool.methods).toContain('onBeforeDetach');
  401. simulate(select, 'focus');
  402. simulate(select, 'blur');
  403. simulate(select, 'change');
  404. expect(tool.events).toEqual([]);
  405. });
  406. });
  407. describe('#onValueChanged()', () => {
  408. it('should update the metadata', () => {
  409. const select = tool.selectNode;
  410. simulate(select, 'focus');
  411. select.selectedIndex = 1;
  412. simulate(select, 'change');
  413. expect(tool.methods).toContain('onValueChanged');
  414. const metadata = notebookTools.activeCell!.model.metadata;
  415. expect(metadata.get('foo')).toEqual([1, 2, 'a']);
  416. });
  417. });
  418. describe('#onActiveCellChanged()', () => {
  419. it('should update the select value', () => {
  420. const cell = panel0.content.model!.cells.get(1);
  421. cell.metadata.set('foo', 1);
  422. panel0.content.activeCellIndex = 1;
  423. expect(tool.methods).toContain('onActiveCellChanged');
  424. expect(tool.selectNode.value).toBe('1');
  425. });
  426. });
  427. describe('#onActiveCellMetadataChanged()', () => {
  428. it('should update the select value', () => {
  429. const metadata = notebookTools.activeCell!.model.metadata;
  430. metadata.set('foo', 1);
  431. expect(tool.methods).toContain('onActiveCellMetadataChanged');
  432. expect(tool.selectNode.value).toBe('1');
  433. });
  434. });
  435. });
  436. describe('NotebookTools.createSlideShowSelector()', () => {
  437. it('should create a slide show selector', () => {
  438. const tool = NotebookTools.createSlideShowSelector();
  439. tool.selectNode.selectedIndex = -1;
  440. notebookTools.addItem({ tool });
  441. simulate(panel0.node, 'focus');
  442. tabpanel.currentIndex = 2;
  443. expect(tool).toBeInstanceOf(NotebookTools.KeySelector);
  444. expect(tool.key).toBe('slideshow');
  445. const select = tool.selectNode;
  446. expect(select.value).toBe('');
  447. const metadata = notebookTools.activeCell!.model.metadata;
  448. expect(metadata.get('slideshow')).toBeUndefined();
  449. simulate(select, 'focus');
  450. tool.selectNode.selectedIndex = 1;
  451. simulate(select, 'change');
  452. expect(metadata.get('slideshow')).toEqual({
  453. slide_type: 'slide'
  454. });
  455. });
  456. });
  457. describe('NotebookTools.createNBConvertSelector()', () => {
  458. it('should create a raw mimetype selector', () => {
  459. const optionValueArray: any = [
  460. [null, '-'],
  461. ['LaTeX', 'text/latex'],
  462. ['reST', 'text/restructuredtext'],
  463. ['HTML', 'text/html'],
  464. ['Markdown', 'text/markdown'],
  465. ['Python', 'text/x-python']
  466. ];
  467. optionValueArray.push(['None', '-']);
  468. const tool = NotebookTools.createNBConvertSelector(optionValueArray);
  469. tool.selectNode.selectedIndex = -1;
  470. notebookTools.addItem({ tool });
  471. simulate(panel0.node, 'focus');
  472. NotebookActions.changeCellType(panel0.content, 'raw');
  473. tabpanel.currentIndex = 2;
  474. expect(tool).toBeInstanceOf(NotebookTools.KeySelector);
  475. expect(tool.key).toBe('raw_mimetype');
  476. const select = tool.selectNode;
  477. expect(select.value).toBe('');
  478. const metadata = notebookTools.activeCell!.model.metadata;
  479. expect(metadata.get('raw_mimetype')).toBeUndefined();
  480. simulate(select, 'focus');
  481. tool.selectNode.selectedIndex = 2;
  482. simulate(select, 'change');
  483. expect(metadata.get('raw_mimetype')).toBe('text/restructuredtext');
  484. });
  485. it('should have no effect on a code cell', () => {
  486. const optionValueArray: any = [
  487. ['None', '-'],
  488. ['LaTeX', 'text/latex'],
  489. ['reST', 'text/restructuredtext'],
  490. ['HTML', 'text/html'],
  491. ['Markdown', 'text/markdown'],
  492. ['Python', 'text/x-python']
  493. ];
  494. const tool = NotebookTools.createNBConvertSelector(optionValueArray);
  495. tool.selectNode.selectedIndex = -1;
  496. notebookTools.addItem({ tool });
  497. simulate(panel0.node, 'focus');
  498. NotebookActions.changeCellType(panel0.content, 'code');
  499. tabpanel.currentIndex = 2;
  500. expect(tool).toBeInstanceOf(NotebookTools.KeySelector);
  501. expect(tool.key).toBe('raw_mimetype');
  502. const select = tool.selectNode;
  503. expect(select.disabled).toBe(true);
  504. expect(select.value).toBe('');
  505. });
  506. });
  507. });
  508. });