widgettracker.spec.ts 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447
  1. // Copyright (c) Jupyter Development Team.
  2. import 'jest';
  3. import { WidgetTracker } from '@jupyterlab/apputils';
  4. import { signalToPromise, testEmission } from '@jupyterlab/testutils';
  5. import { Panel, Widget } from '@lumino/widgets';
  6. import { simulate } from 'simulate-event';
  7. const namespace = 'widget-tracker-test';
  8. class TestTracker<T extends Widget> extends WidgetTracker<T> {
  9. methods: string[] = [];
  10. protected onCurrentChanged(widget: T): void {
  11. super.onCurrentChanged(widget);
  12. this.methods.push('onCurrentChanged');
  13. }
  14. }
  15. function createWidget(): Widget {
  16. const widget = new Widget({ node: document.createElement('button') });
  17. widget.node.style.minHeight = '20px';
  18. widget.node.style.minWidth = '20px';
  19. widget.node.tabIndex = -1;
  20. widget.node.textContent = 'Test Button';
  21. return widget;
  22. }
  23. function focus(widget: Widget): void {
  24. widget.node.focus();
  25. simulate(widget.node, 'focus');
  26. }
  27. describe('@jupyterlab/apputils', () => {
  28. describe('WidgetTracker', () => {
  29. let tracker: WidgetTracker;
  30. beforeEach(() => {
  31. tracker = new WidgetTracker({ namespace });
  32. });
  33. afterEach(() => {
  34. tracker.dispose();
  35. });
  36. describe('#constructor()', () => {
  37. it('should create an WidgetTracker', () => {
  38. expect(tracker).toBeInstanceOf(WidgetTracker);
  39. });
  40. });
  41. describe('#currentChanged', () => {
  42. it('should emit for the first added widget', async () => {
  43. const widget = createWidget();
  44. const promise = signalToPromise(tracker.currentChanged);
  45. void tracker.add(widget);
  46. await promise;
  47. widget.dispose();
  48. });
  49. it('should emit when a widget is added and there is another widget that does not have focus', async () => {
  50. const widget = createWidget();
  51. const widget2 = createWidget();
  52. await tracker.add(widget);
  53. const promise = signalToPromise(tracker.currentChanged);
  54. await tracker.add(widget2);
  55. await promise;
  56. widget.dispose();
  57. widget2.dispose();
  58. });
  59. it('should not emit when a widget is added and there is another widget that has focus', async () => {
  60. const widget = createWidget();
  61. const widget2 = createWidget();
  62. Widget.attach(widget, document.body);
  63. focus(widget);
  64. await tracker.add(widget);
  65. let called = false;
  66. tracker.currentChanged.connect(() => {
  67. called = true;
  68. });
  69. await tracker.add(widget2);
  70. expect(called).toBe(false);
  71. widget.dispose();
  72. widget2.dispose();
  73. });
  74. it('should emit when the focus changes', async () => {
  75. const widget = createWidget();
  76. const widget2 = createWidget();
  77. Widget.attach(widget, document.body);
  78. Widget.attach(widget2, document.body);
  79. focus(widget);
  80. await tracker.add(widget);
  81. await tracker.add(widget2);
  82. const promise = signalToPromise(tracker.currentChanged);
  83. focus(widget2);
  84. await promise;
  85. widget.dispose();
  86. widget2.dispose();
  87. });
  88. });
  89. describe('#widgetAdded', () => {
  90. it('should emit when a widget has been added', async () => {
  91. const widget = createWidget();
  92. const promise = signalToPromise(tracker.widgetAdded);
  93. await tracker.add(widget);
  94. const [sender, args] = await promise;
  95. expect(sender).toBe(tracker);
  96. expect(args).toBe(widget);
  97. widget.dispose();
  98. });
  99. it('should not emit when a widget has been injected', async () => {
  100. const one = createWidget();
  101. const two = createWidget();
  102. let total = 0;
  103. const promise = testEmission(tracker.currentChanged, {
  104. find: () => {
  105. return total === 1;
  106. }
  107. });
  108. tracker.widgetAdded.connect(() => {
  109. total++;
  110. });
  111. void tracker.add(one);
  112. void tracker.inject(two);
  113. Widget.attach(two, document.body);
  114. focus(two);
  115. Widget.detach(two);
  116. await promise;
  117. one.dispose();
  118. two.dispose();
  119. });
  120. });
  121. describe('#currentWidget', () => {
  122. it('should default to null', () => {
  123. expect(tracker.currentWidget).toBeNull();
  124. });
  125. it('should be updated when a widget is added', async () => {
  126. const widget = createWidget();
  127. await tracker.add(widget);
  128. expect(tracker.currentWidget).toBe(widget);
  129. widget.dispose();
  130. });
  131. it('should be updated when a widget is focused', async () => {
  132. const panel = new Panel();
  133. const widget0 = createWidget();
  134. const widget1 = createWidget();
  135. await tracker.add(widget0);
  136. await tracker.add(widget1);
  137. panel.addWidget(widget0);
  138. panel.addWidget(widget1);
  139. Widget.attach(panel, document.body);
  140. expect(tracker.currentWidget).toBe(widget1);
  141. focus(widget0);
  142. expect(tracker.currentWidget).toBe(widget0);
  143. panel.dispose();
  144. widget0.dispose();
  145. widget1.dispose();
  146. });
  147. it('should revert to last added widget on widget disposal', async () => {
  148. const one = createWidget();
  149. const two = createWidget();
  150. await tracker.add(one);
  151. await tracker.add(two);
  152. focus(one);
  153. focus(two);
  154. expect(tracker.currentWidget).toBe(two);
  155. two.dispose();
  156. expect(tracker.currentWidget).toBe(one);
  157. one.dispose();
  158. });
  159. it('should preserve the tracked widget on widget disposal', () => {
  160. const panel = new Panel();
  161. const widgets = [createWidget(), createWidget(), createWidget()];
  162. widgets.forEach(widget => {
  163. void tracker.add(widget);
  164. panel.addWidget(widget);
  165. });
  166. Widget.attach(panel, document.body);
  167. focus(widgets[0]);
  168. expect(tracker.currentWidget).toBe(widgets[0]);
  169. let called = false;
  170. tracker.currentChanged.connect(() => {
  171. called = true;
  172. });
  173. widgets[2].dispose();
  174. expect(tracker.currentWidget).toBe(widgets[0]);
  175. expect(called).toBe(false);
  176. panel.dispose();
  177. widgets.forEach(widget => {
  178. widget.dispose();
  179. });
  180. });
  181. it('should select the previously added widget on widget disposal', () => {
  182. const panel = new Panel();
  183. const widgets = [createWidget(), createWidget(), createWidget()];
  184. Widget.attach(panel, document.body);
  185. widgets.forEach(widget => {
  186. void tracker.add(widget);
  187. panel.addWidget(widget);
  188. focus(widget);
  189. });
  190. let called = false;
  191. tracker.currentChanged.connect(() => {
  192. called = true;
  193. });
  194. widgets[2].dispose();
  195. expect(tracker.currentWidget).toBe(widgets[1]);
  196. expect(called).toBe(true);
  197. panel.dispose();
  198. widgets.forEach(widget => {
  199. widget.dispose();
  200. });
  201. });
  202. });
  203. describe('#isDisposed', () => {
  204. it('should test whether the tracker is disposed', () => {
  205. expect(tracker.isDisposed).toBe(false);
  206. tracker.dispose();
  207. expect(tracker.isDisposed).toBe(true);
  208. });
  209. });
  210. describe('#add()', () => {
  211. it('should add a widget to the tracker', async () => {
  212. const widget = createWidget();
  213. expect(tracker.has(widget)).toBe(false);
  214. await tracker.add(widget);
  215. expect(tracker.has(widget)).toBe(true);
  216. widget.dispose();
  217. });
  218. it('should reject a widget that already exists', async () => {
  219. const widget = createWidget();
  220. let failed = false;
  221. expect(tracker.has(widget)).toBe(false);
  222. await tracker.add(widget);
  223. expect(tracker.has(widget)).toBe(true);
  224. try {
  225. await tracker.add(widget);
  226. } catch (error) {
  227. failed = true;
  228. }
  229. expect(failed).toBe(true);
  230. widget.dispose();
  231. });
  232. it('should reject a widget that is disposed', async () => {
  233. const widget = createWidget();
  234. let failed = false;
  235. expect(tracker.has(widget)).toBe(false);
  236. widget.dispose();
  237. try {
  238. await tracker.add(widget);
  239. } catch (error) {
  240. failed = true;
  241. }
  242. expect(failed).toBe(true);
  243. widget.dispose();
  244. });
  245. it('should remove an added widget if it is disposed', async () => {
  246. const widget = createWidget();
  247. await tracker.add(widget);
  248. expect(tracker.has(widget)).toBe(true);
  249. widget.dispose();
  250. expect(tracker.has(widget)).toBe(false);
  251. });
  252. });
  253. describe('#dispose()', () => {
  254. it('should dispose of the resources used by the tracker', () => {
  255. expect(tracker.isDisposed).toBe(false);
  256. tracker.dispose();
  257. expect(tracker.isDisposed).toBe(true);
  258. });
  259. it('should be safe to call multiple times', () => {
  260. expect(tracker.isDisposed).toBe(false);
  261. tracker.dispose();
  262. tracker.dispose();
  263. expect(tracker.isDisposed).toBe(true);
  264. });
  265. });
  266. describe('#find()', () => {
  267. it('should find a tracked item that matches a filter function', () => {
  268. const widgetA = createWidget();
  269. const widgetB = createWidget();
  270. const widgetC = createWidget();
  271. widgetA.id = 'A';
  272. widgetB.id = 'B';
  273. widgetC.id = 'C';
  274. void tracker.add(widgetA);
  275. void tracker.add(widgetB);
  276. void tracker.add(widgetC);
  277. expect(tracker.find(widget => widget.id === 'B')).toBe(widgetB);
  278. widgetA.dispose();
  279. widgetB.dispose();
  280. widgetC.dispose();
  281. });
  282. it('should return a void if no item is found', () => {
  283. const widgetA = createWidget();
  284. const widgetB = createWidget();
  285. const widgetC = createWidget();
  286. widgetA.id = 'A';
  287. widgetB.id = 'B';
  288. widgetC.id = 'C';
  289. void tracker.add(widgetA);
  290. void tracker.add(widgetB);
  291. void tracker.add(widgetC);
  292. expect(tracker.find(widget => widget.id === 'D')).toBeFalsy();
  293. widgetA.dispose();
  294. widgetB.dispose();
  295. widgetC.dispose();
  296. });
  297. });
  298. describe('#filter()', () => {
  299. it('should filter according to a predicate function', () => {
  300. const widgetA = createWidget();
  301. const widgetB = createWidget();
  302. const widgetC = createWidget();
  303. widgetA.id = 'include-A';
  304. widgetB.id = 'include-B';
  305. widgetC.id = 'exclude-C';
  306. void tracker.add(widgetA);
  307. void tracker.add(widgetB);
  308. void tracker.add(widgetC);
  309. const list = tracker.filter(
  310. widget => widget.id.indexOf('include') !== -1
  311. );
  312. expect(list.length).toBe(2);
  313. expect(list[0]).toBe(widgetA);
  314. expect(list[1]).toBe(widgetB);
  315. widgetA.dispose();
  316. widgetB.dispose();
  317. widgetC.dispose();
  318. });
  319. it('should return an empty array if no item is found', () => {
  320. const widgetA = createWidget();
  321. const widgetB = createWidget();
  322. const widgetC = createWidget();
  323. widgetA.id = 'A';
  324. widgetB.id = 'B';
  325. widgetC.id = 'C';
  326. void tracker.add(widgetA);
  327. void tracker.add(widgetB);
  328. void tracker.add(widgetC);
  329. expect(tracker.filter(widget => widget.id === 'D').length).toBe(0);
  330. widgetA.dispose();
  331. widgetB.dispose();
  332. widgetC.dispose();
  333. });
  334. });
  335. describe('#forEach()', () => {
  336. it('should iterate through all the tracked items', () => {
  337. const widgetA = createWidget();
  338. const widgetB = createWidget();
  339. const widgetC = createWidget();
  340. let visited = '';
  341. widgetA.id = 'A';
  342. widgetB.id = 'B';
  343. widgetC.id = 'C';
  344. void tracker.add(widgetA);
  345. void tracker.add(widgetB);
  346. void tracker.add(widgetC);
  347. tracker.forEach(widget => {
  348. visited += widget.id;
  349. });
  350. expect(visited).toBe('ABC');
  351. widgetA.dispose();
  352. widgetB.dispose();
  353. widgetC.dispose();
  354. });
  355. });
  356. describe('#has()', () => {
  357. it('should return `true` if an item exists in the tracker', () => {
  358. const widget = createWidget();
  359. expect(tracker.has(widget)).toBe(false);
  360. void tracker.add(widget);
  361. expect(tracker.has(widget)).toBe(true);
  362. widget.dispose();
  363. });
  364. });
  365. describe('#inject()', () => {
  366. it('should inject a widget into the tracker', async () => {
  367. const widget = createWidget();
  368. expect(tracker.has(widget)).toBe(false);
  369. void tracker.inject(widget);
  370. expect(tracker.has(widget)).toBe(true);
  371. widget.dispose();
  372. });
  373. it('should remove an injected widget if it is disposed', async () => {
  374. const widget = createWidget();
  375. void tracker.inject(widget);
  376. expect(tracker.has(widget)).toBe(true);
  377. widget.dispose();
  378. expect(tracker.has(widget)).toBe(false);
  379. });
  380. });
  381. describe('#onCurrentChanged()', () => {
  382. it('should be called when the current widget is changed', async () => {
  383. const tracker = new TestTracker({ namespace });
  384. const widget = createWidget();
  385. await tracker.add(widget);
  386. expect(tracker.methods).toEqual(
  387. expect.arrayContaining(['onCurrentChanged'])
  388. );
  389. widget.dispose();
  390. });
  391. });
  392. });
  393. });