@jupyterlab/galata 5.0.0-alpha.2 → 5.0.0-alpha.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/README.md +192 -31
  2. package/lib/benchmarkReporter.d.ts +1 -0
  3. package/lib/benchmarkReporter.js +34 -39
  4. package/lib/benchmarkReporter.js.map +1 -1
  5. package/lib/benchmarkVLTpl.js +19 -5
  6. package/lib/benchmarkVLTpl.js.map +1 -1
  7. package/lib/contents.d.ts +5 -5
  8. package/lib/contents.js +32 -36
  9. package/lib/contents.js.map +1 -1
  10. package/lib/extension/global.d.ts +197 -0
  11. package/lib/extension/global.js +601 -0
  12. package/lib/extension/global.js.map +1 -0
  13. package/lib/extension/index.d.ts +6 -0
  14. package/lib/extension/index.js +27 -0
  15. package/lib/extension/index.js.map +1 -0
  16. package/lib/extension/tokens.d.ts +232 -0
  17. package/lib/extension/tokens.js +13 -0
  18. package/lib/extension/tokens.js.map +1 -0
  19. package/lib/extension.d.ts +223 -0
  20. package/lib/{global.js → extension.js} +1 -2
  21. package/lib/extension.js.map +1 -0
  22. package/lib/fixtures.d.ts +32 -10
  23. package/lib/fixtures.js +64 -17
  24. package/lib/fixtures.js.map +1 -1
  25. package/lib/galata.d.ts +140 -19
  26. package/lib/galata.js +272 -87
  27. package/lib/galata.js.map +1 -1
  28. package/lib/helpers/activity.d.ts +6 -0
  29. package/lib/helpers/activity.js +19 -5
  30. package/lib/helpers/activity.js.map +1 -1
  31. package/lib/helpers/debuggerpanel.d.ts +4 -0
  32. package/lib/helpers/debuggerpanel.js +16 -0
  33. package/lib/helpers/debuggerpanel.js.map +1 -1
  34. package/lib/helpers/filebrowser.js +8 -2
  35. package/lib/helpers/filebrowser.js.map +1 -1
  36. package/lib/helpers/index.d.ts +1 -0
  37. package/lib/helpers/index.js +6 -1
  38. package/lib/helpers/index.js.map +1 -1
  39. package/lib/helpers/kernel.js +7 -7
  40. package/lib/helpers/kernel.js.map +1 -1
  41. package/lib/helpers/menu.d.ts +7 -0
  42. package/lib/helpers/menu.js +17 -1
  43. package/lib/helpers/menu.js.map +1 -1
  44. package/lib/helpers/notebook.d.ts +6 -4
  45. package/lib/helpers/notebook.js +127 -31
  46. package/lib/helpers/notebook.js.map +1 -1
  47. package/lib/helpers/sidebar.d.ts +8 -1
  48. package/lib/helpers/sidebar.js +33 -15
  49. package/lib/helpers/sidebar.js.map +1 -1
  50. package/lib/helpers/statusbar.js +1 -1
  51. package/lib/helpers/statusbar.js.map +1 -1
  52. package/lib/helpers/style.d.ts +42 -0
  53. package/lib/helpers/style.js +50 -0
  54. package/lib/helpers/style.js.map +1 -0
  55. package/lib/helpers/theme.js +1 -1
  56. package/lib/helpers/theme.js.map +1 -1
  57. package/lib/index.d.ts +5 -2
  58. package/lib/index.js +12 -3
  59. package/lib/index.js.map +1 -1
  60. package/lib/jupyterlabpage.d.ts +29 -4
  61. package/lib/jupyterlabpage.js +38 -22
  62. package/lib/jupyterlabpage.js.map +1 -1
  63. package/lib/playwright-config.js +5 -1
  64. package/lib/playwright-config.js.map +1 -1
  65. package/lib/utils.js +5 -1
  66. package/lib/utils.js.map +1 -1
  67. package/package.json +31 -47
  68. package/src/benchmarkReporter.ts +756 -0
  69. package/src/benchmarkVLTpl.ts +91 -0
  70. package/src/contents.ts +472 -0
  71. package/src/extension.ts +281 -0
  72. package/src/fixtures.ts +387 -0
  73. package/src/galata.ts +1035 -0
  74. package/src/helpers/activity.ts +115 -0
  75. package/src/helpers/debuggerpanel.ts +159 -0
  76. package/src/helpers/filebrowser.ts +228 -0
  77. package/src/helpers/index.ts +15 -0
  78. package/src/helpers/kernel.ts +39 -0
  79. package/src/helpers/logconsole.ts +32 -0
  80. package/src/helpers/menu.ts +228 -0
  81. package/src/helpers/notebook.ts +1217 -0
  82. package/src/helpers/performance.ts +57 -0
  83. package/src/helpers/sidebar.ts +289 -0
  84. package/src/helpers/statusbar.ts +56 -0
  85. package/src/helpers/style.ts +100 -0
  86. package/src/helpers/theme.ts +50 -0
  87. package/src/index.ts +19 -0
  88. package/src/jupyterlabpage.ts +704 -0
  89. package/src/playwright-config.ts +26 -0
  90. package/src/utils.ts +264 -0
  91. package/src/vega-statistics.d.ts +15 -0
  92. package/lib/global.d.ts +0 -23
  93. package/lib/global.js.map +0 -1
  94. package/lib/inpage/tokens.d.ts +0 -135
  95. package/lib/inpage/tokens.js +0 -9
  96. package/lib/inpage/tokens.js.map +0 -1
  97. package/lib/lib-inpage/inpage.js +0 -3957
  98. package/lib/lib-inpage/inpage.js.map +0 -1
  99. package/style/index.css +0 -10
  100. package/style/index.js +0 -10
