@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.
- package/README.md +192 -31
- package/lib/benchmarkReporter.d.ts +1 -0
- package/lib/benchmarkReporter.js +34 -39
- package/lib/benchmarkReporter.js.map +1 -1
- package/lib/benchmarkVLTpl.js +19 -5
- package/lib/benchmarkVLTpl.js.map +1 -1
- package/lib/contents.d.ts +5 -5
- package/lib/contents.js +32 -36
- package/lib/contents.js.map +1 -1
- package/lib/extension/global.d.ts +197 -0
- package/lib/extension/global.js +601 -0
- package/lib/extension/global.js.map +1 -0
- package/lib/extension/index.d.ts +6 -0
- package/lib/extension/index.js +27 -0
- package/lib/extension/index.js.map +1 -0
- package/lib/extension/tokens.d.ts +232 -0
- package/lib/extension/tokens.js +13 -0
- package/lib/extension/tokens.js.map +1 -0
- package/lib/extension.d.ts +223 -0
- package/lib/{global.js → extension.js} +1 -2
- package/lib/extension.js.map +1 -0
- package/lib/fixtures.d.ts +32 -10
- package/lib/fixtures.js +64 -17
- package/lib/fixtures.js.map +1 -1
- package/lib/galata.d.ts +140 -19
- package/lib/galata.js +272 -87
- package/lib/galata.js.map +1 -1
- package/lib/helpers/activity.d.ts +6 -0
- package/lib/helpers/activity.js +19 -5
- package/lib/helpers/activity.js.map +1 -1
- package/lib/helpers/debuggerpanel.d.ts +4 -0
- package/lib/helpers/debuggerpanel.js +16 -0
- package/lib/helpers/debuggerpanel.js.map +1 -1
- package/lib/helpers/filebrowser.js +8 -2
- package/lib/helpers/filebrowser.js.map +1 -1
- package/lib/helpers/index.d.ts +1 -0
- package/lib/helpers/index.js +6 -1
- package/lib/helpers/index.js.map +1 -1
- package/lib/helpers/kernel.js +7 -7
- package/lib/helpers/kernel.js.map +1 -1
- package/lib/helpers/menu.d.ts +7 -0
- package/lib/helpers/menu.js +17 -1
- package/lib/helpers/menu.js.map +1 -1
- package/lib/helpers/notebook.d.ts +6 -4
- package/lib/helpers/notebook.js +127 -31
- package/lib/helpers/notebook.js.map +1 -1
- package/lib/helpers/sidebar.d.ts +8 -1
- package/lib/helpers/sidebar.js +33 -15
- package/lib/helpers/sidebar.js.map +1 -1
- package/lib/helpers/statusbar.js +1 -1
- package/lib/helpers/statusbar.js.map +1 -1
- package/lib/helpers/style.d.ts +42 -0
- package/lib/helpers/style.js +50 -0
- package/lib/helpers/style.js.map +1 -0
- package/lib/helpers/theme.js +1 -1
- package/lib/helpers/theme.js.map +1 -1
- package/lib/index.d.ts +5 -2
- package/lib/index.js +12 -3
- package/lib/index.js.map +1 -1
- package/lib/jupyterlabpage.d.ts +29 -4
- package/lib/jupyterlabpage.js +38 -22
- package/lib/jupyterlabpage.js.map +1 -1
- package/lib/playwright-config.js +5 -1
- package/lib/playwright-config.js.map +1 -1
- package/lib/utils.js +5 -1
- package/lib/utils.js.map +1 -1
- package/package.json +31 -47
- package/src/benchmarkReporter.ts +756 -0
- package/src/benchmarkVLTpl.ts +91 -0
- package/src/contents.ts +472 -0
- package/src/extension.ts +281 -0
- package/src/fixtures.ts +387 -0
- package/src/galata.ts +1035 -0
- package/src/helpers/activity.ts +115 -0
- package/src/helpers/debuggerpanel.ts +159 -0
- package/src/helpers/filebrowser.ts +228 -0
- package/src/helpers/index.ts +15 -0
- package/src/helpers/kernel.ts +39 -0
- package/src/helpers/logconsole.ts +32 -0
- package/src/helpers/menu.ts +228 -0
- package/src/helpers/notebook.ts +1217 -0
- package/src/helpers/performance.ts +57 -0
- package/src/helpers/sidebar.ts +289 -0
- package/src/helpers/statusbar.ts +56 -0
- package/src/helpers/style.ts +100 -0
- package/src/helpers/theme.ts +50 -0
- package/src/index.ts +19 -0
- package/src/jupyterlabpage.ts +704 -0
- package/src/playwright-config.ts +26 -0
- package/src/utils.ts +264 -0
- package/src/vega-statistics.d.ts +15 -0
- package/lib/global.d.ts +0 -23
- package/lib/global.js.map +0 -1
- package/lib/inpage/tokens.d.ts +0 -135
- package/lib/inpage/tokens.js +0 -9
- package/lib/inpage/tokens.js.map +0 -1
- package/lib/lib-inpage/inpage.js +0 -3957
- package/lib/lib-inpage/inpage.js.map +0 -1
- package/style/index.css +0 -10
- 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
|
+
}
|