widget.spec.ts 54 KB

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