@@ -0,0 +1,1217 @@
1
+ // Copyright (c) Jupyter Development Team.
2
+ // Distributed under the terms of the Modified BSD License.
3
+
4
+ import type * as nbformat from '@jupyterlab/nbformat';
5
+ import type { NotebookPanel } from '@jupyterlab/notebook';
6
+ import { ElementHandle, Page } from '@playwright/test';
7
+ import * as path from 'path';
8
+ import { ContentsHelper } from '../contents';
9
+ import type { INotebookRunCallback } from '../extension';
10
+ import { galata } from '../galata';
11
+ import * as Utils from '../utils';
12
+ import { ActivityHelper } from './activity';
13
+ import { FileBrowserHelper } from './filebrowser';
14
+ import { MenuHelper } from './menu';
15
+
16
+ /**
17
+ * Maximal number of retries to get a cell
18
+ */
19
+ const MAX_RETRIES = 3;
20
+
21
+ /**
22
+ * Notebook helpers
23
+ */
24
+ export class NotebookHelper {
25
+ constructor(
26
+ readonly page: Page,
27
+ readonly activity: ActivityHelper,
28
+ readonly contents: ContentsHelper,
29
+ readonly filebrowser: FileBrowserHelper,
30
+ readonly menu: MenuHelper
31
+ ) {}
32
+
33
+ /**
34
+ * Whether a given notebook is opened or not
35
+ *
36
+ * @param name Notebook name
37
+ * @returns Notebook opened status
38
+ */
39
+ async isOpen(name: string): Promise<boolean> {
40
+ const tab = await this.activity.getTab(name);
41
+ return tab !== null;
42
+ }
43
+
44
+ /**
45
+ * Whether a given notebook is active or not
46
+ *
47
+ * @param name Notebook name
48
+ * @returns Notebook active status
49
+ */
50
+ async isActive(name: string): Promise<boolean> {
51
+ return this.activity.isTabActive(name);
52
+ }
53
+
54
+ /**
55
+ * Whether a notebook is currently active or not
56
+ *
57
+ * @returns Notebook active status
58
+ */
59
+ async isAnyActive(): Promise<boolean> {
60
+ return (await this.getNotebookInPanel()) !== null;
61
+ }
62
+
63
+ /**
64
+ * Open a notebook from its name
65
+ *
66
+ * The notebook needs to exist in the current folder.
67
+ *
68
+ * @param name Notebook name
69
+ * @returns Action success status
70
+ */
71
+ async open(name: string): Promise<boolean> {
72
+ const isListed = await this.filebrowser.isFileListedInBrowser(name);
73
+ if (!isListed) {
74
+ return false;
75
+ }
76
+
77
+ await this.filebrowser.open(name);
78
+
79
+ return await this.isOpen(name);
80
+ }
81
+
82
+ /**
83
+ * Open a notebook from its path
84
+ *
85
+ * The notebook do not need to exist in the current folder
86
+ *
87
+ * @param filePath Notebook path
88
+ * @returns Action success status
89
+ */
90
+ async openByPath(filePath: string): Promise<boolean> {
91
+ await this.filebrowser.open(filePath);
92
+ const name = path.basename(filePath);
93
+ return await this.isOpen(name);
94
+ }
95
+
96
+ /**
97
+ * Get the handle to a notebook panel
98
+ *
99
+ * @param name Notebook name
100
+ * @returns Handle to the Notebook panel
101
+ */
102
+ async getNotebookInPanel(
103
+ name?: string
104
+ ): Promise<ElementHandle<Element> | null> {
105
+ const nbPanel = await this.activity.getPanel(name);
106
+
107
+ if (nbPanel) {
108
+ return await nbPanel.$('.jp-NotebookPanel-notebook');
109
+ }
110
+
111
+ return null;
112
+ }
113
+
114
+ /**
115
+ * Get the handle to a notebook toolbar
116
+ *
117
+ * @param name Notebook name
118
+ * @returns Handle to the Notebook toolbar
119
+ */
120
+ async getToolbar(name?: string): Promise<ElementHandle<Element> | null> {
121
+ const nbPanel = await this.activity.getPanel(name);
122
+
123
+ if (nbPanel) {
124
+ return await nbPanel.$('.jp-Toolbar');
125
+ }
126
+
127
+ return null;
128
+ }
129
+
130
+ /**
131
+ * Get the handle to a notebook toolbar item from its index
132
+ *
133
+ * @param itemIndex Toolbar item index
134
+ * @param notebookName Notebook name
135
+ * @returns Handle to the notebook toolbar item
136
+ */
137
+ async getToolbarItemByIndex(
138
+ itemIndex: number,
139
+ notebookName?: string
140
+ ): Promise<ElementHandle<Element> | null> {
141
+ if (itemIndex === -1) {
142
+ return null;
143
+ }
144
+
145
+ const toolbar = await this.getToolbar(notebookName);
146
+
147
+ if (toolbar) {
148
+ const toolbarItems = await toolbar.$$('.jp-Toolbar-item');
149
+ if (itemIndex < toolbarItems.length) {
150
+ return toolbarItems[itemIndex];
151
+ }
152
+ }
153
+
154
+ return null;
155
+ }
156
+
157
+ /**
158
+ * Get the handle to a notebook toolbar item from its id
159
+ *
160
+ * @param itemId Toolbar item id
161
+ * @param notebookName Notebook name
162
+ * @returns Handle to the notebook toolbar item
163
+ */
164
+ async getToolbarItem(
165
+ itemId: galata.NotebookToolbarItemId,
166
+ notebookName?: string
167
+ ): Promise<ElementHandle<Element> | null> {
168
+ const toolbar = await this.getToolbar(notebookName);
169
+
170
+ if (toolbar) {
171
+ const itemIndex = await this.page.evaluate(async (itemId: string) => {
172
+ return window.galata.getNotebookToolbarItemIndex(itemId);
173
+ }, itemId);
174
+
175
+ return this.getToolbarItemByIndex(itemIndex);
176
+ }
177
+
178
+ return null;
179
+ }
180
+
181
+ /**
182
+ * Click on a notebook toolbar item
183
+ *
184
+ * @param itemId Toolbar item id
185
+ * @param notebookName Notebook name
186
+ * @returns Action success status
187
+ */
188
+ async clickToolbarItem(
189
+ itemId: galata.NotebookToolbarItemId,
190
+ notebookName?: string
191
+ ): Promise<boolean> {
192
+ const toolbarItem = await this.getToolbarItem(itemId, notebookName);
193
+
194
+ if (toolbarItem) {
195
+ await toolbarItem.click();
196
+
197
+ return true;
198
+ }
199
+
200
+ return false;
201
+ }
202
+
203
+ /**
204
+ * Activate a notebook
205
+ *
206
+ * @param name Notebook name
207
+ * @returns Action success status
208
+ */
209
+ async activate(name: string): Promise<boolean> {
210
+ if (await this.activity.activateTab(name)) {
211
+ await this.page.evaluate(async () => {
212
+ const galata = window.galata;
213
+ const nbPanel = galata.app.shell.currentWidget as NotebookPanel;
214
+ await nbPanel.sessionContext.ready;
215
+ // Assuming that if the session is ready, the kernel is ready also for now and commenting out this line
216
+ // await nbPanel.session.kernel.ready;
217
+ galata.app.shell.activateById(nbPanel.id);
218
+ });
219
+
220
+ return true;
221
+ }
222
+
223
+ return false;
224
+ }
225
+
226
+ /**
227
+ * Save the currently active notebook
228
+ *
229
+ * @returns Action success status
230
+ */
231
+ async save(): Promise<boolean> {
232
+ if (!(await this.isAnyActive())) {
233
+ return false;
234
+ }
235
+
236
+ await this.page.evaluate(async () => {
237
+ await window.galata.saveActiveNotebook();
238
+ });
239
+
240
+ return true;
241
+ }
242
+
243
+ /**
244
+ * Revert changes to the currently active notebook
245
+ *
246
+ * @returns Action success status
247
+ */
248
+ async revertChanges(): Promise<boolean> {
249
+ if (!(await this.isAnyActive())) {
250
+ return false;
251
+ }
252
+
253
+ await this.page.evaluate(async () => {
254
+ const app = window.galata.app;
255
+ const nbPanel = app.shell.currentWidget as NotebookPanel;
256
+ await nbPanel.context.revert();
257
+ });
258
+
259
+ return true;
260
+ }
261
+
262
+ /**
263
+ * Run all cells of the currently active notebook
264
+ *
265
+ * @returns Action success status
266
+ */
267
+ async run(): Promise<boolean> {
268
+ if (!(await this.isAnyActive())) {
269
+ return false;
270
+ }
271
+
272
+ await this.page.evaluate(() => {
273
+ window.galata.resetExecutionCount();
274
+ });
275
+ await this.menu.clickMenuItem('Run>Run All Cells');
276
+ await this.waitForRun();
277
+
278
+ return true;
279
+ }
280
+
281
+ /**
282
+ * Run the currently active notebook cell by cell.
283
+ *
284
+ * @param callback Cell ran callback
285
+ * @returns Action success status
286
+ */
287
+ async runCellByCell(callback?: INotebookRunCallback): Promise<boolean> {
288
+ if (!(await this.isAnyActive())) {
289
+ return false;
290
+ }
291
+
292
+ let callbackName = '';
293
+
294
+ if (callback) {
295
+ callbackName = `_runCallbacksExposed${++this._runCallbacksExposed}`;
296
+
297
+ await this.page.exposeFunction(
298
+ `${callbackName}_onBeforeScroll`,
299
+ async () => {
300
+ if (callback && callback.onBeforeScroll) {
301
+ await callback.onBeforeScroll();
302
+ }
303
+ }
304
+ );
305
+
306
+ await this.page.exposeFunction(
307
+ `${callbackName}_onAfterScroll`,
308
+ async () => {
309
+ if (callback && callback.onAfterScroll) {
310
+ await callback.onAfterScroll();
311
+ }
312
+ }
313
+ );
314
+
315
+ await this.page.exposeFunction(
316
+ `${callbackName}_onAfterCellRun`,
317
+ async (cellIndex: number) => {
318
+ if (callback && callback.onAfterCellRun) {
319
+ await callback.onAfterCellRun(cellIndex);
320
+ }
321
+ }
322
+ );
323
+ }
324
+
325
+ await this.page.evaluate(async (callbackName: string) => {
326
+ const callbacks =
327
+ callbackName === ''
328
+ ? undefined
329
+ : ({
330
+ onBeforeScroll: async () => {
331
+ await (window as any)[`${callbackName}_onBeforeScroll`]();
332
+ },
333
+
334
+ onAfterScroll: async () => {
335
+ await (window as any)[`${callbackName}_onAfterScroll`]();
336
+ },
337
+
338
+ onAfterCellRun: async (cellIndex: number) => {
339
+ await (window as any)[`${callbackName}_onAfterCellRun`](
340
+ cellIndex
341
+ );
342
+ }
343
+ } as INotebookRunCallback);
344
+
345
+ await window.galata.runActiveNotebookCellByCell(callbacks);
346
+ }, callbackName);
347
+
348
+ return true;
349
+ }
350
+
351
+ /**
352
+ * Wait for notebook cells execution to finish
353
+ *
354
+ * @param cellIndex Cell index
355
+ */
356
+ async waitForRun(cellIndex?: number): Promise<void> {
357
+ const idleLocator = this.page.locator('#jp-main-statusbar >> text=Idle');
358
+ await idleLocator.waitFor();
359
+
360
+ // Wait for all cells to have an execution count
361
+ let done = false;
362
+ do {
363
+ await this.page.waitForTimeout(20);
364
+ done = await this.page.evaluate(cellIdx => {
365
+ return window.galata.haveBeenExecuted(cellIdx);
366
+ }, cellIndex);
367
+ } while (!done);
368
+ }
369
+
370
+ /**
371
+ * Close the notebook with or without reverting unsaved changes
372
+ *
373
+ * @param revertChanges Whether to revert changes or not
374
+ * @returns Action success status
375
+ */
376
+ async close(revertChanges = true): Promise<boolean> {
377
+ if (!(await this.isAnyActive())) {
378
+ return false;
379
+ }
380
+
381
+ const page = this.page;
382
+ const tab = await this.activity.getTab();
383
+
384
+ if (!tab) {
385
+ return false;
386
+ }
387
+
388
+ if (revertChanges) {
389
+ if (!(await this.revertChanges())) {
390
+ return false;
391
+ }
392
+ }
393
+
394
+ const closeIcon = await tab.$('.lm-TabBar-tabCloseIcon');
395
+ if (!closeIcon) {
396
+ return false;
397
+ }
398
+
399
+ await closeIcon.click();
400
+
401
+ // close save prompt
402
+ const dialogSelector = '.jp-Dialog .jp-Dialog-content';
403
+ const dialog = await page.$(dialogSelector);
404
+ if (dialog) {
405
+ const dlgBtnSelector = revertChanges
406
+ ? 'button.jp-mod-accept.jp-mod-warn' // discard
407
+ : 'button.jp-mod-accept:not(.jp-mod-warn)'; // save
408
+ const dlgBtn = await dialog.$(dlgBtnSelector);
409
+
410
+ if (dlgBtn) {
411
+ await dlgBtn.click();
412
+ }
413
+ }
414
+ await page.waitForSelector(dialogSelector, { state: 'hidden' });
415
+
416
+ return true;
417
+ }
418
+
419
+ /**
420
+ * Get the number of cells in the currently active notebook
421
+ *
422
+ * @returns Number of cells
423
+ */
424
+ getCellCount = async (): Promise<number> => {
425
+ const notebook = await this.getNotebookInPanel();
426
+ if (!notebook) {
427
+ return -1;
428
+ }
429
+
430
+ const scrollTop = await notebook.evaluate(node => node.scrollTop);
431
+
432
+ // Scroll to bottom
433
+ let previousScrollHeight = scrollTop;
434
+ let scrollHeight =
435
+ previousScrollHeight +
436
+ (await notebook.evaluate(node => node.clientHeight));
437
+ do {
438
+ await notebook.evaluate((node, scrollTarget) => {
439
+ node.scrollTo({ top: scrollTarget });
440
+ }, scrollHeight);
441
+ await this.page.waitForTimeout(50);
442
+ previousScrollHeight = scrollHeight;
443
+ scrollHeight = await notebook.evaluate(
444
+ node => node.scrollHeight - node.clientHeight
445
+ );
446
+ } while (scrollHeight > previousScrollHeight);
447
+
448
+ const lastCell = await notebook.$$('div.jp-Cell >> nth=-1');
449
+ const count =
450
+ parseInt(
451
+ (await lastCell[0].getAttribute('data-windowed-list-index')) ?? '0',
452
+ 10
453
+ ) + 1;
454
+
455
+ // Scroll back to original position
456
+ await notebook.evaluate((node, scrollTarget) => {
457
+ node.scrollTo({ top: scrollTarget });
458
+ }, scrollTop);
459
+
460
+ return count;
461
+ };
462
+
463
+ /**
464
+ * Get a cell handle
465
+ *
466
+ * @param cellIndex Cell index
467
+ * @returns Handle to the cell
468
+ */
469
+ async getCell(cellIndex: number): Promise<ElementHandle<Element> | null> {
470
+ const notebook = await this.getNotebookInPanel();
471
+ if (!notebook) {
472
+ return null;
473
+ }
474
+
475
+ const allCells = await notebook.$$('div.jp-Cell');
476
+ const filters = await Promise.all(allCells.map(c => c.isVisible()));
477
+ const cells = allCells.filter((c, i) => filters[i]);
478
+
479
+ const firstCell = cells[0];
480
+ const lastCell = cells[cells.length - 1];
481
+
482
+ let firstIndex = parseInt(
483
+ (await firstCell.getAttribute('data-windowed-list-index')) ?? '0',
484
+ 10
485
+ );
486
+ let lastIndex = parseInt(
487
+ (await lastCell.getAttribute('data-windowed-list-index')) ?? '0',
488
+ 10
489
+ );
490
+
491
+ if (cellIndex < firstIndex) {
492
+ // Scroll up
493
+ let scrollTop =
494
+ (await firstCell.boundingBox())?.y ??
495
+ (await notebook.evaluate(node => node.scrollTop - node.clientHeight));
496
+
497
+ do {
498
+ await notebook.evaluate((node, scrollTarget) => {
499
+ node.scrollTo({ top: scrollTarget });
500
+ }, scrollTop);
501
+ await this.page.waitForTimeout(50);
502
+
503
+ const cells = await notebook.$$('div.jp-Cell');
504
+ const isVisible = await Promise.all(cells.map(c => c.isVisible()));
505
+ const firstCell = isVisible.findIndex(visibility => visibility);
506
+
507
+ firstIndex = parseInt(
508
+ (await cells[firstCell].getAttribute('data-windowed-list-index')) ??
509
+ '0',
510
+ 10
511
+ );
512
+ scrollTop =
513
+ (await cells[firstCell].boundingBox())?.y ??
514
+ (await notebook.evaluate(node => node.scrollTop - node.clientHeight));
515
+ } while (scrollTop > 0 && firstIndex > cellIndex);
516
+ } else if (cellIndex > lastIndex) {
517
+ const clientHeight = await notebook.evaluate(node => node.clientHeight);
518
+ // Scroll down
519
+ const viewport = await (
520
+ await notebook.$$('.jp-WindowedPanel-window')
521
+ )[0].boundingBox();
522
+ let scrollHeight = viewport!.y + viewport!.height;
523
+ let previousScrollHeight = 0;
524
+
525
+ do {
526
+ previousScrollHeight = scrollHeight;
527
+ await notebook.evaluate((node, scrollTarget) => {
528
+ node.scrollTo({ top: scrollTarget });
529
+ }, scrollHeight);
530
+ await this.page.waitForTimeout(50);
531
+
532
+ const cells = await notebook.$$('div.jp-Cell');
533
+ const isVisible = await Promise.all(cells.map(c => c.isVisible()));
534
+ const lastCell = isVisible.lastIndexOf(true);
535
+
536
+ lastIndex = parseInt(
537
+ (await cells[lastCell].getAttribute('data-windowed-list-index')) ??
538
+ '0',
539
+ 10
540
+ );
541
+
542
+ const viewport = await (
543
+ await notebook.$$('.jp-WindowedPanel-window')
544
+ )[0].boundingBox();
545
+ scrollHeight = viewport!.y + viewport!.height;
546
+ // Avoid jitter
547
+ scrollHeight = Math.max(
548
+ previousScrollHeight + clientHeight,
549
+ scrollHeight
550
+ );
551
+ } while (scrollHeight > previousScrollHeight && lastIndex < cellIndex);
552
+ }
553
+
554
+ if (firstIndex <= cellIndex && cellIndex <= lastIndex) {
555
+ return (
556
+ await notebook.$$(
557
+ `div.jp-Cell[data-windowed-list-index="${cellIndex}"]`
558
+ )
559
+ )[0];
560
+ } else {
561
+ return null;
562
+ }
563
+ }
564
+
565
+ /**
566
+ * Get the handle to the input of a cell
567
+ *
568
+ * @param cellIndex Cell index
569
+ * @returns Handle to the cell input
570
+ */
571
+ async getCellInput(
572
+ cellIndex: number
573
+ ): Promise<ElementHandle<Element> | null> {
574
+ const cell = await this.getCell(cellIndex);
575
+ if (!cell) {
576
+ return null;
577
+ }
578
+
579
+ const cellEditor = await cell.$('.jp-InputArea-editor');
580
+ if (!cellEditor) {
581
+ return null;
582
+ }
583
+
584
+ const isRenderedMarkdown = await cellEditor.evaluate(editor =>
585
+ editor.classList.contains('lm-mod-hidden')
586
+ );
587
+ if (isRenderedMarkdown) {
588
+ return await cell.$('.jp-MarkdownOutput');
589
+ }
590
+
591
+ return cellEditor;
592
+ }
593
+
594
+ /**
595
+ * Get the handle to the input expander of a cell
596
+ *
597
+ * @param cellIndex Cell index
598
+ * @returns Handle to the cell input expander
599
+ */
600
+ async getCellInputExpander(
601
+ cellIndex: number
602
+ ): Promise<ElementHandle<Element> | null> {
603
+ const cell = await this.getCell(cellIndex);
604
+ if (!cell) {
605
+ return null;
606
+ }
607
+
608
+ return await cell.$('.jp-InputCollapser');
609
+ }
610
+
611
+ /**
612
+ * Whether a cell input is expanded or not
613
+ *
614
+ * @param cellIndex Cell index
615
+ * @returns Cell input expanded status
616
+ */
617
+ async isCellInputExpanded(cellIndex: number): Promise<boolean | null> {
618
+ const cell = await this.getCell(cellIndex);
619
+ if (!cell) {
620
+ return null;
621
+ }
622
+
623
+ return (await cell.$('.jp-InputPlaceholder')) === null;
624
+ }
625
+
626
+ /**
627
+ * Set the expanded status of a given input cell
628
+ *
629
+ * @param cellIndex Cell index
630
+ * @param expand Input expanded status
631
+ * @returns Action success status
632
+ */
633
+ async expandCellInput(cellIndex: number, expand: boolean): Promise<boolean> {
634
+ const expanded = await this.isCellInputExpanded(cellIndex);
635
+ if ((expanded && expand) || (!expanded && !expand)) {
636
+ return false;
637
+ }
638
+
639
+ const inputExpander = await this.getCellInputExpander(cellIndex);
640
+ if (!inputExpander) {
641
+ return false;
642
+ }
643
+
644
+ await inputExpander.click();
645
+
646
+ return true;
647
+ }
648
+
649
+ /**
650
+ * Get the handle to a cell output expander
651
+ *
652
+ * @param cellIndex Cell index
653
+ * @returns Handle to the cell output expander
654
+ */
655
+ async getCellOutputExpander(
656
+ cellIndex: number
657
+ ): Promise<ElementHandle<Element> | null> {
658
+ const cell = await this.getCell(cellIndex);
659
+ if (!cell) {
660
+ return null;
661
+ }
662
+
663
+ const cellType = await this.getCellType(cellIndex);
664
+
665
+ return cellType === 'code' ? await cell.$('.jp-OutputCollapser') : null;
666
+ }
667
+
668
+ /**
669
+ * Whether a cell output is expanded or not
670
+ *
671
+ * @param cellIndex Cell index
672
+ * @returns Cell output expanded status
673
+ */
674
+ async isCellOutputExpanded(cellIndex: number): Promise<boolean | null> {
675
+ const cell = await this.getCell(cellIndex);
676
+ if (!cell) {
677
+ return null;
678
+ }
679
+
680
+ return (await cell.$('.jp-OutputPlaceholder')) === null;
681
+ }
682
+
683
+ /**
684
+ * Set the expanded status of a given output cell
685
+ *
686
+ * @param cellIndex Cell index
687
+ * @param expand Output expanded status
688
+ * @returns Action success status
689
+ */
690
+ async expandCellOutput(cellIndex: number, expand: boolean): Promise<boolean> {
691
+ const expanded = await this.isCellOutputExpanded(cellIndex);
692
+ if ((expanded && expand) || (!expanded && !expand)) {
693
+ return false;
694
+ }
695
+
696
+ const outputExpander = await this.getCellOutputExpander(cellIndex);
697
+ if (!outputExpander) {
698
+ return false;
699
+ }
700
+
701
+ await outputExpander.click();
702
+
703
+ return true;
704
+ }
705
+
706
+ /**
707
+ * Get the handle on a given output cell
708
+ *
709
+ * @param cellIndex Cell index
710
+ * @returns Output cell handle
711
+ */
712
+ async getCellOutput(
713
+ cellIndex: number
714
+ ): Promise<ElementHandle<Element> | null> {
715
+ const cell = await this.getCell(cellIndex);
716
+ if (!cell) {
717
+ return null;
718
+ }
719
+
720
+ const codeCellOutput = await cell.$('.jp-Cell-outputArea');
721
+ if (codeCellOutput) {
722
+ return codeCellOutput;
723
+ }
724
+
725
+ const mdCellOutput = await cell.$('.jp-MarkdownOutput');
726
+ if (mdCellOutput) {
727
+ return mdCellOutput;
728
+ }
729
+
730
+ return null;
731
+ }
732
+
733
+ /**
734
+ * Get all cell outputs as text
735
+ *
736
+ * @param cellIndex Cell index
737
+ * @returns List of text outputs
738
+ */
739
+ async getCellTextOutput(cellIndex: number): Promise<string[] | null> {
740
+ const cellOutput = await this.getCellOutput(cellIndex);
741
+ if (!cellOutput) {
742
+ return null;
743
+ }
744
+
745
+ const textOutputs = await cellOutput.$$('.jp-OutputArea-output');
746
+ if (textOutputs.length > 0) {
747
+ const outputs: string[] = [];
748
+ for (const textOutput of textOutputs) {
749
+ outputs.push(
750
+ (await (
751
+ await textOutput.getProperty('textContent')
752
+ ).jsonValue()) as string
753
+ );
754
+ }
755
+
756
+ return outputs;
757
+ }
758
+
759
+ return null;
760
+ }
761
+
762
+ /**
763
+ * Whether the cell is in editing mode or not
764
+ *
765
+ * @param cellIndex Cell index
766
+ * @returns Editing mode
767
+ */
768
+ async isCellInEditingMode(cellIndex: number): Promise<boolean> {
769
+ const cell = await this.getCell(cellIndex);
770
+ if (!cell) {
771
+ return false;
772
+ }
773
+
774
+ const cellEditor = await cell.$('.jp-InputArea-editor');
775
+ if (cellEditor) {
776
+ return await cellEditor.evaluate(editor =>
777
+ editor.classList.contains('jp-mod-focused')
778
+ );
779
+ }
780
+
781
+ return false;
782
+ }
783
+
784
+ /**
785
+ * Enter the editing mode on a given cell
786
+ *
787
+ * @param cellIndex Cell index
788
+ * @returns Action success status
789
+ */
790
+ async enterCellEditingMode(cellIndex: number): Promise<boolean> {
791
+ const cell = await this.getCell(cellIndex);
792
+ if (!cell) {
793
+ return false;
794
+ }
795
+
796
+ const cellEditor = await cell.$('.jp-Cell-inputArea');
797
+ if (cellEditor) {
798
+ let isMarkdown = false;
799
+ const cellType = await this.getCellType(cellIndex);
800
+ if (cellType === 'markdown') {
801
+ const renderedMarkdown = await cell.$('.jp-MarkdownOutput');
802
+ if (renderedMarkdown) {
803
+ isMarkdown = true;
804
+ }
805
+ }
806
+
807
+ if (isMarkdown) {
808
+ await cellEditor.dblclick();
809
+ }
810
+
811
+ await cellEditor.click();
812
+
813
+ return true;
814
+ }
815
+
816
+ return false;
817
+ }
818
+
819
+ /**
820
+ * Leave the editing mode
821
+ *
822
+ * @param cellIndex Cell index
823
+ * @returns Action success status
824
+ */
825
+ async leaveCellEditingMode(cellIndex: number): Promise<boolean> {
826
+ if (await this.isCellInEditingMode(cellIndex)) {
827
+ await this.page.keyboard.press('Escape');
828
+ return true;
829
+ }
830
+
831
+ return false;
832
+ }
833
+
834
+ /**
835
+ * Clicks a cell gutter line for code cells
836
+ *
837
+ * @param cellIndex Cell index
838
+ * @param lineNumber Cell line number, starts at 1
839
+ */
840
+ async clickCellGutter(
841
+ cellIndex: number,
842
+ lineNumber: number
843
+ ): Promise<boolean> {
844
+ if (lineNumber < 1) {
845
+ return false;
846
+ }
847
+
848
+ if (!(await this.isCellGutterPresent(cellIndex))) {
849
+ return false;
850
+ }
851
+
852
+ const cell = await this.getCell(cellIndex);
853
+ const gutters = await cell!.$$(
854
+ '.cm-gutters > .cm-gutter.cm-breakpoint-gutter > .cm-gutterElement'
855
+ );
856
+ if (gutters.length < lineNumber) {
857
+ return false;
858
+ }
859
+ await gutters[lineNumber].click();
860
+ return true;
861
+ }
862
+
863
+ /**
864
+ * Check if cell gutter is present
865
+ *
866
+ * @param cellIndex
867
+ */
868
+ async isCellGutterPresent(cellIndex: number): Promise<boolean> {
869
+ const cell = await this.getCell(cellIndex);
870
+ if (!cell) {
871
+ return false;
872
+ }
873
+ return (await cell.$('.cm-gutters')) !== null;
874
+ }
875
+
876
+ /**
877
+ * Wait until cell gutter is visible
878
+ *
879
+ * @param cellIndex
880
+ */
881
+ async waitForCellGutter(cellIndex: number): Promise<void> {
882
+ const cell = await this.getCell(cellIndex);
883
+ if (cell) {
884
+ await this.page.waitForSelector('.cm-gutters', {
885
+ state: 'attached'
886
+ });
887
+ }
888
+ }
889
+
890
+ /**
891
+ * Clicks a code gutter line for scripts
892
+ *
893
+ * @param lineNumber Cell line number, starts at 1
894
+ */
895
+ async clickCodeGutter(lineNumber: number): Promise<boolean> {
896
+ if (lineNumber < 1) {
897
+ return false;
898
+ }
899
+
900
+ if (!(await this.isCodeGutterPresent())) {
901
+ return false;
902
+ }
903
+
904
+ const panel = await this.activity.getPanel();
905
+ await panel!.waitForSelector(
906
+ '.cm-gutters > .cm-gutter.cm-breakpoint-gutter > .cm-gutterElement',
907
+ { state: 'attached' }
908
+ );
909
+ const gutters = await panel!.$$(
910
+ '.cm-gutters > .cm-gutter.cm-breakpoint-gutter > .cm-gutterElement'
911
+ );
912
+ if (gutters.length < lineNumber) {
913
+ return false;
914
+ }
915
+ await gutters[lineNumber].click();
916
+ return true;
917
+ }
918
+
919
+ /**
920
+ * Check if code gutter is present
921
+ *
922
+ */
923
+ async isCodeGutterPresent(): Promise<boolean> {
924
+ const panel = await this.activity.getPanel();
925
+ if (!panel) {
926
+ return false;
927
+ }
928
+ return (await panel.$('.cm-gutters')) !== null;
929
+ }
930
+
931
+ /**
932
+ * Wait until cell gutter is visible
933
+ *
934
+ * @param cellIndex
935
+ */
936
+ async waitForCodeGutter(): Promise<void> {
937
+ const panel = await this.activity.getPanel();
938
+ if (panel) {
939
+ await this.page.waitForSelector('.cm-gutters', {
940
+ state: 'attached'
941
+ });
942
+ }
943
+ }
944
+
945
+ /**
946
+ * Select cells
947
+ *
948
+ * @param startIndex Start cell index
949
+ * @param endIndex End cell index
950
+ * @returns Action success status
951
+ */
952
+ async selectCells(startIndex: number, endIndex?: number): Promise<boolean> {
953
+ const startCell = await this.getCell(startIndex);
954
+ if (!startCell) {
955
+ return false;
956
+ }
957
+
958
+ const clickPosition: any = { x: 15, y: 5 };
959
+
960
+ await startCell.click({ position: clickPosition });
961
+
962
+ if (endIndex !== undefined) {
963
+ const endCell = await this.getCell(endIndex);
964
+ if (!endCell) {
965
+ return false;
966
+ }
967
+
968
+ await endCell.click({ position: clickPosition, modifiers: ['Shift'] });
969
+ }
970
+
971
+ return true;
972
+ }
973
+
974
+ /**
975
+ * Whether a given cell is selected or not
976
+ *
977
+ * @param cellIndex Cell index
978
+ * @returns Selection status
979
+ */
980
+ async isCellSelected(cellIndex: number): Promise<boolean> {
981
+ return await this.page.evaluate((cellIndex: number) => {
982
+ return window.galata.isNotebookCellSelected(cellIndex);
983
+ }, cellIndex);
984
+ }
985
+
986
+ /**
987
+ * Delete selected cells
988
+ *
989
+ * @returns Action success status
990
+ */
991
+ async deleteCells(): Promise<boolean> {
992
+ if (!(await this.isAnyActive())) {
993
+ return false;
994
+ }
995
+
996
+ await this.page.evaluate(() => {
997
+ return window.galata.deleteNotebookCells();
998
+ });
999
+
1000
+ return true;
1001
+ }
1002
+
1003
+ /**
1004
+ * Add a cell to the currently active notebook
1005
+ *
1006
+ * @param cellType Cell type
1007
+ * @param source Source
1008
+ * @returns Action success status
1009
+ */
1010
+ async addCell(cellType: nbformat.CellType, source: string): Promise<boolean> {
1011
+ if (!(await this.isAnyActive())) {
1012
+ return false;
1013
+ }
1014
+
1015
+ const numCells = await this.getCellCount();
1016
+
1017
+ await this.selectCells(numCells - 1);
1018
+ await this.clickToolbarItem('insert');
1019
+ await Utils.waitForCondition(async (): Promise<boolean> => {
1020
+ return (await this.getCellCount()) === numCells + 1;
1021
+ });
1022
+
1023
+ return await this.setCell(numCells, cellType, source);
1024
+ }
1025
+
1026
+ /**
1027
+ * Set the input source of a cell
1028
+ *
1029
+ * @param cellIndex Cell index
1030
+ * @param cellType Cell type
1031
+ * @param source Source
1032
+ * @returns Action success status
1033
+ */
1034
+ async setCell(
1035
+ cellIndex: number,
1036
+ cellType: nbformat.CellType,
1037
+ source: string
1038
+ ): Promise<boolean> {
1039
+ if (!(await this.isAnyActive())) {
1040
+ return false;
1041
+ }
1042
+
1043
+ await this.setCellType(cellIndex, cellType);
1044
+
1045
+ if (
1046
+ !(await this.isCellSelected(cellIndex)) &&
1047
+ !(await this.selectCells(cellIndex))
1048
+ ) {
1049
+ return false;
1050
+ }
1051
+
1052
+ await this.enterCellEditingMode(cellIndex);
1053
+
1054
+ const keyboard = this.page.keyboard;
1055
+ await keyboard.press('Control+A');
1056
+ // give CodeMirror time to style properly
1057
+ await keyboard.type(source, { delay: cellType === 'code' ? 100 : 0 });
1058
+
1059
+ await this.leaveCellEditingMode(cellIndex);
1060
+
1061
+ // give CodeMirror time to style properly
1062
+ if (cellType === 'code') {
1063
+ await this.page.waitForTimeout(500);
1064
+ }
1065
+
1066
+ return true;
1067
+ }
1068
+
1069
+ /**
1070
+ * Set the type of a cell
1071
+ *
1072
+ * @param cellIndex Cell index
1073
+ * @param cellType Cell type
1074
+ * @returns Action success status
1075
+ */
1076
+ async setCellType(
1077
+ cellIndex: number,
1078
+ cellType: nbformat.CellType
1079
+ ): Promise<boolean> {
1080
+ const nbPanel = await this.activity.getPanel();
1081
+ if (!nbPanel) {
1082
+ return false;
1083
+ }
1084
+
1085
+ if ((await this.getCellType(cellIndex)) === cellType) {
1086
+ return false;
1087
+ }
1088
+
1089
+ if (!(await this.selectCells(cellIndex))) {
1090
+ return false;
1091
+ }
1092
+
1093
+ await this.clickToolbarItem('cellType');
1094
+ const selectInput = await nbPanel.$(
1095
+ 'div.jp-Notebook-toolbarCellTypeDropdown select'
1096
+ );
1097
+ if (!selectInput) {
1098
+ return false;
1099
+ }
1100
+
1101
+ await selectInput.selectOption(cellType);
1102
+
1103
+ // Wait for the new cell to be rendered
1104
+ let cell: ElementHandle | null;
1105
+ let counter = 1;
1106
+ do {
1107
+ await this.page.waitForTimeout(50);
1108
+ cell = await this.getCell(cellIndex);
1109
+ } while (cell === null && counter++ < MAX_RETRIES);
1110
+
1111
+ return true;
1112
+ }
1113
+
1114
+ /**
1115
+ * Get the cell type of a cell
1116
+ *
1117
+ * @param cellIndex Cell index
1118
+ * @returns Cell type
1119
+ */
1120
+ async getCellType(cellIndex: number): Promise<nbformat.CellType | null> {
1121
+ const notebook = await this.getNotebookInPanel();
1122
+ if (!notebook) {
1123
+ return null;
1124
+ }
1125
+
1126
+ const cell = await this.getCell(cellIndex);
1127
+
1128
+ if (!cell) {
1129
+ return null;
1130
+ }
1131
+
1132
+ const classList = await Utils.getElementClassList(cell);
1133
+
1134
+ if (classList.indexOf('jp-CodeCell') !== -1) {
1135
+ return 'code';
1136
+ } else if (classList.indexOf('jp-MarkdownCell') !== -1) {
1137
+ return 'markdown';
1138
+ } else if (classList.indexOf('jp-RawCell') !== -1) {
1139
+ return 'raw';
1140
+ }
1141
+
1142
+ return null;
1143
+ }
1144
+
1145
+ /**
1146
+ * Run a given cell
1147
+ *
1148
+ * @param cellIndex Cell index
1149
+ * @param inplace Whether to stay on the cell or select the next one
1150
+ * @returns Action success status
1151
+ */
1152
+ async runCell(cellIndex: number, inplace?: boolean): Promise<boolean> {
1153
+ if (!(await this.isAnyActive())) {
1154
+ return false;
1155
+ }
1156
+
1157
+ if (
1158
+ !(await this.isCellSelected(cellIndex)) &&
1159
+ !(await this.selectCells(cellIndex))
1160
+ ) {
1161
+ return false;
1162
+ }
1163
+
1164
+ await this.page.evaluate(cellIdx => {
1165
+ window.galata.resetExecutionCount(cellIdx);
1166
+ }, cellIndex);
1167
+ await this.page.keyboard.press(
1168
+ inplace === true ? 'Control+Enter' : 'Shift+Enter'
1169
+ );
1170
+ await this.waitForRun(cellIndex);
1171
+
1172
+ return true;
1173
+ }
1174
+
1175
+ /**
1176
+ * Create a new notebook
1177
+ *
1178
+ * @param name Name of the notebook
1179
+ * @returns Name of the created notebook or null if it failed
1180
+ */
1181
+ async createNew(name?: string): Promise<string | null> {
1182
+ await this.menu.clickMenuItem('File>New>Notebook');
1183
+
1184
+ const page = this.page;
1185
+ await page.waitForSelector('.jp-Dialog');
1186
+ await page.click('.jp-Dialog .jp-mod-accept');
1187
+
1188
+ const activeTab = await this.activity.getTab();
1189
+ if (!activeTab) {
1190
+ return null;
1191
+ }
1192
+
1193
+ const label = await activeTab.$('div.lm-TabBar-tabLabel');
1194
+ if (!label) {
1195
+ return null;
1196
+ }
1197
+
1198
+ const assignedName = (await (
1199
+ await label.getProperty('textContent')
1200
+ ).jsonValue()) as string;
1201
+
1202
+ if (!name) {
1203
+ return assignedName;
1204
+ }
1205
+
1206
+ const currentDir = await this.filebrowser.getCurrentDirectory();
1207
+ await this.contents.renameFile(
1208
+ `${currentDir}/${assignedName}`,
1209
+ `${currentDir}/${name}`
1210
+ );
1211
+ const renamedTab = await this.activity.getTab(name);
1212
+
1213
+ return renamedTab ? name : null;
1214
+ }
1215
+
1216
+ private _runCallbacksExposed = 0;
1217
+ }