widget.spec.ts 55 KB

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