@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
package/src/galata.ts
ADDED
|
@@ -0,0 +1,1035 @@
|
|
|
1
|
+
/* eslint-disable camelcase */
|
|
2
|
+
// Copyright (c) Jupyter Development Team.
|
|
3
|
+
// Distributed under the terms of the Modified BSD License.
|
|
4
|
+
|
|
5
|
+
import type * as nbformat from '@jupyterlab/nbformat';
|
|
6
|
+
import type {
|
|
7
|
+
Session,
|
|
8
|
+
TerminalAPI,
|
|
9
|
+
User,
|
|
10
|
+
Workspace
|
|
11
|
+
} from '@jupyterlab/services';
|
|
12
|
+
import type { ISettingRegistry } from '@jupyterlab/settingregistry';
|
|
13
|
+
import type { JSONObject } from '@lumino/coreutils';
|
|
14
|
+
import { UUID } from '@lumino/coreutils';
|
|
15
|
+
import type { APIRequestContext, Browser, Page } from '@playwright/test';
|
|
16
|
+
import * as json5 from 'json5';
|
|
17
|
+
import { ContentsHelper } from './contents';
|
|
18
|
+
import { PerformanceHelper } from './helpers';
|
|
19
|
+
import {
|
|
20
|
+
IJupyterLabPage,
|
|
21
|
+
IJupyterLabPageFixture,
|
|
22
|
+
JupyterLabPage
|
|
23
|
+
} from './jupyterlabpage';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Galata namespace
|
|
27
|
+
*/
|
|
28
|
+
export namespace galata {
|
|
29
|
+
/**
|
|
30
|
+
* Default user settings:
|
|
31
|
+
* - Deactivate codemirror cursor blinking to avoid noise in screenshots
|
|
32
|
+
*/
|
|
33
|
+
export const DEFAULT_SETTINGS: Record<string, any> = {
|
|
34
|
+
'@jupyterlab/apputils-extension:notification': {
|
|
35
|
+
fetchNews: 'false'
|
|
36
|
+
},
|
|
37
|
+
'@jupyterlab/fileeditor-extension:plugin': {},
|
|
38
|
+
'@jupyterlab/notebook-extension:tracker': {},
|
|
39
|
+
'@jupyterlab/codemirror-extension:plugin': {
|
|
40
|
+
defaultConfig: {
|
|
41
|
+
cursorBlinkRate: 0
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const DEFAULT_DOCUMENTATION_STATE: Record<string, any> = {
|
|
47
|
+
data: {
|
|
48
|
+
'layout-restorer:data': {
|
|
49
|
+
relativeSizes: [0, 1, 0]
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Sidebar position
|
|
56
|
+
*/
|
|
57
|
+
export type SidebarPosition = 'left' | 'right';
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Default sidebar ids
|
|
61
|
+
*/
|
|
62
|
+
export type DefaultSidebarTabId =
|
|
63
|
+
| 'filebrowser'
|
|
64
|
+
| 'jp-running-sessions'
|
|
65
|
+
| 'tab-manager'
|
|
66
|
+
| 'jp-property-inspector'
|
|
67
|
+
| 'table-of-contents'
|
|
68
|
+
| 'extensionmanager.main-view'
|
|
69
|
+
| 'jp-debugger-sidebar';
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Sidebar id type
|
|
73
|
+
*/
|
|
74
|
+
export type SidebarTabId = DefaultSidebarTabId | string;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Default toolbar item ids
|
|
78
|
+
*/
|
|
79
|
+
export type DefaultNotebookToolbarItemId =
|
|
80
|
+
| 'save'
|
|
81
|
+
| 'insert'
|
|
82
|
+
| 'cut'
|
|
83
|
+
| 'copy'
|
|
84
|
+
| 'paste'
|
|
85
|
+
| 'run'
|
|
86
|
+
| 'interrupt'
|
|
87
|
+
| 'restart'
|
|
88
|
+
| 'restart-and-run'
|
|
89
|
+
| 'cellType'
|
|
90
|
+
| 'kernelName'
|
|
91
|
+
| 'kernelStatus';
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Notebook toolbar item type
|
|
95
|
+
*/
|
|
96
|
+
export type NotebookToolbarItemId = DefaultNotebookToolbarItemId | string;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Options to create a new page
|
|
100
|
+
*/
|
|
101
|
+
export interface INewPageOption {
|
|
102
|
+
/**
|
|
103
|
+
* Application base URL
|
|
104
|
+
*/
|
|
105
|
+
baseURL: string;
|
|
106
|
+
/**
|
|
107
|
+
* Playwright browser model
|
|
108
|
+
*/
|
|
109
|
+
browser: Browser;
|
|
110
|
+
/**
|
|
111
|
+
* Callback that resolved when the application page is ready
|
|
112
|
+
*/
|
|
113
|
+
waitForApplication: (page: Page, helpers: IJupyterLabPage) => Promise<void>;
|
|
114
|
+
/**
|
|
115
|
+
* Application URL path fragment
|
|
116
|
+
*
|
|
117
|
+
* Default: /lab
|
|
118
|
+
*/
|
|
119
|
+
appPath?: string;
|
|
120
|
+
/**
|
|
121
|
+
* Whether to go to JupyterLab page within the fixture or not.
|
|
122
|
+
*
|
|
123
|
+
* Default: true
|
|
124
|
+
*/
|
|
125
|
+
autoGoto?: boolean;
|
|
126
|
+
/**
|
|
127
|
+
* Mock Jupyter Server configuration in-memory or not.
|
|
128
|
+
*
|
|
129
|
+
* Default true
|
|
130
|
+
*/
|
|
131
|
+
mockConfig?: boolean | Record<string, unknown>;
|
|
132
|
+
/**
|
|
133
|
+
* Mock JupyterLab state in-memory or not.
|
|
134
|
+
*
|
|
135
|
+
* Default galata.DEFAULT_SETTINGS
|
|
136
|
+
*/
|
|
137
|
+
mockSettings?: boolean | Record<string, unknown>;
|
|
138
|
+
/**
|
|
139
|
+
* Mock JupyterLab settings in-memory or not.
|
|
140
|
+
*
|
|
141
|
+
* Default true
|
|
142
|
+
*/
|
|
143
|
+
mockState?: boolean | Record<string, unknown>;
|
|
144
|
+
/**
|
|
145
|
+
* Mock JupyterLab user in-memory or not.
|
|
146
|
+
*
|
|
147
|
+
* Default true
|
|
148
|
+
*/
|
|
149
|
+
mockUser?: boolean | Partial<User.IUser>;
|
|
150
|
+
/**
|
|
151
|
+
* Whether to store sessions in memory or not.
|
|
152
|
+
*
|
|
153
|
+
* Default true
|
|
154
|
+
*/
|
|
155
|
+
mockSessions?: boolean;
|
|
156
|
+
/**
|
|
157
|
+
* Whether to store terminals in memory or not.
|
|
158
|
+
*
|
|
159
|
+
* Default true
|
|
160
|
+
*/
|
|
161
|
+
mockTerminals?: boolean;
|
|
162
|
+
/**
|
|
163
|
+
* Create and delete a temporary path during the page existence
|
|
164
|
+
*
|
|
165
|
+
* Default ''
|
|
166
|
+
*/
|
|
167
|
+
tmpPath?: string;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Add the Galata helpers to the page model
|
|
172
|
+
*
|
|
173
|
+
* @param page Playwright page model
|
|
174
|
+
* @param baseURL Application base URL
|
|
175
|
+
* @param waitForApplication Callback that resolved when the application page is ready
|
|
176
|
+
* @param appPath Application URL path fragment
|
|
177
|
+
* @returns Playwright page model with Galata helpers
|
|
178
|
+
*/
|
|
179
|
+
export function addHelpersToPage(
|
|
180
|
+
page: Page,
|
|
181
|
+
baseURL: string,
|
|
182
|
+
waitForApplication: (page: Page, helpers: IJupyterLabPage) => Promise<void>,
|
|
183
|
+
appPath?: string
|
|
184
|
+
): IJupyterLabPageFixture {
|
|
185
|
+
const jlabPage = new JupyterLabPage(
|
|
186
|
+
page,
|
|
187
|
+
baseURL,
|
|
188
|
+
waitForApplication,
|
|
189
|
+
appPath
|
|
190
|
+
);
|
|
191
|
+
|
|
192
|
+
const handler = {
|
|
193
|
+
get: function (obj: JupyterLabPage, prop: string) {
|
|
194
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
195
|
+
// @ts-ignore
|
|
196
|
+
return prop in obj ? obj[prop] : page[prop];
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
// Proxy playwright page object
|
|
201
|
+
return new Proxy(jlabPage, handler) as any;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export async function initTestPage(
|
|
205
|
+
appPath: string,
|
|
206
|
+
autoGoto: boolean,
|
|
207
|
+
baseURL: string,
|
|
208
|
+
mockConfig: boolean | Record<string, unknown>,
|
|
209
|
+
mockSettings: boolean | Record<string, unknown>,
|
|
210
|
+
mockState: boolean | Record<string, unknown>,
|
|
211
|
+
mockUser: boolean | Partial<User.IUser>,
|
|
212
|
+
page: Page,
|
|
213
|
+
sessions: Map<string, Session.IModel> | null,
|
|
214
|
+
terminals: Map<string, TerminalAPI.IModel> | null,
|
|
215
|
+
tmpPath: string,
|
|
216
|
+
waitForApplication: (page: Page, helpers: IJupyterLabPage) => Promise<void>
|
|
217
|
+
): Promise<IJupyterLabPageFixture> {
|
|
218
|
+
// Hook the helpers
|
|
219
|
+
const jlabWithPage = addHelpersToPage(
|
|
220
|
+
page,
|
|
221
|
+
baseURL,
|
|
222
|
+
waitForApplication,
|
|
223
|
+
appPath
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
// Add server mocks
|
|
227
|
+
if (mockConfig) {
|
|
228
|
+
const config: Record<string, JSONObject> =
|
|
229
|
+
typeof mockConfig !== 'boolean' ? ({ ...mockConfig } as any) : {};
|
|
230
|
+
await Mock.mockConfig(page, config);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const settings: ISettingRegistry.IPlugin[] = [];
|
|
234
|
+
if (mockSettings) {
|
|
235
|
+
// Settings will be stored in-memory (after loading the initial version from disk)
|
|
236
|
+
await Mock.mockSettings(
|
|
237
|
+
page,
|
|
238
|
+
settings,
|
|
239
|
+
typeof mockSettings === 'boolean' ? {} : { ...mockSettings }
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const workspace: Workspace.IWorkspace = {
|
|
244
|
+
data: {},
|
|
245
|
+
metadata: { id: 'default' }
|
|
246
|
+
};
|
|
247
|
+
if (mockState) {
|
|
248
|
+
if (typeof mockState !== 'boolean') {
|
|
249
|
+
workspace.data = { ...mockState } as any;
|
|
250
|
+
}
|
|
251
|
+
// State will be stored in-memory (after loading the initial version from disk)
|
|
252
|
+
await Mock.mockState(page, workspace);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
let user: User.IUser = {
|
|
256
|
+
identity: {
|
|
257
|
+
username: UUID.uuid4(),
|
|
258
|
+
name: 'jovyan',
|
|
259
|
+
display_name: 'jovyan',
|
|
260
|
+
initials: 'JP',
|
|
261
|
+
color: 'var(--jp-collaborator-color1)'
|
|
262
|
+
},
|
|
263
|
+
permissions: {}
|
|
264
|
+
};
|
|
265
|
+
if (mockUser) {
|
|
266
|
+
if (typeof mockUser !== 'boolean') {
|
|
267
|
+
user = { ...mockUser } as any;
|
|
268
|
+
}
|
|
269
|
+
// The user will be stored in-memory
|
|
270
|
+
await Mock.mockUser(page, user);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Add sessions and terminals trackers
|
|
274
|
+
if (sessions) {
|
|
275
|
+
await Mock.mockRunners(page, sessions, 'sessions');
|
|
276
|
+
}
|
|
277
|
+
if (terminals) {
|
|
278
|
+
await Mock.mockRunners(page, terminals, 'terminals');
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (autoGoto) {
|
|
282
|
+
// Load and initialize JupyterLab and goto test folder
|
|
283
|
+
await jlabWithPage.goto(`tree/${tmpPath}`);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return jlabWithPage;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Create a contents REST API helpers object
|
|
291
|
+
*
|
|
292
|
+
* @param request Playwright API request context
|
|
293
|
+
* @param page Playwright page model
|
|
294
|
+
* @returns Contents REST API helpers
|
|
295
|
+
*/
|
|
296
|
+
export function newContentsHelper(
|
|
297
|
+
request?: APIRequestContext,
|
|
298
|
+
page?: Page
|
|
299
|
+
): ContentsHelper {
|
|
300
|
+
return new ContentsHelper(request, page);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Create a page with Galata helpers for the given browser in a new context.
|
|
305
|
+
*
|
|
306
|
+
* @returns Playwright page model with Galata helpers
|
|
307
|
+
*/
|
|
308
|
+
export async function newPage(options: INewPageOption): Promise<{
|
|
309
|
+
page: IJupyterLabPageFixture;
|
|
310
|
+
sessions: Map<string, Session.IModel> | null;
|
|
311
|
+
terminals: Map<string, TerminalAPI.IModel> | null;
|
|
312
|
+
}> {
|
|
313
|
+
const {
|
|
314
|
+
appPath,
|
|
315
|
+
autoGoto,
|
|
316
|
+
baseURL,
|
|
317
|
+
browser,
|
|
318
|
+
waitForApplication,
|
|
319
|
+
mockConfig,
|
|
320
|
+
mockSessions,
|
|
321
|
+
mockSettings,
|
|
322
|
+
mockState,
|
|
323
|
+
mockTerminals,
|
|
324
|
+
mockUser,
|
|
325
|
+
tmpPath
|
|
326
|
+
} = {
|
|
327
|
+
appPath: '/lab',
|
|
328
|
+
autoGoto: true,
|
|
329
|
+
mockConfig: true,
|
|
330
|
+
mockSessions: true,
|
|
331
|
+
mockSettings: galata.DEFAULT_SETTINGS,
|
|
332
|
+
mockState: true,
|
|
333
|
+
mockTerminals: true,
|
|
334
|
+
mockUser: true,
|
|
335
|
+
tmpPath: '',
|
|
336
|
+
...options
|
|
337
|
+
};
|
|
338
|
+
const context = await browser.newContext();
|
|
339
|
+
const page = await context.newPage();
|
|
340
|
+
|
|
341
|
+
const sessions = mockSessions ? new Map<string, Session.IModel>() : null;
|
|
342
|
+
const terminals = mockTerminals
|
|
343
|
+
? new Map<string, TerminalAPI.IModel>()
|
|
344
|
+
: null;
|
|
345
|
+
|
|
346
|
+
return {
|
|
347
|
+
page: await initTestPage(
|
|
348
|
+
appPath,
|
|
349
|
+
autoGoto,
|
|
350
|
+
baseURL,
|
|
351
|
+
mockConfig,
|
|
352
|
+
mockSettings,
|
|
353
|
+
mockState,
|
|
354
|
+
mockUser,
|
|
355
|
+
page,
|
|
356
|
+
sessions,
|
|
357
|
+
terminals,
|
|
358
|
+
tmpPath,
|
|
359
|
+
waitForApplication
|
|
360
|
+
),
|
|
361
|
+
sessions,
|
|
362
|
+
terminals
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Create a new performance helper
|
|
368
|
+
*
|
|
369
|
+
* @param page Playwright page model
|
|
370
|
+
* @returns Performance helper
|
|
371
|
+
*/
|
|
372
|
+
export function newPerformanceHelper(page: Page): PerformanceHelper {
|
|
373
|
+
return new PerformanceHelper(page);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Regex to capture JupyterLab API call
|
|
378
|
+
*/
|
|
379
|
+
export namespace Routes {
|
|
380
|
+
/**
|
|
381
|
+
* Config API
|
|
382
|
+
*
|
|
383
|
+
* The config section can be found in the named group `section`.
|
|
384
|
+
*/
|
|
385
|
+
export const config = /.*\/api\/config\/(?<section>\w+)/;
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Contents API
|
|
389
|
+
*
|
|
390
|
+
* The content path can be found in the named group `path`.
|
|
391
|
+
*
|
|
392
|
+
* The path will be prefixed by '/'.
|
|
393
|
+
* The path will be undefined for the root folder.
|
|
394
|
+
*/
|
|
395
|
+
export const contents = /.*\/api\/contents(?<path>\/.+)?\?/;
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Extensions API
|
|
399
|
+
*/
|
|
400
|
+
export const extensions = /.*\/lab\/api\/extensions.*/;
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Sessions API
|
|
404
|
+
*
|
|
405
|
+
* The session id can be found in the named group `id`.
|
|
406
|
+
*
|
|
407
|
+
* The id will be prefixed by '/'.
|
|
408
|
+
*/
|
|
409
|
+
export const sessions = /.*\/api\/sessions(?<id>\/[@:-\w]+)?/;
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Settings API
|
|
413
|
+
*
|
|
414
|
+
* The schema name can be found in the named group `id`.
|
|
415
|
+
*
|
|
416
|
+
* The id will be prefixed by '/'.
|
|
417
|
+
*/
|
|
418
|
+
export const settings = /.*\/api\/settings(?<id>(\/[@:-\w]+)*)/;
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Terminals API
|
|
422
|
+
*
|
|
423
|
+
* The terminal id can be found in the named group `id`.
|
|
424
|
+
*
|
|
425
|
+
* The id will be prefixed by '/'.
|
|
426
|
+
*/
|
|
427
|
+
export const terminals = /.*\/api\/terminals(?<id>\/[@:-\w]+)?/;
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Translations API
|
|
431
|
+
*
|
|
432
|
+
* The locale can be found in the named group `id`.
|
|
433
|
+
*
|
|
434
|
+
* The id will be prefixed by '/'.
|
|
435
|
+
*/
|
|
436
|
+
export const translations = /.*\/api\/translations(?<id>\/[@:-\w]+)?/;
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Workspaces API
|
|
440
|
+
*
|
|
441
|
+
* The space name can be found in the named group `id`.
|
|
442
|
+
*
|
|
443
|
+
* The id will be prefixed by '/'.
|
|
444
|
+
*/
|
|
445
|
+
export const workspaces = /.*\/api\/workspaces(?<id>(\/[-\w]+)+)/;
|
|
446
|
+
|
|
447
|
+
/**
|
|
448
|
+
* User API
|
|
449
|
+
*/
|
|
450
|
+
export const user = /.*\/api\/me.*/;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Notebook generation helpers
|
|
455
|
+
*/
|
|
456
|
+
export namespace Notebook {
|
|
457
|
+
/**
|
|
458
|
+
* Generate a notebook with identical cells
|
|
459
|
+
*
|
|
460
|
+
* @param nCells Number of cells
|
|
461
|
+
* @param cellType Type of cells
|
|
462
|
+
* @param defaultInput Default input source
|
|
463
|
+
* @param defaultOutput Default outputs
|
|
464
|
+
* @returns The notebook
|
|
465
|
+
*/
|
|
466
|
+
export function generateNotebook(
|
|
467
|
+
nCells: number = 0,
|
|
468
|
+
cellType: nbformat.CellType = 'code',
|
|
469
|
+
defaultInput: string[] = [],
|
|
470
|
+
defaultOutput: nbformat.IOutput[] = []
|
|
471
|
+
): nbformat.INotebookContent {
|
|
472
|
+
const cells = new Array<nbformat.ICell>();
|
|
473
|
+
for (let i = 0; i < nCells; i++) {
|
|
474
|
+
const execution_count =
|
|
475
|
+
cellType === 'code'
|
|
476
|
+
? defaultOutput.length > 0
|
|
477
|
+
? i + 1
|
|
478
|
+
: null
|
|
479
|
+
: undefined;
|
|
480
|
+
const cell = makeCell({
|
|
481
|
+
cell_type: cellType,
|
|
482
|
+
source: [...defaultInput],
|
|
483
|
+
outputs: cellType === 'code' ? [...defaultOutput] : undefined,
|
|
484
|
+
execution_count
|
|
485
|
+
});
|
|
486
|
+
cells.push(cell);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
return makeNotebook(cells);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Generate a cell object
|
|
494
|
+
*
|
|
495
|
+
* @param skeleton Cell description template
|
|
496
|
+
* @returns A cell
|
|
497
|
+
*/
|
|
498
|
+
export function makeCell(
|
|
499
|
+
skeleton: Partial<nbformat.ICell>
|
|
500
|
+
): nbformat.ICell {
|
|
501
|
+
switch (skeleton.cell_type ?? 'code') {
|
|
502
|
+
case 'code':
|
|
503
|
+
return {
|
|
504
|
+
cell_type: 'code',
|
|
505
|
+
execution_count: null,
|
|
506
|
+
metadata: {},
|
|
507
|
+
outputs: [],
|
|
508
|
+
source: [],
|
|
509
|
+
...skeleton
|
|
510
|
+
};
|
|
511
|
+
default: {
|
|
512
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
513
|
+
const { execution_count, outputs, ...others } = skeleton;
|
|
514
|
+
return {
|
|
515
|
+
cell_type: 'markdown',
|
|
516
|
+
metadata: {},
|
|
517
|
+
source: [],
|
|
518
|
+
...others
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Generate a notebook object from a cell list
|
|
526
|
+
*
|
|
527
|
+
* @param cells Notebook cells
|
|
528
|
+
* @returns Notebook
|
|
529
|
+
*/
|
|
530
|
+
export function makeNotebook(
|
|
531
|
+
cells: Array<nbformat.ICell>
|
|
532
|
+
): nbformat.INotebookContent {
|
|
533
|
+
return {
|
|
534
|
+
cells,
|
|
535
|
+
metadata: {
|
|
536
|
+
kernelspec: {
|
|
537
|
+
display_name: 'Python 3',
|
|
538
|
+
language: 'python',
|
|
539
|
+
name: 'python3'
|
|
540
|
+
},
|
|
541
|
+
language_info: {
|
|
542
|
+
codemirror_mode: {
|
|
543
|
+
name: 'ipython',
|
|
544
|
+
version: 3
|
|
545
|
+
},
|
|
546
|
+
file_extension: '.py',
|
|
547
|
+
mimetype: 'text/x-python',
|
|
548
|
+
name: 'python',
|
|
549
|
+
nbconvert_exporter: 'python',
|
|
550
|
+
pygments_lexer: 'ipython3',
|
|
551
|
+
version: '3.8.0'
|
|
552
|
+
}
|
|
553
|
+
},
|
|
554
|
+
nbformat: 4,
|
|
555
|
+
nbformat_minor: 4
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Mock methods
|
|
562
|
+
*/
|
|
563
|
+
export namespace Mock {
|
|
564
|
+
/**
|
|
565
|
+
* Set last modified attributes one day ago one listing
|
|
566
|
+
* directory content.
|
|
567
|
+
*
|
|
568
|
+
* @param page Page model object
|
|
569
|
+
*
|
|
570
|
+
* #### Notes
|
|
571
|
+
* The goal is to freeze the file browser display
|
|
572
|
+
*/
|
|
573
|
+
export async function freezeContentLastModified(page: Page): Promise<void> {
|
|
574
|
+
// Listen for closing connection (may happen when request are still being processed)
|
|
575
|
+
let isClosed = false;
|
|
576
|
+
const ctxt = page.context();
|
|
577
|
+
ctxt.once('close', () => {
|
|
578
|
+
isClosed = true;
|
|
579
|
+
});
|
|
580
|
+
ctxt.browser()?.once('disconnected', () => {
|
|
581
|
+
isClosed = true;
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
return page.route(Routes.contents, async (route, request) => {
|
|
585
|
+
switch (request.method()) {
|
|
586
|
+
case 'GET': {
|
|
587
|
+
// Proxy the GET request
|
|
588
|
+
const response = await ctxt.request.fetch(request);
|
|
589
|
+
if (!response.ok()) {
|
|
590
|
+
if (!page.isClosed() && !isClosed) {
|
|
591
|
+
return route.fulfill({
|
|
592
|
+
status: response.status(),
|
|
593
|
+
body: await response.text()
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
break;
|
|
597
|
+
}
|
|
598
|
+
const data = await response.json();
|
|
599
|
+
// Modify the last_modified values to be set one day before now.
|
|
600
|
+
if (
|
|
601
|
+
data['type'] === 'directory' &&
|
|
602
|
+
Array.isArray(data['content'])
|
|
603
|
+
) {
|
|
604
|
+
const now = Date.now();
|
|
605
|
+
const aDayAgo = new Date(now - 24 * 3600 * 1000).toISOString();
|
|
606
|
+
for (const entry of data['content'] as any[]) {
|
|
607
|
+
// Mutate the list in-place
|
|
608
|
+
entry['last_modified'] = aDayAgo;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
if (!page.isClosed() && !isClosed) {
|
|
613
|
+
return route.fulfill({
|
|
614
|
+
status: 200,
|
|
615
|
+
body: JSON.stringify(data),
|
|
616
|
+
contentType: 'application/json'
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
break;
|
|
620
|
+
}
|
|
621
|
+
default:
|
|
622
|
+
return route.continue();
|
|
623
|
+
}
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* Clear all wanted sessions or terminals.
|
|
629
|
+
*
|
|
630
|
+
* @param baseURL Application base URL
|
|
631
|
+
* @param runners Session or terminal ids to stop
|
|
632
|
+
* @param type Type of runner; session or terminal
|
|
633
|
+
* @param request API request context
|
|
634
|
+
* @returns Whether the runners were closed or not
|
|
635
|
+
*/
|
|
636
|
+
export async function clearRunners(
|
|
637
|
+
request: APIRequestContext,
|
|
638
|
+
runners: string[],
|
|
639
|
+
type: 'sessions' | 'terminals'
|
|
640
|
+
): Promise<boolean> {
|
|
641
|
+
const responses = await Promise.all(
|
|
642
|
+
[...new Set(runners)].map(id =>
|
|
643
|
+
request.fetch(`/api/${type}/${id}`, {
|
|
644
|
+
method: 'DELETE'
|
|
645
|
+
})
|
|
646
|
+
)
|
|
647
|
+
);
|
|
648
|
+
return responses.every(response => response.ok());
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* Mock config route.
|
|
653
|
+
*
|
|
654
|
+
* @param page Page model object
|
|
655
|
+
* @param config In-memory config
|
|
656
|
+
*/
|
|
657
|
+
export function mockConfig(
|
|
658
|
+
page: Page,
|
|
659
|
+
config: Record<string, JSONObject>
|
|
660
|
+
): Promise<void> {
|
|
661
|
+
return page.route(Routes.config, (route, request) => {
|
|
662
|
+
const section = Routes.config.exec(request.url())?.groups
|
|
663
|
+
?.section as string;
|
|
664
|
+
switch (request.method()) {
|
|
665
|
+
case 'GET':
|
|
666
|
+
return route.fulfill({
|
|
667
|
+
status: 200,
|
|
668
|
+
body: JSON.stringify(config[section] ?? {})
|
|
669
|
+
});
|
|
670
|
+
case 'PATCH': {
|
|
671
|
+
const data = request.postDataJSON();
|
|
672
|
+
// FIXME jupyter-server does a recursive update
|
|
673
|
+
// We are not doing it here as @jupyterlab/services is actually not recursively
|
|
674
|
+
// updating the object.
|
|
675
|
+
config[section] = { ...(config[section] ?? {}), ...data };
|
|
676
|
+
return route.fulfill({
|
|
677
|
+
status: 200,
|
|
678
|
+
body: JSON.stringify(config[section])
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
case 'PUT': {
|
|
682
|
+
const data = request.postDataJSON();
|
|
683
|
+
config[section] = data;
|
|
684
|
+
return route.fulfill({ status: 204 });
|
|
685
|
+
}
|
|
686
|
+
default:
|
|
687
|
+
return route.continue();
|
|
688
|
+
}
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* Mock the runners API to display only those created during a test
|
|
694
|
+
*
|
|
695
|
+
* @param page Page model object
|
|
696
|
+
* @param runners Mapping of current test runners
|
|
697
|
+
* @param type Type of runner; session or terminal
|
|
698
|
+
*/
|
|
699
|
+
export function mockRunners(
|
|
700
|
+
page: Page,
|
|
701
|
+
runners: Map<string, any>,
|
|
702
|
+
type: 'sessions' | 'terminals'
|
|
703
|
+
): Promise<void> {
|
|
704
|
+
const routeRegex =
|
|
705
|
+
type === 'sessions' ? Routes.sessions : Routes.terminals;
|
|
706
|
+
// Listen for closing connection (may happen when request are still being processed)
|
|
707
|
+
let isClosed = false;
|
|
708
|
+
const ctxt = page.context();
|
|
709
|
+
ctxt.once('close', () => {
|
|
710
|
+
isClosed = true;
|
|
711
|
+
});
|
|
712
|
+
ctxt.browser()?.once('disconnected', () => {
|
|
713
|
+
isClosed = true;
|
|
714
|
+
});
|
|
715
|
+
return page.route(routeRegex, async (route, request) => {
|
|
716
|
+
switch (request.method()) {
|
|
717
|
+
case 'DELETE': {
|
|
718
|
+
// slice is used to remove the '/' prefix
|
|
719
|
+
const id = routeRegex.exec(request.url())?.groups?.id?.slice(1);
|
|
720
|
+
|
|
721
|
+
await route.continue();
|
|
722
|
+
|
|
723
|
+
if (id && runners.has(id)) {
|
|
724
|
+
runners.delete(id);
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
break;
|
|
728
|
+
}
|
|
729
|
+
case 'GET': {
|
|
730
|
+
// slice is used to remove the '/' prefix
|
|
731
|
+
const id = routeRegex.exec(request.url())?.groups?.id?.slice(1);
|
|
732
|
+
|
|
733
|
+
if (id) {
|
|
734
|
+
if (runners.has(id)) {
|
|
735
|
+
// Proxy the GET request
|
|
736
|
+
const response = await ctxt.request.fetch(request);
|
|
737
|
+
if (!response.ok()) {
|
|
738
|
+
if (!page.isClosed() && !isClosed) {
|
|
739
|
+
return route.fulfill({
|
|
740
|
+
status: response.status(),
|
|
741
|
+
body: await response.text()
|
|
742
|
+
});
|
|
743
|
+
}
|
|
744
|
+
break;
|
|
745
|
+
}
|
|
746
|
+
const data = await response.json();
|
|
747
|
+
// Update stored runners
|
|
748
|
+
runners.set(type === 'sessions' ? data.id : data.name, data);
|
|
749
|
+
|
|
750
|
+
if (!page.isClosed() && !isClosed) {
|
|
751
|
+
return route.fulfill({
|
|
752
|
+
status: 200,
|
|
753
|
+
body: JSON.stringify(data),
|
|
754
|
+
contentType: 'application/json'
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
break;
|
|
758
|
+
} else {
|
|
759
|
+
if (!page.isClosed() && !isClosed) {
|
|
760
|
+
return route.fulfill({
|
|
761
|
+
status: 404
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
break;
|
|
765
|
+
}
|
|
766
|
+
} else {
|
|
767
|
+
// Proxy the GET request
|
|
768
|
+
const response = await ctxt.request.fetch(request);
|
|
769
|
+
if (!response.ok()) {
|
|
770
|
+
if (!page.isClosed() && !isClosed) {
|
|
771
|
+
return route.fulfill({
|
|
772
|
+
status: response.status(),
|
|
773
|
+
body: await response.text()
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
break;
|
|
777
|
+
}
|
|
778
|
+
const data = (await response.json()) as any[];
|
|
779
|
+
const updated = new Set<string>();
|
|
780
|
+
data.forEach(item => {
|
|
781
|
+
const itemID: string =
|
|
782
|
+
type === 'sessions' ? item.id : item.name;
|
|
783
|
+
if (runners.has(itemID)) {
|
|
784
|
+
updated.add(itemID);
|
|
785
|
+
runners.set(itemID, item);
|
|
786
|
+
}
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
if (updated.size !== runners.size) {
|
|
790
|
+
for (const [runnerID] of runners) {
|
|
791
|
+
if (!updated.has(runnerID)) {
|
|
792
|
+
runners.delete(runnerID);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
if (!page.isClosed() && !isClosed) {
|
|
798
|
+
return route.fulfill({
|
|
799
|
+
status: 200,
|
|
800
|
+
body: JSON.stringify([...runners.values()]),
|
|
801
|
+
contentType: 'application/json'
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
break;
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
case 'PATCH': {
|
|
808
|
+
// Proxy the PATCH request
|
|
809
|
+
const response = await ctxt.request.fetch(request);
|
|
810
|
+
if (!response.ok()) {
|
|
811
|
+
if (!page.isClosed() && !isClosed) {
|
|
812
|
+
return route.fulfill({
|
|
813
|
+
status: response.status(),
|
|
814
|
+
body: await response.text()
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
break;
|
|
818
|
+
}
|
|
819
|
+
const data = await response.json();
|
|
820
|
+
// Update stored runners
|
|
821
|
+
runners.set(type === 'sessions' ? data.id : data.name, data);
|
|
822
|
+
|
|
823
|
+
if (!page.isClosed() && !isClosed) {
|
|
824
|
+
return route.fulfill({
|
|
825
|
+
status: 200,
|
|
826
|
+
body: JSON.stringify(data),
|
|
827
|
+
contentType: 'application/json'
|
|
828
|
+
});
|
|
829
|
+
}
|
|
830
|
+
break;
|
|
831
|
+
}
|
|
832
|
+
case 'POST': {
|
|
833
|
+
// Proxy the POST request
|
|
834
|
+
const response = await ctxt.request.fetch(request);
|
|
835
|
+
if (!response.ok()) {
|
|
836
|
+
if (!page.isClosed() && !isClosed) {
|
|
837
|
+
return route.fulfill({
|
|
838
|
+
status: response.status(),
|
|
839
|
+
body: await response.text()
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
break;
|
|
843
|
+
}
|
|
844
|
+
const data = await response.json();
|
|
845
|
+
const id = type === 'sessions' ? data.id : data.name;
|
|
846
|
+
runners.set(id, data);
|
|
847
|
+
if (!page.isClosed() && !isClosed) {
|
|
848
|
+
return route.fulfill({
|
|
849
|
+
status: type === 'sessions' ? 201 : 200,
|
|
850
|
+
body: JSON.stringify(data),
|
|
851
|
+
contentType: 'application/json',
|
|
852
|
+
headers: response.headers as any
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
break;
|
|
856
|
+
}
|
|
857
|
+
default:
|
|
858
|
+
return route.continue();
|
|
859
|
+
}
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
/**
|
|
864
|
+
* Mock workspace route.
|
|
865
|
+
*
|
|
866
|
+
* @param page Page model object
|
|
867
|
+
* @param workspace In-memory workspace
|
|
868
|
+
*/
|
|
869
|
+
export function mockState(
|
|
870
|
+
page: Page,
|
|
871
|
+
workspace: Workspace.IWorkspace
|
|
872
|
+
): Promise<void> {
|
|
873
|
+
return page.route(Routes.workspaces, (route, request) => {
|
|
874
|
+
switch (request.method()) {
|
|
875
|
+
case 'GET':
|
|
876
|
+
return route.fulfill({
|
|
877
|
+
status: 200,
|
|
878
|
+
body: JSON.stringify(workspace)
|
|
879
|
+
});
|
|
880
|
+
case 'PUT': {
|
|
881
|
+
const data = request.postDataJSON();
|
|
882
|
+
workspace.data = { ...workspace.data, ...data.data };
|
|
883
|
+
workspace.metadata = { ...workspace.metadata, ...data.metadata };
|
|
884
|
+
return route.fulfill({ status: 204 });
|
|
885
|
+
}
|
|
886
|
+
default:
|
|
887
|
+
return route.continue();
|
|
888
|
+
}
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
/**
|
|
893
|
+
* Settings REST API endpoint
|
|
894
|
+
*/
|
|
895
|
+
const settingsRegex = Routes.settings;
|
|
896
|
+
|
|
897
|
+
/**
|
|
898
|
+
* Mock settings route.
|
|
899
|
+
*
|
|
900
|
+
* @param page Page model object
|
|
901
|
+
* @param settings In-memory settings
|
|
902
|
+
* @param mockedSettings Test mocked settings
|
|
903
|
+
*/
|
|
904
|
+
export function mockSettings(
|
|
905
|
+
page: Page,
|
|
906
|
+
settings: ISettingRegistry.IPlugin[],
|
|
907
|
+
mockedSettings: Record<string, any>
|
|
908
|
+
): Promise<void> {
|
|
909
|
+
// Listen for closing connection (may happen when request are still being processed)
|
|
910
|
+
let isClosed = false;
|
|
911
|
+
const ctxt = page.context();
|
|
912
|
+
ctxt.once('close', () => {
|
|
913
|
+
isClosed = true;
|
|
914
|
+
});
|
|
915
|
+
ctxt.browser()?.once('disconnected', () => {
|
|
916
|
+
isClosed = true;
|
|
917
|
+
});
|
|
918
|
+
|
|
919
|
+
return page.route(settingsRegex, async (route, request) => {
|
|
920
|
+
switch (request.method()) {
|
|
921
|
+
case 'GET': {
|
|
922
|
+
// slice is used to remove the '/' prefix
|
|
923
|
+
const id = settingsRegex.exec(request.url())?.groups?.id.slice(1);
|
|
924
|
+
|
|
925
|
+
if (!id) {
|
|
926
|
+
// Get all settings
|
|
927
|
+
if (settings.length === 0) {
|
|
928
|
+
const response = await ctxt.request.fetch(request);
|
|
929
|
+
const loadedSettings = (await response.json())
|
|
930
|
+
.settings as ISettingRegistry.IPlugin[];
|
|
931
|
+
|
|
932
|
+
settings.push(
|
|
933
|
+
...loadedSettings.map(plugin => {
|
|
934
|
+
const mocked = mockedSettings[plugin.id] ?? {};
|
|
935
|
+
return {
|
|
936
|
+
...plugin,
|
|
937
|
+
raw: JSON.stringify(mocked),
|
|
938
|
+
settings: mocked
|
|
939
|
+
};
|
|
940
|
+
})
|
|
941
|
+
);
|
|
942
|
+
}
|
|
943
|
+
if (!page.isClosed() && !isClosed) {
|
|
944
|
+
return route.fulfill({
|
|
945
|
+
status: 200,
|
|
946
|
+
body: JSON.stringify({ settings })
|
|
947
|
+
});
|
|
948
|
+
}
|
|
949
|
+
break;
|
|
950
|
+
} else {
|
|
951
|
+
// Get specific settings
|
|
952
|
+
let pluginSettings = settings.find(setting => setting.id === id);
|
|
953
|
+
if (!pluginSettings) {
|
|
954
|
+
const response = await ctxt.request.fetch(request);
|
|
955
|
+
pluginSettings = await response.json();
|
|
956
|
+
if (pluginSettings) {
|
|
957
|
+
const mocked = mockedSettings[id] ?? {};
|
|
958
|
+
pluginSettings = {
|
|
959
|
+
...pluginSettings,
|
|
960
|
+
raw: JSON.stringify(mocked),
|
|
961
|
+
settings: mocked
|
|
962
|
+
};
|
|
963
|
+
settings.push(pluginSettings);
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
if (!page.isClosed() && !isClosed) {
|
|
968
|
+
return route.fulfill({
|
|
969
|
+
status: 200,
|
|
970
|
+
body: JSON.stringify(pluginSettings)
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
break;
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
case 'PUT': {
|
|
979
|
+
// slice is used to remove the '/' prefix
|
|
980
|
+
const id = settingsRegex.exec(request.url())?.groups?.id?.slice(1);
|
|
981
|
+
if (!id) {
|
|
982
|
+
return route.abort('addressunreachable');
|
|
983
|
+
}
|
|
984
|
+
const pluginSettings = settings.find(setting => setting.id === id);
|
|
985
|
+
const data = request.postDataJSON();
|
|
986
|
+
|
|
987
|
+
if (pluginSettings) {
|
|
988
|
+
pluginSettings.raw = data.raw;
|
|
989
|
+
try {
|
|
990
|
+
pluginSettings.settings = json5.parse(pluginSettings.raw);
|
|
991
|
+
} catch (e) {
|
|
992
|
+
console.warn(
|
|
993
|
+
`Failed to read raw settings ${pluginSettings.raw}`
|
|
994
|
+
);
|
|
995
|
+
pluginSettings.settings = {};
|
|
996
|
+
}
|
|
997
|
+
} else {
|
|
998
|
+
settings.push({
|
|
999
|
+
id,
|
|
1000
|
+
...data
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
1003
|
+
// Stop mocking if a new version is pushed
|
|
1004
|
+
delete mockedSettings[id];
|
|
1005
|
+
return route.fulfill({
|
|
1006
|
+
status: 204
|
|
1007
|
+
});
|
|
1008
|
+
}
|
|
1009
|
+
default:
|
|
1010
|
+
return route.continue();
|
|
1011
|
+
}
|
|
1012
|
+
});
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
/**
|
|
1016
|
+
* Mock user route.
|
|
1017
|
+
*
|
|
1018
|
+
* @param page Page model object
|
|
1019
|
+
* @param user In-memory user
|
|
1020
|
+
*/
|
|
1021
|
+
export function mockUser(page: Page, user: User.IUser): Promise<void> {
|
|
1022
|
+
return page.route(Routes.user, (route, request) => {
|
|
1023
|
+
switch (request.method()) {
|
|
1024
|
+
case 'GET':
|
|
1025
|
+
return route.fulfill({
|
|
1026
|
+
status: 200,
|
|
1027
|
+
body: JSON.stringify(user)
|
|
1028
|
+
});
|
|
1029
|
+
default:
|
|
1030
|
+
return route.continue();
|
|
1031
|
+
}
|
|
1032
|
+
});
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
}
|