widget.spec.ts 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import { Message, MessageLoop } from '@lumino/messaging';
  4. import { Widget } from '@lumino/widgets';
  5. import { SessionContext, ISessionContext } from '@jupyterlab/apputils';
  6. import { CodeEditor, CodeEditorWrapper } from '@jupyterlab/codeeditor';
  7. import {
  8. Cell,
  9. CellModel,
  10. InputPrompt,
  11. CodeCell,
  12. CodeCellModel,
  13. MarkdownCell,
  14. RawCell,
  15. RawCellModel,
  16. MarkdownCellModel,
  17. CellFooter,
  18. CellHeader,
  19. InputArea
  20. } from '@jupyterlab/cells';
  21. import { OutputArea, OutputPrompt } from '@jupyterlab/outputarea';
  22. import {
  23. createSessionContext,
  24. framePromise,
  25. NBTestUtils,
  26. JupyterServer
  27. } from '@jupyterlab/testutils';
  28. const RENDERED_CLASS = 'jp-mod-rendered';
  29. const rendermime = NBTestUtils.defaultRenderMime();
  30. class LogBaseCell extends Cell {
  31. methods: string[] = [];
  32. constructor() {
  33. super({
  34. model: new CellModel({}),
  35. contentFactory: NBTestUtils.createBaseCellFactory()
  36. });
  37. }
  38. protected onAfterAttach(msg: Message): void {
  39. super.onAfterAttach(msg);
  40. this.methods.push('onAfterAttach');
  41. }
  42. protected onActivateRequest(msg: Message): void {
  43. super.onActivateRequest(msg);
  44. this.methods.push('onActivateRequest');
  45. }
  46. protected onUpdateRequest(msg: Message): void {
  47. super.onUpdateRequest(msg);
  48. this.methods.push('onUpdateRequest');
  49. }
  50. }
  51. class LogCodeCell extends CodeCell {
  52. methods: string[] = [];
  53. constructor() {
  54. super({
  55. model: new CodeCellModel({}),
  56. contentFactory: NBTestUtils.createCodeCellFactory(),
  57. rendermime
  58. });
  59. }
  60. protected onUpdateRequest(msg: Message): void {
  61. super.onAfterAttach(msg);
  62. this.methods.push('onUpdateRequest');
  63. }
  64. protected onMetadataChanged(model: any, args: any): void {
  65. super.onMetadataChanged(model, args);
  66. this.methods.push('onMetadataChanged');
  67. }
  68. }
  69. class LogMarkdownCell extends MarkdownCell {
  70. methods: string[] = [];
  71. protected onUpdateRequest(msg: Message): void {
  72. super.onAfterAttach(msg);
  73. this.methods.push('onUpdateRequest');
  74. }
  75. }
  76. const server = new JupyterServer();
  77. beforeAll(async () => {
  78. await server.start();
  79. });
  80. afterAll(async () => {
  81. await server.shutdown();
  82. });
  83. describe('cells/widget', () => {
  84. const editorFactory = NBTestUtils.editorFactory;
  85. describe('Cell', () => {
  86. const contentFactory = NBTestUtils.createBaseCellFactory();
  87. const model = new CellModel({});
  88. describe('#constructor()', () => {
  89. it('should create a base cell widget', () => {
  90. const widget = new Cell({ model, contentFactory }).initializeState();
  91. expect(widget).toBeInstanceOf(Cell);
  92. });
  93. it('should accept a custom contentFactory', () => {
  94. const contentFactory = NBTestUtils.createBaseCellFactory();
  95. const widget = new Cell({ model, contentFactory }).initializeState();
  96. expect(widget).toBeInstanceOf(Cell);
  97. });
  98. it('shoule accept a custom editorConfig', () => {
  99. const editorConfig: Partial<CodeEditor.IConfig> = {
  100. insertSpaces: false,
  101. matchBrackets: false
  102. };
  103. const widget = new Cell({
  104. editorConfig,
  105. model,
  106. contentFactory
  107. }).initializeState();
  108. expect(widget.editor.getOption('insertSpaces')).toEqual(false);
  109. expect(widget.editor.getOption('matchBrackets')).toEqual(false);
  110. expect(widget.editor.getOption('lineNumbers')).toEqual(
  111. CodeEditor.defaultConfig.lineNumbers
  112. );
  113. });
  114. });
  115. describe('#model', () => {
  116. it('should be the model used by the widget', () => {
  117. const model = new CellModel({});
  118. const widget = new Cell({ model, contentFactory }).initializeState();
  119. expect(widget.model).toEqual(model);
  120. });
  121. });
  122. describe('#editorWidget', () => {
  123. it('should be a code editor widget', () => {
  124. const widget = new Cell({ model, contentFactory }).initializeState();
  125. expect(widget.editorWidget).toBeInstanceOf(CodeEditorWrapper);
  126. });
  127. });
  128. describe('#editor', () => {
  129. it('should be a cell editor', () => {
  130. const widget = new Cell({ model, contentFactory }).initializeState();
  131. expect(widget.editor.uuid).toBeTruthy();
  132. });
  133. });
  134. describe('#inputArea', () => {
  135. it('should be the input area for the cell', () => {
  136. const widget = new Cell({ model }).initializeState();
  137. expect(widget.inputArea).toBeInstanceOf(InputArea);
  138. });
  139. });
  140. describe('#readOnly', () => {
  141. it('should be a boolean', () => {
  142. const widget = new Cell({ model, contentFactory }).initializeState();
  143. expect(typeof widget.readOnly).toEqual('boolean');
  144. });
  145. it('should default to false', () => {
  146. const widget = new Cell({ model, contentFactory }).initializeState();
  147. expect(widget.readOnly).toEqual(false);
  148. });
  149. it('should be settable', () => {
  150. const widget = new Cell({
  151. model,
  152. contentFactory
  153. }).initializeState();
  154. widget.readOnly = true;
  155. expect(widget.readOnly).toEqual(true);
  156. });
  157. it('should ignore being set to the same value', async () => {
  158. const widget = new LogBaseCell().initializeState();
  159. widget.readOnly = true;
  160. widget.readOnly = true;
  161. await framePromise();
  162. expect(widget.methods).toEqual(['onUpdateRequest']);
  163. });
  164. it('should reflect model metadata', () => {
  165. model.metadata.set('editable', false);
  166. const widget = new Cell({
  167. model,
  168. contentFactory
  169. }).initializeState();
  170. expect(widget.readOnly).toEqual(true);
  171. });
  172. });
  173. describe('#inputCollapsed', () => {
  174. it('should be the view state of the input being collapsed', () => {
  175. const widget = new LogBaseCell().initializeState();
  176. expect(widget.inputHidden).toEqual(false);
  177. widget.inputHidden = true;
  178. expect(widget.inputHidden).toEqual(true);
  179. });
  180. });
  181. describe('#loadEditableState()', () => {
  182. it('should load the editable state from the model', () => {
  183. const model = new CellModel({});
  184. const widget = new Cell({ model, contentFactory }).initializeState();
  185. expect(widget.readOnly).toEqual(false);
  186. model.metadata.set('editable', false);
  187. widget.loadEditableState();
  188. expect(widget.readOnly).toEqual(true);
  189. model.metadata.set('editable', true);
  190. widget.loadEditableState();
  191. expect(widget.readOnly).toEqual(false);
  192. });
  193. });
  194. describe('#saveEditableState()', () => {
  195. it('should save the editable state to the model', () => {
  196. const model = new CellModel({});
  197. const widget = new Cell({ model, contentFactory }).initializeState();
  198. expect(widget.readOnly).toEqual(false);
  199. widget.readOnly = true;
  200. widget.saveEditableState();
  201. expect(model.metadata.get('editable')).toEqual(false);
  202. widget.readOnly = false;
  203. widget.saveEditableState();
  204. // Default values are not saved explicitly
  205. expect(model.metadata.get('editable')).toEqual(undefined);
  206. });
  207. });
  208. describe('#syncEditable', () => {
  209. it('should control automatic syncing of editable state with model', () => {
  210. const model = new CellModel({});
  211. const widget = new Cell({ model, contentFactory }).initializeState();
  212. expect(widget.syncEditable).toEqual(false);
  213. expect(widget.readOnly).toEqual(false);
  214. // Not synced if setting widget attribute
  215. widget.readOnly = true;
  216. expect(model.metadata.get('editable')).toEqual(undefined);
  217. // Not synced if setting metadata attribute
  218. model.metadata.set('editable', true);
  219. expect(widget.readOnly).toEqual(true);
  220. widget.syncEditable = true;
  221. // Setting sync does an initial sync from model to view. This also sets
  222. // the metadata to undefined if it is the default value.
  223. expect(model.metadata.get('editable')).toEqual(undefined);
  224. expect(widget.readOnly).toEqual(false);
  225. // Synced if setting widget attribute
  226. widget.readOnly = true;
  227. expect(model.metadata.get('editable')).toEqual(false);
  228. // Synced if setting metadata attribute
  229. model.metadata.set('editable', true);
  230. expect(widget.readOnly).toEqual(false);
  231. });
  232. });
  233. describe('#loadCollapseState()', () => {
  234. it('should load the input collapse state from the model', () => {
  235. const model = new CellModel({});
  236. const widget = new Cell({ model, contentFactory }).initializeState();
  237. expect(widget.inputHidden).toEqual(false);
  238. model.metadata.set('jupyter', { source_hidden: true });
  239. widget.loadCollapseState();
  240. expect(widget.inputHidden).toEqual(true);
  241. model.metadata.set('jupyter', { source_hidden: false });
  242. widget.loadCollapseState();
  243. expect(widget.inputHidden).toEqual(false);
  244. });
  245. });
  246. describe('#saveCollapseState()', () => {
  247. it('should save the collapse state to the model', () => {
  248. const model = new CellModel({});
  249. const widget = new Cell({ model, contentFactory }).initializeState();
  250. expect(widget.inputHidden).toEqual(false);
  251. widget.inputHidden = true;
  252. widget.saveCollapseState();
  253. expect(model.metadata.get('jupyter')).toEqual({
  254. source_hidden: true
  255. });
  256. widget.inputHidden = false;
  257. widget.saveCollapseState();
  258. // Default values are not saved explicitly
  259. expect(model.metadata.get('jupyter')).toEqual(undefined);
  260. });
  261. });
  262. describe('#syncCollapse', () => {
  263. it('should control automatic syncing of collapse state with model', () => {
  264. const model = new CellModel({});
  265. const widget = new Cell({ model, contentFactory }).initializeState();
  266. expect(widget.syncCollapse).toEqual(false);
  267. expect(widget.inputHidden).toEqual(false);
  268. // Not synced if setting widget attribute
  269. widget.inputHidden = true;
  270. expect(model.metadata.get('jupyter')).toEqual(undefined);
  271. // Not synced if setting metadata attribute
  272. model.metadata.set('jupyter', { source_hidden: false });
  273. expect(widget.inputHidden).toEqual(true);
  274. widget.syncCollapse = true;
  275. // Setting sync does an initial sync from model to view. This also sets
  276. // the metadata to undefined if it is the default value.
  277. expect(model.metadata.get('jupyter')).toEqual(undefined);
  278. expect(widget.inputHidden).toEqual(false);
  279. // Synced if setting widget attribute
  280. widget.inputHidden = true;
  281. expect(model.metadata.get('jupyter')).toEqual({
  282. source_hidden: true
  283. });
  284. // Synced if setting metadata attribute
  285. model.metadata.set('jupyter', {});
  286. expect(widget.inputHidden).toEqual(false);
  287. });
  288. });
  289. describe('#onActivateRequest()', () => {
  290. it('should focus the cell editor', async () => {
  291. const widget = new LogBaseCell().initializeState();
  292. Widget.attach(widget, document.body);
  293. widget.activate();
  294. await framePromise();
  295. expect(widget.methods).toContain('onActivateRequest');
  296. await framePromise();
  297. expect(widget.editor.hasFocus()).toEqual(true);
  298. widget.dispose();
  299. });
  300. });
  301. describe('#setPrompt()', () => {
  302. it('should not throw an error (full test in input area)', () => {
  303. const widget = new Cell({ model, contentFactory }).initializeState();
  304. expect(() => {
  305. widget.setPrompt('');
  306. }).not.toThrow();
  307. expect(() => {
  308. widget.setPrompt('null');
  309. }).not.toThrow();
  310. expect(() => {
  311. widget.setPrompt('test');
  312. }).not.toThrow();
  313. });
  314. });
  315. describe('#dispose()', () => {
  316. it('should dispose of the resources held by the widget', () => {
  317. const widget = new Cell({ model, contentFactory }).initializeState();
  318. widget.dispose();
  319. expect(widget.isDisposed).toEqual(true);
  320. });
  321. it('should be safe to call multiple times', () => {
  322. const widget = new Cell({ model, contentFactory }).initializeState();
  323. widget.dispose();
  324. widget.dispose();
  325. expect(widget.isDisposed).toEqual(true);
  326. });
  327. });
  328. describe('#onAfterAttach()', () => {
  329. it('should run when widget is attached', () => {
  330. const widget = new LogBaseCell().initializeState();
  331. expect(widget.methods).not.toContain('onAfterAttach');
  332. Widget.attach(widget, document.body);
  333. expect(widget.methods).toContain('onAfterAttach');
  334. widget.dispose();
  335. });
  336. });
  337. describe('#onUpdateRequest()', () => {
  338. it('should update the widget', () => {
  339. const widget = new LogBaseCell().initializeState();
  340. expect(widget.methods).not.toContain('onUpdateRequest');
  341. MessageLoop.sendMessage(widget, Widget.Msg.UpdateRequest);
  342. expect(widget.methods).toContain('onUpdateRequest');
  343. });
  344. });
  345. describe('#.defaultContentFactory', () => {
  346. it('should be a contentFactory', () => {
  347. expect(Cell.defaultContentFactory).toBeInstanceOf(Cell.ContentFactory);
  348. });
  349. });
  350. describe('.ContentFactory', () => {
  351. describe('#constructor', () => {
  352. it('should create a ContentFactory', () => {
  353. const factory = new Cell.ContentFactory({ editorFactory });
  354. expect(factory).toBeInstanceOf(Cell.ContentFactory);
  355. });
  356. });
  357. describe('#editorFactory', () => {
  358. it('should be the editor factory used by the content factory', () => {
  359. const factory = new Cell.ContentFactory({ editorFactory });
  360. expect(factory.editorFactory).toEqual(editorFactory);
  361. });
  362. });
  363. describe('#createCellHeader()', () => {
  364. it('should create a new cell header', () => {
  365. const factory = new Cell.ContentFactory();
  366. expect(factory.createCellHeader()).toBeInstanceOf(CellHeader);
  367. });
  368. });
  369. describe('#createCellFooter()', () => {
  370. it('should create a new cell footer', () => {
  371. const factory = new Cell.ContentFactory();
  372. expect(factory.createCellFooter()).toBeInstanceOf(CellFooter);
  373. });
  374. });
  375. describe('#createOutputPrompt()', () => {
  376. it('should create a new output prompt', () => {
  377. const factory = new Cell.ContentFactory();
  378. expect(factory.createOutputPrompt()).toBeInstanceOf(OutputPrompt);
  379. });
  380. });
  381. describe('#createInputPrompt()', () => {
  382. it('should create a new input prompt', () => {
  383. const factory = new Cell.ContentFactory();
  384. expect(factory.createInputPrompt()).toBeInstanceOf(InputPrompt);
  385. });
  386. });
  387. });
  388. });
  389. describe('CodeCell', () => {
  390. const contentFactory = NBTestUtils.createCodeCellFactory();
  391. const model = new CodeCellModel({});
  392. describe('#constructor()', () => {
  393. it('should create a code cell widget', () => {
  394. const widget = new CodeCell({ model, rendermime, contentFactory });
  395. widget.initializeState();
  396. expect(widget).toBeInstanceOf(CodeCell);
  397. });
  398. it('should accept a custom contentFactory', () => {
  399. const contentFactory = NBTestUtils.createCodeCellFactory();
  400. const widget = new CodeCell({ model, contentFactory, rendermime });
  401. widget.initializeState();
  402. expect(widget).toBeInstanceOf(CodeCell);
  403. });
  404. });
  405. describe('#outputArea', () => {
  406. it('should be the output area used by the cell', () => {
  407. const widget = new CodeCell({ model, rendermime });
  408. widget.initializeState();
  409. expect(widget.outputArea).toBeInstanceOf(OutputArea);
  410. });
  411. });
  412. describe('#outputCollapsed', () => {
  413. it('should initialize from the model', () => {
  414. const collapsedModel = new CodeCellModel({});
  415. let widget = new CodeCell({ model: collapsedModel, rendermime });
  416. widget.initializeState();
  417. expect(widget.outputHidden).toEqual(false);
  418. collapsedModel.metadata.set('collapsed', true);
  419. widget = new CodeCell({ model: collapsedModel, rendermime });
  420. widget.initializeState();
  421. expect(widget.outputHidden).toEqual(true);
  422. collapsedModel.metadata.delete('collapsed');
  423. collapsedModel.metadata.set('jupyter', { outputs_hidden: true });
  424. widget = new CodeCell({ model: collapsedModel, rendermime });
  425. widget.initializeState();
  426. expect(widget.outputHidden).toEqual(true);
  427. });
  428. it('should be the view state of the output being collapsed', () => {
  429. const widget = new CodeCell({ model, rendermime });
  430. widget.initializeState();
  431. expect(widget.outputHidden).toEqual(false);
  432. widget.outputHidden = true;
  433. expect(widget.outputHidden).toEqual(true);
  434. });
  435. });
  436. describe('#outputsScrolled', () => {
  437. it('should initialize from the model', () => {
  438. const model = new CodeCellModel({});
  439. let widget = new CodeCell({ model, rendermime });
  440. widget.initializeState();
  441. expect(widget.outputsScrolled).toEqual(false);
  442. model.metadata.set('scrolled', false);
  443. widget = new CodeCell({ model, rendermime });
  444. widget.initializeState();
  445. expect(widget.outputsScrolled).toEqual(false);
  446. model.metadata.set('scrolled', 'auto');
  447. widget = new CodeCell({ model, rendermime });
  448. widget.initializeState();
  449. expect(widget.outputsScrolled).toEqual(false);
  450. model.metadata.set('scrolled', true);
  451. widget = new CodeCell({ model, rendermime });
  452. widget.initializeState();
  453. expect(widget.outputsScrolled).toEqual(true);
  454. });
  455. });
  456. describe('#loadScrolledState()', () => {
  457. it('should load the output scrolled state from the model', () => {
  458. const model = new CodeCellModel({});
  459. const widget = new CodeCell({ model, rendermime });
  460. widget.initializeState();
  461. expect(widget.outputsScrolled).toEqual(false);
  462. model.metadata.set('scrolled', true);
  463. widget.loadScrolledState();
  464. expect(widget.outputsScrolled).toEqual(true);
  465. model.metadata.set('scrolled', false);
  466. widget.loadScrolledState();
  467. expect(widget.outputsScrolled).toEqual(false);
  468. });
  469. });
  470. describe('#saveScrolledState()', () => {
  471. it('should save the collapse state to the model', () => {
  472. const model = new CodeCellModel({});
  473. const widget = new CodeCell({ model, rendermime });
  474. widget.initializeState();
  475. expect(widget.outputsScrolled).toEqual(false);
  476. widget.outputsScrolled = true;
  477. widget.saveScrolledState();
  478. expect(model.metadata.get('scrolled')).toEqual(true);
  479. widget.outputsScrolled = false;
  480. widget.saveScrolledState();
  481. // Default values are not saved explicitly
  482. expect(model.metadata.get('scrolled')).toEqual(undefined);
  483. });
  484. });
  485. describe('#syncScrolled', () => {
  486. it('should control automatic syncing of scrolled state with model', () => {
  487. const model = new CodeCellModel({});
  488. const widget = new CodeCell({ model, rendermime });
  489. widget.initializeState();
  490. expect(widget.syncScrolled).toEqual(false);
  491. expect(widget.outputsScrolled).toEqual(false);
  492. // Not synced if setting widget attribute
  493. widget.outputsScrolled = true;
  494. expect(model.metadata.get('scrolled')).toEqual(undefined);
  495. // Not synced if setting metadata attribute
  496. model.metadata.set('scrolled', false);
  497. expect(widget.outputsScrolled).toEqual(true);
  498. widget.syncScrolled = true;
  499. // Setting sync does an initial sync from model to view. This also sets
  500. // the metadata to undefined if it is the default value.
  501. expect(model.metadata.get('scrolled')).toEqual(undefined);
  502. expect(widget.outputsScrolled).toEqual(false);
  503. // Synced if setting widget attribute
  504. widget.outputsScrolled = true;
  505. expect(model.metadata.get('scrolled')).toEqual(true);
  506. // Synced if setting metadata attribute
  507. model.metadata.set('scrolled', false);
  508. expect(widget.outputsScrolled).toEqual(false);
  509. });
  510. });
  511. describe('#loadCollapseState()', () => {
  512. it('should load the output collapse state from the model', () => {
  513. const model = new CodeCellModel({});
  514. const widget = new CodeCell({ model, rendermime });
  515. widget.initializeState();
  516. widget.loadCollapseState();
  517. expect(widget.outputHidden).toEqual(false);
  518. model.metadata.set('collapsed', true);
  519. widget.loadCollapseState();
  520. expect(widget.outputHidden).toEqual(true);
  521. model.metadata.set('collapsed', false);
  522. widget.loadCollapseState();
  523. expect(widget.outputHidden).toEqual(false);
  524. });
  525. });
  526. describe('#saveCollapseState()', () => {
  527. it('should save the collapse state to the model `collapsed` metadata', () => {
  528. const model = new CodeCellModel({});
  529. const widget = new CodeCell({ model, rendermime });
  530. widget.initializeState();
  531. expect(widget.outputHidden).toEqual(false);
  532. widget.outputHidden = true;
  533. widget.saveCollapseState();
  534. expect(model.metadata.get('collapsed')).toEqual(true);
  535. // Default values are not saved explicitly
  536. widget.outputHidden = false;
  537. widget.saveCollapseState();
  538. expect(model.metadata.get('collapsed')).toEqual(undefined);
  539. // Default values are explicitly deleted
  540. model.metadata.set('collapsed', false);
  541. widget.outputHidden = false;
  542. widget.saveCollapseState();
  543. expect(model.metadata.get('collapsed')).toEqual(undefined);
  544. });
  545. });
  546. describe('#syncCollapse', () => {
  547. it('should control automatic syncing of collapse state with model', () => {
  548. const model = new CodeCellModel({});
  549. const widget = new CodeCell({ model, rendermime });
  550. widget.initializeState();
  551. expect(widget.syncCollapse).toEqual(false);
  552. expect(widget.outputHidden).toEqual(false);
  553. // Not synced if setting widget attribute
  554. widget.outputHidden = true;
  555. expect(model.metadata.get('collapsed')).toEqual(undefined);
  556. // Not synced if setting metadata attribute
  557. model.metadata.set('collapsed', false);
  558. expect(widget.outputHidden).toEqual(true);
  559. widget.syncCollapse = true;
  560. // Setting sync does an initial sync from model to view.
  561. expect(model.metadata.get('collapsed')).toEqual(undefined);
  562. expect(widget.outputHidden).toEqual(false);
  563. // Synced if setting widget attribute
  564. widget.outputHidden = true;
  565. expect(model.metadata.get('collapsed')).toEqual(true);
  566. // Synced if setting metadata attribute
  567. model.metadata.set('collapsed', false);
  568. expect(widget.outputHidden).toEqual(false);
  569. // Synced if deleting collapsed metadata attribute
  570. widget.outputHidden = true;
  571. expect(model.metadata.get('collapsed')).toEqual(true);
  572. model.metadata.delete('collapsed');
  573. expect(widget.outputHidden).toEqual(false);
  574. });
  575. });
  576. describe('#dispose()', () => {
  577. it('should dispose of the resources held by the widget', () => {
  578. const widget = new CodeCell({ model, rendermime, contentFactory });
  579. widget.initializeState();
  580. widget.dispose();
  581. expect(widget.isDisposed).toEqual(true);
  582. });
  583. it('should be safe to call multiple times', () => {
  584. const widget = new CodeCell({ model, rendermime, contentFactory });
  585. widget.initializeState();
  586. widget.dispose();
  587. widget.dispose();
  588. expect(widget.isDisposed).toEqual(true);
  589. });
  590. });
  591. describe('#onUpdateRequest()', () => {
  592. it('should update the widget', () => {
  593. const widget = new LogCodeCell().initializeState();
  594. expect(widget.methods).not.toContain('onUpdateRequest');
  595. MessageLoop.sendMessage(widget, Widget.Msg.UpdateRequest);
  596. expect(widget.methods).toContain('onUpdateRequest');
  597. });
  598. });
  599. describe('#onMetadataChanged()', () => {
  600. it('should fire when model metadata changes', () => {
  601. const method = 'onMetadataChanged';
  602. const widget = new LogCodeCell().initializeState();
  603. expect(widget.methods).not.toContain(method);
  604. widget.model.metadata.set('foo', 1);
  605. expect(widget.methods).toContain(method);
  606. });
  607. });
  608. describe('.execute()', () => {
  609. let sessionContext: ISessionContext;
  610. beforeEach(async () => {
  611. sessionContext = await createSessionContext();
  612. await (sessionContext as SessionContext).initialize();
  613. await sessionContext.session?.kernel?.info;
  614. });
  615. afterEach(() => {
  616. return sessionContext.shutdown();
  617. });
  618. it('should fulfill a promise if there is no code to execute', async () => {
  619. const widget = new CodeCell({ model, rendermime, contentFactory });
  620. widget.initializeState();
  621. await CodeCell.execute(widget, sessionContext);
  622. });
  623. it('should fulfill a promise if there is code to execute', async () => {
  624. const widget = new CodeCell({ model, rendermime, contentFactory });
  625. widget.initializeState();
  626. let originalCount: number;
  627. widget.model.value.text = 'foo';
  628. originalCount = widget.model.executionCount!;
  629. await CodeCell.execute(widget, sessionContext);
  630. const executionCount = widget.model.executionCount;
  631. expect(executionCount).not.toEqual(originalCount);
  632. });
  633. const TIMING_KEYS = [
  634. 'iopub.execute_input',
  635. 'shell.execute_reply.started',
  636. 'shell.execute_reply',
  637. 'iopub.status.busy',
  638. 'iopub.status.idle'
  639. ];
  640. it('should not save timing info by default', async () => {
  641. const widget = new CodeCell({ model, rendermime, contentFactory });
  642. await CodeCell.execute(widget, sessionContext);
  643. expect(widget.model.metadata.get('execution')).toBeUndefined();
  644. });
  645. it('should save timing info if requested', async () => {
  646. const widget = new CodeCell({ model, rendermime, contentFactory });
  647. await CodeCell.execute(widget, sessionContext, { recordTiming: true });
  648. expect(widget.model.metadata.get('execution')).toBeDefined();
  649. const timingInfo = widget.model.metadata.get('execution') as any;
  650. for (const key of TIMING_KEYS) {
  651. expect(timingInfo[key]).toBeDefined();
  652. }
  653. });
  654. it('should set the cell prompt properly while executing', async () => {
  655. const widget = new CodeCell({ model, rendermime, contentFactory });
  656. widget.initializeState();
  657. widget.model.value.text = 'foo';
  658. const future1 = CodeCell.execute(widget, sessionContext);
  659. expect(widget.promptNode.textContent).toEqual('[*]:');
  660. const future2 = CodeCell.execute(widget, sessionContext);
  661. expect(widget.promptNode.textContent).toEqual('[*]:');
  662. await expect(future1).rejects.toThrow('Canceled');
  663. expect(widget.promptNode.textContent).toEqual('[*]:');
  664. const msg = await future2;
  665. expect(msg).not.toBeUndefined();
  666. // The `if` is a Typescript type guard so that msg.content works below.
  667. if (msg) {
  668. expect(widget.promptNode.textContent).toEqual(
  669. `[${msg.content.execution_count}]:`
  670. );
  671. }
  672. });
  673. });
  674. });
  675. describe('MarkdownCell', () => {
  676. const contentFactory = NBTestUtils.createBaseCellFactory();
  677. const model = new MarkdownCellModel({});
  678. describe('#constructor()', () => {
  679. it('should create a markdown cell widget', () => {
  680. const widget = new MarkdownCell({ model, rendermime, contentFactory });
  681. widget.initializeState();
  682. expect(widget).toBeInstanceOf(MarkdownCell);
  683. });
  684. it('should accept a custom contentFactory', () => {
  685. const widget = new MarkdownCell({ model, rendermime, contentFactory });
  686. widget.initializeState();
  687. expect(widget).toBeInstanceOf(MarkdownCell);
  688. });
  689. it('should set the default mimetype to text/x-ipythongfm', () => {
  690. const widget = new MarkdownCell({ model, rendermime, contentFactory });
  691. widget.initializeState();
  692. expect(widget.model.mimeType).toEqual('text/x-ipythongfm');
  693. });
  694. });
  695. describe('#rendered', () => {
  696. it('should default to true', async () => {
  697. const widget = new MarkdownCell({ model, rendermime, contentFactory });
  698. widget.initializeState();
  699. Widget.attach(widget, document.body);
  700. expect(widget.rendered).toEqual(true);
  701. await framePromise();
  702. expect(widget.node.classList.contains(RENDERED_CLASS)).toEqual(true);
  703. });
  704. it('should unrender the widget', async () => {
  705. const widget = new MarkdownCell({ model, rendermime, contentFactory });
  706. widget.initializeState();
  707. Widget.attach(widget, document.body);
  708. widget.rendered = false;
  709. await framePromise();
  710. expect(widget.node.classList.contains(RENDERED_CLASS)).toEqual(false);
  711. widget.dispose();
  712. });
  713. });
  714. describe('#dispose()', () => {
  715. it('should dispose of the resources held by the widget', () => {
  716. const widget = new MarkdownCell({ model, rendermime, contentFactory });
  717. widget.initializeState();
  718. widget.dispose();
  719. expect(widget.isDisposed).toEqual(true);
  720. });
  721. it('should be safe to call multiple times', () => {
  722. const widget = new MarkdownCell({ model, rendermime, contentFactory });
  723. widget.initializeState();
  724. widget.dispose();
  725. widget.dispose();
  726. expect(widget.isDisposed).toEqual(true);
  727. });
  728. });
  729. describe('#onUpdateRequest()', () => {
  730. it('should update the widget', () => {
  731. const widget = new LogMarkdownCell({
  732. model,
  733. rendermime,
  734. contentFactory
  735. }).initializeState();
  736. expect(widget.methods).not.toContain('onUpdateRequest');
  737. MessageLoop.sendMessage(widget, Widget.Msg.UpdateRequest);
  738. expect(widget.methods).toContain('onUpdateRequest');
  739. });
  740. });
  741. });
  742. describe('RawCell', () => {
  743. const contentFactory = NBTestUtils.createBaseCellFactory();
  744. describe('#constructor()', () => {
  745. it('should create a raw cell widget', () => {
  746. const model = new RawCellModel({});
  747. const widget = new RawCell({ model, contentFactory }).initializeState();
  748. expect(widget).toBeInstanceOf(RawCell);
  749. });
  750. });
  751. });
  752. describe('CellHeader', () => {
  753. describe('#constructor()', () => {
  754. it('should create a new cell header', () => {
  755. expect(new CellHeader()).toBeInstanceOf(CellHeader);
  756. });
  757. });
  758. });
  759. describe('CellFooter', () => {
  760. describe('#constructor()', () => {
  761. it('should create a new cell footer', () => {
  762. expect(new CellFooter()).toBeInstanceOf(CellFooter);
  763. });
  764. });
  765. });
  766. });