@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,91 @@
1
+ /*
2
+ * Copyright (c) Jupyter Development Team.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+
6
+ // Vega-Lite configuration
7
+
8
+ /**
9
+ * General Vega-Lite configuration
10
+ */
11
+ const GENERAL_CONFIG = {
12
+ $schema: 'https://vega.github.io/schema/vega-lite/v5.json',
13
+ description: 'Box plots of some action time.',
14
+ title: 'Duration of common actions',
15
+ data: {} as Record<string, any>,
16
+ config: { facet: { spacing: 80 } }
17
+ };
18
+
19
+ /**
20
+ * Matrix of figures per test file
21
+ *
22
+ * @param tests Kind of test
23
+ * @param comparison Field name to compare
24
+ * @returns The specification
25
+ */
26
+ function configPerFile(
27
+ tests: string[],
28
+ comparison: string
29
+ ): Record<string, any> {
30
+ return {
31
+ vconcat: tests.map(t => {
32
+ return {
33
+ title: t,
34
+ transform: [{ filter: `datum.test === '${t}'` }],
35
+ facet: {
36
+ column: { field: 'browser', type: 'nominal' }
37
+ },
38
+ spec: {
39
+ mark: { type: 'boxplot', extent: 'min-max' },
40
+ encoding: {
41
+ y: { field: comparison, type: 'nominal' },
42
+ x: {
43
+ field: 'time',
44
+ title: 'Time (ms)',
45
+ type: 'quantitative',
46
+ scale: { zero: false }
47
+ }
48
+ }
49
+ }
50
+ };
51
+ })
52
+ };
53
+ }
54
+
55
+ /**
56
+ * Generate the Vega-Lite specification for test
57
+ *
58
+ * Note: The data field is set to empty
59
+ *
60
+ * @param tests Kind of test
61
+ * @param comparison Field name to compare
62
+ * @param filenames Test file name list
63
+ * @returns The specification
64
+ */
65
+ function generateVegaLiteSpec(
66
+ tests: string[],
67
+ comparison: string,
68
+ filenames?: string[]
69
+ ): Record<string, any> {
70
+ const files = filenames ?? [];
71
+
72
+ if (files.length === 0) {
73
+ return {
74
+ ...GENERAL_CONFIG,
75
+ ...configPerFile(tests, comparison)
76
+ };
77
+ } else {
78
+ return {
79
+ ...GENERAL_CONFIG,
80
+ hconcat: files.map(b => {
81
+ return {
82
+ title: b,
83
+ transform: [{ filter: `datum.file === '${b}'` }],
84
+ ...configPerFile(tests, comparison)
85
+ };
86
+ })
87
+ };
88
+ }
89
+ }
90
+
91
+ export default generateVegaLiteSpec;
@@ -0,0 +1,472 @@
1
+ // Copyright (c) Jupyter Development Team.
2
+ // Distributed under the terms of the Modified BSD License.
3
+
4
+ import { URLExt } from '@jupyterlab/coreutils';
5
+ import type { IDocumentManager } from '@jupyterlab/docmanager';
6
+ import type { Contents } from '@jupyterlab/services';
7
+ import type { APIRequestContext, APIResponse, Page } from '@playwright/test';
8
+ import type { ReadStream } from 'fs-extra';
9
+ import * as path from 'path';
10
+ import type { IPluginNameToInterfaceMap } from './extension';
11
+ import * as Utils from './utils';
12
+
13
+ /**
14
+ * Helper class to interact with contents server API
15
+ *
16
+ * Those helpers are directly requesting the Jupyter server to
17
+ * carry contents tasks; except rename operations if the page model
18
+ * is provided.
19
+ */
20
+ export class ContentsHelper {
21
+ /**
22
+ * Construct a new instance of ContentsHelper
23
+ *
24
+ * @param request Playwright API request context
25
+ * @param page Playwright page model object
26
+ */
27
+ constructor(request?: APIRequestContext, readonly page?: Page) {
28
+ if (request) {
29
+ this.request = request;
30
+ } else if (page) {
31
+ this.request = page.context().request;
32
+ } else {
33
+ throw new TypeError(
34
+ 'You must provide `request` or `page` to the contents helper.'
35
+ );
36
+ }
37
+ }
38
+
39
+ readonly request: APIRequestContext;
40
+
41
+ /**
42
+ * Return the model for a path.
43
+ *
44
+ * @param path Path
45
+ * @param type Path type
46
+ * @returns Element metadata
47
+ */
48
+ async getContentMetadata(
49
+ path: string,
50
+ type: 'file' | 'directory' = 'file'
51
+ ): Promise<Contents.IModel | null> {
52
+ const data = {
53
+ type,
54
+ // Get the content only for directory
55
+ content: type === 'directory' ? 1 : 0
56
+ };
57
+
58
+ let response = null;
59
+
60
+ try {
61
+ response = await this._fetch(path + URLExt.objectToQueryString(data));
62
+ } catch (error) {
63
+ console.error(`Fail to get content metadata for ${path}`, error);
64
+ }
65
+
66
+ const succeeded = response?.status() === 200;
67
+
68
+ if (succeeded) {
69
+ return response!.json();
70
+ }
71
+
72
+ return null;
73
+ }
74
+
75
+ /**
76
+ * Whether a directory exists or not
77
+ *
78
+ * @param dirPath Directory path
79
+ * @returns Directory existence status
80
+ */
81
+ async directoryExists(dirPath: string): Promise<boolean> {
82
+ const content = await this.getContentMetadata(dirPath, 'directory');
83
+
84
+ return content?.type === 'directory';
85
+ }
86
+
87
+ /**
88
+ * Whether a file exists or not
89
+ *
90
+ * @param filePath File path
91
+ * @returns File existence status
92
+ */
93
+ async fileExists(filePath: string): Promise<boolean> {
94
+ const content = await this.getContentMetadata(filePath);
95
+
96
+ return content?.type === 'notebook' || content?.type === 'file';
97
+ }
98
+
99
+ /**
100
+ * Create a directory
101
+ *
102
+ * @param dirPath Directory path
103
+ * @returns Action success status
104
+ */
105
+ async createDirectory(dirPath: string): Promise<boolean> {
106
+ const directories = dirPath.split('/');
107
+ let path = '';
108
+
109
+ for (const directory of directories) {
110
+ if (directory.trim() === '') {
111
+ continue;
112
+ }
113
+ if (path !== '') {
114
+ path += '/';
115
+ }
116
+ path += directory;
117
+ await this._createDirectory(path);
118
+ }
119
+
120
+ return true;
121
+ }
122
+
123
+ /**
124
+ * Upload a directory recursively in the Jupyter server
125
+ *
126
+ * @param sourcePath Local source path
127
+ * @param destinationPath Server destination path
128
+ * @returns Action success status
129
+ */
130
+ async uploadDirectory(
131
+ sourcePath: string,
132
+ destinationPath?: string
133
+ ): Promise<boolean> {
134
+ const pos = sourcePath.lastIndexOf('/');
135
+ const sourceDirName = sourcePath.substring(pos + 1);
136
+ destinationPath = destinationPath ?? sourceDirName;
137
+
138
+ const files = Utils.getFilesInDirectory(sourcePath);
139
+ for (const file of files) {
140
+ const relativePath = file.substring(sourcePath.length + 1);
141
+ await this.uploadFile(file, `${destinationPath}/${relativePath}`);
142
+ }
143
+
144
+ return true;
145
+ }
146
+
147
+ /**
148
+ * Upload content as file to JupyterLab.
149
+ *
150
+ * Note: the destinationPath is the filepath on the server.
151
+ *
152
+ * @param content Content file to upload
153
+ * @param format Content format
154
+ * @param destinationPath Destination filepath
155
+ * @returns Whether the action succeeded or not.
156
+ */
157
+
158
+ async uploadContent(
159
+ content: string,
160
+ format: 'base64' | 'text' | 'json',
161
+ destinationPath: string
162
+ ): Promise<boolean> {
163
+ const pos = destinationPath.lastIndexOf('/');
164
+ if (pos !== -1) {
165
+ const destDir = destinationPath?.substring(0, pos);
166
+ if (destDir && !(await this.directoryExists(destDir))) {
167
+ await this.createDirectory(destDir);
168
+ }
169
+ }
170
+
171
+ const data = JSON.stringify({
172
+ content,
173
+ format,
174
+ type: 'file'
175
+ });
176
+
177
+ let response = null;
178
+
179
+ try {
180
+ response = await this._fetch(destinationPath, {
181
+ method: 'PUT',
182
+ data
183
+ });
184
+ } catch (error) {
185
+ console.error(
186
+ `Failed to upload content to server ${destinationPath}`,
187
+ error
188
+ );
189
+ }
190
+
191
+ const succeeded = response?.status() === 201;
192
+
193
+ if (succeeded) {
194
+ return await this.fileExists(destinationPath);
195
+ }
196
+
197
+ return false;
198
+ }
199
+
200
+ /**
201
+ * Upload a file to JupyterLab.
202
+ *
203
+ * Note: the destinationPath is the filepath on the server.
204
+ *
205
+ * @param sourcePath Filepath to upload
206
+ * @param destinationPath Destination filepath
207
+ * @returns Whether the action succeeded or not.
208
+ */
209
+ async uploadFile(
210
+ sourcePath: string,
211
+ destinationPath?: string
212
+ ): Promise<boolean> {
213
+ return this.uploadContent(
214
+ Utils.base64EncodeFile(sourcePath),
215
+ 'base64',
216
+ destinationPath ?? path.basename(sourcePath)
217
+ );
218
+ }
219
+
220
+ /**
221
+ * Delete a file
222
+ *
223
+ * @param filePath File path
224
+ * @returns Action success status
225
+ */
226
+ async deleteFile(filePath: string): Promise<boolean> {
227
+ const fileName = filePath;
228
+
229
+ let response = null;
230
+
231
+ try {
232
+ response = await this._fetch(fileName, {
233
+ method: 'DELETE'
234
+ });
235
+ } catch (error) {
236
+ console.error(`Failed to delete file ${filePath}`, error);
237
+ }
238
+
239
+ const succeeded = response?.status() === 204;
240
+
241
+ if (succeeded) {
242
+ return !(await this.fileExists(fileName));
243
+ }
244
+
245
+ return false;
246
+ }
247
+
248
+ /**
249
+ * Delete recursively a directory
250
+ *
251
+ * @param dirPath Directory path
252
+ * @returns Action success status
253
+ */
254
+ async deleteDirectory(dirPath: string): Promise<boolean> {
255
+ const dirContent = await this.getContentMetadata(dirPath, 'directory');
256
+
257
+ if (!(dirContent && dirContent.type === 'directory')) {
258
+ return false;
259
+ }
260
+
261
+ let anyFailed = false;
262
+
263
+ // delete directory contents first
264
+ for (const item of dirContent.content) {
265
+ if (item.type === 'directory') {
266
+ if (!(await this.deleteDirectory(item.path))) {
267
+ anyFailed = true;
268
+ }
269
+ } else {
270
+ if (!(await this.deleteFile(item.path))) {
271
+ anyFailed = true;
272
+ }
273
+ }
274
+ }
275
+
276
+ if (!(await this.deleteFile(dirPath))) {
277
+ anyFailed = true;
278
+ }
279
+
280
+ return !anyFailed;
281
+ }
282
+
283
+ /**
284
+ * Rename a file
285
+ *
286
+ * @param oldName Old name
287
+ * @param newName New name
288
+ * @returns Action success status
289
+ */
290
+ async renameFile(oldName: string, newName: string): Promise<boolean> {
291
+ if (this.page) {
292
+ // Rename through REST API does not propagate to opened widgets
293
+ // => Use galata in-page if page is available
294
+ return await this.page.evaluate(
295
+ async ({ pluginId, oldName, newName }) => {
296
+ const docManager = (await window.galata.getPlugin(
297
+ pluginId
298
+ )) as IDocumentManager;
299
+ const result = await docManager.rename(oldName, newName);
300
+ return result !== null;
301
+ },
302
+ {
303
+ pluginId:
304
+ '@jupyterlab/docmanager-extension:manager' as keyof IPluginNameToInterfaceMap,
305
+ oldName: oldName,
306
+ newName: newName
307
+ }
308
+ );
309
+ }
310
+
311
+ let response = null;
312
+
313
+ try {
314
+ response = await this._fetch(oldName, {
315
+ method: 'PATCH',
316
+ data: JSON.stringify({ path: newName })
317
+ });
318
+ } catch (error) {
319
+ console.error(`Failed to rename file ${oldName} to ${newName}`, error);
320
+ }
321
+
322
+ const succeeded = response?.status() === 200;
323
+
324
+ if (succeeded) {
325
+ return await this.fileExists(newName);
326
+ }
327
+
328
+ return false;
329
+ }
330
+
331
+ /**
332
+ * Rename a directory
333
+ *
334
+ * @param oldName Old name
335
+ * @param newName New name
336
+ * @returns Action success status
337
+ */
338
+ async renameDirectory(oldName: string, newName: string): Promise<boolean> {
339
+ return await this.renameFile(oldName, newName);
340
+ }
341
+
342
+ /**
343
+ * Wait for a contents API response
344
+ *
345
+ * @param trigger Action to trigger while waiting
346
+ */
347
+ async waitForAPIResponse(
348
+ trigger?: () => Promise<void> | void
349
+ ): Promise<void> {
350
+ if (!this.page) {
351
+ return Promise.reject('No page available.');
352
+ }
353
+
354
+ await Promise.all([
355
+ this.page.waitForResponse(response =>
356
+ response.url().includes('api/contents')
357
+ ),
358
+ Promise.resolve(trigger?.call(this))
359
+ ]);
360
+ }
361
+
362
+ protected async _createDirectory(dirPath: string): Promise<boolean> {
363
+ const data = JSON.stringify({
364
+ format: 'json',
365
+ type: 'directory'
366
+ });
367
+
368
+ let response = null;
369
+
370
+ try {
371
+ response = await this._fetch(dirPath, {
372
+ method: 'PUT',
373
+ data
374
+ });
375
+ } catch (error) {
376
+ console.error(`Failed to create directory ${dirPath}`, error);
377
+ }
378
+
379
+ return response?.status() === 201;
380
+ }
381
+
382
+ private async _fetch(
383
+ path: string,
384
+ options: {
385
+ /**
386
+ * Allows to set post data of the request. If the data parameter is an object, it will be serialized to json string and
387
+ * `content-type` header will be set to `application/json` if not explicitly set. Otherwise the `content-type` header will
388
+ * be set to `application/octet-stream` if not explicitly set.
389
+ */
390
+ data?: string | Buffer;
391
+
392
+ /**
393
+ * Whether to throw on response codes other than 2xx and 3xx. By default response object is returned for all status codes.
394
+ */
395
+ failOnStatusCode?: boolean;
396
+
397
+ /**
398
+ * Provides an object that will be serialized as html form using `application/x-www-form-urlencoded` encoding and sent as
399
+ * this request body. If this parameter is specified `content-type` header will be set to
400
+ * `application/x-www-form-urlencoded` unless explicitly provided.
401
+ */
402
+ form?: { [key: string]: string | number | boolean };
403
+
404
+ /**
405
+ * Allows to set HTTP headers.
406
+ */
407
+ headers?: { [key: string]: string };
408
+
409
+ /**
410
+ * Whether to ignore HTTPS errors when sending network requests. Defaults to `false`.
411
+ */
412
+ ignoreHTTPSErrors?: boolean;
413
+
414
+ /**
415
+ * If set changes the fetch method (e.g. [PUT](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/PUT) or
416
+ * [POST](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST)). If not specified, GET method is used.
417
+ */
418
+ method?: string;
419
+
420
+ /**
421
+ * Provides an object that will be serialized as html form using `multipart/form-data` encoding and sent as this request
422
+ * body. If this parameter is specified `content-type` header will be set to `multipart/form-data` unless explicitly
423
+ * provided. File values can be passed either as [`fs.ReadStream`](https://nodejs.org/api/fs.html#fs_class_fs_readstream)
424
+ * or as file-like object containing file name, mime-type and its content.
425
+ */
426
+ multipart?: {
427
+ [key: string]:
428
+ | string
429
+ | number
430
+ | boolean
431
+ | ReadStream
432
+ | {
433
+ /**
434
+ * File name
435
+ */
436
+ name: string;
437
+
438
+ /**
439
+ * File type
440
+ */
441
+ mimeType: string;
442
+
443
+ /**
444
+ * File content
445
+ */
446
+ buffer: Buffer;
447
+ };
448
+ };
449
+
450
+ /**
451
+ * Query parameters to be sent with the URL.
452
+ */
453
+ params?: { [key: string]: string | number | boolean };
454
+
455
+ /**
456
+ * Request timeout in milliseconds. Defaults to `30000` (30 seconds). Pass `0` to disable timeout.
457
+ */
458
+ timeout?: number;
459
+ } = { method: 'GET' }
460
+ ): Promise<APIResponse> {
461
+ const baseUrl = this.page ? await Utils.getBaseUrl(this.page) : '/';
462
+ const token = this.page ? await Utils.getToken(this.page) : '';
463
+
464
+ let url = URLExt.join(baseUrl, 'api/contents', path);
465
+
466
+ if (token) {
467
+ options.headers = { Authorization: `Token ${token}` };
468
+ }
469
+
470
+ return this.request.fetch(url, options);
471
+ }
472
+ }