widget.spec.ts 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import expect = require('expect.js');
  4. import {
  5. MessageLoop, Message
  6. } from '@phosphor/messaging';
  7. import {
  8. Widget
  9. } from '@phosphor/widgets';
  10. import {
  11. generate, simulate
  12. } from 'simulate-event';
  13. import {
  14. CodeCellModel, CodeCell, MarkdownCellModel, MarkdownCell,
  15. RawCellModel, RawCell, Cell
  16. } from '@jupyterlab/cells';
  17. import {
  18. INotebookModel, NotebookModel
  19. } from '@jupyterlab/notebook';
  20. import {
  21. Notebook, StaticNotebook
  22. } from '@jupyterlab/notebook';
  23. import {
  24. DEFAULT_CONTENT, createNotebookFactory, rendermime, mimeTypeService,
  25. editorFactory
  26. } from './utils';
  27. const contentFactory = createNotebookFactory();
  28. const options: Notebook.IOptions = {
  29. rendermime, contentFactory, mimeTypeService
  30. };
  31. function createWidget(): LogStaticNotebook {
  32. let model = new NotebookModel();
  33. let widget = new LogStaticNotebook(options);
  34. widget.model = model;
  35. return widget;
  36. }
  37. class LogStaticNotebook extends StaticNotebook {
  38. methods: string[] = [];
  39. protected onUpdateRequest(msg: Message): void {
  40. super.onUpdateRequest(msg);
  41. this.methods.push('onUpdateRequest');
  42. }
  43. protected onModelChanged(oldValue: INotebookModel, newValue: INotebookModel): void {
  44. super.onModelChanged(oldValue, newValue);
  45. this.methods.push('onModelChanged');
  46. }
  47. protected onMetadataChanged(model: any, args: any): void {
  48. super.onMetadataChanged(model, args);
  49. this.methods.push('onMetadataChanged');
  50. }
  51. protected onCellInserted(index: number, cell: Cell): void {
  52. super.onCellInserted(index, cell);
  53. this.methods.push('onCellInserted');
  54. }
  55. protected onCellMoved(fromIndex: number, toIndex: number): void {
  56. super.onCellMoved(fromIndex, toIndex);
  57. this.methods.push('onCellMoved');
  58. }
  59. protected onCellRemoved(index: number, cell: Cell): void {
  60. super.onCellRemoved(index, cell);
  61. this.methods.push('onCellRemoved');
  62. }
  63. }
  64. class LogNotebook extends Notebook {
  65. events: string[] = [];
  66. methods: string[] = [];
  67. handleEvent(event: Event): void {
  68. this.events.push(event.type);
  69. super.handleEvent(event);
  70. }
  71. protected onAfterAttach(msg: Message): void {
  72. super.onAfterAttach(msg);
  73. this.methods.push('onAfterAttach');
  74. }
  75. protected onBeforeDetach(msg: Message): void {
  76. super.onBeforeDetach(msg);
  77. this.methods.push('onBeforeDetach');
  78. }
  79. protected onActivateRequest(msg: Message): void {
  80. super.onActivateRequest(msg);
  81. this.methods.push('onActivateRequest');
  82. }
  83. protected onUpdateRequest(msg: Message): void {
  84. super.onUpdateRequest(msg);
  85. this.methods.push('onUpdateRequest');
  86. }
  87. protected onCellInserted(index: number, cell: Cell): void {
  88. super.onCellInserted(index, cell);
  89. this.methods.push('onCellInserted');
  90. }
  91. protected onCellMoved(fromIndex: number, toIndex: number): void {
  92. super.onCellMoved(fromIndex, toIndex);
  93. this.methods.push('onCellMoved');
  94. }
  95. protected onCellRemoved(index: number, cell: Cell): void {
  96. super.onCellRemoved(index, cell);
  97. this.methods.push('onCellRemoved');
  98. }
  99. }
  100. function createActiveWidget(): LogNotebook {
  101. let model = new NotebookModel();
  102. let widget = new LogNotebook(options);
  103. widget.model = model;
  104. return widget;
  105. }
  106. describe('notebook/widget', () => {
  107. describe('StaticNotebook', () => {
  108. describe('#constructor()', () => {
  109. it('should create a notebook widget', () => {
  110. let widget = new StaticNotebook(options);
  111. expect(widget).to.be.a(StaticNotebook);
  112. });
  113. it('should add the `jp-Notebook` class', () => {
  114. let widget = new StaticNotebook(options);
  115. expect(widget.hasClass('jp-Notebook')).to.be(true);
  116. });
  117. it('should accept an optional render', () => {
  118. let widget = new StaticNotebook(options);
  119. expect(widget.contentFactory).to.be(contentFactory);
  120. });
  121. });
  122. describe('#modelChanged', () => {
  123. it('should be emitted when the model changes', () => {
  124. let widget = new StaticNotebook(options);
  125. let model = new NotebookModel();
  126. let called = false;
  127. widget.modelChanged.connect((sender, args) => {
  128. expect(sender).to.be(widget);
  129. expect(args).to.be(void 0);
  130. called = true;
  131. });
  132. widget.model = model;
  133. expect(called).to.be(true);
  134. });
  135. });
  136. describe('#modelContentChanged', () => {
  137. it('should be emitted when a cell is added', () => {
  138. let widget = new StaticNotebook(options);
  139. widget.model = new NotebookModel();
  140. let called = false;
  141. widget.modelContentChanged.connect(() => { called = true; });
  142. let cell = widget.model.contentFactory.createCodeCell({});
  143. widget.model.cells.pushBack(cell);
  144. expect(called).to.be(true);
  145. });
  146. it('should be emitted when metadata is set', () => {
  147. let widget = new StaticNotebook(options);
  148. widget.model = new NotebookModel();
  149. let called = false;
  150. widget.modelContentChanged.connect(() => { called = true; });
  151. let cursor = widget.model.metadata.set('foo', 1);
  152. expect(called).to.be(true);
  153. });
  154. });
  155. describe('#model', () => {
  156. it('should get the model for the widget', () => {
  157. let widget = new StaticNotebook(options);
  158. expect(widget.model).to.be(null);
  159. });
  160. it('should set the model for the widget', () => {
  161. let widget = new StaticNotebook(options);
  162. let model = new NotebookModel();
  163. widget.model = model;
  164. expect(widget.model).to.be(model);
  165. });
  166. it('should emit the `modelChanged` signal', () => {
  167. let widget = new StaticNotebook(options);
  168. let model = new NotebookModel();
  169. widget.model = model;
  170. let called = false;
  171. widget.modelChanged.connect(() => { called = true; });
  172. widget.model = new NotebookModel();
  173. expect(called).to.be(true);
  174. });
  175. it('should be a no-op if the value does not change', () => {
  176. let widget = new StaticNotebook(options);
  177. let model = new NotebookModel();
  178. widget.model = model;
  179. let called = false;
  180. widget.modelChanged.connect(() => { called = true; });
  181. widget.model = model;
  182. expect(called).to.be(false);
  183. });
  184. it('should add the model cells to the layout', () => {
  185. let widget = new LogStaticNotebook(options);
  186. let model = new NotebookModel();
  187. model.fromJSON(DEFAULT_CONTENT);
  188. widget.model = model;
  189. expect(widget.widgets.length).to.be(6);
  190. });
  191. it('should set the mime types of the cell widgets', () => {
  192. let widget = new LogStaticNotebook(options);
  193. let model = new NotebookModel();
  194. let value = { name: 'python', codemirror_mode: 'python' };
  195. model.metadata.set('language_info', value);
  196. widget.model = model;
  197. let child = widget.widgets[0];
  198. expect(child.model.mimeType).to.be('text/x-python');
  199. });
  200. context('`cells.changed` signal', () => {
  201. let widget: LogStaticNotebook;
  202. beforeEach(() => {
  203. widget = createWidget();
  204. widget.model.fromJSON(DEFAULT_CONTENT);
  205. });
  206. afterEach(() => {
  207. widget.dispose();
  208. });
  209. it('should handle changes to the model cell list', (done) => {
  210. widget = createWidget();
  211. widget.model.cells.clear();
  212. // The model should add a single code cell.
  213. requestAnimationFrame(() => {
  214. expect(widget.widgets.length).to.be(1);
  215. done();
  216. });
  217. });
  218. it('should handle a remove', () => {
  219. let cell = widget.model.cells.at(1);
  220. let child = widget.widgets[1];
  221. widget.model.cells.remove(cell);
  222. expect(cell.isDisposed).to.be(false);
  223. expect(child.isDisposed).to.be(true);
  224. });
  225. it('should handle an add', () => {
  226. let cell = widget.model.contentFactory.createCodeCell({});
  227. widget.model.cells.pushBack(cell);
  228. expect(widget.widgets.length).to.be(7);
  229. let child = widget.widgets[0];
  230. expect(child.hasClass('jp-Notebook-cell')).to.be(true);
  231. });
  232. it('should handle a move', () => {
  233. let child = widget.widgets[1];
  234. widget.model.cells.move(1, 2);
  235. expect(widget.widgets[2]).to.be(child);
  236. });
  237. it('should handle a clear', () => {
  238. let cell = widget.model.contentFactory.createCodeCell({});
  239. widget.model.cells.pushBack(cell);
  240. widget.model.cells.clear();
  241. expect(widget.widgets.length).to.be(0);
  242. });
  243. });
  244. });
  245. describe('#rendermime', () => {
  246. it('should be the rendermime instance used by the widget', () => {
  247. let widget = new StaticNotebook(options);
  248. expect(widget.rendermime).to.be(rendermime);
  249. });
  250. });
  251. describe('#contentFactory', () => {
  252. it('should be the cell widget contentFactory used by the widget', () => {
  253. let widget = new StaticNotebook(options);
  254. expect(widget.contentFactory).to.be.a(StaticNotebook.ContentFactory);
  255. });
  256. });
  257. describe('#codeMimetype', () => {
  258. it('should get the mime type for code cells', () => {
  259. let widget = new StaticNotebook(options);
  260. expect(widget.codeMimetype).to.be('text/plain');
  261. });
  262. it('should be set from language metadata', () => {
  263. let widget = new LogStaticNotebook(options);
  264. let model = new NotebookModel();
  265. let value = { name: 'python', codemirror_mode: 'python' };
  266. model.metadata.set('language_info', value);
  267. widget.model = model;
  268. expect(widget.codeMimetype).to.be('text/x-python');
  269. });
  270. });
  271. describe('#widgets', () => {
  272. it('should get the child widget at a specified index', () => {
  273. let widget = createWidget();
  274. let child = widget.widgets[0];
  275. expect(child).to.be.a(CodeCell);
  276. });
  277. it('should return `undefined` if out of range', () => {
  278. let widget = createWidget();
  279. let child = widget.widgets[1];
  280. expect(child).to.be(void 0);
  281. });
  282. it('should get the number of child widgets', () => {
  283. let widget = createWidget();
  284. expect(widget.widgets.length).to.be(1);
  285. widget.model.fromJSON(DEFAULT_CONTENT);
  286. expect(widget.widgets.length).to.be(6);
  287. });
  288. });
  289. describe('#dispose()', () => {
  290. it('should dispose of the resources held by the widget', () => {
  291. let widget = createWidget();
  292. widget.dispose();
  293. expect(widget.isDisposed).to.be(true);
  294. });
  295. it('should be safe to call multiple times', () => {
  296. let widget = createWidget();
  297. widget.dispose();
  298. widget.dispose();
  299. expect(widget.isDisposed).to.be(true);
  300. });
  301. });
  302. describe('#onModelChanged()', () => {
  303. it('should be called when the model changes', () => {
  304. let widget = new LogStaticNotebook(options);
  305. widget.model = new NotebookModel();
  306. expect(widget.methods).to.contain('onModelChanged');
  307. });
  308. it('should not be called if the model does not change', () => {
  309. let widget = createWidget();
  310. widget.methods = [];
  311. widget.model = widget.model;
  312. expect(widget.methods).to.not.contain('onModelChanged');
  313. });
  314. });
  315. describe('#onMetadataChanged()', () => {
  316. it('should be called when the metadata on the notebook changes', () => {
  317. let widget = createWidget();
  318. widget.model.metadata.set('foo', 1);
  319. expect(widget.methods).to.contain('onMetadataChanged');
  320. });
  321. it('should update the `codeMimetype`', () => {
  322. let widget = createWidget();
  323. let value = { name: 'python', codemirror_mode: 'python' };
  324. widget.model.metadata.set('language_info', value);
  325. expect(widget.methods).to.contain('onMetadataChanged');
  326. expect(widget.codeMimetype).to.be('text/x-python');
  327. });
  328. it('should update the cell widget mimetype', () => {
  329. let widget = createWidget();
  330. let value = { name: 'python', mimetype: 'text/x-python' };
  331. widget.model.metadata.set('language_info', value);
  332. expect(widget.methods).to.contain('onMetadataChanged');
  333. let child = widget.widgets[0];
  334. expect(child.model.mimeType).to.be('text/x-python');
  335. });
  336. });
  337. describe('#onCellInserted()', () => {
  338. it('should be called when a cell is inserted', () => {
  339. let widget = createWidget();
  340. widget.model.fromJSON(DEFAULT_CONTENT);
  341. expect(widget.methods).to.contain('onCellInserted');
  342. });
  343. });
  344. describe('#onCellMoved()', () => {
  345. it('should be called when a cell is moved', () => {
  346. let widget = createWidget();
  347. widget.model.fromJSON(DEFAULT_CONTENT);
  348. widget.model.cells.move(0, 1);
  349. expect(widget.methods).to.contain('onCellMoved');
  350. });
  351. });
  352. describe('#onCellRemoved()', () => {
  353. it('should be called when a cell is removed', () => {
  354. let widget = createWidget();
  355. let cell = widget.model.cells.at(0);
  356. widget.model.cells.remove(cell);
  357. expect(widget.methods).to.contain('onCellRemoved');
  358. });
  359. });
  360. describe('.ContentFactory', () => {
  361. describe('#constructor', () => {
  362. it('should create a new ContentFactory', () => {
  363. let factory = new StaticNotebook.ContentFactory({ editorFactory });
  364. expect(factory).to.be.a(StaticNotebook.ContentFactory);
  365. });
  366. });
  367. describe('#codeCellContentFactory', () => {
  368. it('should be a CodeCell.ContentFactory', () => {
  369. let factory = new StaticNotebook.ContentFactory({ editorFactory });
  370. expect(factory.codeCellContentFactory).to.be.a(CodeCell.ContentFactory);
  371. });
  372. });
  373. describe('#markdownCellContentFactory', () => {
  374. it('should be a Cell.ContentFactory', () => {
  375. let factory = new StaticNotebook.ContentFactory({ editorFactory });
  376. expect(factory.markdownCellContentFactory).to.be.a(Cell.ContentFactory);
  377. });
  378. });
  379. describe('#rawCellContentFactory', () => {
  380. it('should be a Cell.ContentFactory', () => {
  381. let factory = new StaticNotebook.ContentFactory({ editorFactory });
  382. expect(factory.rawCellContentFactory).to.be.a(Cell.ContentFactory);
  383. });
  384. });
  385. describe('#createCodeCell({})', () => {
  386. it('should create a `CodeCell`', () => {
  387. let factory = new StaticNotebook.ContentFactory({ editorFactory });
  388. let contentFactory = factory.codeCellContentFactory;
  389. let model = new CodeCellModel({});
  390. let codeOptions = { model, rendermime, contentFactory };
  391. let parent = new StaticNotebook(options);
  392. let widget = factory.createCodeCell(codeOptions, parent);
  393. expect(widget).to.be.a(CodeCell);
  394. });
  395. });
  396. describe('#createMarkdownCell({})', () => {
  397. it('should create a `MarkdownCell`', () => {
  398. let factory = new StaticNotebook.ContentFactory({ editorFactory });
  399. let contentFactory = factory.markdownCellContentFactory;
  400. let model = new MarkdownCellModel({});
  401. let mdOptions = { model, rendermime, contentFactory };
  402. let parent = new StaticNotebook(options);
  403. let widget = factory.createMarkdownCell(mdOptions, parent);
  404. expect(widget).to.be.a(MarkdownCell);
  405. });
  406. });
  407. describe('#createRawCell()', () => {
  408. it('should create a `RawCell`', () => {
  409. let factory = new StaticNotebook.ContentFactory({ editorFactory });
  410. let contentFactory = factory.rawCellContentFactory;
  411. let model = new RawCellModel({});
  412. let rawOptions = { model, contentFactory };
  413. let parent = new StaticNotebook(options);
  414. let widget = factory.createRawCell(rawOptions, parent);
  415. expect(widget).to.be.a(RawCell);
  416. });
  417. });
  418. });
  419. });
  420. describe('Notebook', () => {
  421. describe('#stateChanged', () => {
  422. it('should be emitted when the state of the notebook changes', () => {
  423. let widget = createActiveWidget();
  424. let called = false;
  425. widget.stateChanged.connect((sender, args) => {
  426. expect(sender).to.be(widget);
  427. expect(args.name).to.be('mode');
  428. expect(args.oldValue).to.be('command');
  429. expect(args.newValue).to.be('edit');
  430. called = true;
  431. });
  432. widget.mode = 'edit';
  433. expect(called).to.be(true);
  434. });
  435. });
  436. describe('#activeCellChanged', () => {
  437. it('should be emitted when the active cell changes', () => {
  438. let widget = createActiveWidget();
  439. widget.model.fromJSON(DEFAULT_CONTENT);
  440. let called = false;
  441. widget.activeCellChanged.connect((sender, args) => {
  442. expect(sender).to.be(widget);
  443. expect(args).to.be(widget.activeCell);
  444. called = true;
  445. });
  446. widget.activeCellIndex++;
  447. expect(called).to.be(true);
  448. });
  449. it('should not be emitted when the active cell does not change', () => {
  450. let widget = createActiveWidget();
  451. widget.model.fromJSON(DEFAULT_CONTENT);
  452. let called = false;
  453. widget.activeCellChanged.connect(() => { called = true; });
  454. widget.activeCellIndex = widget.activeCellIndex;
  455. expect(called).to.be(false);
  456. });
  457. });
  458. describe('#selectionChanged', () => {
  459. it('should be emitted when the selection changes', () => {
  460. let widget = createActiveWidget();
  461. widget.model.fromJSON(DEFAULT_CONTENT);
  462. let called = false;
  463. widget.selectionChanged.connect((sender, args) => {
  464. expect(sender).to.be(widget);
  465. expect(args).to.be(void 0);
  466. called = true;
  467. });
  468. widget.select(widget.widgets[1]);
  469. expect(called).to.be(true);
  470. });
  471. it('should not be emitted when the selection does not change', () => {
  472. let widget = createActiveWidget();
  473. widget.model.fromJSON(DEFAULT_CONTENT);
  474. let called = false;
  475. widget.select(widget.widgets[1]);
  476. widget.selectionChanged.connect(() => { called = true; });
  477. widget.select(widget.widgets[1]);
  478. expect(called).to.be(false);
  479. });
  480. });
  481. describe('#mode', () => {
  482. it('should get the interactivity mode of the notebook', () => {
  483. let widget = createActiveWidget();
  484. expect(widget.mode).to.be('command');
  485. });
  486. it('should set the interactivity mode of the notebook', () => {
  487. let widget = createActiveWidget();
  488. widget.mode = 'edit';
  489. expect(widget.mode).to.be('edit');
  490. });
  491. it('should emit the `stateChanged` signal', () => {
  492. let widget = createActiveWidget();
  493. let called = false;
  494. widget.stateChanged.connect((sender, args) => {
  495. expect(sender).to.be(widget);
  496. expect(args.name).to.be('mode');
  497. expect(args.oldValue).to.be('command');
  498. expect(args.newValue).to.be('edit');
  499. called = true;
  500. });
  501. widget.mode = 'edit';
  502. expect(called).to.be(true);
  503. });
  504. it('should be a no-op if the value does not change', () => {
  505. let widget = createActiveWidget();
  506. let called = false;
  507. widget.stateChanged.connect(() => { called = true; });
  508. widget.mode = 'command';
  509. expect(called).to.be(false);
  510. });
  511. it('should post an update request', (done) => {
  512. let widget = createActiveWidget();
  513. requestAnimationFrame(() => {
  514. expect(widget.methods).to.contain('onUpdateRequest');
  515. done();
  516. });
  517. widget.mode = 'edit';
  518. });
  519. it('should deselect all cells if switching to edit mode', (done) => {
  520. let widget = createActiveWidget();
  521. widget.model.fromJSON(DEFAULT_CONTENT);
  522. Widget.attach(widget, document.body);
  523. requestAnimationFrame(() => {
  524. for (let i = 0; i < widget.widgets.length; i++) {
  525. let cell = widget.widgets[i];
  526. widget.select(cell);
  527. expect(widget.isSelected(cell)).to.be(true);
  528. }
  529. widget.mode = 'edit';
  530. for (let i = 0; i < widget.widgets.length; i++) {
  531. if (i === widget.activeCellIndex) {
  532. continue;
  533. }
  534. let cell = widget.widgets[i];
  535. expect(widget.isSelected(cell)).to.be(false);
  536. }
  537. widget.dispose();
  538. done();
  539. });
  540. });
  541. it('should unrender a markdown cell when switching to edit mode', () => {
  542. let widget = createActiveWidget();
  543. Widget.attach(widget, document.body);
  544. MessageLoop.sendMessage(widget, Widget.Msg.ActivateRequest);
  545. let cell = widget.model.contentFactory.createMarkdownCell({});
  546. widget.model.cells.pushBack(cell);
  547. let child = widget.widgets[widget.widgets.length - 1] as MarkdownCell;
  548. expect(child.rendered).to.be(true);
  549. widget.activeCellIndex = widget.widgets.length - 1;
  550. widget.mode = 'edit';
  551. expect(child.rendered).to.be(false);
  552. });
  553. });
  554. describe('#activeCellIndex', () => {
  555. it('should get the active cell index of the notebook', () => {
  556. let widget = createActiveWidget();
  557. expect(widget.activeCellIndex).to.be(0);
  558. });
  559. it('should set the active cell index of the notebook', () => {
  560. let widget = createActiveWidget();
  561. widget.model.fromJSON(DEFAULT_CONTENT);
  562. widget.activeCellIndex = 1;
  563. expect(widget.activeCellIndex).to.be(1);
  564. });
  565. it('should clamp the index to the bounds of the notebook cells', () => {
  566. let widget = createActiveWidget();
  567. widget.model.fromJSON(DEFAULT_CONTENT);
  568. widget.activeCellIndex = -2;
  569. expect(widget.activeCellIndex).to.be(0);
  570. widget.activeCellIndex = 100;
  571. expect(widget.activeCellIndex).to.be(5);
  572. });
  573. it('should emit the `stateChanged` signal', () => {
  574. let widget = createActiveWidget();
  575. let called = false;
  576. widget.model.fromJSON(DEFAULT_CONTENT);
  577. widget.stateChanged.connect((sender, args) => {
  578. expect(sender).to.be(widget);
  579. expect(args.name).to.be('activeCellIndex');
  580. expect(args.oldValue).to.be(0);
  581. expect(args.newValue).to.be(1);
  582. called = true;
  583. });
  584. widget.activeCellIndex = 1;
  585. expect(called).to.be(true);
  586. });
  587. it('should be a no-op if the value does not change', () => {
  588. let widget = createActiveWidget();
  589. let called = false;
  590. widget.model.fromJSON(DEFAULT_CONTENT);
  591. widget.stateChanged.connect(() => { called = true; });
  592. widget.activeCellIndex = 0;
  593. expect(called).to.be(false);
  594. });
  595. it('should post an update request', (done) => {
  596. let widget = createActiveWidget();
  597. widget.model.fromJSON(DEFAULT_CONTENT);
  598. requestAnimationFrame(() => {
  599. expect(widget.methods).to.contain('onUpdateRequest');
  600. done();
  601. });
  602. widget.activeCellIndex = 1;
  603. });
  604. it('should update the active cell if necessary', () => {
  605. let widget = createActiveWidget();
  606. widget.model.fromJSON(DEFAULT_CONTENT);
  607. widget.activeCellIndex = 1;
  608. expect(widget.activeCell).to.be(widget.widgets[1]);
  609. });
  610. });
  611. describe('#activeCell', () => {
  612. it('should get the active cell widget', () => {
  613. let widget = createActiveWidget();
  614. expect(widget.activeCell).to.be(widget.widgets[0]);
  615. });
  616. });
  617. describe('#select()', () => {
  618. it('should select a cell widget', () => {
  619. let widget = createActiveWidget();
  620. widget.model.fromJSON(DEFAULT_CONTENT);
  621. let cell = widget.widgets[0];
  622. widget.select(cell);
  623. expect(widget.isSelected(cell)).to.be(true);
  624. });
  625. it('should allow multiple widgets to be selected', () => {
  626. let widget = createActiveWidget();
  627. widget.model.fromJSON(DEFAULT_CONTENT);
  628. for (let i = 0; i < widget.widgets.length; i++) {
  629. let cell = widget.widgets[i];
  630. widget.select(cell);
  631. expect(widget.isSelected(cell)).to.be(true);
  632. }
  633. });
  634. });
  635. describe('#deselect()', () => {
  636. it('should deselect a cell', () => {
  637. let widget = createActiveWidget();
  638. widget.model.fromJSON(DEFAULT_CONTENT);
  639. for (let i = 0; i < widget.widgets.length; i++) {
  640. if (i === widget.activeCellIndex) {
  641. continue;
  642. }
  643. let cell = widget.widgets[i];
  644. widget.select(cell);
  645. expect(widget.isSelected(cell)).to.be(true);
  646. widget.deselect(cell);
  647. expect(widget.isSelected(cell)).to.be(false);
  648. }
  649. });
  650. it('should have no effect on the active cell', () => {
  651. let widget = createActiveWidget();
  652. widget.model.fromJSON(DEFAULT_CONTENT);
  653. let cell = widget.widgets[widget.activeCellIndex];
  654. expect(widget.isSelected(cell)).to.be(true);
  655. widget.deselect(cell);
  656. expect(widget.isSelected(cell)).to.be(true);
  657. });
  658. });
  659. describe('#isSelected()', () => {
  660. it('should get whether the cell is selected', () => {
  661. let widget = createActiveWidget();
  662. widget.model.fromJSON(DEFAULT_CONTENT);
  663. for (let i = 0; i < widget.widgets.length; i++) {
  664. let cell = widget.widgets[i];
  665. if (i === widget.activeCellIndex) {
  666. expect(widget.isSelected(cell)).to.be(true);
  667. } else {
  668. expect(widget.isSelected(cell)).to.be(false);
  669. }
  670. }
  671. });
  672. });
  673. describe('#handleEvent()', () => {
  674. let widget: LogNotebook;
  675. beforeEach((done) => {
  676. widget = createActiveWidget();
  677. widget.model.fromJSON(DEFAULT_CONTENT);
  678. Widget.attach(widget, document.body);
  679. requestAnimationFrame(() => { done(); });
  680. });
  681. afterEach(() => {
  682. widget.dispose();
  683. });
  684. context('mousedown', () => {
  685. it('should set the active cell index', () => {
  686. let child = widget.widgets[1];
  687. simulate(child.node, 'mousedown');
  688. expect(widget.events).to.contain('mousedown');
  689. expect(widget.activeCellIndex).to.be(1);
  690. });
  691. it('should be a no-op if the model is read only', () => {
  692. let child = widget.widgets[1];
  693. widget.model.readOnly = true;
  694. simulate(child.node, 'mousedown');
  695. expect(widget.events).to.contain('mousedown');
  696. expect(widget.activeCellIndex).to.be(0);
  697. });
  698. it('should be a no-op if not not a cell', () => {
  699. simulate(widget.node, 'mousedown');
  700. expect(widget.events).to.contain('mousedown');
  701. expect(widget.activeCellIndex).to.be(0);
  702. });
  703. it('should preserve "command" mode if in a markdown cell', () => {
  704. let cell = widget.model.contentFactory.createMarkdownCell({});
  705. widget.model.cells.pushBack(cell);
  706. let count = widget.widgets.length;
  707. let child = widget.widgets[count - 1] as MarkdownCell;
  708. expect(child.rendered).to.be(true);
  709. simulate(child.node, 'mousedown');
  710. expect(child.rendered).to.be(true);
  711. expect(widget.activeCell).to.be(child);
  712. });
  713. });
  714. context('dblclick', () => {
  715. it('should unrender a markdown cell', () => {
  716. let cell = widget.model.contentFactory.createMarkdownCell({});
  717. widget.model.cells.pushBack(cell);
  718. let child = widget.widgets[widget.widgets.length - 1] as MarkdownCell;
  719. expect(child.rendered).to.be(true);
  720. expect(widget.mode).to.be('command');
  721. simulate(child.node, 'dblclick');
  722. expect(widget.mode).to.be('command');
  723. expect(child.rendered).to.be(false);
  724. });
  725. it('should be a no-op if the model is read only', () => {
  726. let cell = widget.model.contentFactory.createMarkdownCell({});
  727. widget.model.cells.pushBack(cell);
  728. widget.model.readOnly = true;
  729. let child = widget.widgets[widget.widgets.length - 1] as MarkdownCell;
  730. expect(child.rendered).to.be(true);
  731. simulate(child.node, 'dblclick');
  732. expect(child.rendered).to.be(true);
  733. });
  734. });
  735. context('focus', () => {
  736. it('should change to edit mode if a child cell takes focus', () => {
  737. let child = widget.widgets[0];
  738. simulate(child.editorWidget.node, 'focus');
  739. expect(widget.events).to.contain('focus');
  740. expect(widget.mode).to.be('edit');
  741. });
  742. it('should change to command mode if the widget takes focus', () => {
  743. let child = widget.widgets[0];
  744. simulate(child.editorWidget.node, 'focus');
  745. expect(widget.events).to.contain('focus');
  746. expect(widget.mode).to.be('edit');
  747. widget.events = [];
  748. simulate(widget.node, 'focus');
  749. expect(widget.events).to.contain('focus');
  750. expect(widget.mode).to.be('command');
  751. });
  752. });
  753. context('blur', () => {
  754. it('should preserve the mode', () => {
  755. simulate(widget.node, 'focus');
  756. widget.mode = 'edit';
  757. let other = document.createElement('div');
  758. simulate(widget.node, 'blur', { relatedTarget: other });
  759. expect(widget.mode).to.be('edit');
  760. MessageLoop.sendMessage(widget, Widget.Msg.ActivateRequest);
  761. expect(widget.mode).to.be('edit');
  762. expect(widget.activeCell.editor.hasFocus()).to.be(true);
  763. });
  764. it('should set command mode', () => {
  765. simulate(widget.node, 'focus');
  766. widget.mode = 'edit';
  767. let evt = generate('blur');
  768. (evt as any).relatedTarget = widget.activeCell.node;
  769. widget.node.dispatchEvent(evt);
  770. expect(widget.mode).to.be('command');
  771. });
  772. });
  773. });
  774. describe('#onAfterAttach()', () => {
  775. it('should add event listeners', (done) => {
  776. let widget = createActiveWidget();
  777. widget.model.fromJSON(DEFAULT_CONTENT);
  778. Widget.attach(widget, document.body);
  779. let child = widget.widgets[0];
  780. requestAnimationFrame(() => {
  781. expect(widget.methods).to.contain('onAfterAttach');
  782. simulate(widget.node, 'mousedown');
  783. expect(widget.events).to.contain('mousedown');
  784. simulate(widget.node, 'dblclick');
  785. expect(widget.events).to.contain('dblclick');
  786. simulate(child.node, 'focus');
  787. expect(widget.events).to.contain('focus');
  788. widget.dispose();
  789. done();
  790. });
  791. });
  792. it('should post an update request', (done) => {
  793. let widget = createActiveWidget();
  794. widget.model.fromJSON(DEFAULT_CONTENT);
  795. Widget.attach(widget, document.body);
  796. requestAnimationFrame(() => {
  797. expect(widget.methods).to.contain('onAfterAttach');
  798. requestAnimationFrame(() => {
  799. expect(widget.methods).to.contain('onUpdateRequest');
  800. widget.dispose();
  801. done();
  802. });
  803. });
  804. });
  805. });
  806. describe('#onBeforeDetach()', () => {
  807. it('should remove event listeners', (done) => {
  808. let widget = createActiveWidget();
  809. widget.model.fromJSON(DEFAULT_CONTENT);
  810. Widget.attach(widget, document.body);
  811. let child = widget.widgets[0];
  812. requestAnimationFrame(() => {
  813. Widget.detach(widget);
  814. expect(widget.methods).to.contain('onBeforeDetach');
  815. widget.events = [];
  816. simulate(widget.node, 'mousedown');
  817. expect(widget.events).to.not.contain('mousedown');
  818. simulate(widget.node, 'dblclick');
  819. expect(widget.events).to.not.contain('dblclick');
  820. simulate(child.node, 'focus');
  821. expect(widget.events).to.not.contain('focus');
  822. widget.dispose();
  823. done();
  824. });
  825. });
  826. });
  827. describe('#onActivateRequest()', () => {
  828. it('should focus the node after an update', (done) => {
  829. let widget = createActiveWidget();
  830. Widget.attach(widget, document.body);
  831. MessageLoop.sendMessage(widget, Widget.Msg.ActivateRequest);
  832. expect(widget.methods).to.contain('onActivateRequest');
  833. requestAnimationFrame(() => {
  834. expect(document.activeElement).to.be(widget.node);
  835. widget.dispose();
  836. done();
  837. });
  838. });
  839. it('should post an `update-request', (done) => {
  840. let widget = createActiveWidget();
  841. MessageLoop.sendMessage(widget, Widget.Msg.ActivateRequest);
  842. expect(widget.methods).to.contain('onActivateRequest');
  843. requestAnimationFrame(() => {
  844. expect(widget.methods).to.contain('onUpdateRequest');
  845. widget.dispose();
  846. done();
  847. });
  848. });
  849. });
  850. describe('#onUpdateRequest()', () => {
  851. let widget: LogNotebook;
  852. beforeEach((done) => {
  853. widget = createActiveWidget();
  854. widget.model.fromJSON(DEFAULT_CONTENT);
  855. Widget.attach(widget, document.body);
  856. requestAnimationFrame(() => { done(); });
  857. });
  858. afterEach(() => {
  859. widget.dispose();
  860. });
  861. it('should apply the command class if in command mode', () => {
  862. expect(widget.methods).to.contain('onUpdateRequest');
  863. expect(widget.hasClass('jp-mod-commandMode')).to.be(true);
  864. });
  865. it('should apply the edit class if in edit mode', (done) => {
  866. widget.mode = 'edit';
  867. requestAnimationFrame(() => {
  868. expect(widget.hasClass('jp-mod-editMode')).to.be(true);
  869. done();
  870. });
  871. });
  872. it('should add the active class to the active widget', () => {
  873. let cell = widget.widgets[widget.activeCellIndex];
  874. expect(cell.hasClass('jp-mod-active')).to.be(true);
  875. });
  876. it('should set the selected class on the selected widgets', (done) => {
  877. widget.select(widget.widgets[1]);
  878. requestAnimationFrame(() => {
  879. for (let i = 0; i < 2; i++) {
  880. let cell = widget.widgets[i];
  881. expect(cell.hasClass('jp-mod-selected')).to.be(true);
  882. done();
  883. }
  884. });
  885. });
  886. it('should add the multi select class if there is more than one widget', (done) => {
  887. widget.select(widget.widgets[1]);
  888. expect(widget.hasClass('jp-mod-multSelected')).to.be(false);
  889. requestAnimationFrame(() => {
  890. expect(widget.hasClass('jp-mod-multSelected')).to.be(false);
  891. done();
  892. });
  893. });
  894. });
  895. describe('#onCellInserted()', () => {
  896. it('should post an `update-request', (done) => {
  897. let widget = createActiveWidget();
  898. widget.model.fromJSON(DEFAULT_CONTENT);
  899. expect(widget.methods).to.contain('onCellInserted');
  900. requestAnimationFrame(() => {
  901. expect(widget.methods).to.contain('onUpdateRequest');
  902. done();
  903. });
  904. });
  905. it('should update the active cell if necessary', () => {
  906. let widget = createActiveWidget();
  907. widget.model.fromJSON(DEFAULT_CONTENT);
  908. expect(widget.activeCell).to.be(widget.widgets[0]);
  909. });
  910. it('should keep the currently active cell active', () => {
  911. let widget = createActiveWidget();
  912. widget.model.fromJSON(DEFAULT_CONTENT);
  913. widget.activeCellIndex = 1;
  914. let cell = widget.model.contentFactory.createCodeCell({});
  915. widget.model.cells.insert(1, cell);
  916. expect(widget.activeCell).to.be(widget.widgets[2]);
  917. });
  918. context('`edgeRequested` signal', () => {
  919. it('should activate the previous cell if top is requested', () => {
  920. let widget = createActiveWidget();
  921. widget.model.fromJSON(DEFAULT_CONTENT);
  922. widget.activeCellIndex = 1;
  923. let child = widget.widgets[widget.activeCellIndex];
  924. (child.editor.edgeRequested as any).emit('top');
  925. expect(widget.activeCellIndex).to.be(0);
  926. });
  927. it('should activate the next cell if bottom is requested', () => {
  928. let widget = createActiveWidget();
  929. widget.model.fromJSON(DEFAULT_CONTENT);
  930. let child = widget.widgets[widget.activeCellIndex];
  931. (child.editor.edgeRequested as any).emit('bottom');
  932. expect(widget.activeCellIndex).to.be(1);
  933. });
  934. });
  935. });
  936. describe('#onCellMoved()', () => {
  937. it('should update the active cell index if necessary', () => {
  938. let widget = createActiveWidget();
  939. widget.model.fromJSON(DEFAULT_CONTENT);
  940. widget.model.cells.move(1, 0);
  941. expect(widget.activeCellIndex).to.be(0);
  942. });
  943. });
  944. describe('#onCellRemoved()', () => {
  945. it('should post an `update-request', (done) => {
  946. let widget = createActiveWidget();
  947. let cell = widget.model.cells.at(0);
  948. widget.model.cells.remove(cell);
  949. expect(widget.methods).to.contain('onCellRemoved');
  950. requestAnimationFrame(() => {
  951. expect(widget.methods).to.contain('onUpdateRequest');
  952. done();
  953. });
  954. });
  955. it('should update the active cell if necessary', () => {
  956. let widget = createActiveWidget();
  957. widget.model.fromJSON(DEFAULT_CONTENT);
  958. widget.model.cells.removeAt(0);
  959. expect(widget.activeCell).to.be(widget.widgets[0]);
  960. });
  961. it('should keep the currently active cell active', () => {
  962. let widget = createActiveWidget();
  963. widget.model.fromJSON(DEFAULT_CONTENT);
  964. widget.activeCellIndex = 2;
  965. widget.model.cells.removeAt(1);
  966. expect(widget.activeCell).to.be(widget.widgets[1]);
  967. });
  968. });
  969. });
  970. });