@gooddata/sdk-e2e-utils 11.60.0-alpha.3 → 11.60.0
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/esm/create-test/feature-hub/mock.d.ts +5 -0
- package/esm/create-test/feature-hub/mock.d.ts.map +1 -0
- package/esm/create-test/feature-hub/mock.js +15 -0
- package/esm/create-test/feature-hub/types.d.ts +21 -0
- package/esm/create-test/feature-hub/types.d.ts.map +1 -0
- package/esm/create-test/feature-hub/types.js +2 -0
- package/esm/create-test/goodmock/admin.d.ts +34 -0
- package/esm/create-test/goodmock/admin.d.ts.map +1 -0
- package/esm/create-test/goodmock/admin.js +99 -0
- package/esm/create-test/goodmock/mode.d.ts +14 -0
- package/esm/create-test/goodmock/mode.d.ts.map +1 -0
- package/esm/create-test/goodmock/mode.js +17 -0
- package/esm/create-test/goodmock/playwright.d.ts +5 -0
- package/esm/create-test/goodmock/playwright.d.ts.map +1 -0
- package/esm/create-test/goodmock/playwright.js +37 -0
- package/esm/create-test/goodmock/snapshot/assert-no-leak-patterns.d.ts +9 -0
- package/esm/create-test/goodmock/snapshot/assert-no-leak-patterns.d.ts.map +1 -0
- package/esm/create-test/goodmock/snapshot/assert-no-leak-patterns.js +20 -0
- package/esm/create-test/goodmock/snapshot/assert-no-secret-leaks.d.ts +4 -0
- package/esm/create-test/goodmock/snapshot/assert-no-secret-leaks.d.ts.map +1 -0
- package/esm/create-test/goodmock/snapshot/assert-no-secret-leaks.js +14 -0
- package/esm/create-test/goodmock/snapshot/build-masked-form-pattern.d.ts +2 -0
- package/esm/create-test/goodmock/snapshot/build-masked-form-pattern.d.ts.map +1 -0
- package/esm/create-test/goodmock/snapshot/build-masked-form-pattern.js +17 -0
- package/esm/create-test/goodmock/snapshot/get-effective-secret-mappings.d.ts +3 -0
- package/esm/create-test/goodmock/snapshot/get-effective-secret-mappings.d.ts.map +1 -0
- package/esm/create-test/goodmock/snapshot/get-effective-secret-mappings.js +2 -0
- package/esm/create-test/goodmock/snapshot/replace-secrets.d.ts +10 -0
- package/esm/create-test/goodmock/snapshot/replace-secrets.d.ts.map +1 -0
- package/esm/create-test/goodmock/snapshot/replace-secrets.js +23 -0
- package/esm/create-test/goodmock/snapshot/sanitize-credentials.d.ts +4 -0
- package/esm/create-test/goodmock/snapshot/sanitize-credentials.d.ts.map +1 -0
- package/esm/create-test/goodmock/snapshot/sanitize-credentials.js +41 -0
- package/esm/create-test/goodmock/snapshot/sanitize-workspace-ids.d.ts +4 -0
- package/esm/create-test/goodmock/snapshot/sanitize-workspace-ids.d.ts.map +1 -0
- package/esm/create-test/goodmock/snapshot/sanitize-workspace-ids.js +21 -0
- package/esm/create-test/goodmock/snapshot/snapshot-and-save-recording.d.ts +50 -0
- package/esm/create-test/goodmock/snapshot/snapshot-and-save-recording.d.ts.map +1 -0
- package/esm/create-test/goodmock/snapshot/snapshot-and-save-recording.js +64 -0
- package/esm/create-test/goodmock/snapshot/snapshot-param.d.ts +11 -0
- package/esm/create-test/goodmock/snapshot/snapshot-param.d.ts.map +1 -0
- package/esm/create-test/goodmock/snapshot/snapshot-param.js +9 -0
- package/esm/create-test/goodmock/types.d.ts +60 -0
- package/esm/create-test/goodmock/types.d.ts.map +1 -0
- package/esm/create-test/goodmock/types.js +2 -0
- package/esm/create-test/playwright/call-original-fn.d.ts +17 -0
- package/esm/create-test/playwright/call-original-fn.d.ts.map +1 -0
- package/esm/create-test/playwright/call-original-fn.js +25 -0
- package/esm/create-test/playwright/describe/factory.d.ts +27 -0
- package/esm/create-test/playwright/describe/factory.d.ts.map +1 -0
- package/esm/create-test/playwright/describe/factory.js +18 -0
- package/esm/create-test/playwright/fixtures.d.ts +7 -0
- package/esm/create-test/playwright/fixtures.d.ts.map +1 -0
- package/esm/create-test/playwright/fixtures.js +2 -0
- package/esm/create-test/playwright/playwright-types.d.ts +42 -0
- package/esm/create-test/playwright/playwright-types.d.ts.map +1 -0
- package/esm/create-test/playwright/playwright-types.js +2 -0
- package/esm/create-test/playwright/test/factory.d.ts +7 -0
- package/esm/create-test/playwright/test/factory.d.ts.map +1 -0
- package/esm/create-test/playwright/test/factory.js +42 -0
- package/esm/create-test/playwright/test/types.d.ts +27 -0
- package/esm/create-test/playwright/test/types.d.ts.map +1 -0
- package/esm/create-test/playwright/test/types.js +2 -0
- package/esm/create-test/playwright/top-level-describe/build-suite-callback.d.ts +9 -0
- package/esm/create-test/playwright/top-level-describe/build-suite-callback.d.ts.map +1 -0
- package/esm/create-test/playwright/top-level-describe/build-suite-callback.js +17 -0
- package/esm/create-test/playwright/top-level-describe/factory.d.ts +8 -0
- package/esm/create-test/playwright/top-level-describe/factory.d.ts.map +1 -0
- package/esm/create-test/playwright/top-level-describe/factory.js +12 -0
- package/esm/create-test/playwright/top-level-describe/types.d.ts +17 -0
- package/esm/create-test/playwright/top-level-describe/types.d.ts.map +1 -0
- package/esm/create-test/playwright/top-level-describe/types.js +2 -0
- package/esm/create-test/playwright/types.d.ts +17 -0
- package/esm/create-test/playwright/types.d.ts.map +1 -0
- package/esm/create-test/playwright/types.js +2 -0
- package/esm/create-test/playwright/with-describe-details.d.ts +6 -0
- package/esm/create-test/playwright/with-describe-details.d.ts.map +1 -0
- package/esm/create-test/playwright/with-describe-details.js +39 -0
- package/esm/index.d.ts +14 -3
- package/esm/index.d.ts.map +1 -1
- package/esm/index.js +8 -3
- package/esm/sdk-e2e-utils.d.ts +112 -38
- package/package.json +3 -3
- package/esm/goodmock.d.ts +0 -138
- package/esm/goodmock.d.ts.map +0 -1
- package/esm/goodmock.js +0 -284
- package/esm/playwright.d.ts +0 -97
- package/esm/playwright.d.ts.map +0 -1
- package/esm/playwright.js +0 -189
package/esm/sdk-e2e-utils.d.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { APIResponse } from '@playwright/test';
|
|
2
|
-
import { Fixtures } from '@playwright/test';
|
|
2
|
+
import type { Fixtures as Fixtures_2 } from '@playwright/test';
|
|
3
3
|
import { Locator } from '@playwright/test';
|
|
4
4
|
import { Page } from '@playwright/test';
|
|
5
|
-
import { PlaywrightTestArgs } from '@playwright/test';
|
|
6
|
-
import { PlaywrightTestOptions } from '@playwright/test';
|
|
7
|
-
import { PlaywrightWorkerArgs } from '@playwright/test';
|
|
8
|
-
import { PlaywrightWorkerOptions } from '@playwright/test';
|
|
5
|
+
import type { PlaywrightTestArgs } from '@playwright/test';
|
|
6
|
+
import type { PlaywrightTestOptions } from '@playwright/test';
|
|
7
|
+
import type { PlaywrightWorkerArgs } from '@playwright/test';
|
|
8
|
+
import type { PlaywrightWorkerOptions } from '@playwright/test';
|
|
9
9
|
import { Route } from '@playwright/test';
|
|
10
|
-
import { test } from '@playwright/test';
|
|
11
|
-
import { TestDetails } from '@playwright/test';
|
|
10
|
+
import type { test } from '@playwright/test';
|
|
11
|
+
import type { TestDetails } from '@playwright/test';
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* @internal
|
|
@@ -45,6 +45,11 @@ export declare type BaseTestArgs = PlaywrightTestArgs & PlaywrightTestOptions;
|
|
|
45
45
|
*/
|
|
46
46
|
export declare type BaseWorkerArgs = PlaywrightWorkerArgs & PlaywrightWorkerOptions;
|
|
47
47
|
|
|
48
|
+
/**
|
|
49
|
+
* @internal
|
|
50
|
+
*/
|
|
51
|
+
export declare type Callback = () => void;
|
|
52
|
+
|
|
48
53
|
/**
|
|
49
54
|
* @internal
|
|
50
55
|
* Click at the center of the bounding box of the given locator.
|
|
@@ -61,7 +66,42 @@ export declare function clickByBoundingBox(page: Page, locator: Locator, options
|
|
|
61
66
|
* @internal
|
|
62
67
|
* @param options -
|
|
63
68
|
*/
|
|
64
|
-
export declare function createTest<T extends
|
|
69
|
+
export declare function createTest<T extends {} = {}, W extends {} = {}>(options: ICreateTestOptions<T, W>): ITest;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* @internal
|
|
73
|
+
*/
|
|
74
|
+
export declare type DescribeConfigure = typeof test.describe.configure;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* @internal
|
|
78
|
+
*/
|
|
79
|
+
export declare type DescribeFixme = typeof test.describe.fixme;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @internal
|
|
83
|
+
*/
|
|
84
|
+
export declare type DescribeOnly = typeof test.describe.only;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* @internal
|
|
88
|
+
*/
|
|
89
|
+
export declare type DescribeParallel = typeof test.describe.parallel;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* @internal
|
|
93
|
+
*/
|
|
94
|
+
export declare type DescribeSerial = typeof test.describe.serial;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* @internal
|
|
98
|
+
*/
|
|
99
|
+
export declare type DescribeSkip = typeof test.describe.skip;
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* @internal
|
|
103
|
+
*/
|
|
104
|
+
export declare type Fixtures<T extends {} = {}, W extends {} = {}> = Fixtures_2<T, W, BaseTestArgs, BaseWorkerArgs>;
|
|
65
105
|
|
|
66
106
|
/**
|
|
67
107
|
* @internal
|
|
@@ -115,50 +155,37 @@ export declare function hoverByBoundingBox(page: Page, locator: Locator, options
|
|
|
115
155
|
/**
|
|
116
156
|
* @internal
|
|
117
157
|
*/
|
|
118
|
-
export declare interface ICreateTestOptions<T extends
|
|
119
|
-
|
|
120
|
-
fixtures?: Fixtures<T, W, BaseTestArgs & T, BaseWorkerArgs & W>;
|
|
121
|
-
featureHubResponse?: IFeatureHubEnvironment[];
|
|
158
|
+
export declare interface ICreateTestOptions<T extends {} = {}, W extends {} = {}> extends ICustomCreateTestOptions {
|
|
159
|
+
fixtures?: Fixtures<T, W>;
|
|
122
160
|
}
|
|
123
161
|
|
|
124
162
|
/**
|
|
125
163
|
* @internal
|
|
126
164
|
*/
|
|
127
|
-
export declare interface
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
skip: {
|
|
131
|
-
(suiteName: string, specName: string, fn: () => void): void;
|
|
132
|
-
(suiteName: string, specName: string, details: IE2eTestDetails, fn: () => void): void;
|
|
133
|
-
};
|
|
165
|
+
export declare interface ICustomCreateTestOptions {
|
|
166
|
+
featureHubResponse: IFeatureHubEnvironment[];
|
|
167
|
+
goodmock?: IGoodmockOptions;
|
|
134
168
|
}
|
|
135
169
|
|
|
136
170
|
/**
|
|
137
171
|
* @internal
|
|
138
172
|
*/
|
|
139
|
-
export declare
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
};
|
|
148
|
-
only: (typeof test)["describe"]["only"];
|
|
149
|
-
configure: (typeof test)["describe"]["configure"];
|
|
150
|
-
fixme: (typeof test)["describe"]["fixme"];
|
|
151
|
-
serial: (typeof test)["describe"]["serial"];
|
|
152
|
-
parallel: (typeof test)["describe"]["parallel"];
|
|
153
|
-
};
|
|
154
|
-
};
|
|
173
|
+
export declare interface IDescribe extends IDescribeFunction {
|
|
174
|
+
skip: IDescribeFunction;
|
|
175
|
+
only: DescribeOnly;
|
|
176
|
+
configure: DescribeConfigure;
|
|
177
|
+
fixme: DescribeFixme;
|
|
178
|
+
serial: DescribeSerial;
|
|
179
|
+
parallel: DescribeParallel;
|
|
180
|
+
}
|
|
155
181
|
|
|
156
182
|
/**
|
|
157
183
|
* @internal
|
|
184
|
+
* Call signatures of the overridden `test.describe` and `test.describe.skip`.
|
|
158
185
|
*/
|
|
159
|
-
export declare interface
|
|
160
|
-
|
|
161
|
-
|
|
186
|
+
export declare interface IDescribeFunction {
|
|
187
|
+
(title: string, callback: Callback): void;
|
|
188
|
+
(title: string, details: ITestDetails, callback: Callback): void;
|
|
162
189
|
}
|
|
163
190
|
|
|
164
191
|
/**
|
|
@@ -280,6 +307,38 @@ export declare interface ISnapshotAndSaveRecordingOptions {
|
|
|
280
307
|
sanitizeMappings?: (mappings: IGoodmockMapping[]) => IGoodmockMapping[];
|
|
281
308
|
}
|
|
282
309
|
|
|
310
|
+
/**
|
|
311
|
+
* @internal
|
|
312
|
+
*/
|
|
313
|
+
export declare type ITest = Test & {
|
|
314
|
+
topLevelDescribe: ITopLevelDescribe;
|
|
315
|
+
describe: IDescribe;
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* @internal
|
|
320
|
+
*/
|
|
321
|
+
export declare interface ITestDetails extends TestDetails {
|
|
322
|
+
workspaceSettings?: WorkspaceSettings;
|
|
323
|
+
additionalWindowProperties?: WindowProperties;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* @internal
|
|
328
|
+
*/
|
|
329
|
+
export declare interface ITopLevelDescribe extends ITopLevelDescribeFunction {
|
|
330
|
+
skip: ITopLevelDescribeFunction;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* @internal
|
|
335
|
+
* Call signatures of the overridden `test.describe` and `test.describe.skip`.
|
|
336
|
+
*/
|
|
337
|
+
export declare interface ITopLevelDescribeFunction {
|
|
338
|
+
(suiteName: string, specName: string, fn: Callback): void;
|
|
339
|
+
(suiteName: string, specName: string, details: ITestDetails, fn: Callback): void;
|
|
340
|
+
}
|
|
341
|
+
|
|
283
342
|
/**
|
|
284
343
|
* @internal
|
|
285
344
|
*/
|
|
@@ -341,4 +400,19 @@ export declare function snapshotAndSaveRecording(host: string, mappingFilePath:
|
|
|
341
400
|
*/
|
|
342
401
|
export declare function startRecording(host: string, backendHost: string): Promise<void>;
|
|
343
402
|
|
|
403
|
+
/**
|
|
404
|
+
* @internal
|
|
405
|
+
*/
|
|
406
|
+
export declare type Test = typeof test;
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* @internal
|
|
410
|
+
*/
|
|
411
|
+
export declare type WindowProperties = Record<string, unknown>;
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* @internal
|
|
415
|
+
*/
|
|
416
|
+
export declare type WorkspaceSettings = Record<string, unknown>;
|
|
417
|
+
|
|
344
418
|
export { }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gooddata/sdk-e2e-utils",
|
|
3
|
-
"version": "11.60.0
|
|
3
|
+
"version": "11.60.0",
|
|
4
4
|
"description": "GoodData utility functions for Playwright E2E tests",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "GoodData",
|
|
@@ -49,8 +49,8 @@
|
|
|
49
49
|
"oxlint-plugin-eslint": "1.82.0",
|
|
50
50
|
"oxlint-tsgolint": "7.0.2001",
|
|
51
51
|
"typescript": "7.0.2",
|
|
52
|
-
"@gooddata/eslint-config": "11.60.0
|
|
53
|
-
"@gooddata/oxlint-config": "11.60.0
|
|
52
|
+
"@gooddata/eslint-config": "11.60.0",
|
|
53
|
+
"@gooddata/oxlint-config": "11.60.0"
|
|
54
54
|
},
|
|
55
55
|
"scripts": {
|
|
56
56
|
"_phase:build": "npm run build",
|
package/esm/goodmock.d.ts
DELETED
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @internal
|
|
3
|
-
* Load goodmock stub mappings from a JSON file on disk.
|
|
4
|
-
*
|
|
5
|
-
* @param host - Goodmock host:port (e.g. "backend-mock:8080")
|
|
6
|
-
* @param mappingFilePath - Absolute path to the mapping JSON file
|
|
7
|
-
*/
|
|
8
|
-
export declare function loadMappings(host: string, mappingFilePath: string): Promise<void>;
|
|
9
|
-
/**
|
|
10
|
-
* @internal
|
|
11
|
-
* Reset goodmock scenario state (sequence counters) without clearing mappings.
|
|
12
|
-
* Must be called between tests so scenario-driven stubs start from the
|
|
13
|
-
* beginning for every test — mirrors the Cypress `resetRecordingsScenarios` task.
|
|
14
|
-
*/
|
|
15
|
-
export declare function resetScenarios(host: string): Promise<void>;
|
|
16
|
-
/**
|
|
17
|
-
* @internal
|
|
18
|
-
* Clear all goodmock stub mappings.
|
|
19
|
-
*/
|
|
20
|
-
export declare function resetMappings(host: string): Promise<void>;
|
|
21
|
-
/**
|
|
22
|
-
* @internal
|
|
23
|
-
* Add a goodmock stub that mocks log requests (POST /gdc/app/projects/.../log).
|
|
24
|
-
* This prevents the app from failing when it tries to send logs.
|
|
25
|
-
*/
|
|
26
|
-
export declare function mockLogRequests(host: string): Promise<void>;
|
|
27
|
-
/**
|
|
28
|
-
* @internal
|
|
29
|
-
* Which mode goodmock is running in
|
|
30
|
-
*/
|
|
31
|
-
export declare enum GoodmockMode {
|
|
32
|
-
Replay = "replay",
|
|
33
|
-
Record = "record",
|
|
34
|
-
Proxy = "proxy"
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* @internal
|
|
38
|
-
*/
|
|
39
|
-
export declare function goodmockMode(): GoodmockMode;
|
|
40
|
-
/**
|
|
41
|
-
* @internal
|
|
42
|
-
* Add a catch-all proxy mapping so goodmock forwards every request to the
|
|
43
|
-
* real backend and records the interactions. Must be called after
|
|
44
|
-
* {@link resetMappings} in recording mode.
|
|
45
|
-
*/
|
|
46
|
-
export declare function startRecording(host: string, backendHost: string): Promise<void>;
|
|
47
|
-
/**
|
|
48
|
-
* @internal
|
|
49
|
-
*/
|
|
50
|
-
export interface IGoodmockMapping {
|
|
51
|
-
request: {
|
|
52
|
-
url?: string;
|
|
53
|
-
urlPath?: string;
|
|
54
|
-
bodyPatterns?: unknown;
|
|
55
|
-
};
|
|
56
|
-
response: {
|
|
57
|
-
headers?: Record<string, string>;
|
|
58
|
-
jsonBody?: unknown;
|
|
59
|
-
body?: string;
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
/**
|
|
63
|
-
* @internal
|
|
64
|
-
*/
|
|
65
|
-
export interface IWorkspaceIdMapping {
|
|
66
|
-
sourceWorkspaceId: string;
|
|
67
|
-
targetWorkspaceId: string;
|
|
68
|
-
}
|
|
69
|
-
/**
|
|
70
|
-
* @internal
|
|
71
|
-
* Rewrite of a secret value in saved recordings. Every occurrence of `secret` (request bodies,
|
|
72
|
-
* response bodies, URLs) is replaced with `placeholder`, so tests can type the placeholder at
|
|
73
|
-
* replay time and still match the recorded request exactly.
|
|
74
|
-
*/
|
|
75
|
-
export interface ISecretMapping {
|
|
76
|
-
secret: string;
|
|
77
|
-
placeholder: string;
|
|
78
|
-
}
|
|
79
|
-
/**
|
|
80
|
-
* @internal
|
|
81
|
-
* A credential shape that must never appear in a saved recording (checked against the fully
|
|
82
|
-
* serialized output). Provide domain-specific shapes (e.g. a provider's API key format) via
|
|
83
|
-
* {@link ISnapshotAndSaveRecordingOptions.leakPatterns} — this module has no built-in patterns of
|
|
84
|
-
* its own, since it has no knowledge of what a given consumer's traffic considers a secret.
|
|
85
|
-
*/
|
|
86
|
-
export interface ILeakPattern {
|
|
87
|
-
label: string;
|
|
88
|
-
pattern: RegExp;
|
|
89
|
-
}
|
|
90
|
-
/**
|
|
91
|
-
* @internal
|
|
92
|
-
*/
|
|
93
|
-
export interface ISnapshotAndSaveRecordingOptions {
|
|
94
|
-
/** Source/target workspace ID rewrite(s) applied before mappings are saved. */
|
|
95
|
-
workspaceIdMappings?: IWorkspaceIdMapping | IWorkspaceIdMapping[];
|
|
96
|
-
/**
|
|
97
|
-
* Real backend URL (e.g. "https://example.gooddata.com"). When provided together with
|
|
98
|
-
* baseUrl, all occurrences are replaced in the saved mappings so that recorded responses
|
|
99
|
-
* (e.g. geo tile URLs) resolve correctly during replay.
|
|
100
|
-
*/
|
|
101
|
-
backendHost?: string;
|
|
102
|
-
/** App base URL (e.g. "http://kpi-dashboards-ui:9500") that replaces backendHost references. */
|
|
103
|
-
baseUrl?: string;
|
|
104
|
-
/**
|
|
105
|
-
* Secret rewrites. Each secret value is replaced by its placeholder everywhere in the saved
|
|
106
|
-
* mappings (request bodyPatterns included, and masked variants such as "sk-prox***abcd").
|
|
107
|
-
* Saving fails hard if any secret still remains afterwards.
|
|
108
|
-
*/
|
|
109
|
-
secretMappings?: ISecretMapping[];
|
|
110
|
-
/**
|
|
111
|
-
* Credential-shaped patterns that must never appear anywhere in the saved output, regardless
|
|
112
|
-
* of which field they came from. A backstop for secrets `secretMappings` doesn't know about
|
|
113
|
-
* (e.g. a value a backend echoed back that the test never typed). Saving fails hard on a match.
|
|
114
|
-
*/
|
|
115
|
-
leakPatterns?: ILeakPattern[];
|
|
116
|
-
/**
|
|
117
|
-
* Consumer-supplied hook to redact domain-specific secrets from the collected mappings (e.g.
|
|
118
|
-
* blank a `content.apiKey` field on a settings entity) before they're merged and written out.
|
|
119
|
-
* Runs once, right after the built-in credential handling (login cookie, resolveSettings map
|
|
120
|
-
* tokens) and before workspace-id rewriting.
|
|
121
|
-
*/
|
|
122
|
-
sanitizeMappings?: (mappings: IGoodmockMapping[]) => IGoodmockMapping[];
|
|
123
|
-
}
|
|
124
|
-
/**
|
|
125
|
-
* @internal
|
|
126
|
-
* Snapshot goodmock recordings, save the combined mappings to a JSON file
|
|
127
|
-
* on disk, and sanitize any credentials from the output.
|
|
128
|
-
*
|
|
129
|
-
* A single snapshot is taken with repeatsAsScenarios enabled: goodmock turns a
|
|
130
|
-
* request into a scenario chain only when its responses differ across the
|
|
131
|
-
* recording, so stateful sequences are preserved and everything else stays flat.
|
|
132
|
-
*
|
|
133
|
-
* @param host - Goodmock host:port (e.g. "backend-mock:8080")
|
|
134
|
-
* @param mappingFilePath - Absolute path to write the mapping JSON file to
|
|
135
|
-
* @param options - See {@link ISnapshotAndSaveRecordingOptions}.
|
|
136
|
-
*/
|
|
137
|
-
export declare function snapshotAndSaveRecording(host: string, mappingFilePath: string, options?: ISnapshotAndSaveRecordingOptions): Promise<void>;
|
|
138
|
-
//# sourceMappingURL=goodmock.d.ts.map
|
package/esm/goodmock.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"goodmock.d.ts","sourceRoot":"","sources":["../src/goodmock.ts"],"names":[],"mappings":"AAKA;;;;;;GAMG;AACH,wBAAsB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,eAAe,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAyBvF;AAED;;;;;GAKG;AACH,wBAAsB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAMhE;AAED;;;GAGG;AACH,wBAAsB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAM/D;AAED;;;;GAIG;AACH,wBAAsB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAiBjE;AAED;;;GAGG;AACH,oBAAY,YAAY;IACpB,MAAM,WAAW;IACjB,MAAM,WAAW;IACjB,KAAK,UAAU;CAClB;AAED;;GAEG;AACH,wBAAgB,YAAY,IAAI,YAAY,CAE3C;AAED;;;;;GAKG;AACH,wBAAsB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAWrF;AAWD;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC7B,OAAO,EAAE;QAAE,GAAG,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;IACpE,QAAQ,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CACrF;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAChC,iBAAiB,EAAE,MAAM,CAAC;IAC1B,iBAAiB,EAAE,MAAM,CAAC;CAC7B;AAED;;;;;GAKG;AACH,MAAM,WAAW,cAAc;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;CACvB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,YAAY;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,gCAAgC;IAC7C,+EAA+E;IAC/E,mBAAmB,CAAC,EAAE,mBAAmB,GAAG,mBAAmB,EAAE,CAAC;IAClE;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,gGAAgG;IAChG,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,cAAc,CAAC,EAAE,cAAc,EAAE,CAAC;IAClC;;;;OAIG;IACH,YAAY,CAAC,EAAE,YAAY,EAAE,CAAC;IAC9B;;;;;OAKG;IACH,gBAAgB,CAAC,EAAE,CAAC,QAAQ,EAAE,gBAAgB,EAAE,KAAK,gBAAgB,EAAE,CAAC;CAC3E;AAqBD;;;;;;;;;;;;GAYG;AACH,wBAAsB,wBAAwB,CAC1C,IAAI,EAAE,MAAM,EACZ,eAAe,EAAE,MAAM,EACvB,OAAO,GAAE,gCAAqC,GAC/C,OAAO,CAAC,IAAI,CAAC,CAkKf"}
|
package/esm/goodmock.js
DELETED
|
@@ -1,284 +0,0 @@
|
|
|
1
|
-
// (C) 2026 GoodData Corporation
|
|
2
|
-
import { mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
3
|
-
import { dirname } from "path";
|
|
4
|
-
/**
|
|
5
|
-
* @internal
|
|
6
|
-
* Load goodmock stub mappings from a JSON file on disk.
|
|
7
|
-
*
|
|
8
|
-
* @param host - Goodmock host:port (e.g. "backend-mock:8080")
|
|
9
|
-
* @param mappingFilePath - Absolute path to the mapping JSON file
|
|
10
|
-
*/
|
|
11
|
-
export async function loadMappings(host, mappingFilePath) {
|
|
12
|
-
let mappings;
|
|
13
|
-
try {
|
|
14
|
-
mappings = readFileSync(mappingFilePath, "utf-8");
|
|
15
|
-
}
|
|
16
|
-
catch {
|
|
17
|
-
console.warn(`No mapping file found: ${mappingFilePath}, skipping`);
|
|
18
|
-
return;
|
|
19
|
-
}
|
|
20
|
-
let json;
|
|
21
|
-
try {
|
|
22
|
-
json = JSON.parse(mappings);
|
|
23
|
-
}
|
|
24
|
-
catch {
|
|
25
|
-
console.error(`Failed to parse mapping file: ${mappingFilePath}`);
|
|
26
|
-
return;
|
|
27
|
-
}
|
|
28
|
-
const response = await fetch(`http://${host}/__admin/mappings/import`, {
|
|
29
|
-
method: "POST",
|
|
30
|
-
headers: { "Content-Type": "application/json" },
|
|
31
|
-
body: JSON.stringify(json),
|
|
32
|
-
});
|
|
33
|
-
// oxlint-disable-next-line eslint-js/no-console
|
|
34
|
-
console.log(`Goodmock mappings loaded from ${mappingFilePath} (status: ${response.status})`);
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* @internal
|
|
38
|
-
* Reset goodmock scenario state (sequence counters) without clearing mappings.
|
|
39
|
-
* Must be called between tests so scenario-driven stubs start from the
|
|
40
|
-
* beginning for every test — mirrors the Cypress `resetRecordingsScenarios` task.
|
|
41
|
-
*/
|
|
42
|
-
export async function resetScenarios(host) {
|
|
43
|
-
const response = await fetch(`http://${host}/__admin/scenarios/reset`, {
|
|
44
|
-
method: "POST",
|
|
45
|
-
});
|
|
46
|
-
// oxlint-disable-next-line eslint-js/no-console
|
|
47
|
-
console.log(`Goodmock scenarios reset (status: ${response.status})`);
|
|
48
|
-
}
|
|
49
|
-
/**
|
|
50
|
-
* @internal
|
|
51
|
-
* Clear all goodmock stub mappings.
|
|
52
|
-
*/
|
|
53
|
-
export async function resetMappings(host) {
|
|
54
|
-
const response = await fetch(`http://${host}/__admin/reset`, {
|
|
55
|
-
method: "POST",
|
|
56
|
-
});
|
|
57
|
-
// oxlint-disable-next-line eslint-js/no-console
|
|
58
|
-
console.log(`Goodmock mappings reset (status: ${response.status})`);
|
|
59
|
-
}
|
|
60
|
-
/**
|
|
61
|
-
* @internal
|
|
62
|
-
* Add a goodmock stub that mocks log requests (POST /gdc/app/projects/.../log).
|
|
63
|
-
* This prevents the app from failing when it tries to send logs.
|
|
64
|
-
*/
|
|
65
|
-
export async function mockLogRequests(host) {
|
|
66
|
-
const response = await fetch(`http://${host}/__admin/mappings`, {
|
|
67
|
-
method: "POST",
|
|
68
|
-
headers: { "Content-Type": "application/json" },
|
|
69
|
-
body: JSON.stringify({
|
|
70
|
-
request: {
|
|
71
|
-
method: "POST",
|
|
72
|
-
urlPattern: "/gdc/app/projects/.*/log",
|
|
73
|
-
},
|
|
74
|
-
response: {
|
|
75
|
-
body: "",
|
|
76
|
-
status: 200,
|
|
77
|
-
},
|
|
78
|
-
}),
|
|
79
|
-
});
|
|
80
|
-
// oxlint-disable-next-line eslint-js/no-console
|
|
81
|
-
console.log(`Goodmock log requests mocked (status: ${response.status})`);
|
|
82
|
-
}
|
|
83
|
-
/**
|
|
84
|
-
* @internal
|
|
85
|
-
* Which mode goodmock is running in
|
|
86
|
-
*/
|
|
87
|
-
export var GoodmockMode;
|
|
88
|
-
(function (GoodmockMode) {
|
|
89
|
-
GoodmockMode["Replay"] = "replay";
|
|
90
|
-
GoodmockMode["Record"] = "record";
|
|
91
|
-
GoodmockMode["Proxy"] = "proxy";
|
|
92
|
-
})(GoodmockMode || (GoodmockMode = {}));
|
|
93
|
-
/**
|
|
94
|
-
* @internal
|
|
95
|
-
*/
|
|
96
|
-
export function goodmockMode() {
|
|
97
|
-
return process.env["GOODMOCK_MODE"];
|
|
98
|
-
}
|
|
99
|
-
/**
|
|
100
|
-
* @internal
|
|
101
|
-
* Add a catch-all proxy mapping so goodmock forwards every request to the
|
|
102
|
-
* real backend and records the interactions. Must be called after
|
|
103
|
-
* {@link resetMappings} in recording mode.
|
|
104
|
-
*/
|
|
105
|
-
export async function startRecording(host, backendHost) {
|
|
106
|
-
const response = await fetch(`http://${host}/__admin/mappings`, {
|
|
107
|
-
method: "POST",
|
|
108
|
-
headers: { "Content-Type": "application/json" },
|
|
109
|
-
body: JSON.stringify({
|
|
110
|
-
request: { method: "ANY", urlPattern: ".*" },
|
|
111
|
-
response: { proxyBaseUrl: backendHost },
|
|
112
|
-
}),
|
|
113
|
-
});
|
|
114
|
-
// oxlint-disable-next-line eslint-js/no-console
|
|
115
|
-
console.log(`Goodmock recording started, proxying to ${backendHost} (status: ${response.status})`);
|
|
116
|
-
}
|
|
117
|
-
const snapshotParams = {
|
|
118
|
-
captureHeaders: { "X-GDC-TEST-NAME": {} },
|
|
119
|
-
requestBodyPattern: {
|
|
120
|
-
matcher: "equalToJson",
|
|
121
|
-
ignoreArrayOrder: false,
|
|
122
|
-
ignoreExtraElements: false,
|
|
123
|
-
},
|
|
124
|
-
};
|
|
125
|
-
function sanitizeWorkspaceId(mappings, sourceWorkspaceId, targetWorkspaceId) {
|
|
126
|
-
if (sourceWorkspaceId === targetWorkspaceId) {
|
|
127
|
-
return mappings;
|
|
128
|
-
}
|
|
129
|
-
const dataString = JSON.stringify({ mappings });
|
|
130
|
-
const sanitizedDataString = dataString.split(sourceWorkspaceId).join(targetWorkspaceId);
|
|
131
|
-
const sanitizedData = JSON.parse(sanitizedDataString);
|
|
132
|
-
// oxlint-disable-next-line eslint-js/no-console
|
|
133
|
-
console.log(`Sanitized workspaceId in mappings (${sourceWorkspaceId} -> ${targetWorkspaceId})`);
|
|
134
|
-
return sanitizedData.mappings;
|
|
135
|
-
}
|
|
136
|
-
/**
|
|
137
|
-
* @internal
|
|
138
|
-
* Snapshot goodmock recordings, save the combined mappings to a JSON file
|
|
139
|
-
* on disk, and sanitize any credentials from the output.
|
|
140
|
-
*
|
|
141
|
-
* A single snapshot is taken with repeatsAsScenarios enabled: goodmock turns a
|
|
142
|
-
* request into a scenario chain only when its responses differ across the
|
|
143
|
-
* recording, so stateful sequences are preserved and everything else stays flat.
|
|
144
|
-
*
|
|
145
|
-
* @param host - Goodmock host:port (e.g. "backend-mock:8080")
|
|
146
|
-
* @param mappingFilePath - Absolute path to write the mapping JSON file to
|
|
147
|
-
* @param options - See {@link ISnapshotAndSaveRecordingOptions}.
|
|
148
|
-
*/
|
|
149
|
-
export async function snapshotAndSaveRecording(host, mappingFilePath, options = {}) {
|
|
150
|
-
const { workspaceIdMappings, backendHost, baseUrl, secretMappings, leakPatterns, sanitizeMappings } = options;
|
|
151
|
-
// Snapshot everything with scenarios enabled. goodmock only emits a scenario
|
|
152
|
-
// chain when a given request produced *different* responses across the
|
|
153
|
-
// recording; identical repeats collapse to a single mapping. This captures
|
|
154
|
-
// stateful sequences (e.g. a GET whose result changes after a POST) that the
|
|
155
|
-
// old per-URL dedup path silently dropped, while keeping idempotent traffic flat.
|
|
156
|
-
const snapshotRes = await fetch(`http://${host}/__admin/recordings/snapshot`, {
|
|
157
|
-
method: "POST",
|
|
158
|
-
headers: { "Content-Type": "application/json" },
|
|
159
|
-
body: JSON.stringify({
|
|
160
|
-
repeatsAsScenarios: true,
|
|
161
|
-
persist: false,
|
|
162
|
-
...snapshotParams,
|
|
163
|
-
}),
|
|
164
|
-
});
|
|
165
|
-
const snapshotData = (await snapshotRes.json());
|
|
166
|
-
const mappings = [...snapshotData.mappings];
|
|
167
|
-
// An empty snapshot means the recording journal was lost (e.g. a worker crash re-ran the
|
|
168
|
-
// beforeAll reset mid-spec) or the spec made no requests at all. Never overwrite a previous
|
|
169
|
-
// recording with an empty one, and fail the run so the broken recording cannot go unnoticed.
|
|
170
|
-
if (mappings.length === 0) {
|
|
171
|
-
throw new Error(`Recording snapshot for ${mappingFilePath} is empty — keeping the existing mapping ` +
|
|
172
|
-
`file. Investigate (worker crash mid-spec? goodmock proxy misconfigured?) and re-record.`);
|
|
173
|
-
}
|
|
174
|
-
// Sanitize credentials
|
|
175
|
-
for (const mapping of mappings) {
|
|
176
|
-
if (mapping?.request?.url === "/gdc/account/login") {
|
|
177
|
-
delete mapping.request.bodyPatterns;
|
|
178
|
-
if (mapping.response.headers) {
|
|
179
|
-
delete mapping.response.headers["Set-Cookie"];
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
// Sanitize secrets from resolveSettings responses. Match both org-level
|
|
183
|
-
// (/api/v1/actions/resolveSettings) and workspace-level variants, via either `url`
|
|
184
|
-
// (no query) or `urlPath` (with query params).
|
|
185
|
-
const requestPath = mapping?.request?.url ?? mapping?.request?.urlPath;
|
|
186
|
-
if (typeof requestPath === "string" && requestPath.includes("/resolveSettings")) {
|
|
187
|
-
try {
|
|
188
|
-
const body = mapping?.response?.jsonBody;
|
|
189
|
-
const settings = Array.isArray(body)
|
|
190
|
-
? body
|
|
191
|
-
: Array.isArray(body?.data)
|
|
192
|
-
? body.data
|
|
193
|
-
: null;
|
|
194
|
-
if (settings) {
|
|
195
|
-
for (const setting of settings) {
|
|
196
|
-
const content = setting?.content;
|
|
197
|
-
if (!content || typeof content !== "object") {
|
|
198
|
-
continue;
|
|
199
|
-
}
|
|
200
|
-
// Map tokens (agGrid, mapbox) are stored under content.value.
|
|
201
|
-
if (setting?.id === "agGridToken" || setting?.id === "mapboxToken") {
|
|
202
|
-
content["value"] = "";
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
catch {
|
|
208
|
-
console.warn(`sanitizeCredentials – resolveSettings body is not valid JSON for: ${requestPath}`);
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
// Consumer-supplied redaction for secrets this module has no knowledge of (e.g. an LLM
|
|
213
|
-
// provider's API key echoed back on a settings entity).
|
|
214
|
-
const sanitizedByCaller = sanitizeMappings ? sanitizeMappings(mappings) : mappings;
|
|
215
|
-
const mappingsToApply = Array.isArray(workspaceIdMappings)
|
|
216
|
-
? workspaceIdMappings
|
|
217
|
-
: workspaceIdMappings
|
|
218
|
-
? [workspaceIdMappings]
|
|
219
|
-
: [];
|
|
220
|
-
const sanitizedMappings = mappingsToApply.reduce((acc, { sourceWorkspaceId, targetWorkspaceId }) => sanitizeWorkspaceId(acc, sourceWorkspaceId, targetWorkspaceId), sanitizedByCaller);
|
|
221
|
-
let output = JSON.stringify({ mappings: sanitizedMappings }, null, 4) + "\n";
|
|
222
|
-
if (backendHost && baseUrl) {
|
|
223
|
-
output = output.replaceAll(backendHost, baseUrl);
|
|
224
|
-
}
|
|
225
|
-
// Replace secrets typed into the UI / sent by API helpers during recording with their
|
|
226
|
-
// replay-time placeholders. JSON-escape both sides so values containing characters that
|
|
227
|
-
// JSON.stringify escapes (quotes, backslashes) are still found in the serialized output.
|
|
228
|
-
// Provider error messages echo keys in a masked form ("sk-prox***********abcd"), so that
|
|
229
|
-
// shape is rewritten too — it still leaks the ends of a real key.
|
|
230
|
-
const escapeRegex = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
231
|
-
// Below this, prefix/suffix would overlap or leave nothing to mask — there's no safe
|
|
232
|
-
// "partial reveal" shape to detect, so such secrets rely on the exact-match replace above.
|
|
233
|
-
const MIN_MASKABLE_SECRET_LENGTH = 6;
|
|
234
|
-
// Masking keeps a variable-length prefix/suffix of the key around the asterisks; scale how
|
|
235
|
-
// much we match by the secret's own length instead of assuming a fixed provider convention
|
|
236
|
-
// (e.g. OpenAI's "sk-prox***********abcd") so shorter secrets and other providers' masking
|
|
237
|
-
// shapes are still caught.
|
|
238
|
-
const buildMaskedFormPattern = (escapedSecret) => {
|
|
239
|
-
if (escapedSecret.length < MIN_MASKABLE_SECRET_LENGTH) {
|
|
240
|
-
return undefined;
|
|
241
|
-
}
|
|
242
|
-
const prefixLength = Math.min(7, Math.floor(escapedSecret.length / 3));
|
|
243
|
-
const suffixLength = Math.min(4, Math.floor(escapedSecret.length / 4));
|
|
244
|
-
return new RegExp(`${escapeRegex(escapedSecret.slice(0, prefixLength))}[^"*\\\\]*[*]+[^"*\\\\]*${escapeRegex(escapedSecret.slice(-suffixLength))}`, "g");
|
|
245
|
-
};
|
|
246
|
-
const effectiveSecretMappings = (secretMappings ?? []).filter(({ secret, placeholder }) => secret && secret !== placeholder);
|
|
247
|
-
for (const { secret, placeholder } of effectiveSecretMappings) {
|
|
248
|
-
const escapedSecret = JSON.stringify(secret).slice(1, -1);
|
|
249
|
-
const escapedPlaceholder = JSON.stringify(placeholder).slice(1, -1);
|
|
250
|
-
output = output.replaceAll(escapedSecret, escapedPlaceholder);
|
|
251
|
-
const maskedForm = buildMaskedFormPattern(escapedSecret);
|
|
252
|
-
if (maskedForm) {
|
|
253
|
-
output = output.replace(maskedForm, escapedPlaceholder);
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
// Leak guard: never write a recording that still contains a real secret (full or masked).
|
|
257
|
-
for (const { secret } of effectiveSecretMappings) {
|
|
258
|
-
const escapedSecret = JSON.stringify(secret).slice(1, -1);
|
|
259
|
-
const maskedForm = buildMaskedFormPattern(escapedSecret);
|
|
260
|
-
if (output.includes(escapedSecret) || output.includes(secret) || maskedForm?.test(output)) {
|
|
261
|
-
throw new Error(`Recording for ${mappingFilePath} still contains a secret after sanitization ` +
|
|
262
|
-
`(e.g. in an encoded form). Refusing to save the mapping file.`);
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
// Pattern guard: catch credential-shaped values from ANY source (not just the ones the
|
|
266
|
-
// tests typed), e.g. a real key stored in a backend org setting under an unexpected field.
|
|
267
|
-
// Patterns are consumer-supplied — this module has no built-in notion of what a secret
|
|
268
|
-
// looks like for a given domain (see ISnapshotAndSaveRecordingOptions.leakPatterns).
|
|
269
|
-
for (const { label, pattern } of leakPatterns ?? []) {
|
|
270
|
-
// Reset lastIndex in case pattern carries the g/y flag — exec() on a stateful regex
|
|
271
|
-
// would otherwise resume from wherever a previous call (e.g. a prior recording) left off.
|
|
272
|
-
pattern.lastIndex = 0;
|
|
273
|
-
const match = pattern.exec(output);
|
|
274
|
-
if (match) {
|
|
275
|
-
throw new Error(`Recording for ${mappingFilePath} contains a value shaped like a ${label} ` +
|
|
276
|
-
`("${match[0].slice(0, 8)}…"). Refusing to save the mapping file — extend ` +
|
|
277
|
-
`sanitizeMappings / secretMappings so it gets sanitized.`);
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
mkdirSync(dirname(mappingFilePath), { recursive: true });
|
|
281
|
-
writeFileSync(mappingFilePath, output);
|
|
282
|
-
// oxlint-disable-next-line eslint-js/no-console
|
|
283
|
-
console.log(`Recording saved to ${mappingFilePath} (${sanitizedMappings.length} mappings)`);
|
|
284
|
-
}
|