widget.spec.ts 55 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import {
  4. Cell,
  5. CodeCell,
  6. CodeCellModel,
  7. MarkdownCell,
  8. MarkdownCellModel,
  9. RawCell,
  10. RawCellModel
  11. } from '@jupyterlab/cells';
  12. import { framePromise, signalToPromise } from '@jupyterlab/testutils';
  13. import { JupyterServer } from '@jupyterlab/testutils/lib/start_jupyter_server';
  14. import { Message, MessageLoop } from '@lumino/messaging';
  15. import { Widget } from '@lumino/widgets';
  16. import { generate, simulate } from 'simulate-event';
  17. import * as nbformat from '@jupyterlab/nbformat';
  18. import { INotebookModel, Notebook, NotebookModel, StaticNotebook } from '..';
  19. import * as utils from './utils';
  20. const server = new JupyterServer();
  21. beforeAll(async () => {
  22. jest.setTimeout(20000);
  23. await server.start();
  24. });
  25. afterAll(async () => {
  26. await server.shutdown();
  27. });
  28. const contentFactory = utils.createNotebookFactory();
  29. const editorConfig = utils.defaultEditorConfig;
  30. const rendermime = utils.defaultRenderMime();
  31. const notebookConfig = {
  32. scrollPastEnd: true,
  33. defaultCell: 'code' as nbformat.CellType,
  34. recordTiming: false,
  35. numberCellsToRenderDirectly: 2,
  36. renderCellOnIdle: true,
  37. observedTopMargin: '1000px',
  38. observedBottomMargin: '1000px',
  39. maxNumberOutputs: 50,
  40. disableDocumentWideUndoRedo: true
  41. };
  42. const options: Notebook.IOptions = {
  43. rendermime,
  44. contentFactory,
  45. mimeTypeService: utils.mimeTypeService,
  46. editorConfig,
  47. notebookConfig
  48. };
  49. function createWidget(): LogStaticNotebook {
  50. const model = new NotebookModel();
  51. const widget = new LogStaticNotebook(options);
  52. widget.model = model;
  53. return widget;
  54. }
  55. class LogStaticNotebook extends StaticNotebook {
  56. methods: string[] = [];
  57. protected onUpdateRequest(msg: Message): void {
  58. super.onUpdateRequest(msg);
  59. this.methods.push('onUpdateRequest');
  60. }
  61. protected onModelChanged(
  62. oldValue: INotebookModel,
  63. newValue: INotebookModel
  64. ): void {
  65. super.onModelChanged(oldValue, newValue);
  66. this.methods.push('onModelChanged');
  67. }
  68. protected onMetadataChanged(model: any, args: any): void {
  69. super.onMetadataChanged(model, args);
  70. this.methods.push('onMetadataChanged');
  71. }
  72. protected onCellInserted(index: number, cell: Cell): void {
  73. super.onCellInserted(index, cell);
  74. this.methods.push('onCellInserted');
  75. }
  76. protected onCellMoved(fromIndex: number, toIndex: number): void {
  77. super.onCellMoved(fromIndex, toIndex);
  78. this.methods.push('onCellMoved');
  79. }
  80. protected onCellRemoved(index: number, cell: Cell): void {
  81. super.onCellRemoved(index, cell);
  82. this.methods.push('onCellRemoved');
  83. }
  84. }
  85. class LogNotebook extends Notebook {
  86. events: string[] = [];
  87. methods: string[] = [];
  88. handleEvent(event: Event): void {
  89. this.events.push(event.type);
  90. super.handleEvent(event);
  91. }
  92. protected onAfterAttach(msg: Message): void {
  93. super.onAfterAttach(msg);
  94. this.methods.push('onAfterAttach');
  95. }
  96. protected onBeforeDetach(msg: Message): void {
  97. super.onBeforeDetach(msg);
  98. this.methods.push('onBeforeDetach');
  99. }
  100. protected onActivateRequest(msg: Message): void {
  101. super.onActivateRequest(msg);
  102. this.methods.push('onActivateRequest');
  103. }
  104. protected onUpdateRequest(msg: Message): void {
  105. super.onUpdateRequest(msg);
  106. this.methods.push('onUpdateRequest');
  107. }
  108. protected onCellInserted(index: number, cell: Cell): void {
  109. super.onCellInserted(index, cell);
  110. this.methods.push('onCellInserted');
  111. }
  112. protected onCellMoved(fromIndex: number, toIndex: number): void {
  113. super.onCellMoved(fromIndex, toIndex);
  114. this.methods.push('onCellMoved');
  115. }
  116. protected onCellRemoved(index: number, cell: Cell): void {
  117. super.onCellRemoved(index, cell);
  118. this.methods.push('onCellRemoved');
  119. }
  120. }
  121. function createActiveWidget(): LogNotebook {
  122. const model = new NotebookModel();
  123. const widget = new LogNotebook(options);
  124. widget.model = model;
  125. return widget;
  126. }
  127. function selected(nb: Notebook): number[] {
  128. const selected = [];
  129. const cells = nb.widgets;
  130. for (let i = 0; i < cells.length; i++) {
  131. if (nb.isSelected(cells[i])) {
  132. selected.push(i);
  133. }
  134. }
  135. return selected;
  136. }
  137. describe('@jupyter/notebook', () => {
  138. describe('StaticNotebook', () => {
  139. describe('#constructor()', () => {
  140. it('should create a notebook widget', () => {
  141. const widget = new StaticNotebook(options);
  142. expect(widget).toBeInstanceOf(StaticNotebook);
  143. });
  144. it('should add the `jp-Notebook` class', () => {
  145. const widget = new StaticNotebook(options);
  146. expect(widget.hasClass('jp-Notebook')).toBe(true);
  147. });
  148. it('should accept an optional render', () => {
  149. const widget = new StaticNotebook(options);
  150. expect(widget.contentFactory).toBe(contentFactory);
  151. });
  152. it('should accept an optional editor config', () => {
  153. const widget = new StaticNotebook(options);
  154. expect(widget.editorConfig).toBe(editorConfig);
  155. });
  156. });
  157. describe('#modelChanged', () => {
  158. it('should be emitted when the model changes', () => {
  159. const widget = new StaticNotebook(options);
  160. const model = new NotebookModel();
  161. let called = false;
  162. widget.modelChanged.connect((sender, args) => {
  163. expect(sender).toBe(widget);
  164. expect(args).toBeUndefined();
  165. called = true;
  166. });
  167. widget.model = model;
  168. expect(called).toBe(true);
  169. });
  170. });
  171. describe('#modelContentChanged', () => {
  172. it('should be emitted when a cell is added', () => {
  173. const widget = new StaticNotebook(options);
  174. widget.model = new NotebookModel();
  175. let called = false;
  176. widget.modelContentChanged.connect(() => {
  177. called = true;
  178. });
  179. const cell = widget.model!.contentFactory.createCodeCell({});
  180. widget.model!.cells.push(cell);
  181. expect(called).toBe(true);
  182. });
  183. it('should be emitted when metadata is set', () => {
  184. const widget = new StaticNotebook(options);
  185. widget.model = new NotebookModel();
  186. let called = false;
  187. widget.modelContentChanged.connect(() => {
  188. called = true;
  189. });
  190. widget.model!.metadata.set('foo', 1);
  191. expect(called).toBe(true);
  192. });
  193. });
  194. describe('#model', () => {
  195. it('should get the model for the widget', () => {
  196. const widget = new StaticNotebook(options);
  197. expect(widget.model).toBeNull();
  198. });
  199. it('should set the model for the widget', () => {
  200. const widget = new StaticNotebook(options);
  201. const model = new NotebookModel();
  202. widget.model = model;
  203. expect(widget.model).toBe(model);
  204. });
  205. it('should emit the `modelChanged` signal', () => {
  206. const widget = new StaticNotebook(options);
  207. const model = new NotebookModel();
  208. widget.model = model;
  209. let called = false;
  210. widget.modelChanged.connect(() => {
  211. called = true;
  212. });
  213. widget.model = new NotebookModel();
  214. expect(called).toBe(true);
  215. });
  216. it('should be a no-op if the value does not change', () => {
  217. const widget = new StaticNotebook(options);
  218. const model = new NotebookModel();
  219. widget.model = model;
  220. let called = false;
  221. widget.modelChanged.connect(() => {
  222. called = true;
  223. });
  224. widget.model = model;
  225. expect(called).toBe(false);
  226. });
  227. it('should add the model cells to the layout', () => {
  228. const widget = new LogStaticNotebook(options);
  229. const model = new NotebookModel();
  230. model.fromJSON(utils.DEFAULT_CONTENT);
  231. widget.model = model;
  232. expect(widget.widgets.length).toBe(model.cells.length);
  233. });
  234. it('should add a default cell if the notebook model is empty', () => {
  235. const widget = new LogStaticNotebook(options);
  236. const model1 = new NotebookModel();
  237. expect(model1.cells.length).toBe(0);
  238. widget.model = model1;
  239. expect(model1.cells.length).toBe(1);
  240. expect(model1.cells.get(0).type).toBe('code');
  241. widget.notebookConfig = {
  242. ...widget.notebookConfig,
  243. defaultCell: 'markdown'
  244. };
  245. const model2 = new NotebookModel();
  246. expect(model2.cells.length).toBe(0);
  247. widget.model = model2;
  248. expect(model2.cells.length).toBe(1);
  249. expect(model2.cells.get(0).type).toBe('markdown');
  250. });
  251. it('should set the mime types of the cell widgets', () => {
  252. const widget = new LogStaticNotebook(options);
  253. const model = new NotebookModel();
  254. const value = { name: 'python', codemirror_mode: 'python' };
  255. model.metadata.set('language_info', value);
  256. widget.model = model;
  257. const child = widget.widgets[0];
  258. expect(child.model.mimeType).toBe('text/x-python');
  259. });
  260. describe('`cells.changed` signal', () => {
  261. let widget: LogStaticNotebook;
  262. beforeEach(() => {
  263. widget = createWidget();
  264. widget.model!.fromJSON(utils.DEFAULT_CONTENT);
  265. });
  266. afterEach(() => {
  267. widget.dispose();
  268. });
  269. it('should handle changes to the model cell list', async () => {
  270. widget = createWidget();
  271. widget.model!.cells.clear();
  272. await framePromise();
  273. expect(widget.widgets.length).toBe(1);
  274. });
  275. it('should handle a remove', () => {
  276. const cell = widget.model!.cells.get(1);
  277. const child = widget.widgets[1];
  278. widget.model!.cells.removeValue(cell);
  279. expect(cell.isDisposed).toBe(false);
  280. expect(child.isDisposed).toBe(true);
  281. });
  282. it('should handle an add', () => {
  283. const cell = widget.model!.contentFactory.createCodeCell({});
  284. widget.model!.cells.push(cell);
  285. expect(widget.widgets.length).toBe(widget.model!.cells.length);
  286. const child = widget.widgets[0];
  287. expect(child.hasClass('jp-Notebook-cell')).toBe(true);
  288. });
  289. it('should initially render markdown cells with content', () => {
  290. const cell1 = widget.model!.contentFactory.createMarkdownCell({});
  291. const cell2 = widget.model!.contentFactory.createMarkdownCell({});
  292. cell1.value.text = '# Hello';
  293. widget.model!.cells.push(cell1);
  294. widget.model!.cells.push(cell2);
  295. expect(widget.widgets.length).toBe(widget.model!.cells.length);
  296. const child1 = widget.widgets[
  297. widget.model!.cells.length - 2
  298. ] as MarkdownCell;
  299. const child2 = widget.widgets[
  300. widget.model!.cells.length - 1
  301. ] as MarkdownCell;
  302. expect(child1.rendered).toBe(true);
  303. expect(child2.rendered).toBe(false);
  304. });
  305. it('should handle a move', () => {
  306. const child = widget.widgets[1];
  307. widget.model!.cells.move(1, 2);
  308. expect(widget.widgets[2]).toBe(child);
  309. });
  310. it('should handle a clear', () => {
  311. const cell = widget.model!.contentFactory.createCodeCell({});
  312. widget.model!.cells.push(cell);
  313. widget.model!.cells.clear();
  314. expect(widget.widgets.length).toBe(0);
  315. });
  316. it('should add a new default cell when cells are cleared', async () => {
  317. const model = widget.model!;
  318. widget.notebookConfig = {
  319. ...widget.notebookConfig,
  320. defaultCell: 'raw'
  321. };
  322. const promise = signalToPromise(model.cells.changed);
  323. model.cells.clear();
  324. await promise;
  325. expect(model.cells.length).toBe(0);
  326. await signalToPromise(model.cells.changed);
  327. expect(model.cells.length).toBe(1);
  328. expect(model.cells.get(0)).toBeInstanceOf(RawCellModel);
  329. });
  330. });
  331. });
  332. describe('#rendermime', () => {
  333. it('should be the rendermime instance used by the widget', () => {
  334. const widget = new StaticNotebook(options);
  335. expect(widget.rendermime).toBe(rendermime);
  336. });
  337. });
  338. describe('#contentFactory', () => {
  339. it('should be the cell widget contentFactory used by the widget', () => {
  340. const widget = new StaticNotebook(options);
  341. expect(widget.contentFactory).toBeInstanceOf(
  342. StaticNotebook.ContentFactory
  343. );
  344. });
  345. });
  346. describe('#editorConfig', () => {
  347. it('should be the cell widget contentFactory used by the widget', () => {
  348. const widget = new StaticNotebook(options);
  349. expect(widget.editorConfig).toBe(options.editorConfig);
  350. });
  351. it('should be settable', () => {
  352. const widget = createWidget();
  353. expect(widget.widgets[0].editor.getOption('autoClosingBrackets')).toBe(
  354. false
  355. );
  356. const newConfig = {
  357. raw: editorConfig.raw,
  358. markdown: editorConfig.markdown,
  359. code: {
  360. ...editorConfig.code,
  361. autoClosingBrackets: true
  362. }
  363. };
  364. widget.editorConfig = newConfig;
  365. expect(widget.widgets[0].editor.getOption('autoClosingBrackets')).toBe(
  366. true
  367. );
  368. });
  369. });
  370. describe('#codeMimetype', () => {
  371. it('should get the mime type for code cells', () => {
  372. const widget = new StaticNotebook(options);
  373. expect(widget.codeMimetype).toBe('text/plain');
  374. });
  375. it('should be set from language metadata', () => {
  376. const widget = new LogStaticNotebook(options);
  377. const model = new NotebookModel();
  378. const value = { name: 'python', codemirror_mode: 'python' };
  379. model.metadata.set('language_info', value);
  380. widget.model = model;
  381. expect(widget.codeMimetype).toBe('text/x-python');
  382. });
  383. });
  384. describe('#widgets', () => {
  385. it('should get the child widget at a specified index', () => {
  386. const widget = createWidget();
  387. const child = widget.widgets[0];
  388. expect(child).toBeInstanceOf(CodeCell);
  389. });
  390. it('should return `undefined` if out of range', () => {
  391. const widget = createWidget();
  392. const child = widget.widgets[1];
  393. expect(child).toBeUndefined();
  394. });
  395. it('should get the number of child widgets', () => {
  396. const widget = createWidget();
  397. expect(widget.widgets.length).toBe(1);
  398. widget.model!.fromJSON(utils.DEFAULT_CONTENT);
  399. expect(widget.widgets.length).toBe(utils.DEFAULT_CONTENT.cells.length);
  400. });
  401. });
  402. describe('#dispose()', () => {
  403. it('should dispose of the resources held by the widget', () => {
  404. const widget = createWidget();
  405. widget.dispose();
  406. expect(widget.isDisposed).toBe(true);
  407. });
  408. it('should be safe to call multiple times', () => {
  409. const widget = createWidget();
  410. widget.dispose();
  411. widget.dispose();
  412. expect(widget.isDisposed).toBe(true);
  413. });
  414. });
  415. describe('#onModelChanged()', () => {
  416. it('should be called when the model changes', () => {
  417. const widget = new LogStaticNotebook(options);
  418. widget.model = new NotebookModel();
  419. expect(widget.methods).toEqual(
  420. expect.arrayContaining(['onModelChanged'])
  421. );
  422. });
  423. it('should not be called if the model does not change', () => {
  424. const widget = createWidget();
  425. widget.methods = [];
  426. widget.model = widget.model; // eslint-disable-line
  427. expect(widget.methods).toEqual(
  428. expect.not.arrayContaining(['onModelChanged'])
  429. );
  430. });
  431. });
  432. describe('#onMetadataChanged()', () => {
  433. it('should be called when the metadata on the notebook changes', () => {
  434. const widget = createWidget();
  435. widget.model!.metadata.set('foo', 1);
  436. expect(widget.methods).toEqual(
  437. expect.arrayContaining(['onMetadataChanged'])
  438. );
  439. });
  440. it('should update the `codeMimetype`', () => {
  441. const widget = createWidget();
  442. const value = { name: 'python', codemirror_mode: 'python' };
  443. widget.model!.metadata.set('language_info', value);
  444. expect(widget.methods).toEqual(
  445. expect.arrayContaining(['onMetadataChanged'])
  446. );
  447. expect(widget.codeMimetype).toBe('text/x-python');
  448. });
  449. it('should update the cell widget mimetype', () => {
  450. const widget = createWidget();
  451. const value = { name: 'python', mimetype: 'text/x-python' };
  452. widget.model!.metadata.set('language_info', value);
  453. expect(widget.methods).toEqual(
  454. expect.arrayContaining(['onMetadataChanged'])
  455. );
  456. const child = widget.widgets[0];
  457. expect(child.model.mimeType).toBe('text/x-python');
  458. });
  459. });
  460. describe('#onCellInserted()', () => {
  461. it('should be called when a cell is inserted', () => {
  462. const widget = createWidget();
  463. widget.model!.fromJSON(utils.DEFAULT_CONTENT);
  464. expect(widget.methods).toEqual(
  465. expect.arrayContaining(['onCellInserted'])
  466. );
  467. });
  468. });
  469. describe('#onCellMoved()', () => {
  470. it('should be called when a cell is moved', () => {
  471. const widget = createWidget();
  472. widget.model!.fromJSON(utils.DEFAULT_CONTENT);
  473. widget.model!.cells.move(0, 1);
  474. expect(widget.methods).toEqual(expect.arrayContaining(['onCellMoved']));
  475. });
  476. });
  477. describe('#onCellRemoved()', () => {
  478. it('should be called when a cell is removed', () => {
  479. const widget = createWidget();
  480. const cell = widget.model!.cells.get(0);
  481. widget.model!.cells.removeValue(cell);
  482. expect(widget.methods).toEqual(
  483. expect.arrayContaining(['onCellRemoved'])
  484. );
  485. });
  486. });
  487. describe('.ContentFactory', () => {
  488. describe('#constructor', () => {
  489. it('should create a new ContentFactory', () => {
  490. const editorFactory = utils.editorFactory;
  491. const factory = new StaticNotebook.ContentFactory({ editorFactory });
  492. expect(factory).toBeInstanceOf(StaticNotebook.ContentFactory);
  493. });
  494. });
  495. describe('#createCodeCell({})', () => {
  496. it('should create a `CodeCell`', () => {
  497. const contentFactory = new StaticNotebook.ContentFactory();
  498. const model = new CodeCellModel({});
  499. const codeOptions = { model, rendermime, contentFactory };
  500. const parent = new StaticNotebook(options);
  501. const widget = contentFactory.createCodeCell(codeOptions, parent);
  502. expect(widget).toBeInstanceOf(CodeCell);
  503. });
  504. });
  505. describe('#createMarkdownCell({})', () => {
  506. it('should create a `MarkdownCell`', () => {
  507. const contentFactory = new StaticNotebook.ContentFactory();
  508. const model = new MarkdownCellModel({});
  509. const mdOptions = { model, rendermime, contentFactory };
  510. const parent = new StaticNotebook(options);
  511. const widget = contentFactory.createMarkdownCell(mdOptions, parent);
  512. expect(widget).toBeInstanceOf(MarkdownCell);
  513. });
  514. });
  515. describe('#createRawCell()', () => {
  516. it('should create a `RawCell`', () => {
  517. const contentFactory = new StaticNotebook.ContentFactory();
  518. const model = new RawCellModel({});
  519. const rawOptions = { model, contentFactory };
  520. const parent = new StaticNotebook(options);
  521. const widget = contentFactory.createRawCell(rawOptions, parent);
  522. expect(widget).toBeInstanceOf(RawCell);
  523. });
  524. });
  525. });
  526. });
  527. describe('Notebook', () => {
  528. describe('#stateChanged', () => {
  529. it('should be emitted when the state of the notebook changes', () => {
  530. const widget = createActiveWidget();
  531. let called = false;
  532. widget.stateChanged.connect((sender, args) => {
  533. expect(sender).toBe(widget);
  534. expect(args.name).toBe('mode');
  535. expect(args.oldValue).toBe('command');
  536. expect(args.newValue).toBe('edit');
  537. called = true;
  538. });
  539. widget.mode = 'edit';
  540. expect(called).toBe(true);
  541. });
  542. });
  543. describe('#activeCellChanged', () => {
  544. it('should be emitted when the active cell changes', () => {
  545. const widget = createActiveWidget();
  546. widget.model!.fromJSON(utils.DEFAULT_CONTENT);
  547. let called = false;
  548. widget.activeCellChanged.connect((sender, args) => {
  549. expect(sender).toBe(widget);
  550. expect(args).toBe(widget.activeCell);
  551. called = true;
  552. });
  553. widget.activeCellIndex++;
  554. expect(called).toBe(true);
  555. });
  556. it('should not be emitted when the active cell does not change', () => {
  557. const widget = createActiveWidget();
  558. widget.model!.fromJSON(utils.DEFAULT_CONTENT);
  559. let called = false;
  560. widget.activeCellChanged.connect(() => {
  561. called = true;
  562. });
  563. widget.activeCellIndex = widget.activeCellIndex; // eslint-disable-line
  564. expect(called).toBe(false);
  565. });
  566. });
  567. describe('#selectionChanged', () => {
  568. it('should be emitted when the selection changes', () => {
  569. const widget = createActiveWidget();
  570. widget.model!.fromJSON(utils.DEFAULT_CONTENT);
  571. let called = false;
  572. widget.selectionChanged.connect((sender, args) => {
  573. expect(sender).toBe(widget);
  574. expect(args).toBeUndefined();
  575. called = true;
  576. });
  577. widget.select(widget.widgets[1]);
  578. expect(called).toBe(true);
  579. });
  580. it('should not be emitted when the selection does not change', () => {
  581. const widget = createActiveWidget();
  582. widget.model!.fromJSON(utils.DEFAULT_CONTENT);
  583. let called = false;
  584. widget.select(widget.widgets[1]);
  585. widget.selectionChanged.connect(() => {
  586. called = true;
  587. });
  588. widget.select(widget.widgets[1]);
  589. expect(called).toBe(false);
  590. });
  591. });
  592. describe('#mode', () => {
  593. it('should get the interactivity mode of the notebook', () => {
  594. const widget = createActiveWidget();
  595. expect(widget.mode).toBe('command');
  596. });
  597. it('should set the interactivity mode of the notebook', () => {
  598. const widget = createActiveWidget();
  599. widget.mode = 'edit';
  600. expect(widget.mode).toBe('edit');
  601. });
  602. it('should emit the `stateChanged` signal', () => {
  603. const widget = createActiveWidget();
  604. let called = false;
  605. widget.stateChanged.connect((sender, args) => {
  606. expect(sender).toBe(widget);
  607. expect(args.name).toBe('mode');
  608. expect(args.oldValue).toBe('command');
  609. expect(args.newValue).toBe('edit');
  610. called = true;
  611. });
  612. widget.mode = 'edit';
  613. expect(called).toBe(true);
  614. });
  615. it('should be a no-op if the value does not change', () => {
  616. const widget = createActiveWidget();
  617. let called = false;
  618. widget.stateChanged.connect(() => {
  619. called = true;
  620. });
  621. widget.mode = 'command';
  622. expect(called).toBe(false);
  623. });
  624. it('should post an update request', async () => {
  625. const widget = createActiveWidget();
  626. widget.mode = 'edit';
  627. await framePromise();
  628. expect(widget.methods).toEqual(
  629. expect.arrayContaining(['onUpdateRequest'])
  630. );
  631. });
  632. it('should deselect all cells if switching to edit mode', async () => {
  633. const widget = createActiveWidget();
  634. widget.model!.fromJSON(utils.DEFAULT_CONTENT);
  635. Widget.attach(widget, document.body);
  636. await framePromise();
  637. widget.extendContiguousSelectionTo(widget.widgets.length - 1);
  638. const selectedRange = Array.from(Array(widget.widgets.length).keys());
  639. expect(selected(widget)).toEqual(selectedRange);
  640. widget.mode = 'edit';
  641. expect(selected(widget)).toEqual([]);
  642. widget.dispose();
  643. });
  644. it('should unrender a markdown cell when switching to edit mode', () => {
  645. const widget = createActiveWidget();
  646. Widget.attach(widget, document.body);
  647. MessageLoop.sendMessage(widget, Widget.Msg.ActivateRequest);
  648. const cell = widget.model!.contentFactory.createMarkdownCell({});
  649. cell.value.text = '# Hello'; // Should be rendered with content.
  650. widget.model!.cells.push(cell);
  651. const child = widget.widgets[widget.widgets.length - 1] as MarkdownCell;
  652. expect(child.rendered).toBe(true);
  653. widget.activeCellIndex = widget.widgets.length - 1;
  654. widget.mode = 'edit';
  655. expect(child.rendered).toBe(false);
  656. });
  657. });
  658. describe('#activeCellIndex', () => {
  659. it('should get the active cell index of the notebook', () => {
  660. const widget = createActiveWidget();
  661. expect(widget.activeCellIndex).toBe(0);
  662. });
  663. it('should set the active cell index of the notebook', () => {
  664. const widget = createActiveWidget();
  665. widget.model!.fromJSON(utils.DEFAULT_CONTENT);
  666. widget.activeCellIndex = 1;
  667. expect(widget.activeCellIndex).toBe(1);
  668. });
  669. it('should clamp the index to the bounds of the notebook cells', () => {
  670. const widget = createActiveWidget();
  671. widget.model!.fromJSON(utils.DEFAULT_CONTENT);
  672. widget.activeCellIndex = -2;
  673. expect(widget.activeCellIndex).toBe(0);
  674. widget.activeCellIndex = 100;
  675. expect(widget.activeCellIndex).toBe(widget.model!.cells.length - 1);
  676. });
  677. it('should emit the `stateChanged` signal', () => {
  678. const widget = createActiveWidget();
  679. let called = false;
  680. widget.model!.fromJSON(utils.DEFAULT_CONTENT);
  681. widget.stateChanged.connect((sender, args) => {
  682. expect(sender).toBe(widget);
  683. expect(args.name).toBe('activeCellIndex');
  684. expect(args.oldValue).toBe(0);
  685. expect(args.newValue).toBe(1);
  686. called = true;
  687. });
  688. widget.activeCellIndex = 1;
  689. expect(called).toBe(true);
  690. });
  691. it('should be a no-op if the value does not change', () => {
  692. const widget = createActiveWidget();
  693. let called = false;
  694. widget.model!.fromJSON(utils.DEFAULT_CONTENT);
  695. widget.stateChanged.connect(() => {
  696. called = true;
  697. });
  698. widget.activeCellIndex = 0;
  699. expect(called).toBe(false);
  700. });
  701. it('should post an update request', async () => {
  702. const widget = createActiveWidget();
  703. widget.model!.fromJSON(utils.DEFAULT_CONTENT);
  704. await framePromise();
  705. expect(widget.methods).toEqual(
  706. expect.arrayContaining(['onUpdateRequest'])
  707. );
  708. widget.activeCellIndex = 1;
  709. });
  710. it('should update the active cell if necessary', () => {
  711. const widget = createActiveWidget();
  712. widget.model!.fromJSON(utils.DEFAULT_CONTENT);
  713. widget.activeCellIndex = 1;
  714. expect(widget.activeCell).toBe(widget.widgets[1]);
  715. });
  716. });
  717. describe('#activeCell', () => {
  718. it('should get the active cell widget', () => {
  719. const widget = createActiveWidget();
  720. expect(widget.activeCell).toBe(widget.widgets[0]);
  721. });
  722. });
  723. describe('#select()', () => {
  724. it('should select a cell widget', () => {
  725. const widget = createActiveWidget();
  726. widget.model!.fromJSON(utils.DEFAULT_CONTENT);
  727. const cell = widget.widgets[0];
  728. widget.select(cell);
  729. expect(widget.isSelected(cell)).toBe(true);
  730. });
  731. it('should allow multiple widgets to be selected', () => {
  732. const widget = createActiveWidget();
  733. widget.model!.fromJSON(utils.DEFAULT_CONTENT);
  734. widget.widgets.forEach(cell => {
  735. widget.select(cell);
  736. });
  737. const expectSelected = Array.from(Array(widget.widgets.length).keys());
  738. expect(selected(widget)).toEqual(expectSelected);
  739. });
  740. });
  741. describe('#deselect()', () => {
  742. it('should deselect a cell', () => {
  743. const widget = createActiveWidget();
  744. widget.model!.fromJSON(utils.DEFAULT_CONTENT);
  745. for (let i = 0; i < widget.widgets.length; i++) {
  746. const cell = widget.widgets[i];
  747. widget.select(cell);
  748. expect(widget.isSelected(cell)).toBe(true);
  749. widget.deselect(cell);
  750. expect(widget.isSelected(cell)).toBe(false);
  751. }
  752. });
  753. it('should const the active cell be deselected', () => {
  754. const widget = createActiveWidget();
  755. widget.model!.fromJSON(utils.DEFAULT_CONTENT);
  756. const cell = widget.activeCell!;
  757. widget.select(cell);
  758. expect(widget.isSelected(cell)).toBe(true);
  759. widget.deselect(cell);
  760. expect(widget.isSelected(cell)).toBe(false);
  761. });
  762. });
  763. describe('#isSelected()', () => {
  764. it('should get whether the cell is selected', () => {
  765. const widget = createActiveWidget();
  766. widget.model!.fromJSON(utils.DEFAULT_CONTENT);
  767. widget.select(widget.widgets[0]);
  768. widget.select(widget.widgets[2]);
  769. expect(selected(widget)).toEqual([0, 2]);
  770. });
  771. it('reports selection whether or not cell is active', () => {
  772. const widget = createActiveWidget();
  773. widget.model!.fromJSON(utils.DEFAULT_CONTENT);
  774. expect(selected(widget)).toEqual([]);
  775. widget.select(widget.activeCell!);
  776. expect(selected(widget)).toEqual([widget.activeCellIndex]);
  777. });
  778. });
  779. describe('#deselectAll()', () => {
  780. it('should deselect all cells', () => {
  781. const widget = createActiveWidget();
  782. widget.model!.fromJSON(utils.DEFAULT_CONTENT);
  783. widget.select(widget.widgets[0]);
  784. widget.select(widget.widgets[2]);
  785. widget.select(widget.widgets[3]);
  786. widget.select(widget.widgets[4]);
  787. expect(selected(widget)).toEqual([0, 2, 3, 4]);
  788. widget.deselectAll();
  789. expect(selected(widget)).toEqual([]);
  790. });
  791. });
  792. describe('#extendContiguousSelectionTo()', () => {
  793. // Test a permutation for extending a selection.
  794. const checkSelection = (
  795. widget: Notebook,
  796. anchor: number,
  797. head: number,
  798. index: number,
  799. select = true
  800. ) => {
  801. if (!select && anchor !== head) {
  802. throw new Error('anchor must equal head if select is false');
  803. }
  804. // Set up the test by pre-selecting appropriate cells if select is true.
  805. if (select) {
  806. for (
  807. let i = Math.min(anchor, head);
  808. i <= Math.max(anchor, head);
  809. i++
  810. ) {
  811. widget.select(widget.widgets[i]);
  812. }
  813. }
  814. // Set the active cell to indicate the head of the selection.
  815. widget.activeCellIndex = head;
  816. // Set up a selection event listener.
  817. let selectionChanged = 0;
  818. const countSelectionChanged = (sender: Notebook, args: void) => {
  819. selectionChanged += 1;
  820. };
  821. widget.selectionChanged.connect(countSelectionChanged);
  822. // Check the contiguous selection.
  823. let selection = widget.getContiguousSelection();
  824. if (select) {
  825. expect(selection.anchor).toBe(anchor);
  826. expect(selection.head).toBe(head);
  827. } else {
  828. expect(selection.anchor).toBeNull();
  829. expect(selection.head).toBeNull();
  830. }
  831. // Extend the selection.
  832. widget.extendContiguousSelectionTo(index);
  833. // Clip index to fall within the cell index range.
  834. index = Math.max(0, Math.min(widget.widgets.length - 1, index));
  835. // Check the active cell is now at the index.
  836. expect(widget.activeCellIndex).toBe(index);
  837. // Check the contiguous selection.
  838. selection = widget.getContiguousSelection();
  839. // Check the selection changed signal was emitted once if necessary.
  840. if (head === index) {
  841. if (index === anchor && select) {
  842. // we should have collapsed the single cell selection
  843. expect(selectionChanged).toBe(1);
  844. } else {
  845. expect(selectionChanged).toBe(0);
  846. }
  847. } else {
  848. expect(selectionChanged).toBe(1);
  849. }
  850. if (anchor !== index) {
  851. expect(selection.anchor).toBe(anchor);
  852. expect(selection.head).toBe(index);
  853. } else {
  854. // If the anchor and index are the same, the selection is collapsed.
  855. expect(selection.anchor).toBe(null);
  856. expect(selection.head).toBe(null);
  857. }
  858. // Clean up widget
  859. widget.selectionChanged.disconnect(countSelectionChanged);
  860. widget.activeCellIndex = 0;
  861. widget.deselectAll();
  862. };
  863. // Lists are of the form [anchor, head, index].
  864. const permutations = [
  865. // Anchor, head, and index are distinct
  866. [1, 3, 5],
  867. [1, 5, 3],
  868. [3, 1, 5],
  869. [3, 5, 1],
  870. [5, 1, 3],
  871. [5, 3, 1],
  872. // Two of anchor, head, and index are equal
  873. [1, 3, 3],
  874. [3, 1, 3],
  875. [3, 3, 1],
  876. // Anchor, head, and index all equal
  877. [3, 3, 3]
  878. ];
  879. it('should work in each permutation of anchor, head, and index', () => {
  880. const widget = createActiveWidget();
  881. widget.model!.fromJSON(utils.DEFAULT_CONTENT);
  882. permutations.forEach(p => {
  883. checkSelection(widget, p[0], p[1], p[2]);
  884. });
  885. });
  886. it('should work when we only have an active cell, with no existing selection', () => {
  887. const widget = createActiveWidget();
  888. widget.model!.fromJSON(utils.DEFAULT_CONTENT);
  889. permutations.forEach(p => {
  890. if (p[0] === p[1]) {
  891. checkSelection(widget, p[0], p[1], p[2], false);
  892. }
  893. });
  894. });
  895. it('should clip when the index is greater than the last index', () => {
  896. const widget = createActiveWidget();
  897. widget.model!.fromJSON(utils.DEFAULT_CONTENT);
  898. permutations.forEach(p => {
  899. checkSelection(widget, p[0], p[1], Number.MAX_SAFE_INTEGER);
  900. });
  901. });
  902. it('should clip when the index is greater than the last index with no existing selection', () => {
  903. const widget = createActiveWidget();
  904. widget.model!.fromJSON(utils.DEFAULT_CONTENT);
  905. permutations.forEach(p => {
  906. if (p[0] === p[1]) {
  907. checkSelection(widget, p[0], p[1], Number.MAX_SAFE_INTEGER, false);
  908. }
  909. });
  910. });
  911. it('should clip when the index is less than 0', () => {
  912. const widget = createActiveWidget();
  913. widget.model!.fromJSON(utils.DEFAULT_CONTENT);
  914. permutations.forEach(p => {
  915. checkSelection(widget, p[0], p[1], -10);
  916. });
  917. });
  918. it('should clip when the index is less than 0 with no existing selection', () => {
  919. const widget = createActiveWidget();
  920. widget.model!.fromJSON(utils.DEFAULT_CONTENT);
  921. permutations.forEach(p => {
  922. if (p[0] === p[1]) {
  923. checkSelection(widget, p[0], p[1], -10, false);
  924. }
  925. });
  926. });
  927. it('handles the case of no cells', () => {
  928. const widget = createActiveWidget();
  929. widget.model!.cells.clear();
  930. expect(widget.widgets.length).toBe(0);
  931. // Set up a selection event listener.
  932. let selectionChanged = 0;
  933. widget.selectionChanged.connect((sender, args) => {
  934. selectionChanged += 1;
  935. });
  936. widget.extendContiguousSelectionTo(3);
  937. expect(widget.activeCellIndex).toBe(-1);
  938. expect(selectionChanged).toBe(0);
  939. });
  940. });
  941. describe('#getContiguousSelection()', () => {
  942. it('throws an error when the selection is not contiguous', () => {
  943. const widget = createActiveWidget();
  944. widget.model!.fromJSON(utils.DEFAULT_CONTENT);
  945. widget.select(widget.widgets[1]);
  946. widget.select(widget.widgets[3]);
  947. widget.activeCellIndex = 3;
  948. expect(() => widget.getContiguousSelection()).toThrowError(
  949. /Selection not contiguous/
  950. );
  951. });
  952. it('throws an error if the active cell is not at an endpoint', () => {
  953. const widget = createActiveWidget();
  954. widget.model!.fromJSON(utils.DEFAULT_CONTENT);
  955. widget.select(widget.widgets[1]);
  956. widget.select(widget.widgets[2]);
  957. widget.select(widget.widgets[3]);
  958. // Check if active cell is outside selection.
  959. widget.activeCellIndex = 0;
  960. expect(() => widget.getContiguousSelection()).toThrowError(
  961. /Active cell not at endpoint of selection/
  962. );
  963. // Check if active cell is inside selection.
  964. widget.activeCellIndex = 2;
  965. expect(() => widget.getContiguousSelection()).toThrowError(
  966. /Active cell not at endpoint of selection/
  967. );
  968. });
  969. it('returns null values if there is no selection', () => {
  970. const widget = createActiveWidget();
  971. widget.model!.fromJSON(utils.DEFAULT_CONTENT);
  972. const selection = widget.getContiguousSelection();
  973. expect(selection).toEqual({ head: null, anchor: null });
  974. });
  975. it('handles the case of no cells', () => {
  976. const widget = createActiveWidget();
  977. widget.model!.cells.clear();
  978. expect(widget.widgets.length).toBe(0);
  979. const selection = widget.getContiguousSelection();
  980. expect(selection).toEqual({ head: null, anchor: null });
  981. });
  982. it('works if head is before the anchor', () => {
  983. const widget = createActiveWidget();
  984. widget.model!.fromJSON(utils.DEFAULT_CONTENT);
  985. widget.select(widget.widgets[1]);
  986. widget.select(widget.widgets[2]);
  987. widget.select(widget.widgets[3]);
  988. widget.activeCellIndex = 1;
  989. const selection = widget.getContiguousSelection();
  990. expect(selection).toEqual({ head: 1, anchor: 3 });
  991. });
  992. it('works if head is after the anchor', () => {
  993. const widget = createActiveWidget();
  994. widget.model!.fromJSON(utils.DEFAULT_CONTENT);
  995. widget.select(widget.widgets[1]);
  996. widget.select(widget.widgets[2]);
  997. widget.select(widget.widgets[3]);
  998. widget.activeCellIndex = 3;
  999. const selection = widget.getContiguousSelection();
  1000. expect(selection).toEqual({ head: 3, anchor: 1 });
  1001. });
  1002. it('works if head and anchor are the same', () => {
  1003. const widget = createActiveWidget();
  1004. widget.model!.fromJSON(utils.DEFAULT_CONTENT);
  1005. widget.select(widget.widgets[3]);
  1006. widget.activeCellIndex = 3;
  1007. const selection = widget.getContiguousSelection();
  1008. expect(selection).toEqual({ head: 3, anchor: 3 });
  1009. });
  1010. });
  1011. describe('#handleEvent()', () => {
  1012. let widget: LogNotebook;
  1013. beforeEach(async () => {
  1014. widget = createActiveWidget();
  1015. widget.model!.fromJSON(utils.DEFAULT_CONTENT);
  1016. Widget.attach(widget, document.body);
  1017. await framePromise();
  1018. });
  1019. afterEach(() => {
  1020. widget.dispose();
  1021. });
  1022. describe('mousedown', () => {
  1023. it('should set the active cell index', () => {
  1024. const child = widget.widgets[1];
  1025. simulate(child.node, 'mousedown');
  1026. expect(widget.events).toEqual(expect.arrayContaining(['mousedown']));
  1027. expect(widget.isSelected(widget.widgets[0])).toBe(false);
  1028. expect(widget.activeCellIndex).toBe(1);
  1029. });
  1030. it('should be a no-op if not not a cell', () => {
  1031. simulate(widget.node, 'mousedown');
  1032. expect(widget.events).toEqual(expect.arrayContaining(['mousedown']));
  1033. expect(widget.activeCellIndex).toBe(0);
  1034. });
  1035. it('should preserve "command" mode if in a markdown cell', () => {
  1036. const cell = widget.model!.contentFactory.createMarkdownCell({});
  1037. cell.value.text = '# Hello'; // Should be rendered with content.
  1038. widget.model!.cells.push(cell);
  1039. const count = widget.widgets.length;
  1040. const child = widget.widgets[count - 1] as MarkdownCell;
  1041. expect(child.rendered).toBe(true);
  1042. simulate(child.node, 'mousedown');
  1043. expect(child.rendered).toBe(true);
  1044. expect(widget.activeCell).toBe(child);
  1045. });
  1046. it('should extend selection if invoked with shift', () => {
  1047. widget.activeCellIndex = 3;
  1048. // shift click below
  1049. simulate(widget.widgets[4].node, 'mousedown', { shiftKey: true });
  1050. expect(widget.activeCellIndex).toBe(4);
  1051. expect(selected(widget)).toEqual([3, 4]);
  1052. // shift click above
  1053. simulate(widget.widgets[1].node, 'mousedown', { shiftKey: true });
  1054. expect(widget.activeCellIndex).toBe(1);
  1055. expect(selected(widget)).toEqual([1, 2, 3]);
  1056. // shift click expand
  1057. simulate(widget.widgets[0].node, 'mousedown', { shiftKey: true });
  1058. expect(widget.activeCellIndex).toBe(0);
  1059. expect(selected(widget)).toEqual([0, 1, 2, 3]);
  1060. // shift click contract
  1061. simulate(widget.widgets[2].node, 'mousedown', { shiftKey: true });
  1062. expect(widget.activeCellIndex).toBe(2);
  1063. expect(selected(widget)).toEqual([2, 3]);
  1064. });
  1065. it('should not extend a selection if there is text selected in the output', () => {
  1066. const codeCellIndex = 3;
  1067. widget.activeCellIndex = codeCellIndex;
  1068. // Set a selection in the active cell outputs.
  1069. const selection = window.getSelection()!;
  1070. selection.selectAllChildren(
  1071. (widget.activeCell as CodeCell).outputArea.node
  1072. );
  1073. // Shift click below, which should not extend cells selection.
  1074. simulate(widget.widgets[codeCellIndex + 2].node, 'mousedown', {
  1075. shiftKey: true
  1076. });
  1077. expect(widget.activeCellIndex).toBe(codeCellIndex);
  1078. expect(selected(widget)).toEqual([]);
  1079. });
  1080. it('should leave a markdown cell rendered', () => {
  1081. const code = widget.model!.contentFactory.createCodeCell({});
  1082. const md = widget.model!.contentFactory.createMarkdownCell({});
  1083. md.value.text = '# Hello'; // Should be rendered with content.
  1084. widget.model!.cells.push(code);
  1085. widget.model!.cells.push(md);
  1086. const count = widget.widgets.length;
  1087. const codeChild = widget.widgets[count - 2];
  1088. const mdChild = widget.widgets[count - 1] as MarkdownCell;
  1089. widget.select(codeChild);
  1090. widget.select(mdChild);
  1091. widget.activeCellIndex = count - 2;
  1092. expect(mdChild.rendered).toBe(true);
  1093. simulate(codeChild.editorWidget.node, 'mousedown');
  1094. simulate(codeChild.editorWidget.node, 'focusin');
  1095. expect(mdChild.rendered).toBe(true);
  1096. expect(widget.activeCell).toBe(codeChild);
  1097. expect(widget.mode).toBe('edit');
  1098. });
  1099. it('should remove selection and switch to command mode', () => {
  1100. const code = widget.model!.contentFactory.createCodeCell({});
  1101. const md = widget.model!.contentFactory.createMarkdownCell({});
  1102. widget.model!.cells.push(code);
  1103. widget.model!.cells.push(md);
  1104. const count = widget.widgets.length;
  1105. const codeChild = widget.widgets[count - 2];
  1106. const mdChild = widget.widgets[count - 1] as MarkdownCell;
  1107. widget.select(codeChild);
  1108. widget.select(mdChild);
  1109. widget.activeCellIndex = count - 2;
  1110. simulate(codeChild.editorWidget.node, 'mousedown');
  1111. simulate(codeChild.editorWidget.node, 'focusin');
  1112. expect(widget.mode).toBe('edit');
  1113. simulate(codeChild.editorWidget.node, 'mousedown', { button: 2 });
  1114. expect(widget.isSelected(mdChild)).toBe(false);
  1115. expect(widget.mode).toBe('command');
  1116. });
  1117. it('should have no effect on shift right click', () => {
  1118. const code = widget.model!.contentFactory.createCodeCell({});
  1119. const md = widget.model!.contentFactory.createMarkdownCell({});
  1120. widget.model!.cells.push(code);
  1121. widget.model!.cells.push(md);
  1122. const count = widget.widgets.length;
  1123. const codeChild = widget.widgets[count - 2];
  1124. const mdChild = widget.widgets[count - 1] as MarkdownCell;
  1125. widget.select(codeChild);
  1126. widget.select(mdChild);
  1127. widget.activeCellIndex = count - 2;
  1128. simulate(codeChild.editorWidget.node, 'mousedown', {
  1129. shiftKey: true,
  1130. button: 2
  1131. });
  1132. expect(widget.isSelected(mdChild)).toBe(true);
  1133. expect(widget.mode).toBe('command');
  1134. });
  1135. });
  1136. describe('dblclick', () => {
  1137. it('should unrender a markdown cell', () => {
  1138. const cell = widget.model!.contentFactory.createMarkdownCell({});
  1139. cell.value.text = '# Hello'; // Should be rendered with content.
  1140. widget.model!.cells.push(cell);
  1141. const child = widget.widgets[
  1142. widget.widgets.length - 1
  1143. ] as MarkdownCell;
  1144. expect(child.rendered).toBe(true);
  1145. expect(widget.mode).toBe('command');
  1146. simulate(child.node, 'dblclick');
  1147. expect(widget.mode).toBe('command');
  1148. expect(child.rendered).toBe(false);
  1149. });
  1150. });
  1151. describe('focusin', () => {
  1152. it('should change to edit mode if a child cell takes focus', () => {
  1153. const child = widget.widgets[0];
  1154. simulate(child.editorWidget.node, 'focusin');
  1155. expect(widget.events).toEqual(expect.arrayContaining(['focusin']));
  1156. expect(widget.mode).toBe('edit');
  1157. });
  1158. it('should change to command mode if the widget takes focus', () => {
  1159. const child = widget.widgets[0];
  1160. simulate(child.editorWidget.node, 'focusin');
  1161. expect(widget.events).toEqual(expect.arrayContaining(['focusin']));
  1162. expect(widget.mode).toBe('edit');
  1163. widget.events = [];
  1164. simulate(widget.node, 'focusin');
  1165. expect(widget.events).toEqual(expect.arrayContaining(['focusin']));
  1166. expect(widget.mode).toBe('command');
  1167. });
  1168. });
  1169. describe('focusout', () => {
  1170. it('should switch to command mode', () => {
  1171. simulate(widget.node, 'focusin');
  1172. widget.mode = 'edit';
  1173. const event = generate('focusout');
  1174. (event as any).relatedTarget = document.body;
  1175. widget.node.dispatchEvent(event);
  1176. expect(widget.mode).toBe('command');
  1177. MessageLoop.sendMessage(widget, Widget.Msg.ActivateRequest);
  1178. expect(widget.mode).toBe('command');
  1179. expect(widget.activeCell!.editor.hasFocus()).toBe(false);
  1180. });
  1181. it('should set command mode', () => {
  1182. simulate(widget.node, 'focusin');
  1183. widget.mode = 'edit';
  1184. const evt = generate('focusout');
  1185. (evt as any).relatedTarget = widget.activeCell!.node;
  1186. widget.node.dispatchEvent(evt);
  1187. expect(widget.mode).toBe('command');
  1188. });
  1189. });
  1190. });
  1191. describe('#onAfterAttach()', () => {
  1192. it('should add event listeners', async () => {
  1193. const widget = createActiveWidget();
  1194. widget.model!.fromJSON(utils.DEFAULT_CONTENT);
  1195. Widget.attach(widget, document.body);
  1196. const child = widget.widgets[0];
  1197. await framePromise();
  1198. expect(widget.methods).toEqual(
  1199. expect.arrayContaining(['onAfterAttach'])
  1200. );
  1201. simulate(widget.node, 'mousedown');
  1202. expect(widget.events).toEqual(expect.arrayContaining(['mousedown']));
  1203. simulate(widget.node, 'dblclick');
  1204. expect(widget.events).toEqual(expect.arrayContaining(['dblclick']));
  1205. simulate(child.node, 'focusin');
  1206. expect(widget.events).toEqual(expect.arrayContaining(['focusin']));
  1207. widget.dispose();
  1208. });
  1209. it('should post an update request', async () => {
  1210. const widget = createActiveWidget();
  1211. widget.model!.fromJSON(utils.DEFAULT_CONTENT);
  1212. Widget.attach(widget, document.body);
  1213. await framePromise();
  1214. expect(widget.methods).toEqual(
  1215. expect.arrayContaining(['onAfterAttach'])
  1216. );
  1217. await framePromise();
  1218. expect(widget.methods).toEqual(
  1219. expect.arrayContaining(['onUpdateRequest'])
  1220. );
  1221. widget.dispose();
  1222. });
  1223. });
  1224. describe('#onBeforeDetach()', () => {
  1225. it('should remove event listeners', async () => {
  1226. const widget = createActiveWidget();
  1227. widget.model!.fromJSON(utils.DEFAULT_CONTENT);
  1228. Widget.attach(widget, document.body);
  1229. const child = widget.widgets[0];
  1230. await framePromise();
  1231. Widget.detach(widget);
  1232. expect(widget.methods).toEqual(
  1233. expect.arrayContaining(['onBeforeDetach'])
  1234. );
  1235. widget.events = [];
  1236. simulate(widget.node, 'mousedown');
  1237. expect(widget.events).toEqual(
  1238. expect.not.arrayContaining(['mousedown'])
  1239. );
  1240. simulate(widget.node, 'dblclick');
  1241. expect(widget.events).toEqual(expect.not.arrayContaining(['dblclick']));
  1242. simulate(child.node, 'focusin');
  1243. expect(widget.events).toEqual(expect.not.arrayContaining(['focusin']));
  1244. widget.dispose();
  1245. });
  1246. });
  1247. describe('#onActivateRequest()', () => {
  1248. it('should focus the node after an update', async () => {
  1249. const widget = createActiveWidget();
  1250. Widget.attach(widget, document.body);
  1251. MessageLoop.sendMessage(widget, Widget.Msg.ActivateRequest);
  1252. expect(widget.methods).toEqual(
  1253. expect.arrayContaining(['onActivateRequest'])
  1254. );
  1255. await framePromise();
  1256. expect(document.activeElement).toBe(widget.node);
  1257. widget.dispose();
  1258. });
  1259. });
  1260. describe('#onUpdateRequest()', () => {
  1261. let widget: LogNotebook;
  1262. beforeEach(async () => {
  1263. widget = createActiveWidget();
  1264. widget.model!.fromJSON(utils.DEFAULT_CONTENT);
  1265. Widget.attach(widget, document.body);
  1266. await framePromise();
  1267. });
  1268. afterEach(() => {
  1269. widget.dispose();
  1270. });
  1271. it('should apply the command class if in command mode', () => {
  1272. expect(widget.methods).toEqual(
  1273. expect.arrayContaining(['onUpdateRequest'])
  1274. );
  1275. expect(widget.hasClass('jp-mod-commandMode')).toBe(true);
  1276. });
  1277. it('should apply the edit class if in edit mode', async () => {
  1278. widget.mode = 'edit';
  1279. await framePromise();
  1280. expect(widget.hasClass('jp-mod-editMode')).toBe(true);
  1281. });
  1282. it('should add the active class to the active widget', () => {
  1283. const cell = widget.widgets[widget.activeCellIndex];
  1284. expect(cell.hasClass('jp-mod-active')).toBe(true);
  1285. });
  1286. it('should set the selected class on the selected widgets', async () => {
  1287. widget.select(widget.widgets[1]);
  1288. await framePromise();
  1289. for (let i = 0; i < 2; i++) {
  1290. const cell = widget.widgets[i];
  1291. expect(cell.hasClass('jp-mod-selected')).toBe(true);
  1292. }
  1293. });
  1294. it('should add the multi select class if there is more than one widget', async () => {
  1295. widget.select(widget.widgets[1]);
  1296. expect(widget.hasClass('jp-mod-multSelected')).toBe(false);
  1297. await framePromise();
  1298. expect(widget.hasClass('jp-mod-multSelected')).toBe(false);
  1299. });
  1300. });
  1301. describe('#onCellInserted()', () => {
  1302. it('should post an `update-request', async () => {
  1303. const widget = createActiveWidget();
  1304. widget.model!.fromJSON(utils.DEFAULT_CONTENT);
  1305. expect(widget.methods).toEqual(
  1306. expect.arrayContaining(['onCellInserted'])
  1307. );
  1308. await framePromise();
  1309. expect(widget.methods).toEqual(
  1310. expect.arrayContaining(['onUpdateRequest'])
  1311. );
  1312. });
  1313. it('should update the active cell if necessary', () => {
  1314. const widget = createActiveWidget();
  1315. widget.model!.fromJSON(utils.DEFAULT_CONTENT);
  1316. expect(widget.activeCell).toBe(widget.widgets[0]);
  1317. });
  1318. it('should keep the currently active cell active', () => {
  1319. const widget = createActiveWidget();
  1320. widget.model!.fromJSON(utils.DEFAULT_CONTENT);
  1321. widget.activeCellIndex = 1;
  1322. const cell = widget.model!.contentFactory.createCodeCell({});
  1323. widget.model!.cells.insert(1, cell);
  1324. expect(widget.activeCell).toBe(widget.widgets[2]);
  1325. });
  1326. describe('`edgeRequested` signal', () => {
  1327. it('should activate the previous cell if top is requested', () => {
  1328. const widget = createActiveWidget();
  1329. widget.model!.fromJSON(utils.DEFAULT_CONTENT);
  1330. widget.activeCellIndex = 1;
  1331. const child = widget.widgets[widget.activeCellIndex];
  1332. (child.editor.edgeRequested as any).emit('top');
  1333. expect(widget.activeCellIndex).toBe(0);
  1334. });
  1335. it('should activate the next cell if bottom is requested', () => {
  1336. const widget = createActiveWidget();
  1337. widget.model!.fromJSON(utils.DEFAULT_CONTENT);
  1338. const child = widget.widgets[widget.activeCellIndex];
  1339. (child.editor.edgeRequested as any).emit('bottom');
  1340. expect(widget.activeCellIndex).toBe(1);
  1341. });
  1342. });
  1343. });
  1344. describe('#onCellMoved()', () => {
  1345. it('should update the active cell index if necessary', () => {
  1346. const widget = createActiveWidget();
  1347. // [fromIndex, toIndex, activeIndex], starting with activeIndex=3.
  1348. const moves = [
  1349. [0, 2, 3],
  1350. [0, 3, 2],
  1351. [0, 4, 2],
  1352. [3, 2, 2],
  1353. [3, 3, 3],
  1354. [3, 4, 4],
  1355. [4, 2, 4],
  1356. [4, 3, 4],
  1357. [4, 5, 3]
  1358. ];
  1359. moves.forEach(m => {
  1360. const [fromIndex, toIndex, activeIndex] = m;
  1361. widget.model!.fromJSON(utils.DEFAULT_CONTENT);
  1362. const cell = widget.widgets[3];
  1363. widget.activeCellIndex = 3;
  1364. widget.model!.cells.move(fromIndex, toIndex);
  1365. expect(widget.activeCellIndex).toBe(activeIndex);
  1366. expect(widget.widgets[activeIndex]).toBe(cell);
  1367. });
  1368. });
  1369. });
  1370. describe('#onCellRemoved()', () => {
  1371. it('should post an `update-request', async () => {
  1372. const widget = createActiveWidget();
  1373. const cell = widget.model!.cells.get(0);
  1374. widget.model!.cells.removeValue(cell);
  1375. expect(widget.methods).toEqual(
  1376. expect.arrayContaining(['onCellRemoved'])
  1377. );
  1378. await framePromise();
  1379. expect(widget.methods).toEqual(
  1380. expect.arrayContaining(['onUpdateRequest'])
  1381. );
  1382. });
  1383. it('should update the active cell if necessary', () => {
  1384. const widget = createActiveWidget();
  1385. widget.model!.fromJSON(utils.DEFAULT_CONTENT);
  1386. widget.model!.cells.remove(0);
  1387. expect(widget.activeCell).toBe(widget.widgets[0]);
  1388. });
  1389. it('should keep the currently active cell active', () => {
  1390. const widget = createActiveWidget();
  1391. widget.model!.fromJSON(utils.DEFAULT_CONTENT);
  1392. widget.activeCellIndex = 2;
  1393. widget.model!.cells.remove(1);
  1394. expect(widget.activeCell).toBe(widget.widgets[1]);
  1395. });
  1396. });
  1397. // WIP See https://github.com/jupyterlab/jupyterlab/issues/10526
  1398. describe('#virtualNotebook()', () => {
  1399. it('should render the last cell widget', () => {
  1400. const model = new NotebookModel();
  1401. const widget = new StaticNotebook(options);
  1402. widget.model = model;
  1403. widget.model!.fromJSON(utils.DEFAULT_CONTENT);
  1404. const cell = widget.widgets[5];
  1405. expect(
  1406. cell.inputArea.editorWidget.model.value.text.startsWith(
  1407. 'from IPython.display import Latex'
  1408. )
  1409. ).toBe(true);
  1410. console.log();
  1411. });
  1412. });
  1413. });
  1414. });