model.spec.ts 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import 'jest';
  4. import { toArray } from '@lumino/algorithm';
  5. import { IChangedArgs } from '@jupyterlab/coreutils';
  6. import {
  7. CellModel,
  8. RawCellModel,
  9. MarkdownCellModel,
  10. CodeCellModel
  11. } from '@jupyterlab/cells';
  12. import * as nbformat from '@jupyterlab/nbformat';
  13. import { OutputAreaModel } from '@jupyterlab/outputarea';
  14. import { NBTestUtils } from '@jupyterlab/testutils';
  15. import { JSONObject } from '@lumino/coreutils';
  16. class TestModel extends CellModel {
  17. get type(): 'raw' {
  18. return 'raw';
  19. }
  20. }
  21. describe('cells/model', () => {
  22. describe('CellModel', () => {
  23. describe('#constructor()', () => {
  24. it('should create a cell model', () => {
  25. const model = new CellModel({});
  26. expect(model).toBeInstanceOf(CellModel);
  27. });
  28. it('should accept a base cell argument', () => {
  29. const cell: nbformat.IRawCell = {
  30. cell_type: 'raw',
  31. source: 'foo',
  32. metadata: { trusted: false }
  33. };
  34. const model = new CellModel({ cell });
  35. expect(model).toBeInstanceOf(CellModel);
  36. expect(model.value.text).toBe(cell.source);
  37. });
  38. it('should accept a base cell argument with a multiline source', () => {
  39. const cell: nbformat.IRawCell = {
  40. cell_type: 'raw',
  41. source: ['foo\n', 'bar\n', 'baz'],
  42. metadata: { trusted: false }
  43. };
  44. const model = new CellModel({ cell });
  45. expect(model).toBeInstanceOf(CellModel);
  46. expect(model.value.text).toBe((cell.source as string[]).join(''));
  47. });
  48. });
  49. describe('#contentChanged', () => {
  50. it('should signal when model content has changed', () => {
  51. const model = new CellModel({});
  52. let called = false;
  53. model.contentChanged.connect(() => {
  54. called = true;
  55. });
  56. expect(called).toBe(false);
  57. model.value.text = 'foo';
  58. expect(called).toBe(true);
  59. });
  60. });
  61. describe('#stateChanged', () => {
  62. it('should signal when model state has changed', () => {
  63. const model = new CodeCellModel({});
  64. let called = false;
  65. const listener = (sender: any, args: IChangedArgs<any>) => {
  66. expect(args.newValue).toBe(1);
  67. called = true;
  68. };
  69. model.stateChanged.connect(listener);
  70. model.executionCount = 1;
  71. expect(called).toBe(true);
  72. });
  73. it('should not signal when model state has not changed', () => {
  74. const model = new CodeCellModel({});
  75. let called = 0;
  76. model.stateChanged.connect(() => {
  77. called++;
  78. });
  79. expect(called).toBe(0);
  80. model.executionCount = 1;
  81. expect(called).toBe(1);
  82. model.executionCount = 1;
  83. expect(called).toBe(1);
  84. });
  85. });
  86. describe('#trusted', () => {
  87. it('should be the trusted state of the cell', () => {
  88. const model = new CodeCellModel({});
  89. expect(model.trusted).toBe(false);
  90. model.trusted = true;
  91. expect(model.trusted).toBe(true);
  92. const other = new CodeCellModel({ cell: model.toJSON() });
  93. expect(other.trusted).toBe(true);
  94. });
  95. it('should update the trusted state of the output models', () => {
  96. const model = new CodeCellModel({});
  97. model.outputs.add(NBTestUtils.DEFAULT_OUTPUTS[0]);
  98. expect(model.outputs.get(0).trusted).toBe(false);
  99. model.trusted = true;
  100. expect(model.outputs.get(0).trusted).toBe(true);
  101. });
  102. });
  103. describe('#metadataChanged', () => {
  104. it('should signal when model metadata has changed', () => {
  105. const model = new TestModel({});
  106. const listener = (sender: any, args: any) => {
  107. value = args.newValue;
  108. };
  109. let value = '';
  110. model.metadata.changed.connect(listener);
  111. expect(Object.keys(value)).toHaveLength(0);
  112. model.metadata.set('foo', 'bar');
  113. expect(value).toBe('bar');
  114. });
  115. it('should not signal when model metadata has not changed', () => {
  116. const model = new TestModel({});
  117. let called = 0;
  118. model.metadata.changed.connect(() => {
  119. called++;
  120. });
  121. expect(called).toBe(0);
  122. model.metadata.set('foo', 'bar');
  123. expect(called).toBe(1);
  124. model.metadata.set('foo', 'bar');
  125. expect(called).toBe(1);
  126. });
  127. });
  128. describe('#source', () => {
  129. it('should default to an empty string', () => {
  130. const model = new CellModel({});
  131. expect(model.value.text).toHaveLength(0);
  132. });
  133. it('should be settable', () => {
  134. const model = new CellModel({});
  135. expect(model.value.text).toHaveLength(0);
  136. model.value.text = 'foo';
  137. expect(model.value.text).toBe('foo');
  138. });
  139. });
  140. describe('#isDisposed', () => {
  141. it('should be false by default', () => {
  142. const model = new CellModel({});
  143. expect(model.isDisposed).toBe(false);
  144. });
  145. it('should be true after model is disposed', () => {
  146. const model = new CellModel({});
  147. model.dispose();
  148. expect(model.isDisposed).toBe(true);
  149. });
  150. });
  151. describe('#dispose()', () => {
  152. it('should dispose of the resources held by the model', () => {
  153. const model = new TestModel({});
  154. model.dispose();
  155. expect(model.isDisposed).toBe(true);
  156. });
  157. it('should be safe to call multiple times', () => {
  158. const model = new CellModel({});
  159. model.dispose();
  160. model.dispose();
  161. expect(model.isDisposed).toBe(true);
  162. });
  163. });
  164. describe('#toJSON()', () => {
  165. it('should return a base cell encapsulation of the model value', () => {
  166. const cell: nbformat.IRawCell = {
  167. cell_type: 'raw',
  168. source: 'foo',
  169. metadata: { trusted: false }
  170. };
  171. const model = new TestModel({ cell });
  172. expect(model.toJSON()).not.toBe(cell);
  173. expect(model.toJSON()).toEqual(cell);
  174. });
  175. it('should always return a string source', () => {
  176. const cell: nbformat.IRawCell = {
  177. cell_type: 'raw',
  178. source: ['foo\n', 'bar\n', 'baz'],
  179. metadata: { trusted: false }
  180. };
  181. const model = new TestModel({ cell });
  182. cell.source = (cell.source as string[]).join('');
  183. expect(model.toJSON()).not.toBe(cell);
  184. expect(model.toJSON()).toEqual(cell);
  185. });
  186. });
  187. describe('#metadata', () => {
  188. it('should handle a metadata for the cell', () => {
  189. const model = new CellModel({});
  190. expect(model.metadata.get('foo')).toBeUndefined();
  191. model.metadata.set('foo', 1);
  192. expect(model.metadata.get('foo')).toBe(1);
  193. });
  194. it('should get a list of user metadata keys', () => {
  195. const model = new CellModel({});
  196. expect(toArray(model.metadata.keys())).toHaveLength(0);
  197. model.metadata.set('foo', 1);
  198. expect(model.metadata.keys()).toEqual(['foo']);
  199. });
  200. it('should trigger changed signal', () => {
  201. const model = new CellModel({});
  202. let called = false;
  203. model.metadata.changed.connect(() => {
  204. called = true;
  205. });
  206. model.metadata.set('foo', 1);
  207. expect(called).toBe(true);
  208. });
  209. });
  210. });
  211. describe('RawCellModel', () => {
  212. describe('#type', () => {
  213. it('should be set with type "raw"', () => {
  214. const model = new RawCellModel({});
  215. expect(model.type).toBe('raw');
  216. });
  217. });
  218. });
  219. describe('MarkdownCellModel', () => {
  220. describe('#type', () => {
  221. it('should be set with type "markdown"', () => {
  222. const model = new MarkdownCellModel({});
  223. expect(model.type).toBe('markdown');
  224. });
  225. });
  226. });
  227. describe('CodeCellModel', () => {
  228. describe('#constructor()', () => {
  229. it('should create a code cell model', () => {
  230. const model = new CodeCellModel({});
  231. expect(model).toBeInstanceOf(CodeCellModel);
  232. });
  233. it('should accept a code cell argument', () => {
  234. const cell: nbformat.ICodeCell = {
  235. cell_type: 'code',
  236. execution_count: 1,
  237. outputs: [
  238. {
  239. output_type: 'display_data',
  240. data: { 'text/plain': 'foo' },
  241. metadata: {}
  242. } as nbformat.IDisplayData
  243. ],
  244. source: 'foo',
  245. metadata: { trusted: false }
  246. };
  247. const model = new CodeCellModel({ cell });
  248. expect(model).toBeInstanceOf(CodeCellModel);
  249. expect(model.value.text).toBe(cell.source);
  250. });
  251. it('should connect the outputs changes to content change signal', () => {
  252. const data = {
  253. output_type: 'display_data',
  254. data: { 'text/plain': 'foo' },
  255. metadata: {}
  256. } as nbformat.IDisplayData;
  257. const model = new CodeCellModel({});
  258. let called = false;
  259. model.contentChanged.connect(() => {
  260. called = true;
  261. });
  262. expect(called).toBe(false);
  263. model.outputs.add(data);
  264. expect(called).toBe(true);
  265. });
  266. it('should sync collapsed and jupyter.outputs_hidden metadata on construction', () => {
  267. let model: CodeCellModel;
  268. let jupyter: JSONObject | undefined;
  269. // Setting `collapsed` works
  270. model = new CodeCellModel({
  271. cell: { cell_type: 'code', source: '', metadata: { collapsed: true } }
  272. });
  273. expect(model.metadata.get('collapsed')).toBe(true);
  274. jupyter = model.metadata.get('jupyter') as JSONObject;
  275. expect(jupyter.outputs_hidden).toBe(true);
  276. // Setting `jupyter.outputs_hidden` works
  277. model = new CodeCellModel({
  278. cell: {
  279. cell_type: 'code',
  280. source: '',
  281. metadata: { jupyter: { outputs_hidden: true } }
  282. }
  283. });
  284. expect(model.metadata.get('collapsed')).toBe(true);
  285. jupyter = model.metadata.get('jupyter') as JSONObject;
  286. expect(jupyter.outputs_hidden).toBe(true);
  287. // `collapsed` takes precedence
  288. model = new CodeCellModel({
  289. cell: {
  290. cell_type: 'code',
  291. source: '',
  292. metadata: { collapsed: false, jupyter: { outputs_hidden: true } }
  293. }
  294. });
  295. expect(model.metadata.get('collapsed')).toBe(false);
  296. jupyter = model.metadata.get('jupyter') as JSONObject;
  297. expect(jupyter.outputs_hidden).toBe(false);
  298. });
  299. });
  300. describe('#type', () => {
  301. it('should be set with type "code"', () => {
  302. const model = new CodeCellModel({});
  303. expect(model.type).toBe('code');
  304. });
  305. });
  306. describe('#executionCount', () => {
  307. it('should show the execution count of the cell', () => {
  308. const cell: nbformat.ICodeCell = {
  309. cell_type: 'code',
  310. execution_count: 1,
  311. outputs: [],
  312. source: 'foo',
  313. metadata: { trusted: false }
  314. };
  315. const model = new CodeCellModel({ cell });
  316. expect(model.executionCount).toBe(1);
  317. });
  318. it('should be settable', () => {
  319. const model = new CodeCellModel({});
  320. expect(model.executionCount).toBeNull();
  321. model.executionCount = 1;
  322. expect(model.executionCount).toBe(1);
  323. });
  324. it('should emit a state change signal when set', () => {
  325. const model = new CodeCellModel({});
  326. let called = false;
  327. model.stateChanged.connect(() => {
  328. called = true;
  329. });
  330. expect(model.executionCount).toBeNull();
  331. expect(called).toBe(false);
  332. model.executionCount = 1;
  333. expect(model.executionCount).toBe(1);
  334. expect(called).toBe(true);
  335. });
  336. it('should not signal when state has not changed', () => {
  337. const model = new CodeCellModel({});
  338. let called = 0;
  339. model.stateChanged.connect(() => {
  340. called++;
  341. });
  342. expect(model.executionCount).toBeNull();
  343. expect(called).toBe(0);
  344. model.executionCount = 1;
  345. expect(model.executionCount).toBe(1);
  346. model.executionCount = 1;
  347. expect(called).toBe(1);
  348. });
  349. });
  350. describe('#outputs', () => {
  351. it('should be an output area model', () => {
  352. const model = new CodeCellModel({});
  353. expect(model.outputs).toBeInstanceOf(OutputAreaModel);
  354. });
  355. });
  356. describe('#dispose()', () => {
  357. it('should dispose of the resources held by the model', () => {
  358. const model = new CodeCellModel({});
  359. expect(model.outputs).toBeInstanceOf(OutputAreaModel);
  360. model.dispose();
  361. expect(model.isDisposed).toBe(true);
  362. expect(model.outputs).toBeNull();
  363. });
  364. it('should be safe to call multiple times', () => {
  365. const model = new CodeCellModel({});
  366. model.dispose();
  367. model.dispose();
  368. expect(model.isDisposed).toBe(true);
  369. });
  370. });
  371. describe('#toJSON()', () => {
  372. it('should return a code cell encapsulation of the model value', () => {
  373. const cell: nbformat.ICodeCell = {
  374. cell_type: 'code',
  375. execution_count: 1,
  376. outputs: [
  377. {
  378. output_type: 'display_data',
  379. data: {
  380. 'text/plain': 'foo',
  381. 'application/json': { bar: 1 }
  382. },
  383. metadata: {}
  384. } as nbformat.IDisplayData
  385. ],
  386. source: 'foo',
  387. metadata: { trusted: false }
  388. };
  389. const model = new CodeCellModel({ cell });
  390. const serialized = model.toJSON();
  391. expect(serialized).not.toBe(cell);
  392. expect(serialized).toEqual(cell);
  393. const output = serialized.outputs[0] as any;
  394. expect(output.data['application/json']['bar']).toBe(1);
  395. });
  396. });
  397. describe('.metadata', () => {
  398. it('should sync collapsed and jupyter.outputs_hidden metadata when changed', () => {
  399. const metadata = new CodeCellModel({}).metadata;
  400. expect(metadata.get('collapsed')).toBeUndefined();
  401. expect(metadata.get('jupyter')).toBeUndefined();
  402. // Setting collapsed sets jupyter.outputs_hidden
  403. metadata.set('collapsed', true);
  404. expect(metadata.get('collapsed')).toBe(true);
  405. expect(metadata.get('jupyter')).toEqual({
  406. outputs_hidden: true
  407. });
  408. metadata.set('collapsed', false);
  409. expect(metadata.get('collapsed')).toBe(false);
  410. expect(metadata.get('jupyter')).toEqual({
  411. outputs_hidden: false
  412. });
  413. metadata.delete('collapsed');
  414. expect(metadata.get('collapsed')).toBeUndefined();
  415. expect(metadata.get('jupyter')).toBeUndefined();
  416. // Setting jupyter.outputs_hidden sets collapsed
  417. metadata.set('jupyter', { outputs_hidden: true });
  418. expect(metadata.get('collapsed')).toBe(true);
  419. expect(metadata.get('jupyter')).toEqual({
  420. outputs_hidden: true
  421. });
  422. metadata.set('jupyter', { outputs_hidden: false });
  423. expect(metadata.get('collapsed')).toBe(false);
  424. expect(metadata.get('jupyter')).toEqual({
  425. outputs_hidden: false
  426. });
  427. metadata.delete('jupyter');
  428. expect(metadata.get('collapsed')).toBeUndefined();
  429. expect(metadata.get('jupyter')).toBeUndefined();
  430. // Deleting jupyter.outputs_hidden preserves other jupyter fields
  431. metadata.set('jupyter', { outputs_hidden: true, other: true });
  432. expect(metadata.get('collapsed')).toBe(true);
  433. expect(metadata.get('jupyter')).toEqual({
  434. outputs_hidden: true,
  435. other: true
  436. });
  437. metadata.set('jupyter', { other: true });
  438. expect(metadata.get('collapsed')).toBeUndefined();
  439. expect(metadata.get('jupyter')).toEqual({
  440. other: true
  441. });
  442. // Deleting collapsed preserves other jupyter fields
  443. metadata.set('jupyter', { outputs_hidden: true, other: true });
  444. expect(metadata.get('collapsed')).toBe(true);
  445. expect(metadata.get('jupyter')).toEqual({
  446. outputs_hidden: true,
  447. other: true
  448. });
  449. metadata.delete('collapsed');
  450. expect(metadata.get('collapsed')).toBeUndefined();
  451. expect(metadata.get('jupyter')).toEqual({
  452. other: true
  453. });
  454. });
  455. });
  456. describe('.ContentFactory', () => {
  457. describe('#constructor()', () => {
  458. it('should create a new output area factory', () => {
  459. const factory = new CodeCellModel.ContentFactory();
  460. expect(factory).toBeInstanceOf(CodeCellModel.ContentFactory);
  461. });
  462. });
  463. describe('#createOutputArea()', () => {
  464. it('should create an output area model', () => {
  465. const factory = new CodeCellModel.ContentFactory();
  466. expect(factory.createOutputArea({ trusted: true })).toBeInstanceOf(
  467. OutputAreaModel
  468. );
  469. });
  470. });
  471. });
  472. describe('.defaultContentFactory', () => {
  473. it('should be an ContentFactory', () => {
  474. expect(CodeCellModel.defaultContentFactory).toBeInstanceOf(
  475. CodeCellModel.ContentFactory
  476. );
  477. });
  478. });
  479. });
  480. });