model.spec.ts 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import 'jest';
  4. import { ArrayExt, toArray } from '@lumino/algorithm';
  5. import { CodeCellModel } from '@jupyterlab/cells';
  6. import * as nbformat from '@jupyterlab/nbformat';
  7. import { NotebookModel } from '../src';
  8. import { ModelDB } from '@jupyterlab/observables';
  9. import { acceptDialog } from '@jupyterlab/testutils';
  10. import * as utils from './utils';
  11. describe('@jupyterlab/notebook', () => {
  12. describe('NotebookModel', () => {
  13. describe('#constructor()', () => {
  14. it('should create a notebook model', () => {
  15. const model = new NotebookModel();
  16. expect(model).toBeInstanceOf(NotebookModel);
  17. });
  18. it('should accept an optional language preference', () => {
  19. const model = new NotebookModel({ languagePreference: 'python' });
  20. const lang = model.metadata.get(
  21. 'language_info'
  22. ) as nbformat.ILanguageInfoMetadata;
  23. expect(lang.name).toBe('python');
  24. });
  25. it('should accept an optional factory', () => {
  26. const contentFactory = new NotebookModel.ContentFactory({});
  27. const model = new NotebookModel({ contentFactory });
  28. expect(model.contentFactory.codeCellContentFactory).toBe(
  29. contentFactory.codeCellContentFactory
  30. );
  31. });
  32. });
  33. describe('#metadataChanged', () => {
  34. it('should be emitted when a metadata field changes', () => {
  35. const model = new NotebookModel();
  36. let called = false;
  37. model.metadata.changed.connect((sender, args) => {
  38. expect(sender).toBe(model.metadata);
  39. expect(args.key).toBe('foo');
  40. expect(args.oldValue).toBeUndefined();
  41. expect(args.newValue).toBe(1);
  42. called = true;
  43. });
  44. model.metadata.set('foo', 1);
  45. expect(called).toBe(true);
  46. });
  47. it('should not be emitted when the value does not change', () => {
  48. const model = new NotebookModel();
  49. let called = false;
  50. model.metadata.set('foo', 1);
  51. model.metadata.changed.connect(() => {
  52. called = true;
  53. });
  54. model.metadata.set('foo', 1);
  55. expect(called).toBe(false);
  56. });
  57. });
  58. describe('#cells', () => {
  59. it('should be reset when loading from disk', () => {
  60. const model = new NotebookModel();
  61. const cell = model.contentFactory.createCodeCell({});
  62. model.cells.push(cell);
  63. model.fromJSON(utils.DEFAULT_CONTENT);
  64. expect(ArrayExt.firstIndexOf(toArray(model.cells), cell)).toBe(-1);
  65. expect(model.cells.length).toBe(6);
  66. });
  67. it('should allow undoing a change', () => {
  68. const model = new NotebookModel();
  69. const cell = model.contentFactory.createCodeCell({});
  70. cell.value.text = 'foo';
  71. model.cells.push(cell);
  72. model.fromJSON(utils.DEFAULT_CONTENT);
  73. model.cells.undo();
  74. expect(model.cells.length).toBe(1);
  75. expect(model.cells.get(0).value.text).toBe('foo');
  76. expect(model.cells.get(0)).toBe(cell); // should be ===.
  77. });
  78. describe('cells `changed` signal', () => {
  79. it('should emit a `contentChanged` signal upon cell addition', () => {
  80. const model = new NotebookModel();
  81. const cell = model.contentFactory.createCodeCell({});
  82. let called = false;
  83. model.contentChanged.connect(() => {
  84. called = true;
  85. });
  86. model.cells.push(cell);
  87. expect(called).toBe(true);
  88. });
  89. it('should emit a `contentChanged` signal upon cell removal', () => {
  90. const model = new NotebookModel();
  91. const cell = model.contentFactory.createCodeCell({});
  92. model.cells.push(cell);
  93. let called = false;
  94. model.contentChanged.connect(() => {
  95. called = true;
  96. });
  97. model.cells.remove(0);
  98. expect(called).toBe(true);
  99. });
  100. it('should emit a `contentChanged` signal upon cell move', () => {
  101. const model = new NotebookModel();
  102. const cell0 = model.contentFactory.createCodeCell({});
  103. const cell1 = model.contentFactory.createCodeCell({});
  104. model.cells.push(cell0);
  105. model.cells.push(cell1);
  106. let called = false;
  107. model.contentChanged.connect(() => {
  108. called = true;
  109. });
  110. model.cells.move(0, 1);
  111. expect(called).toBe(true);
  112. });
  113. it('should set the dirty flag', () => {
  114. const model = new NotebookModel();
  115. const cell = model.contentFactory.createCodeCell({});
  116. model.cells.push(cell);
  117. expect(model.dirty).toBe(true);
  118. });
  119. });
  120. describe('cell `changed` signal', () => {
  121. it('should be called when a cell content changes', () => {
  122. const model = new NotebookModel();
  123. const cell = model.contentFactory.createCodeCell({});
  124. model.cells.push(cell);
  125. cell.value.text = 'foo';
  126. });
  127. it('should emit the `contentChanged` signal', () => {
  128. const model = new NotebookModel();
  129. const cell = model.contentFactory.createCodeCell({});
  130. model.cells.push(cell);
  131. let called = false;
  132. model.contentChanged.connect(() => {
  133. called = true;
  134. });
  135. model.metadata.set('foo', 'bar');
  136. expect(called).toBe(true);
  137. });
  138. it('should set the dirty flag', () => {
  139. const model = new NotebookModel();
  140. const cell = model.contentFactory.createCodeCell({});
  141. model.cells.push(cell);
  142. model.dirty = false;
  143. cell.value.text = 'foo';
  144. expect(model.dirty).toBe(true);
  145. });
  146. });
  147. });
  148. describe('#contentFactory', () => {
  149. it('should be the cell model factory used by the model', () => {
  150. const model = new NotebookModel();
  151. expect(model.contentFactory.codeCellContentFactory).toBe(
  152. NotebookModel.defaultContentFactory.codeCellContentFactory
  153. );
  154. });
  155. });
  156. describe('#nbformat', () => {
  157. it('should get the major version number of the nbformat', () => {
  158. const model = new NotebookModel();
  159. model.fromJSON(utils.DEFAULT_CONTENT);
  160. expect(model.nbformat).toBe(utils.DEFAULT_CONTENT.nbformat);
  161. });
  162. it('should present a dialog when the format changes', () => {
  163. const model = new NotebookModel();
  164. const content = {
  165. ...utils.DEFAULT_CONTENT,
  166. metadata: {
  167. ...utils.DEFAULT_CONTENT.metadata,
  168. orig_nbformat: 1
  169. }
  170. };
  171. model.fromJSON(content);
  172. expect(model.nbformat).toBe(nbformat.MAJOR_VERSION);
  173. return acceptDialog();
  174. });
  175. });
  176. describe('#nbformatMinor', () => {
  177. it('should get the minor version number of the nbformat', () => {
  178. const model = new NotebookModel();
  179. model.fromJSON(utils.DEFAULT_CONTENT);
  180. expect(model.nbformatMinor).toBe(nbformat.MINOR_VERSION);
  181. });
  182. });
  183. describe('#defaultKernelName()', () => {
  184. it('should get the default kernel name of the document', () => {
  185. const model = new NotebookModel();
  186. model.metadata.set('kernelspec', { name: 'python3' });
  187. expect(model.defaultKernelName).toBe('python3');
  188. });
  189. it('should default to an empty string', () => {
  190. const model = new NotebookModel();
  191. expect(model.defaultKernelName).toBe('');
  192. });
  193. });
  194. describe('#defaultKernelLanguage', () => {
  195. it('should get the default kernel language of the document', () => {
  196. const model = new NotebookModel();
  197. model.metadata.set('language_info', { name: 'python' });
  198. expect(model.defaultKernelLanguage).toBe('python');
  199. });
  200. it('should default to an empty string', () => {
  201. const model = new NotebookModel();
  202. expect(model.defaultKernelLanguage).toBe('');
  203. });
  204. it('should be set from the constructor arg', () => {
  205. const model = new NotebookModel({ languagePreference: 'foo' });
  206. expect(model.defaultKernelLanguage).toBe('foo');
  207. });
  208. });
  209. describe('#dispose()', () => {
  210. it('should dispose of the resources held by the model', () => {
  211. const model = new NotebookModel();
  212. model.fromJSON(utils.DEFAULT_CONTENT);
  213. model.dispose();
  214. expect(model.cells).toBeNull();
  215. expect(model.isDisposed).toBe(true);
  216. });
  217. it('should be safe to call multiple times', () => {
  218. const model = new NotebookModel();
  219. model.dispose();
  220. model.dispose();
  221. expect(model.isDisposed).toBe(true);
  222. });
  223. });
  224. describe('#toString()', () => {
  225. it('should serialize the model to a string', () => {
  226. const model = new NotebookModel();
  227. model.fromJSON(utils.DEFAULT_CONTENT);
  228. const text = model.toString();
  229. const data = JSON.parse(text);
  230. expect(data.cells.length).toBe(6);
  231. });
  232. });
  233. describe('#fromString()', () => {
  234. it('should deserialize the model from a string', () => {
  235. const model = new NotebookModel();
  236. model.fromString(JSON.stringify(utils.DEFAULT_CONTENT));
  237. expect(model.cells.length).toBe(6);
  238. });
  239. it('should set the dirty flag', () => {
  240. const model = new NotebookModel();
  241. model.dirty = false;
  242. model.fromString(JSON.stringify(utils.DEFAULT_CONTENT));
  243. expect(model.dirty).toBe(true);
  244. });
  245. });
  246. describe('#toJSON()', () => {
  247. it('should serialize the model to JSON', () => {
  248. const model = new NotebookModel();
  249. model.fromJSON(utils.DEFAULT_CONTENT);
  250. const data = model.toJSON();
  251. expect(data.cells.length).toBe(6);
  252. });
  253. });
  254. describe('#fromJSON()', () => {
  255. it('should serialize the model from JSON', () => {
  256. const model = new NotebookModel();
  257. model.fromJSON(utils.DEFAULT_CONTENT);
  258. expect(model.cells.length).toBe(6);
  259. expect(model.nbformat).toBe(utils.DEFAULT_CONTENT.nbformat);
  260. expect(model.nbformatMinor).toBe(nbformat.MINOR_VERSION);
  261. });
  262. it('should set the dirty flag', () => {
  263. const model = new NotebookModel();
  264. model.dirty = false;
  265. model.fromJSON(utils.DEFAULT_CONTENT);
  266. expect(model.dirty).toBe(true);
  267. });
  268. });
  269. describe('#metadata', () => {
  270. it('should have default values', () => {
  271. const model = new NotebookModel();
  272. const metadata = model.metadata;
  273. expect(metadata.has('kernelspec')).toBeTruthy();
  274. expect(metadata.has('language_info')).toBeTruthy();
  275. expect(metadata.size).toBe(2);
  276. });
  277. it('should set the dirty flag when changed', () => {
  278. const model = new NotebookModel();
  279. expect(model.dirty).toBe(false);
  280. model.metadata.set('foo', 'bar');
  281. expect(model.dirty).toBe(true);
  282. });
  283. it('should emit the `contentChanged` signal', () => {
  284. const model = new NotebookModel();
  285. let called = false;
  286. model.contentChanged.connect(() => {
  287. called = true;
  288. });
  289. model.metadata.set('foo', 'bar');
  290. expect(called).toBe(true);
  291. });
  292. it('should emit the `metadataChanged` signal', () => {
  293. const model = new NotebookModel();
  294. let called = false;
  295. model.metadata.changed.connect((sender, args) => {
  296. expect(sender).toBe(model.metadata);
  297. expect(args.key).toBe('foo');
  298. expect(args.oldValue).toBeUndefined();
  299. expect(args.newValue).toBe('bar');
  300. called = true;
  301. });
  302. model.metadata.set('foo', 'bar');
  303. expect(called).toBe(true);
  304. });
  305. });
  306. describe('#initialize()', () => {
  307. it('should add one code cell if the model is empty', () => {
  308. const model = new NotebookModel();
  309. expect(model.cells.length).toBe(0);
  310. model.initialize();
  311. expect(model.cells.length).toBe(1);
  312. expect(model.cells.get(0).type).toBe('code');
  313. });
  314. it('should clear undo state', () => {
  315. const model = new NotebookModel();
  316. const cell = model.contentFactory.createCodeCell({});
  317. cell.value.text = 'foo';
  318. model.cells.push(cell);
  319. expect(model.cells.canUndo).toBe(true);
  320. model.initialize();
  321. expect(model.cells.canUndo).toBe(false);
  322. });
  323. });
  324. describe('.ContentFactory', () => {
  325. let factory = new NotebookModel.ContentFactory({});
  326. describe('#codeCellContentFactory', () => {
  327. it('should be a code cell content factory', () => {
  328. expect(factory.codeCellContentFactory).toBe(
  329. CodeCellModel.defaultContentFactory
  330. );
  331. });
  332. it('should be settable in the constructor', () => {
  333. const codeCellContentFactory = new CodeCellModel.ContentFactory();
  334. factory = new NotebookModel.ContentFactory({
  335. codeCellContentFactory
  336. });
  337. expect(factory.codeCellContentFactory).toBe(codeCellContentFactory);
  338. });
  339. });
  340. describe('#createCell()', () => {
  341. it('should create a new code cell', () => {
  342. const cell = factory.createCell('code', {});
  343. expect(cell.type).toBe('code');
  344. });
  345. it('should create a new markdown cell', () => {
  346. const cell = factory.createCell('markdown', {});
  347. expect(cell.type).toBe('markdown');
  348. });
  349. it('should create a new raw cell', () => {
  350. const cell = factory.createCell('raw', {});
  351. expect(cell.type).toBe('raw');
  352. });
  353. });
  354. describe('#createCodeCell()', () => {
  355. it('should create a new code cell', () => {
  356. const cell = factory.createCodeCell({});
  357. expect(cell.type).toBe('code');
  358. });
  359. it('should clone an existing code cell', () => {
  360. const orig = factory.createCodeCell({});
  361. orig.value.text = 'foo';
  362. const cell = orig.toJSON();
  363. const newCell = factory.createCodeCell({ cell });
  364. expect(newCell.value.text).toBe('foo');
  365. });
  366. it('should clone an existing raw cell', () => {
  367. const orig = factory.createRawCell({});
  368. orig.value.text = 'foo';
  369. const cell = orig.toJSON();
  370. const newCell = factory.createCodeCell({ cell });
  371. expect(newCell.value.text).toBe('foo');
  372. });
  373. });
  374. describe('#createRawCell()', () => {
  375. it('should create a new raw cell', () => {
  376. const cell = factory.createRawCell({});
  377. expect(cell.type).toBe('raw');
  378. });
  379. it('should clone an existing raw cell', () => {
  380. const orig = factory.createRawCell({});
  381. orig.value.text = 'foo';
  382. const cell = orig.toJSON();
  383. const newCell = factory.createRawCell({ cell });
  384. expect(newCell.value.text).toBe('foo');
  385. });
  386. it('should clone an existing code cell', () => {
  387. const orig = factory.createCodeCell({});
  388. orig.value.text = 'foo';
  389. const cell = orig.toJSON();
  390. const newCell = factory.createRawCell({ cell });
  391. expect(newCell.value.text).toBe('foo');
  392. });
  393. });
  394. describe('#createMarkdownCell()', () => {
  395. it('should create a new markdown cell', () => {
  396. const cell = factory.createMarkdownCell({});
  397. expect(cell.type).toBe('markdown');
  398. });
  399. it('should clone an existing markdown cell', () => {
  400. const orig = factory.createMarkdownCell({});
  401. orig.value.text = 'foo';
  402. const cell = orig.toJSON();
  403. const newCell = factory.createMarkdownCell({ cell });
  404. expect(newCell.value.text).toBe('foo');
  405. });
  406. it('should clone an existing raw cell', () => {
  407. const orig = factory.createRawCell({});
  408. orig.value.text = 'foo';
  409. const cell = orig.toJSON();
  410. const newCell = factory.createMarkdownCell({ cell });
  411. expect(newCell.value.text).toBe('foo');
  412. });
  413. });
  414. describe('#modelDB', () => {
  415. it('should be undefined by default', () => {
  416. expect(factory.modelDB).toBeUndefined();
  417. });
  418. });
  419. describe('#clone()', () => {
  420. it('should create a new content factory with a new IModelDB', () => {
  421. const modelDB = new ModelDB();
  422. const factory = new NotebookModel.ContentFactory({ modelDB });
  423. expect(factory.modelDB).toBe(modelDB);
  424. const newModelDB = new ModelDB();
  425. const newFactory = factory.clone(newModelDB);
  426. expect(newFactory.modelDB).toBe(newModelDB);
  427. expect(newFactory.codeCellContentFactory).toBe(
  428. factory.codeCellContentFactory
  429. );
  430. });
  431. });
  432. });
  433. describe('.defaultContentFactory', () => {
  434. it('should be a ContentFactory', () => {
  435. expect(NotebookModel.defaultContentFactory).toBeInstanceOf(
  436. NotebookModel.ContentFactory
  437. );
  438. });
  439. });
  440. });
  441. });