notebooktools.spec.ts 19 KB

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