manager.spec.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import 'jest';
  4. import { ServiceManager } from '@jupyterlab/services';
  5. import { Widget } from '@lumino/widgets';
  6. import { DocumentManager } from '../src';
  7. import {
  8. DocumentRegistry,
  9. TextModelFactory,
  10. ABCWidgetFactory,
  11. DocumentWidget,
  12. IDocumentWidget
  13. } from '@jupyterlab/docregistry';
  14. import { dismissDialog } from '@jupyterlab/testutils';
  15. import * as Mock from '@jupyterlab/testutils/lib/mock';
  16. class WidgetFactory extends ABCWidgetFactory<IDocumentWidget> {
  17. protected createNewWidget(
  18. context: DocumentRegistry.Context
  19. ): IDocumentWidget {
  20. const content = new Widget();
  21. const widget = new DocumentWidget({ content, context });
  22. widget.addClass('WidgetFactory');
  23. return widget;
  24. }
  25. }
  26. /**
  27. * A test documentWidget that maintains some state in
  28. * count
  29. */
  30. class CloneTestWidget extends DocumentWidget {
  31. constructor(args: any) {
  32. super(args);
  33. this.counter = args.count;
  34. }
  35. counter: number = 0;
  36. }
  37. /**
  38. * A widget factory for CloneTestWidget widgets
  39. */
  40. class WidgetFactoryWithSharedState extends ABCWidgetFactory<CloneTestWidget> {
  41. protected createNewWidget(
  42. context: DocumentRegistry.Context,
  43. source: CloneTestWidget
  44. ): CloneTestWidget {
  45. return new CloneTestWidget({
  46. context,
  47. content: new Widget(),
  48. count: source ? source.counter + 1 : 0
  49. });
  50. }
  51. }
  52. describe('@jupyterlab/docmanager', () => {
  53. let manager: DocumentManager;
  54. let services: ServiceManager.IManager;
  55. let context: DocumentRegistry.Context;
  56. let widget: Widget | undefined;
  57. const textModelFactory = new TextModelFactory();
  58. const widgetFactory = new WidgetFactory({
  59. name: 'test',
  60. fileTypes: ['text'],
  61. canStartKernel: true,
  62. preferKernel: true
  63. });
  64. const widgetFactoryShared = new WidgetFactoryWithSharedState({
  65. name: 'CloneTestWidget',
  66. fileTypes: []
  67. });
  68. beforeAll(() => {
  69. services = new Mock.ServiceManagerMock();
  70. });
  71. beforeEach(() => {
  72. const registry = new DocumentRegistry({ textModelFactory });
  73. registry.addWidgetFactory(widgetFactory);
  74. registry.addWidgetFactory(widgetFactoryShared);
  75. DocumentRegistry.defaultFileTypes.forEach(ft => {
  76. registry.addFileType(ft);
  77. });
  78. manager = new DocumentManager({
  79. registry,
  80. manager: services,
  81. opener: {
  82. open: (widget: Widget) => {
  83. // no-op
  84. }
  85. }
  86. });
  87. });
  88. afterEach(() => {
  89. manager.dispose();
  90. });
  91. describe('DocumentWidgetManager', () => {
  92. describe('#constructor()', () => {
  93. it('should create a new document manager', () => {
  94. expect(manager).toBeInstanceOf(DocumentManager);
  95. });
  96. });
  97. describe('#isDisposed', () => {
  98. it('should test whether the manager is disposed', () => {
  99. expect(manager.isDisposed).toBe(false);
  100. manager.dispose();
  101. expect(manager.isDisposed).toBe(true);
  102. });
  103. });
  104. describe('#dispose()', () => {
  105. it('should dispose of the resources used by the manager', () => {
  106. expect(manager.isDisposed).toBe(false);
  107. manager.dispose();
  108. expect(manager.isDisposed).toBe(true);
  109. manager.dispose();
  110. expect(manager.isDisposed).toBe(true);
  111. });
  112. });
  113. describe('#services', () => {
  114. it('should get the service manager for the manager', async () => {
  115. await manager.services.ready;
  116. });
  117. });
  118. describe('#registry', () => {
  119. it('should get the registry used by the manager', () => {
  120. expect(manager.registry).toBeInstanceOf(DocumentRegistry);
  121. });
  122. });
  123. describe('#open()', () => {
  124. it('should open a file and return the widget used to view it', async () => {
  125. const model = await services.contents.newUntitled({
  126. type: 'file',
  127. ext: '.txt'
  128. });
  129. widget = manager.open(model.path)!;
  130. expect(widget.hasClass('WidgetFactory')).toBe(true);
  131. await dismissDialog();
  132. });
  133. it('should start a kernel if one is given', async () => {
  134. const model = await services.contents.newUntitled({
  135. type: 'file',
  136. ext: '.txt'
  137. });
  138. const session = await services.sessions.startNew({
  139. name: '',
  140. path: model.path,
  141. type: 'test'
  142. });
  143. const id = session.kernel!.id;
  144. widget = manager.open(session.path, 'default', { id })!;
  145. context = manager.contextForWidget(widget)!;
  146. await context.ready;
  147. await context.sessionContext.ready;
  148. expect(context.sessionContext.session?.kernel).toBeTruthy();
  149. await context.sessionContext.shutdown();
  150. });
  151. it('should not auto-start a kernel if there is none given', async () => {
  152. const model = await services.contents.newUntitled({
  153. type: 'file',
  154. ext: '.txt'
  155. });
  156. widget = manager.open(model.path, 'default')!;
  157. context = manager.contextForWidget(widget)!;
  158. await dismissDialog();
  159. expect(context.sessionContext.session?.kernel).toBeFalsy();
  160. });
  161. it('should return undefined if the factory is not found', async () => {
  162. const model = await services.contents.newUntitled({
  163. type: 'file',
  164. ext: '.txt'
  165. });
  166. widget = manager.open(model.path, 'foo');
  167. expect(widget).toBeUndefined();
  168. });
  169. it('should return undefined if the factory has no model factory', async () => {
  170. const widgetFactory2 = new WidgetFactory({
  171. name: 'test',
  172. modelName: 'foo',
  173. fileTypes: ['text']
  174. });
  175. manager.registry.addWidgetFactory(widgetFactory2);
  176. const model = await services.contents.newUntitled({
  177. type: 'file',
  178. ext: '.txt'
  179. });
  180. widget = manager.open(model.path, 'foo');
  181. expect(widget).toBeUndefined();
  182. });
  183. });
  184. describe('#createNew()', () => {
  185. it('should open a file and return the widget used to view it', async () => {
  186. const model = await services.contents.newUntitled({
  187. type: 'file',
  188. ext: '.txt'
  189. });
  190. widget = manager.createNew(model.path)!;
  191. expect(widget.hasClass('WidgetFactory')).toBe(true);
  192. await dismissDialog();
  193. });
  194. it('should start a kernel if one is given', async () => {
  195. const model = await services.contents.newUntitled({
  196. type: 'file',
  197. ext: '.txt'
  198. });
  199. const session = await services.sessions.startNew({
  200. name: '',
  201. path: model.path,
  202. type: 'test'
  203. });
  204. const id = session.kernel!.id;
  205. widget = manager.createNew(session.path, 'default', { id })!;
  206. context = manager.contextForWidget(widget)!;
  207. await context.ready;
  208. await context.sessionContext.ready;
  209. expect(context.sessionContext.session!.kernel!.id).toBe(id);
  210. await context.sessionContext.shutdown();
  211. });
  212. it('should not start a kernel if not given', async () => {
  213. const model = await services.contents.newUntitled({
  214. type: 'file',
  215. ext: '.txt'
  216. });
  217. widget = manager.createNew(model.path, 'default')!;
  218. context = manager.contextForWidget(widget)!;
  219. await dismissDialog();
  220. expect(context.sessionContext.session?.kernel).toBeFalsy();
  221. });
  222. it('should return undefined if the factory is not found', async () => {
  223. const model = await services.contents.newUntitled({
  224. type: 'file',
  225. ext: '.txt'
  226. });
  227. widget = manager.createNew(model.path, 'foo');
  228. expect(widget).toBeUndefined();
  229. });
  230. it('should return undefined if the factory has no model factory', async () => {
  231. const widgetFactory2 = new WidgetFactory({
  232. name: 'test',
  233. modelName: 'foo',
  234. fileTypes: ['text']
  235. });
  236. manager.registry.addWidgetFactory(widgetFactory2);
  237. const model = await services.contents.newUntitled({
  238. type: 'file',
  239. ext: '.txt'
  240. });
  241. widget = manager.createNew(model.path, 'foo');
  242. expect(widget).toBeUndefined();
  243. });
  244. });
  245. describe('#findWidget()', () => {
  246. it('should find a widget given a file and a widget name', async () => {
  247. const model = await services.contents.newUntitled({
  248. type: 'file',
  249. ext: '.txt'
  250. });
  251. widget = manager.createNew(model.path);
  252. expect(manager.findWidget(model.path, 'test')).toBe(widget);
  253. await dismissDialog();
  254. });
  255. it('should find a widget given a file', async () => {
  256. const model = await services.contents.newUntitled({
  257. type: 'file',
  258. ext: '.txt'
  259. });
  260. widget = manager.createNew(model.path);
  261. expect(manager.findWidget(model.path)).toBe(widget);
  262. await dismissDialog();
  263. });
  264. it('should fail to find a widget', () => {
  265. expect(manager.findWidget('foo')).toBeUndefined();
  266. });
  267. it('should fail to find a widget with non default factory and the default widget name', async () => {
  268. const widgetFactory2 = new WidgetFactory({
  269. name: 'test2',
  270. fileTypes: ['text']
  271. });
  272. manager.registry.addWidgetFactory(widgetFactory2);
  273. const model = await services.contents.newUntitled({
  274. type: 'file',
  275. ext: '.txt'
  276. });
  277. widget = manager.createNew(model.path, 'test2');
  278. expect(manager.findWidget(model.path)).toBeUndefined();
  279. });
  280. it('should find a widget with non default factory given a file and a null widget name', async () => {
  281. const widgetFactory2 = new WidgetFactory({
  282. name: 'test2',
  283. fileTypes: ['text']
  284. });
  285. manager.registry.addWidgetFactory(widgetFactory2);
  286. const model = await services.contents.newUntitled({
  287. type: 'file',
  288. ext: '.txt'
  289. });
  290. widget = manager.createNew(model.path, 'test2');
  291. expect(manager.findWidget(model.path, null)).toBe(widget);
  292. await dismissDialog();
  293. });
  294. });
  295. describe('#contextForWidget()', () => {
  296. it('should find the context for a widget', async () => {
  297. const model = await services.contents.newUntitled({
  298. type: 'file',
  299. ext: '.txt'
  300. });
  301. widget = manager.createNew(model.path)!;
  302. context = manager.contextForWidget(widget)!;
  303. expect(context.path).toBe(model.path);
  304. await dismissDialog();
  305. });
  306. it('should fail to find the context for the widget', () => {
  307. widget = new Widget();
  308. expect(manager.contextForWidget(widget)).toBeUndefined();
  309. });
  310. });
  311. describe('#cloneWidget()', () => {
  312. it('should clone the given widget', async () => {
  313. const model = await services.contents.newUntitled({
  314. type: 'file',
  315. ext: '.txt'
  316. });
  317. widget = manager.createNew(model.path)!;
  318. const clone = manager.cloneWidget(widget)!;
  319. expect(manager.contextForWidget(widget)).toBe(
  320. manager.contextForWidget(clone)
  321. );
  322. await dismissDialog();
  323. });
  324. it('should return undefined if the source widget is not managed', () => {
  325. widget = new Widget();
  326. expect(manager.cloneWidget(widget)).toBeUndefined();
  327. });
  328. it('should allow widget factories to have custom clone behavior', () => {
  329. widget = manager.createNew('foo', 'CloneTestWidget')!;
  330. const clonedWidget: CloneTestWidget = manager.cloneWidget(
  331. widget
  332. ) as CloneTestWidget;
  333. expect(clonedWidget.counter).toBe(1);
  334. const newWidget: CloneTestWidget = manager.createNew(
  335. 'bar',
  336. 'CloneTestWidget'
  337. ) as CloneTestWidget;
  338. expect(newWidget.counter).toBe(0);
  339. expect(
  340. (manager.cloneWidget(clonedWidget) as CloneTestWidget).counter
  341. ).toBe(2);
  342. });
  343. });
  344. describe('#closeFile()', () => {
  345. it('should close the widgets associated with a given path', async () => {
  346. let called = 0;
  347. let path = '';
  348. const model = await services.contents.newUntitled({
  349. type: 'file',
  350. ext: '.txt'
  351. });
  352. path = model.path;
  353. widget = manager.createNew(path)!;
  354. const clone = manager.cloneWidget(widget)!;
  355. widget.disposed.connect(() => {
  356. called++;
  357. });
  358. clone.disposed.connect(() => {
  359. called++;
  360. });
  361. await dismissDialog();
  362. await manager.closeFile(path);
  363. expect(called).toBe(2);
  364. });
  365. it('should be a no-op if there are no open files on that path', () => {
  366. return manager.closeFile('foo');
  367. });
  368. });
  369. describe('#closeAll()', () => {
  370. it('should close all of the open documents', async () => {
  371. let called = 0;
  372. let path = '';
  373. const model = await services.contents.newUntitled({
  374. type: 'file',
  375. ext: '.txt'
  376. });
  377. path = model.path;
  378. const widget0 = manager.createNew(path)!;
  379. widget0.disposed.connect(() => {
  380. called++;
  381. });
  382. await dismissDialog();
  383. const widget1 = manager.createNew(path)!;
  384. widget1.disposed.connect(() => {
  385. called++;
  386. });
  387. await dismissDialog();
  388. await manager.closeAll();
  389. expect(called).toBe(2);
  390. });
  391. it('should be a no-op if there are no open documents', async () => {
  392. await manager.closeAll();
  393. });
  394. });
  395. });
  396. });