widget.spec.ts 37 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189
  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, CodeCellWidget, MarkdownCellModel, MarkdownCellWidget,
  15. RawCellModel, RawCellWidget, BaseCellWidget
  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: BaseCellWidget): 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(cell: BaseCellWidget): void {
  60. super.onCellRemoved(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: BaseCellWidget): 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(cell: BaseCellWidget): void {
  96. super.onCellRemoved(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(CodeCellWidget);
  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 CodeCellWidget.ContentFactory', () => {
  369. let factory = new StaticNotebook.ContentFactory({ editorFactory });
  370. expect(factory.codeCellContentFactory).to.be.a(CodeCellWidget.ContentFactory);
  371. });
  372. });
  373. describe('#markdownCellContentFactory', () => {
  374. it('should be a BaseCellWidget.ContentFactory', () => {
  375. let factory = new StaticNotebook.ContentFactory({ editorFactory });
  376. expect(factory.markdownCellContentFactory).to.be.a(BaseCellWidget.ContentFactory);
  377. });
  378. });
  379. describe('#rawCellContentFactory', () => {
  380. it('should be a BaseCellWidget.ContentFactory', () => {
  381. let factory = new StaticNotebook.ContentFactory({ editorFactory });
  382. expect(factory.rawCellContentFactory).to.be.a(BaseCellWidget.ContentFactory);
  383. });
  384. });
  385. describe('#createCodeCell({})', () => {
  386. it('should create a `CodeCellWidget`', () => {
  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(CodeCellWidget);
  394. });
  395. });
  396. describe('#createMarkdownCell({})', () => {
  397. it('should create a `MarkdownCellWidget`', () => {
  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(MarkdownCellWidget);
  405. });
  406. });
  407. describe('#createRawCell()', () => {
  408. it('should create a `RawCellWidget`', () => {
  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(RawCellWidget);
  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 MarkdownCellWidget;
  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 MarkdownCellWidget;
  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 MarkdownCellWidget;
  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 MarkdownCellWidget;
  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. context('`edgeRequested` signal', () => {
  911. it('should activate the previous cell if top is requested', () => {
  912. let widget = createActiveWidget();
  913. widget.model.fromJSON(DEFAULT_CONTENT);
  914. widget.activeCellIndex = 1;
  915. let child = widget.widgets[widget.activeCellIndex];
  916. (child.editor.edgeRequested as any).emit('top');
  917. expect(widget.activeCellIndex).to.be(0);
  918. });
  919. it('should activate the next cell if bottom is requested', () => {
  920. let widget = createActiveWidget();
  921. widget.model.fromJSON(DEFAULT_CONTENT);
  922. let child = widget.widgets[widget.activeCellIndex];
  923. (child.editor.edgeRequested as any).emit('bottom');
  924. expect(widget.activeCellIndex).to.be(1);
  925. });
  926. });
  927. });
  928. describe('#onCellMoved()', () => {
  929. it('should update the active cell index if necessary', () => {
  930. let widget = createActiveWidget();
  931. widget.model.fromJSON(DEFAULT_CONTENT);
  932. widget.model.cells.move(1, 0);
  933. expect(widget.activeCellIndex).to.be(0);
  934. });
  935. });
  936. describe('#onCellRemoved()', () => {
  937. it('should post an `update-request', (done) => {
  938. let widget = createActiveWidget();
  939. let cell = widget.model.cells.at(0);
  940. widget.model.cells.remove(cell);
  941. expect(widget.methods).to.contain('onCellRemoved');
  942. requestAnimationFrame(() => {
  943. expect(widget.methods).to.contain('onUpdateRequest');
  944. done();
  945. });
  946. });
  947. it('should update the active cell if necessary', () => {
  948. let widget = createActiveWidget();
  949. widget.model.fromJSON(DEFAULT_CONTENT);
  950. widget.model.cells.removeAt(0);
  951. expect(widget.activeCell).to.be(widget.widgets[0]);
  952. });
  953. });
  954. });
  955. });