default.spec.ts 18 KB

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