widget.spec.ts 54 KB

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