widget.spec.ts 55 KB

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