widgetmanager.spec.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import { ServiceManager } from '@jupyterlab/services';
  4. import { DocumentWidgetManager } from '../src';
  5. import {
  6. DocumentRegistry,
  7. TextModelFactory,
  8. ABCWidgetFactory,
  9. Context,
  10. DocumentWidget,
  11. IDocumentWidget
  12. } from '@jupyterlab/docregistry';
  13. import { PromiseDelegate, UUID } from '@lumino/coreutils';
  14. import { IMessageHandler, Message, MessageLoop } from '@lumino/messaging';
  15. import { Widget } from '@lumino/widgets';
  16. import { dangerDialog, dismissDialog } from '@jupyterlab/testutils';
  17. import * as Mock from '@jupyterlab/testutils/lib/mock';
  18. class WidgetFactory extends ABCWidgetFactory<IDocumentWidget> {
  19. protected createNewWidget(
  20. context: DocumentRegistry.Context
  21. ): IDocumentWidget {
  22. const content = new Widget();
  23. const widget = new DocumentWidget({ content, context });
  24. widget.addClass('WidgetFactory');
  25. return widget;
  26. }
  27. }
  28. class LoggingManager extends DocumentWidgetManager {
  29. methods: string[] = [];
  30. messageHook(handler: IMessageHandler, msg: Message): boolean {
  31. this.methods.push('messageHook');
  32. return super.messageHook(handler, msg);
  33. }
  34. setCaption(widget: Widget): Promise<void> {
  35. this.methods.push('setCaption');
  36. return super.setCaption(widget);
  37. }
  38. onClose(widget: Widget): Promise<boolean> {
  39. this.methods.push('onClose');
  40. return super.onClose(widget);
  41. }
  42. }
  43. describe('@jupyterlab/docmanager', () => {
  44. let manager: LoggingManager;
  45. let services: ServiceManager.IManager;
  46. const textModelFactory = new TextModelFactory();
  47. let context: Context<DocumentRegistry.IModel>;
  48. const widgetFactory = new WidgetFactory({
  49. name: 'test',
  50. fileTypes: ['text']
  51. });
  52. const readOnlyFactory = new WidgetFactory({
  53. name: 'readonly',
  54. fileTypes: ['text'],
  55. readOnly: true
  56. });
  57. beforeAll(() => {
  58. services = new Mock.ServiceManagerMock();
  59. });
  60. beforeEach(() => {
  61. const registry = new DocumentRegistry({ textModelFactory });
  62. registry.addWidgetFactory(widgetFactory);
  63. manager = new LoggingManager({ registry });
  64. context = new Context({
  65. manager: services,
  66. factory: textModelFactory,
  67. path: UUID.uuid4()
  68. });
  69. });
  70. afterEach(() => {
  71. manager.dispose();
  72. context.dispose();
  73. });
  74. describe('DocumentWidgetManager', () => {
  75. describe('#constructor()', () => {
  76. it('should create a new document widget manager', () => {
  77. expect(manager).toBeInstanceOf(DocumentWidgetManager);
  78. });
  79. });
  80. describe('#isDisposed', () => {
  81. it('should test whether the manager is disposed', () => {
  82. expect(manager.isDisposed).toBe(false);
  83. manager.dispose();
  84. expect(manager.isDisposed).toBe(true);
  85. });
  86. });
  87. describe('#dispose()', () => {
  88. it('should dispose of the resources used by the manager', () => {
  89. expect(manager.isDisposed).toBe(false);
  90. manager.dispose();
  91. expect(manager.isDisposed).toBe(true);
  92. manager.dispose();
  93. expect(manager.isDisposed).toBe(true);
  94. });
  95. });
  96. describe('#createWidget()', () => {
  97. it('should create a widget', () => {
  98. const widget = manager.createWidget(widgetFactory, context);
  99. expect(widget).toBeInstanceOf(Widget);
  100. });
  101. it('should emit the widgetCreated signal', () => {
  102. let called = false;
  103. widgetFactory.widgetCreated.connect(() => {
  104. called = true;
  105. });
  106. manager.createWidget(widgetFactory, context);
  107. expect(called).toBe(true);
  108. });
  109. });
  110. describe('#adoptWidget()', () => {
  111. it('should install a message hook', () => {
  112. const content = new Widget();
  113. const widget = new DocumentWidget({ content, context });
  114. manager.adoptWidget(context, widget);
  115. MessageLoop.sendMessage(widget, new Message('foo'));
  116. expect(manager.methods).toEqual(
  117. expect.arrayContaining(['messageHook'])
  118. );
  119. });
  120. it('should add the document class', () => {
  121. const content = new Widget();
  122. const widget = new DocumentWidget({ content, context });
  123. manager.adoptWidget(context, widget);
  124. expect(widget.hasClass('jp-Document')).toBe(true);
  125. });
  126. it('should be retrievable', () => {
  127. const content = new Widget();
  128. const widget = new DocumentWidget({ content, context });
  129. manager.adoptWidget(context, widget);
  130. expect(manager.contextForWidget(widget)).toBe(context);
  131. });
  132. });
  133. describe('#findWidget()', () => {
  134. it('should find a registered widget', () => {
  135. const widget = manager.createWidget(widgetFactory, context);
  136. expect(manager.findWidget(context, 'test')).toBe(widget);
  137. });
  138. it('should return undefined if not found', () => {
  139. expect(manager.findWidget(context, 'test')).toBeUndefined();
  140. });
  141. });
  142. describe('#contextForWidget()', () => {
  143. it('should return the context for a widget', () => {
  144. const widget = manager.createWidget(widgetFactory, context);
  145. expect(manager.contextForWidget(widget)).toBe(context);
  146. });
  147. it('should return undefined if not tracked', () => {
  148. expect(manager.contextForWidget(new Widget())).toBeUndefined();
  149. });
  150. });
  151. describe('#cloneWidget()', () => {
  152. it('should create a new widget with the same context using the same factory', () => {
  153. const widget = manager.createWidget(widgetFactory, context);
  154. const clone = manager.cloneWidget(widget)!;
  155. expect(clone.hasClass('WidgetFactory')).toBe(true);
  156. expect(clone.hasClass('jp-Document')).toBe(true);
  157. expect(manager.contextForWidget(clone)).toBe(context);
  158. });
  159. it('should return undefined if the source widget is not managed', () => {
  160. expect(manager.cloneWidget(new Widget())).toBeUndefined();
  161. });
  162. });
  163. describe('#closeWidgets()', () => {
  164. it('should close all of the widgets associated with a context', async () => {
  165. const widget = manager.createWidget(widgetFactory, context);
  166. const clone = manager.cloneWidget(widget)!;
  167. await manager.closeWidgets(context);
  168. expect(widget.isDisposed).toBe(true);
  169. expect(clone.isDisposed).toBe(true);
  170. });
  171. });
  172. describe('#messageHook()', () => {
  173. it('should be called for a message to a tracked widget', () => {
  174. const content = new Widget();
  175. const widget = new DocumentWidget({ content, context });
  176. manager.adoptWidget(context, widget);
  177. MessageLoop.sendMessage(widget, new Message('foo'));
  178. expect(manager.methods).toEqual(
  179. expect.arrayContaining(['messageHook'])
  180. );
  181. });
  182. it('should return false for close-request messages', () => {
  183. const widget = manager.createWidget(widgetFactory, context);
  184. const msg = new Message('close-request');
  185. expect(manager.messageHook(widget, msg)).toBe(false);
  186. });
  187. it('should return true for other messages', () => {
  188. const widget = manager.createWidget(widgetFactory, context);
  189. const msg = new Message('foo');
  190. expect(manager.messageHook(widget, msg)).toBe(true);
  191. });
  192. });
  193. describe('#setCaption()', () => {
  194. it('should set the title of the widget', async () => {
  195. await context.initialize(true);
  196. const widget = manager.createWidget(widgetFactory, context);
  197. const delegate = new PromiseDelegate();
  198. widget.title.changed.connect(async () => {
  199. expect(manager.methods).toEqual(
  200. expect.arrayContaining(['setCaption'])
  201. );
  202. expect(widget.title.caption).toContain('Last Checkpoint');
  203. await dismissDialog();
  204. delegate.resolve(undefined);
  205. });
  206. await delegate.promise;
  207. });
  208. });
  209. describe('#onClose()', () => {
  210. it('should be called when a widget is closed', async () => {
  211. const widget = manager.createWidget(widgetFactory, context);
  212. const delegate = new PromiseDelegate();
  213. widget.disposed.connect(async () => {
  214. expect(manager.methods).toEqual(expect.arrayContaining(['onClose']));
  215. await dismissDialog();
  216. delegate.resolve(undefined);
  217. });
  218. widget.close();
  219. await delegate.promise;
  220. });
  221. it('should prompt the user before closing', async () => {
  222. // Populate the model with content.
  223. context.model.fromString('foo');
  224. const widget = manager.createWidget(widgetFactory, context);
  225. const closed = manager.onClose(widget);
  226. await Promise.all([dangerDialog(), closed]);
  227. expect(widget.isDisposed).toBe(true);
  228. });
  229. it('should not prompt if the factory is readonly', async () => {
  230. const readonly = manager.createWidget(readOnlyFactory, context);
  231. await manager.onClose(readonly);
  232. expect(readonly.isDisposed).toBe(true);
  233. });
  234. it('should not prompt if the other widget is writable', async () => {
  235. // Populate the model with content.
  236. context.model.fromString('foo');
  237. const one = manager.createWidget(widgetFactory, context);
  238. const two = manager.createWidget(widgetFactory, context);
  239. await manager.onClose(one);
  240. expect(one.isDisposed).toBe(true);
  241. expect(two.isDisposed).toBe(false);
  242. two.dispose();
  243. });
  244. it('should prompt if the only other widget has a readonly factory', async () => {
  245. // Populate the model with content.
  246. context.model.fromString('foo');
  247. const writable = manager.createWidget(widgetFactory, context);
  248. const readonly = manager.createWidget(readOnlyFactory, context);
  249. const closed = manager.onClose(writable);
  250. await dangerDialog();
  251. await closed;
  252. expect(writable.isDisposed).toBe(true);
  253. expect(readonly.isDisposed).toBe(false);
  254. readonly.dispose();
  255. });
  256. it('should close the widget', async () => {
  257. context.model.fromString('foo');
  258. const widget = manager.createWidget(widgetFactory, context);
  259. const promise = manager.onClose(widget);
  260. await dismissDialog();
  261. await promise;
  262. expect(widget.isDisposed).toBe(false);
  263. });
  264. });
  265. });
  266. });