manager.spec.ts 13 KB

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