default.spec.ts 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623
  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 { UUID } from '@lumino/coreutils';
  6. import { Widget } from '@lumino/widgets';
  7. import {
  8. ABCWidgetFactory,
  9. Base64ModelFactory,
  10. DocumentModel,
  11. DocumentRegistry,
  12. DocumentWidget,
  13. IDocumentWidget,
  14. TextModelFactory,
  15. Context
  16. } from '@jupyterlab/docregistry';
  17. import { ServiceManager } from '@jupyterlab/services';
  18. import { sleep } from '@jupyterlab/testutils';
  19. import * as Mock from '@jupyterlab/testutils/lib/mock';
  20. class WidgetFactory extends ABCWidgetFactory<IDocumentWidget> {
  21. protected createNewWidget(
  22. context: DocumentRegistry.Context
  23. ): IDocumentWidget {
  24. const content = new Widget();
  25. const widget = new DocumentWidget({ content, context });
  26. widget.addClass('WidgetFactory');
  27. return widget;
  28. }
  29. }
  30. function createFactory(): WidgetFactory {
  31. return new WidgetFactory({
  32. name: 'test',
  33. fileTypes: ['text']
  34. });
  35. }
  36. describe('docregistry/default', () => {
  37. describe('ABCWidgetFactory', () => {
  38. describe('#fileTypes', () => {
  39. it('should be the value passed in', () => {
  40. const factory = new WidgetFactory({
  41. name: 'test',
  42. fileTypes: ['text']
  43. });
  44. expect(factory.fileTypes).toEqual(['text']);
  45. });
  46. });
  47. describe('#name', () => {
  48. it('should be the value passed in', () => {
  49. const factory = new WidgetFactory({
  50. name: 'test',
  51. fileTypes: ['text']
  52. });
  53. expect(factory.name).toBe('test');
  54. });
  55. });
  56. describe('#defaultFor', () => {
  57. it('should default to an empty array', () => {
  58. const factory = new WidgetFactory({
  59. name: 'test',
  60. fileTypes: ['text']
  61. });
  62. expect(factory.defaultFor).toEqual([]);
  63. });
  64. it('should be the value passed in', () => {
  65. const factory = new WidgetFactory({
  66. name: 'test',
  67. fileTypes: ['text'],
  68. defaultFor: ['text']
  69. });
  70. expect(factory.defaultFor).toEqual(['text']);
  71. });
  72. });
  73. describe('#defaultRendered', () => {
  74. it('should default to an empty array', () => {
  75. const factory = new WidgetFactory({
  76. name: 'test',
  77. fileTypes: ['text']
  78. });
  79. expect(factory.defaultRendered).toEqual([]);
  80. });
  81. it('should be the value passed in', () => {
  82. const factory = new WidgetFactory({
  83. name: 'test',
  84. fileTypes: ['text'],
  85. defaultRendered: ['text']
  86. });
  87. expect(factory.defaultRendered).toEqual(['text']);
  88. });
  89. });
  90. describe('#readOnly', () => {
  91. it('should default to false', () => {
  92. const factory = new WidgetFactory({
  93. name: 'test',
  94. fileTypes: ['text']
  95. });
  96. expect(factory.readOnly).toBe(false);
  97. });
  98. it('should be the value passed in', () => {
  99. const factory = new WidgetFactory({
  100. name: 'test',
  101. fileTypes: ['text'],
  102. readOnly: true
  103. });
  104. expect(factory.readOnly).toBe(true);
  105. });
  106. });
  107. describe('#modelName', () => {
  108. it('should default to `text`', () => {
  109. const factory = new WidgetFactory({
  110. name: 'test',
  111. fileTypes: ['text']
  112. });
  113. expect(factory.modelName).toBe('text');
  114. });
  115. it('should be the value passed in', () => {
  116. const factory = new WidgetFactory({
  117. name: 'test',
  118. fileTypes: ['text'],
  119. modelName: 'notebook'
  120. });
  121. expect(factory.modelName).toBe('notebook');
  122. });
  123. });
  124. describe('#preferKernel', () => {
  125. it('should default to false', () => {
  126. const factory = new WidgetFactory({
  127. name: 'test',
  128. fileTypes: ['text']
  129. });
  130. expect(factory.preferKernel).toBe(false);
  131. });
  132. it('should be the value passed in', () => {
  133. const factory = new WidgetFactory({
  134. name: 'test',
  135. fileTypes: ['text'],
  136. preferKernel: true
  137. });
  138. expect(factory.preferKernel).toBe(true);
  139. });
  140. });
  141. describe('#canStartKernel', () => {
  142. it('should default to false', () => {
  143. const factory = new WidgetFactory({
  144. name: 'test',
  145. fileTypes: ['text']
  146. });
  147. expect(factory.canStartKernel).toBe(false);
  148. });
  149. it('should be the value passed in', () => {
  150. const factory = new WidgetFactory({
  151. name: 'test',
  152. fileTypes: ['text'],
  153. canStartKernel: true
  154. });
  155. expect(factory.canStartKernel).toBe(true);
  156. });
  157. it('should have toolbar items', async () => {
  158. const factory = new WidgetFactory({
  159. name: 'test',
  160. fileTypes: ['text'],
  161. toolbarFactory: () => [
  162. {
  163. name: 'foo',
  164. widget: new Widget()
  165. },
  166. {
  167. name: 'bar',
  168. widget: new Widget()
  169. }
  170. ]
  171. });
  172. const context = await Mock.createFileContext();
  173. const widget = factory.createNew(context);
  174. const widget2 = factory.createNew(context);
  175. expect(toArray(widget.toolbar.names())).toEqual(['foo', 'bar']);
  176. expect(toArray(widget2.toolbar.names())).toEqual(['foo', 'bar']);
  177. expect(toArray(widget.toolbar.children()).length).toBe(2);
  178. expect(toArray(widget2.toolbar.children()).length).toBe(2);
  179. });
  180. });
  181. describe('#isDisposed', () => {
  182. it('should get whether the factory has been disposed', () => {
  183. const factory = createFactory();
  184. expect(factory.isDisposed).toBe(false);
  185. factory.dispose();
  186. expect(factory.isDisposed).toBe(true);
  187. });
  188. });
  189. describe('#dispose()', () => {
  190. it('should dispose of the resources held by the factory', () => {
  191. const factory = createFactory();
  192. factory.dispose();
  193. expect(factory.isDisposed).toBe(true);
  194. });
  195. it('should be safe to call multiple times', () => {
  196. const factory = createFactory();
  197. factory.dispose();
  198. factory.dispose();
  199. expect(factory.isDisposed).toBe(true);
  200. });
  201. });
  202. describe('#createNew()', () => {
  203. it('should create a new widget given a document model and a context', async () => {
  204. const factory = createFactory();
  205. const context = await Mock.createFileContext();
  206. const widget = factory.createNew(context);
  207. expect(widget).toBeInstanceOf(Widget);
  208. });
  209. it('should take an optional source widget for cloning', async () => {
  210. const factory = createFactory();
  211. const context = await Mock.createFileContext();
  212. const widget = factory.createNew(context);
  213. const clonedWidget: IDocumentWidget = factory.createNew(
  214. context,
  215. widget
  216. );
  217. expect(clonedWidget).not.toBe(widget);
  218. expect(clonedWidget.hasClass('WidgetFactory')).toBe(true);
  219. expect(clonedWidget.context).toBe(widget.context);
  220. });
  221. });
  222. });
  223. describe('Base64ModelFactory', () => {
  224. describe('#name', () => {
  225. it('should get the name of the model type', () => {
  226. const factory = new Base64ModelFactory();
  227. expect(factory.name).toBe('base64');
  228. });
  229. });
  230. describe('#contentType', () => {
  231. it('should get the file type', () => {
  232. const factory = new Base64ModelFactory();
  233. expect(factory.contentType).toBe('file');
  234. });
  235. });
  236. describe('#fileFormat', () => {
  237. it('should get the file format', () => {
  238. const factory = new Base64ModelFactory();
  239. expect(factory.fileFormat).toBe('base64');
  240. });
  241. });
  242. });
  243. describe('DocumentModel', () => {
  244. describe('#constructor()', () => {
  245. it('should create a new document model', () => {
  246. const model = new DocumentModel();
  247. expect(model).toBeInstanceOf(DocumentModel);
  248. });
  249. it('should accept an optional language preference', () => {
  250. const model = new DocumentModel('foo');
  251. expect(model.defaultKernelLanguage).toBe('foo');
  252. });
  253. });
  254. describe('#isDisposed', () => {
  255. it('should get whether the model has been disposed', () => {
  256. const model = new DocumentModel();
  257. expect(model.isDisposed).toBe(false);
  258. model.dispose();
  259. expect(model.isDisposed).toBe(true);
  260. });
  261. });
  262. describe('#contentChanged', () => {
  263. it('should be emitted when the content of the model changes', () => {
  264. const model = new DocumentModel();
  265. let called = false;
  266. model.contentChanged.connect((sender, args) => {
  267. expect(sender).toBe(model);
  268. expect(args).toBeUndefined();
  269. called = true;
  270. });
  271. model.fromString('foo');
  272. expect(called).toBe(true);
  273. });
  274. it('should not be emitted if the content does not change', () => {
  275. const model = new DocumentModel();
  276. let called = false;
  277. model.contentChanged.connect(() => {
  278. called = true;
  279. });
  280. model.fromString('');
  281. expect(called).toBe(false);
  282. });
  283. });
  284. describe('#stateChanged', () => {
  285. it('should be emitted when the state of the model changes', () => {
  286. const model = new DocumentModel();
  287. let called = false;
  288. model.stateChanged.connect((sender, args) => {
  289. expect(sender).toBe(model);
  290. expect(args.name).toBe('readOnly');
  291. expect(args.oldValue).toBe(false);
  292. expect(args.newValue).toBe(true);
  293. called = true;
  294. });
  295. model.readOnly = true;
  296. expect(called).toBe(true);
  297. });
  298. it('should not be emitted if the state does not change', () => {
  299. const model = new DocumentModel();
  300. let called = false;
  301. model.stateChanged.connect(() => {
  302. called = true;
  303. });
  304. model.dirty = false;
  305. expect(called).toBe(false);
  306. });
  307. });
  308. describe('#dirty', () => {
  309. it('should get the dirty state of the document', () => {
  310. const model = new DocumentModel();
  311. expect(model.dirty).toBe(false);
  312. });
  313. it('should emit `stateChanged` when changed', () => {
  314. const model = new DocumentModel();
  315. let called = false;
  316. model.stateChanged.connect((sender, args) => {
  317. expect(sender).toBe(model);
  318. expect(args.name).toBe('dirty');
  319. expect(args.oldValue).toBe(false);
  320. expect(args.newValue).toBe(true);
  321. called = true;
  322. });
  323. model.dirty = true;
  324. expect(called).toBe(true);
  325. });
  326. it('should not emit `stateChanged` when not changed', () => {
  327. const model = new DocumentModel();
  328. let called = false;
  329. model.stateChanged.connect(() => {
  330. called = true;
  331. });
  332. model.dirty = false;
  333. expect(called).toBe(false);
  334. });
  335. });
  336. describe('#readOnly', () => {
  337. it('should get the read only state of the document', () => {
  338. const model = new DocumentModel();
  339. expect(model.readOnly).toBe(false);
  340. });
  341. it('should emit `stateChanged` when changed', () => {
  342. const model = new DocumentModel();
  343. let called = false;
  344. model.stateChanged.connect((sender, args) => {
  345. expect(sender).toBe(model);
  346. expect(args.name).toBe('readOnly');
  347. expect(args.oldValue).toBe(false);
  348. expect(args.newValue).toBe(true);
  349. called = true;
  350. });
  351. model.readOnly = true;
  352. expect(called).toBe(true);
  353. });
  354. it('should not emit `stateChanged` when not changed', () => {
  355. const model = new DocumentModel();
  356. let called = false;
  357. model.stateChanged.connect(() => {
  358. called = true;
  359. });
  360. model.readOnly = false;
  361. expect(called).toBe(false);
  362. });
  363. });
  364. describe('#defaultKernelName', () => {
  365. it('should get the default kernel name of the document', () => {
  366. const model = new DocumentModel();
  367. expect(model.defaultKernelName).toBe('');
  368. });
  369. });
  370. describe('defaultKernelLanguage', () => {
  371. it('should get the default kernel language of the document', () => {
  372. const model = new DocumentModel();
  373. expect(model.defaultKernelLanguage).toBe('');
  374. });
  375. it('should be set by the constructor arg', () => {
  376. const model = new DocumentModel('foo');
  377. expect(model.defaultKernelLanguage).toBe('foo');
  378. });
  379. });
  380. describe('#dispose()', () => {
  381. it('should dispose of the resources held by the document manager', () => {
  382. const model = new DocumentModel();
  383. model.dispose();
  384. expect(model.isDisposed).toBe(true);
  385. });
  386. it('should be safe to call more than once', () => {
  387. const model = new DocumentModel();
  388. model.dispose();
  389. model.dispose();
  390. expect(model.isDisposed).toBe(true);
  391. });
  392. });
  393. describe('#toString()', () => {
  394. it('should serialize the model to a string', () => {
  395. const model = new DocumentModel();
  396. expect(model.toString()).toBe('');
  397. });
  398. });
  399. describe('#fromString()', () => {
  400. it('should deserialize the model from a string', () => {
  401. const model = new DocumentModel();
  402. model.fromString('foo');
  403. expect(model.toString()).toBe('foo');
  404. });
  405. });
  406. describe('#toJSON()', () => {
  407. it('should serialize the model to JSON', () => {
  408. const model = new DocumentModel();
  409. const data = { foo: 1 };
  410. model.fromJSON(data);
  411. expect(model.toJSON()).toEqual(data);
  412. });
  413. });
  414. describe('#fromJSON()', () => {
  415. it('should deserialize the model from JSON', () => {
  416. const model = new DocumentModel();
  417. const data: null = null;
  418. model.fromJSON(data);
  419. expect(model.toString()).toBe('null');
  420. });
  421. });
  422. });
  423. describe('TextModelFactory', () => {
  424. describe('#name', () => {
  425. it('should get the name of the model type', () => {
  426. const factory = new TextModelFactory();
  427. expect(factory.name).toBe('text');
  428. });
  429. });
  430. describe('#contentType', () => {
  431. it('should get the file type', () => {
  432. const factory = new TextModelFactory();
  433. expect(factory.contentType).toBe('file');
  434. });
  435. });
  436. describe('#fileFormat', () => {
  437. it('should get the file format', () => {
  438. const factory = new TextModelFactory();
  439. expect(factory.fileFormat).toBe('text');
  440. });
  441. });
  442. describe('#isDisposed', () => {
  443. it('should get whether the factory is disposed', () => {
  444. const factory = new TextModelFactory();
  445. expect(factory.isDisposed).toBe(false);
  446. factory.dispose();
  447. expect(factory.isDisposed).toBe(true);
  448. });
  449. });
  450. describe('#dispose()', () => {
  451. it('should dispose of the resources held by the factory', () => {
  452. const factory = new TextModelFactory();
  453. factory.dispose();
  454. expect(factory.isDisposed).toBe(true);
  455. });
  456. it('should be safe to call multiple times', () => {
  457. const factory = new TextModelFactory();
  458. factory.dispose();
  459. factory.dispose();
  460. expect(factory.isDisposed).toBe(true);
  461. });
  462. });
  463. describe('#createNew()', () => {
  464. it('should create a new model', () => {
  465. const factory = new TextModelFactory();
  466. const model = factory.createNew();
  467. expect(model).toBeInstanceOf(DocumentModel);
  468. });
  469. it('should accept a language preference', () => {
  470. const factory = new TextModelFactory();
  471. const model = factory.createNew('foo');
  472. expect(model.defaultKernelLanguage).toBe('foo');
  473. });
  474. });
  475. describe('#preferredLanguage()', () => {
  476. it('should get the preferred kernel language given an extension', () => {
  477. const factory = new TextModelFactory();
  478. expect(factory.preferredLanguage('.py')).toBe('python');
  479. expect(factory.preferredLanguage('.jl')).toBe('julia');
  480. });
  481. });
  482. });
  483. describe('DocumentWidget', () => {
  484. let manager: ServiceManager.IManager;
  485. let context: Context<DocumentRegistry.IModel>;
  486. let content: Widget;
  487. let widget: DocumentWidget;
  488. const setup = async () => {
  489. context = await Mock.createFileContext(false, manager);
  490. content = new Widget();
  491. widget = new DocumentWidget({ context, content });
  492. };
  493. beforeAll(async () => {
  494. manager = new Mock.ServiceManagerMock();
  495. await manager.ready;
  496. });
  497. describe('#constructor', () => {
  498. beforeEach(setup);
  499. it('should set the title for the path', () => {
  500. expect(widget.title.label).toBe(context.localPath);
  501. });
  502. it('should update the title when the path changes', async () => {
  503. const path = UUID.uuid4() + '.jl';
  504. await manager.contents.rename(context.path, path);
  505. expect(widget.title.label).toBe(path);
  506. });
  507. it('should add the dirty class when the model is dirty', async () => {
  508. context.model.fromString('bar');
  509. expect(widget.title.className).toContain('jp-mod-dirty');
  510. });
  511. it('should store the context', () => {
  512. expect(widget.context).toBe(context);
  513. });
  514. });
  515. describe('#revealed', () => {
  516. beforeEach(setup);
  517. it('should resolve after the reveal and context ready promises', async () => {
  518. const thisContext = new Context({
  519. manager,
  520. factory: new TextModelFactory(),
  521. path: UUID.uuid4()
  522. });
  523. const x = Object.create(null);
  524. const reveal = sleep(300, x);
  525. const contextReady = Promise.all([thisContext.ready, x]);
  526. const widget = new DocumentWidget({
  527. context: thisContext,
  528. content,
  529. reveal
  530. });
  531. expect(widget.isRevealed).toBe(false);
  532. // Our promise should resolve before the widget reveal promise.
  533. expect(await Promise.race([widget.revealed, reveal])).toBe(x);
  534. // The context ready promise should also resolve first.
  535. void thisContext.initialize(true);
  536. expect(await Promise.race([widget.revealed, contextReady])).toEqual([
  537. undefined,
  538. x
  539. ]);
  540. // The widget.revealed promise should finally resolve.
  541. expect(await widget.revealed).toBeUndefined();
  542. thisContext.dispose();
  543. });
  544. });
  545. });
  546. });