@testim/testim-cli 3.255.0 → 3.256.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/agent/routers/cliJsCode/service.js +11 -8
- package/cli.js +2 -2
- package/cliAgentMode.js +7 -7
- package/codim/codim-cli.js +4 -1
- package/commons/featureFlags.js +21 -7
- package/commons/httpRequest.js +1 -1
- package/commons/initializeUserWithAuth.js +7 -4
- package/commons/preloadTests.js +5 -2
- package/commons/prepareRunner.js +5 -5
- package/commons/prepareRunnerAndTestimStartUtils.js +11 -4
- package/commons/runnerFileCache.js +9 -1
- package/commons/testimServicesApi.js +36 -5
- package/npm-shrinkwrap.json +41 -41
- package/package.json +1 -1
- package/player/stepActions/apiStepAction.js +49 -43
- package/player/stepActions/baseCliJsStepAction.js +19 -14
- package/player/stepActions/baseJsStepAction.js +9 -8
- package/player/stepActions/dropFileStepAction.js +1 -3
- package/player/stepActions/inputFileStepAction.js +10 -8
- package/player/stepActions/mouseStepAction.js +21 -22
- package/player/stepActions/nodePackageStepAction.js +34 -35
- package/player/stepActions/stepAction.js +1 -0
- package/player/utils/imageCaptureUtils.js +63 -63
- package/player/utils/screenshotUtils.js +16 -13
- package/player/utils/windowUtils.js +20 -8
- package/processHandler.js +4 -0
- package/runOptions.d.ts +27 -1
- package/runOptions.js +7 -7
- package/runner.js +62 -23
- package/runners/ParallelWorkerManager.js +3 -2
- package/runners/TestPlanRunner.js +9 -6
- package/runners/buildCodeTests.js +1 -0
- package/runners/runnerUtils.js +11 -2
- package/services/branchService.js +11 -5
- package/services/localRCASaver.js +4 -0
|
@@ -16,16 +16,6 @@ class RectIsOutsideOfImageError extends Error {
|
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
/**
|
|
20
|
-
* @param {never} tabId
|
|
21
|
-
* @param {import('./windowUtils')} windowUtils
|
|
22
|
-
* @param {import('./screenshotUtils')} screenshotUtils
|
|
23
|
-
* */
|
|
24
|
-
const ImageCaptureUtils = function (tabId, windowUtils, screenshotUtils) {
|
|
25
|
-
this.windowUtils = windowUtils;
|
|
26
|
-
this.screenshotUtils = screenshotUtils;
|
|
27
|
-
};
|
|
28
|
-
|
|
29
19
|
async function cropImageFromImageData(imageData, imageInfo) {
|
|
30
20
|
const _image = imageInfo.image || imageInfo;
|
|
31
21
|
const pixelRatio = imageInfo.devicePixelRatio;
|
|
@@ -114,6 +104,7 @@ function stitchImage(fullSize, parts) {
|
|
|
114
104
|
return chromeStitchImage(fullSize, parts);
|
|
115
105
|
}
|
|
116
106
|
|
|
107
|
+
// ???????????
|
|
117
108
|
function uploadDataUrl() {
|
|
118
109
|
return Promise.resolve();
|
|
119
110
|
}
|
|
@@ -148,74 +139,80 @@ function getElementAbsoluteRectangle(elementRect, pixelRatio) {
|
|
|
148
139
|
};
|
|
149
140
|
}
|
|
150
141
|
|
|
151
|
-
ImageCaptureUtils
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
142
|
+
class ImageCaptureUtils {
|
|
143
|
+
/**
|
|
144
|
+
* @param {unknown} _unusedId
|
|
145
|
+
* @param {import('./windowUtils')} windowUtils
|
|
146
|
+
* @param {import('./screenshotUtils')} screenshotUtils
|
|
147
|
+
* */
|
|
148
|
+
constructor(_unusedId, windowUtils, screenshotUtils) {
|
|
149
|
+
this.windowUtils = windowUtils;
|
|
150
|
+
this.screenshotUtils = screenshotUtils;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async takeViewPortImage() {
|
|
154
|
+
const imageInfo = await this.screenshotUtils.takeScreenshot();
|
|
155
|
+
return ((typeof imageInfo === 'string') ? imageInfo : imageInfo.image);
|
|
156
|
+
}
|
|
156
157
|
|
|
157
158
|
takeImageForComparison() {
|
|
158
159
|
return this.takeViewPortImage();
|
|
159
|
-
}
|
|
160
|
+
}
|
|
160
161
|
|
|
161
|
-
takeAreaDataUrl(areas, format) {
|
|
162
|
+
async takeAreaDataUrl(areas, format) {
|
|
162
163
|
// Future changes in clickim will pass parameters to this function as a single object
|
|
163
164
|
if (areas.areas) {
|
|
164
165
|
areas = areas.areas;
|
|
165
166
|
}
|
|
166
167
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
},
|
|
168
|
+
const imageInfo = await this.screenshotUtils.takeScreenshot(format);
|
|
169
|
+
const result = await cropImageFromImageData(areas, imageInfo);
|
|
170
|
+
result.screenImage = imageInfo.image;
|
|
171
|
+
result.absoluteScreenHighlight = getElementAbsoluteRectangle(areas.elementRect, imageInfo.devicePixelRatio);
|
|
172
|
+
return result;
|
|
173
|
+
}
|
|
174
174
|
|
|
175
|
-
takeArea(areas) {
|
|
175
|
+
async takeArea(areas) {
|
|
176
176
|
// Future changes in clickim will pass parameters to this function as a single object
|
|
177
177
|
if (areas.areas) {
|
|
178
178
|
areas = areas.areas;
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
}).then(uploadAllDataUrls);
|
|
188
|
-
},
|
|
181
|
+
const imageInfo = await this.screenshotUtils.takeScreenshot();
|
|
182
|
+
return uploadAllDataUrls({
|
|
183
|
+
screenImage: imageInfo.image,
|
|
184
|
+
absoluteScreenHighlight: getElementAbsoluteRectangle(areas.elementRect, imageInfo.devicePixelRatio),
|
|
185
|
+
});
|
|
186
|
+
}
|
|
189
187
|
|
|
190
188
|
forcePixelRatio(forceRatio) {
|
|
191
189
|
return this.screenshotUtils.forcePixelRatio(forceRatio);
|
|
192
|
-
}
|
|
190
|
+
}
|
|
193
191
|
|
|
194
192
|
getCurrentDevicePixelRatio() {
|
|
195
193
|
return this.screenshotUtils.getCurrentDevicePixelRatio();
|
|
196
|
-
}
|
|
194
|
+
}
|
|
197
195
|
|
|
198
196
|
async takeStitchedDataUrl(useImprovedScreenshotStitching) {
|
|
199
|
-
const
|
|
200
|
-
const getCurrentScrollPosition = windowUtil.getCurrentScrollPosition.bind(windowUtil);
|
|
197
|
+
const { windowUtils, screenshotUtils } = this;
|
|
201
198
|
|
|
202
|
-
const that = this;
|
|
203
199
|
const stabilize = () => utils.delay(250);
|
|
204
200
|
const usingImprovedStitching = Boolean(useImprovedScreenshotStitching);
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
(
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
201
|
+
|
|
202
|
+
async function createPart(position, crop) {
|
|
203
|
+
if (usingImprovedStitching) {
|
|
204
|
+
await windowUtils.scrollToPositionWithoutAnimation(position);
|
|
205
|
+
} else {
|
|
206
|
+
await windowUtils.scrollToPosition(position);
|
|
207
|
+
}
|
|
208
|
+
await stabilize();
|
|
209
|
+
const imageInfo = await screenshotUtils.takeScreenshot();
|
|
210
|
+
const cropResult = await cropImageFromImageData({ elementRect: crop }, imageInfo);
|
|
211
|
+
return ({
|
|
212
|
+
position: { left: position.x + crop.left, top: position.y + crop.top },
|
|
213
|
+
size: { width: crop.width, height: crop.height },
|
|
214
|
+
image: cropResult.elementImage,
|
|
215
|
+
});
|
|
219
216
|
}
|
|
220
217
|
|
|
221
218
|
async function takeAllParts(positionsData) {
|
|
@@ -232,36 +229,39 @@ ImageCaptureUtils.prototype = {
|
|
|
232
229
|
const VPW = viewPortSize.width;
|
|
233
230
|
const FPH = Math.max(fullPageSize.height, viewPortSize.height);
|
|
234
231
|
const VPH = viewPortSize.height;
|
|
235
|
-
const Ws =
|
|
232
|
+
const Ws = Array.from({ length: Math.ceil(FPW / VPW) }, (__, i) => ({
|
|
236
233
|
scrollX: Math.min(i * VPW, FPW - VPW),
|
|
237
234
|
cropX: i * VPW - Math.min(i * VPW, FPW - VPW),
|
|
238
235
|
cropW: VPW - (i * VPW - Math.min(i * VPW, FPW - VPW)),
|
|
239
236
|
}));
|
|
240
|
-
const Hs =
|
|
237
|
+
const Hs = Array.from({ length: Math.ceil(FPH / VPH) }, (__, i) => ({
|
|
241
238
|
scrollY: Math.min(i * VPH, FPH - VPH),
|
|
242
239
|
cropY: i * VPH - Math.min(i * VPH, FPH - VPH),
|
|
243
240
|
cropH: VPH - (i * VPH - Math.min(i * VPH, FPH - VPH)),
|
|
244
241
|
}));
|
|
245
|
-
|
|
242
|
+
return Ws.flatMap(w => Hs.map(h => ({
|
|
246
243
|
scrollPos: { x: w.scrollX, y: h.scrollY },
|
|
247
244
|
cropData: {
|
|
248
|
-
top: h.cropY,
|
|
245
|
+
top: h.cropY,
|
|
246
|
+
left: w.cropX,
|
|
247
|
+
width: w.cropW,
|
|
248
|
+
height: h.cropH,
|
|
249
249
|
},
|
|
250
|
-
}))
|
|
251
|
-
|
|
250
|
+
}))
|
|
251
|
+
);
|
|
252
252
|
}
|
|
253
253
|
|
|
254
254
|
async function createStitchImage(fullPageSize, viewPortSize) {
|
|
255
|
-
const originalPosition = await getCurrentScrollPosition();
|
|
255
|
+
const originalPosition = await windowUtils.getCurrentScrollPosition();
|
|
256
256
|
const positions = getPartsPositions(fullPageSize, viewPortSize);
|
|
257
257
|
const parts = await takeAllParts(positions);
|
|
258
|
-
await
|
|
258
|
+
await windowUtils.scrollToPosition(originalPosition);
|
|
259
259
|
return stitchImage(fullPageSize, parts);
|
|
260
260
|
}
|
|
261
261
|
|
|
262
|
-
const [fullPageSize, viewPortSize] = await Promise.all([
|
|
262
|
+
const [fullPageSize, viewPortSize] = await Promise.all([windowUtils.getFullPageSize(), windowUtils.getViewportSize()]);
|
|
263
263
|
return await createStitchImage(fullPageSize, viewPortSize);
|
|
264
|
-
}
|
|
265
|
-
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
266
|
|
|
267
267
|
module.exports = ImageCaptureUtils;
|
|
@@ -1,9 +1,15 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
const Promise = require('bluebird');
|
|
4
6
|
const pRetry = require('p-retry');
|
|
5
7
|
|
|
6
8
|
class ScreenshotUtils {
|
|
9
|
+
/**
|
|
10
|
+
* @param {any} tabId
|
|
11
|
+
* @param {import('../webdriver')} driver
|
|
12
|
+
*/
|
|
7
13
|
constructor(tabId, driver, options = { takeScreenshots: true }) {
|
|
8
14
|
this.tabId = tabId;
|
|
9
15
|
this.driver = driver;
|
|
@@ -21,23 +27,21 @@ class ScreenshotUtils {
|
|
|
21
27
|
return this.options.takeScreenshots;
|
|
22
28
|
}
|
|
23
29
|
|
|
24
|
-
takeScreenshot() {
|
|
30
|
+
async takeScreenshot() {
|
|
25
31
|
if (!this.shouldTakeScreenshots()) {
|
|
26
|
-
return
|
|
32
|
+
return { devicePixelRatio: 1, image: '' };
|
|
27
33
|
}
|
|
28
34
|
const MAX_RETRY_COUNT = 3;
|
|
29
35
|
const SCREENSHOT_RETRY_DELAY = 2000;
|
|
30
36
|
const devicePixelRatioPromise = this.currentDevicePixelRatio ? Promise.resolve(this.currentDevicePixelRatio) : this.getDevicePixelRatio();
|
|
31
37
|
const getScreenshot = () => Promise.all([devicePixelRatioPromise, this.driver.takeScreenshot()]);
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
};
|
|
40
|
-
});
|
|
38
|
+
const [devicePixelRatio, image] = await pRetry(getScreenshot, { retries: MAX_RETRY_COUNT, minTimeout: SCREENSHOT_RETRY_DELAY });
|
|
39
|
+
const base64 = image ? image.value : '';
|
|
40
|
+
const dataUrl = `data:image/png;base64,${this.base64AddPadding(base64.replace(/[\r\n]/g, ''))}`;
|
|
41
|
+
return {
|
|
42
|
+
image: dataUrl,
|
|
43
|
+
devicePixelRatio,
|
|
44
|
+
};
|
|
41
45
|
}
|
|
42
46
|
|
|
43
47
|
getDevicePixelRatio() {
|
|
@@ -49,7 +53,7 @@ class ScreenshotUtils {
|
|
|
49
53
|
}
|
|
50
54
|
}
|
|
51
55
|
|
|
52
|
-
return this.driver.executeJS(devicePixelRatioJS).then(result =>
|
|
56
|
+
return this.driver.executeJS(devicePixelRatioJS).then(result => result.value);
|
|
53
57
|
}
|
|
54
58
|
|
|
55
59
|
forcePixelRatio(forceRatio = 1) {
|
|
@@ -63,4 +67,3 @@ class ScreenshotUtils {
|
|
|
63
67
|
}
|
|
64
68
|
|
|
65
69
|
module.exports = ScreenshotUtils;
|
|
66
|
-
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
|
|
1
3
|
'use strict';
|
|
2
4
|
|
|
3
5
|
const Promise = require('bluebird');
|
|
@@ -7,6 +9,10 @@ const { PageNotAvailableError } = require('../../errors');
|
|
|
7
9
|
const logger = require('../../commons/logger').getLogger('window-utils');
|
|
8
10
|
|
|
9
11
|
class WindowUtils {
|
|
12
|
+
/**
|
|
13
|
+
* @param {number} id tab or worker id
|
|
14
|
+
* @param {import('../webdriver')} driver
|
|
15
|
+
* */
|
|
10
16
|
constructor(id, driver) {
|
|
11
17
|
this.id = id;
|
|
12
18
|
this.driver = driver;
|
|
@@ -131,12 +137,12 @@ class WindowUtils {
|
|
|
131
137
|
});
|
|
132
138
|
}
|
|
133
139
|
|
|
134
|
-
setViewportSize(size) {
|
|
135
|
-
|
|
136
|
-
|
|
140
|
+
async setViewportSize(size) {
|
|
141
|
+
await this.driver.setViewportSize(size.width, size.height);
|
|
142
|
+
return await this.checkSize(size);
|
|
137
143
|
}
|
|
138
144
|
|
|
139
|
-
validatePageIsAvailable() {
|
|
145
|
+
async validatePageIsAvailable() {
|
|
140
146
|
/* eslint-disable */
|
|
141
147
|
function pageIsAvailable() {
|
|
142
148
|
var locationObj;
|
|
@@ -153,7 +159,8 @@ class WindowUtils {
|
|
|
153
159
|
}
|
|
154
160
|
/* eslint-enable */
|
|
155
161
|
|
|
156
|
-
|
|
162
|
+
const result = await this.driver.executeJS(pageIsAvailable);
|
|
163
|
+
return await (result.value ? Promise.resolve() : Promise.reject(new PageNotAvailableError()));
|
|
157
164
|
}
|
|
158
165
|
|
|
159
166
|
focusTab() {
|
|
@@ -164,9 +171,14 @@ class WindowUtils {
|
|
|
164
171
|
return undefined;
|
|
165
172
|
}
|
|
166
173
|
|
|
167
|
-
getOsAndBrowser() {
|
|
168
|
-
|
|
169
|
-
|
|
174
|
+
async getOsAndBrowser() {
|
|
175
|
+
const osAndBrowser = await pRetry(() => this.driver.getBrowserAndOS(), { retries: 3 });
|
|
176
|
+
return {
|
|
177
|
+
uaBrowserName: osAndBrowser.browser,
|
|
178
|
+
uaOs: osAndBrowser.os,
|
|
179
|
+
userAgent: osAndBrowser.userAgent,
|
|
180
|
+
browserVersion: osAndBrowser.browserVersion,
|
|
181
|
+
};
|
|
170
182
|
}
|
|
171
183
|
|
|
172
184
|
getUserAgentInfo() {
|
package/processHandler.js
CHANGED
|
@@ -7,6 +7,10 @@ const logger = require('./commons/logger').getLogger('process-handler');
|
|
|
7
7
|
|
|
8
8
|
const exitHooks = [];
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* @param {(e: any) => void} onExit
|
|
12
|
+
* @param {NodeJS.Process=} _process
|
|
13
|
+
*/
|
|
10
14
|
module.exports = function (onExit, _process = process) {
|
|
11
15
|
async function cleanup(err) {
|
|
12
16
|
// give cleanup and socket reports a chance to run
|
package/runOptions.d.ts
CHANGED
|
@@ -70,6 +70,11 @@ interface LightweightSettings {
|
|
|
70
70
|
disableProjectDefaults: boolean;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
type InitUserWithAuth = Awaited<ReturnType<typeof import('./commons/testimServicesApi')['initializeUserWithAuth']>>;
|
|
74
|
+
interface ProjectData extends Pick<InitUserWithAuth['projectById'], 'type' | 'name' | 'defaults'> {
|
|
75
|
+
projectId: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
73
78
|
// TODO: consider typing better, based on the validations we do (example: must have one of grid/grid-id/host&port/test plan)
|
|
74
79
|
interface RunnerOptions extends Partial<Omit<TunnelOptions, 'tunnelOnlyMode' | 'tunnel'>> {
|
|
75
80
|
token: string;
|
|
@@ -103,6 +108,26 @@ interface RunnerOptions extends Partial<Omit<TunnelOptions, 'tunnelOnlyMode' | '
|
|
|
103
108
|
retentionDays?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10;
|
|
104
109
|
retries?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20;
|
|
105
110
|
|
|
111
|
+
// #region Data set based on values from server
|
|
112
|
+
company: {
|
|
113
|
+
companyId: string;
|
|
114
|
+
onprem: boolean;
|
|
115
|
+
storageBaseUrl: string;
|
|
116
|
+
storageType: string;
|
|
117
|
+
name: string;
|
|
118
|
+
planType: 'pro' | 'free' | 'trial';
|
|
119
|
+
isPOC: boolean;
|
|
120
|
+
isStartUp: boolean;
|
|
121
|
+
activePlan: import('services/src/commons/mongo/db-dto-definitions/DbCompanyProjects').Plan;
|
|
122
|
+
};
|
|
123
|
+
gridData?: Awaited<ReturnType<typeof import('./services/gridService')['getGridData']>>;
|
|
124
|
+
sfdcCredential?: string;
|
|
125
|
+
editorUrl?: string;
|
|
126
|
+
allGrids?: InitUserWithAuth['allGrids'];
|
|
127
|
+
authData?: InitUserWithAuth['authData'];
|
|
128
|
+
projectData?: ProjectData;
|
|
129
|
+
// #endregion
|
|
130
|
+
|
|
106
131
|
// #region What to execute
|
|
107
132
|
testId: string[];
|
|
108
133
|
name: string[];
|
|
@@ -161,7 +186,7 @@ interface RunnerOptions extends Partial<Omit<TunnelOptions, 'tunnelOnlyMode' | '
|
|
|
161
186
|
|
|
162
187
|
// #region Extension debugging
|
|
163
188
|
ext?: string;
|
|
164
|
-
extensionLocation
|
|
189
|
+
extensionLocation: string[];
|
|
165
190
|
extensionPath?: string;
|
|
166
191
|
// #endregion
|
|
167
192
|
|
|
@@ -243,6 +268,7 @@ interface RunnerOptions extends Partial<Omit<TunnelOptions, 'tunnelOnlyMode' | '
|
|
|
243
268
|
lightweightMode?: LightweightSettings;
|
|
244
269
|
createPrefechedData?: boolean;
|
|
245
270
|
saveRCALocally?: boolean | string;
|
|
271
|
+
localRCASaver?: string;
|
|
246
272
|
// #endregion
|
|
247
273
|
|
|
248
274
|
// #region intersections
|
package/runOptions.js
CHANGED
|
@@ -804,7 +804,7 @@ module.exports = {
|
|
|
804
804
|
|
|
805
805
|
program.port = program.port && Number(program.port);
|
|
806
806
|
|
|
807
|
-
if (program.retries <= 0 ||
|
|
807
|
+
if (program.retries <= 0 || Number.isNaN(program.retries)) {
|
|
808
808
|
throw new ArgError('test failure retry count could not be a negative number or string, --retries <max_num_of_retries>');
|
|
809
809
|
}
|
|
810
810
|
|
|
@@ -823,27 +823,27 @@ module.exports = {
|
|
|
823
823
|
}
|
|
824
824
|
}
|
|
825
825
|
|
|
826
|
-
if (program.browserTimeout <= 0 ||
|
|
826
|
+
if (program.browserTimeout <= 0 || Number.isNaN(program.browserTimeout)) {
|
|
827
827
|
throw new ArgError('get browser timeout could not be a negative number, --browser-timeout <get-browser-timeout>');
|
|
828
828
|
}
|
|
829
829
|
|
|
830
|
-
if (program.newBrowserWaitTimeout <= 0 ||
|
|
830
|
+
if (program.newBrowserWaitTimeout <= 0 || Number.isNaN(program.newBrowserWaitTimeout)) {
|
|
831
831
|
throw new ArgError('max new browser wait timeout could not be a negative number, --new-browser-wait-timeout <max-wait-to-browser>');
|
|
832
832
|
}
|
|
833
833
|
|
|
834
|
-
if (program.timeout <= 0 ||
|
|
834
|
+
if (program.timeout <= 0 || Number.isNaN(program.timeout)) {
|
|
835
835
|
throw new ArgError('test run timeout could not be a negative number, --timeout <run-timeout>');
|
|
836
836
|
}
|
|
837
837
|
|
|
838
|
-
if (program.beforeParallel <= 0 ||
|
|
838
|
+
if (program.beforeParallel <= 0 || Number.isNaN(program.beforeParallel)) {
|
|
839
839
|
throw new ArgError('before-parallel could not be a negative number or not number, --before-parallel <number-of-tests>');
|
|
840
840
|
}
|
|
841
841
|
|
|
842
|
-
if (program.parallel <= 0 ||
|
|
842
|
+
if (program.parallel <= 0 || Number.isNaN(program.parallel)) {
|
|
843
843
|
throw new ArgError('parallel could not be a negative number or not number, --parallel <number-of-tests>');
|
|
844
844
|
}
|
|
845
845
|
|
|
846
|
-
if (program.afterParallel <= 0 ||
|
|
846
|
+
if (program.afterParallel <= 0 || Number.isNaN(program.afterParallel)) {
|
|
847
847
|
throw new ArgError('after-parallel could not be a negative number or not number, --after-parallel <number-of-tests>');
|
|
848
848
|
}
|
|
849
849
|
|
package/runner.js
CHANGED
|
@@ -1,32 +1,34 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
/* eslint-disable no-console */
|
|
4
|
-
const { CLI_MODE } = require('./commons/constants');
|
|
5
4
|
const _ = require('lodash');
|
|
6
|
-
const { EDITOR_URL } = require('./commons/config');
|
|
7
|
-
const tunnel = require('./commons/testimTunnel');
|
|
8
5
|
const utils = require('./utils');
|
|
9
6
|
const reporter = require('./reports/reporter');
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
const
|
|
7
|
+
const npmDriver = require('./testimNpmDriver');
|
|
8
|
+
const tunnel = require('./commons/testimTunnel');
|
|
9
|
+
const perf = require('./commons/performance-logger');
|
|
10
|
+
const gridService = require('./services/gridService');
|
|
14
11
|
const analytics = require('./commons/testimAnalytics');
|
|
12
|
+
const featureFlags = require('./commons/featureFlags');
|
|
15
13
|
const branchService = require('./services/branchService');
|
|
16
|
-
const
|
|
14
|
+
const servicesApi = require('./commons/testimServicesApi');
|
|
15
|
+
const TestPlanRunner = require('./runners/TestPlanRunner');
|
|
16
|
+
const socketService = require('./commons/socket/socketService');
|
|
17
|
+
const testimCustomToken = require('./commons/testimCustomToken');
|
|
18
|
+
const labFeaturesService = require('./services/labFeaturesService');
|
|
19
|
+
const featureAvailabilityService = require('./commons/featureAvailabilityService');
|
|
20
|
+
const { getLogger } = require('./commons/logger');
|
|
21
|
+
const { EDITOR_URL } = require('./commons/config');
|
|
22
|
+
const { CLI_MODE } = require('./commons/constants');
|
|
17
23
|
const { ArgError, QuotaDepletedError } = require('./errors');
|
|
18
|
-
const featureFlags = require('./commons/featureFlags');
|
|
19
|
-
const perf = require('./commons/performance-logger');
|
|
20
24
|
const { prepareMockNetwork, initializeUserWithAuth } = require('./commons/prepareRunner');
|
|
21
25
|
|
|
22
26
|
const FREE_PLAN_MINIMUM_BROWSER_TIMEOUT = 30 * 60 * 1000;
|
|
23
27
|
|
|
24
|
-
const TestPlanRunner = require('./runners/TestPlanRunner');
|
|
25
|
-
const labFeaturesService = require('./services/labFeaturesService');
|
|
26
|
-
const featureAvailabilityService = require('./commons/featureAvailabilityService');
|
|
27
28
|
|
|
28
|
-
const logger =
|
|
29
|
+
const logger = getLogger('runner');
|
|
29
30
|
|
|
31
|
+
/** @param {import('./runOptions').RunnerOptions} options */
|
|
30
32
|
function validateCLIRunsAreAllowed(options) {
|
|
31
33
|
const hasCliAccess = _.get(options, 'company.activePlan.premiumFeatures.allowCLI');
|
|
32
34
|
|
|
@@ -34,14 +36,16 @@ function validateCLIRunsAreAllowed(options) {
|
|
|
34
36
|
const projectId = options.project;
|
|
35
37
|
analytics.track(options.authData.uid, 'cli-not-supported', { projectId });
|
|
36
38
|
console.warn('Testim CLI is not supported in this plan');
|
|
39
|
+
// TODO: shouldn't this throw an error? 🤔
|
|
37
40
|
}
|
|
38
41
|
}
|
|
39
42
|
|
|
43
|
+
/** @param {import('./runOptions').RunnerOptions} options */
|
|
40
44
|
async function validateProjectQuotaNotDepleted(options) {
|
|
41
45
|
const projectId = options.project;
|
|
42
46
|
|
|
43
47
|
const usage = await servicesApi.getUsageForCurrentBillingPeriod(projectId);
|
|
44
|
-
const isExecutionBlocked = usage
|
|
48
|
+
const isExecutionBlocked = usage?.isExecutionBlocked;
|
|
45
49
|
if (!isExecutionBlocked) {
|
|
46
50
|
return;
|
|
47
51
|
}
|
|
@@ -51,6 +55,10 @@ async function validateProjectQuotaNotDepleted(options) {
|
|
|
51
55
|
throw new QuotaDepletedError();
|
|
52
56
|
}
|
|
53
57
|
|
|
58
|
+
/**
|
|
59
|
+
* @param {import('./runOptions').RunnerOptions} options
|
|
60
|
+
* @param {Awaited<ReturnType<initializeUserWithAuth>>['companyByProjectId']} company
|
|
61
|
+
*/
|
|
54
62
|
function validateOptionsForCompany(options, company) {
|
|
55
63
|
const optionsRetention = options.retentionDays;
|
|
56
64
|
if (!optionsRetention) {
|
|
@@ -63,8 +71,9 @@ function validateOptionsForCompany(options, company) {
|
|
|
63
71
|
}
|
|
64
72
|
}
|
|
65
73
|
|
|
74
|
+
/** @param {import('./runOptions').RunnerOptions} options */
|
|
66
75
|
async function validateCliAccount(options) {
|
|
67
|
-
if (options.lightweightMode
|
|
76
|
+
if (options.lightweightMode?.disableQuotaBlocking) {
|
|
68
77
|
return;
|
|
69
78
|
}
|
|
70
79
|
try {
|
|
@@ -73,13 +82,14 @@ async function validateCliAccount(options) {
|
|
|
73
82
|
validateCLIRunsAreAllowed(options),
|
|
74
83
|
]);
|
|
75
84
|
} catch (err) {
|
|
76
|
-
if (
|
|
85
|
+
if ([ArgError, QuotaDepletedError].some(errType => err instanceof errType)) {
|
|
77
86
|
throw err;
|
|
78
87
|
}
|
|
79
88
|
logger.error('could not validate cli account', { err });
|
|
80
89
|
}
|
|
81
90
|
}
|
|
82
91
|
|
|
92
|
+
/** @param {string} projectId */
|
|
83
93
|
function analyticsIdentify(projectId) {
|
|
84
94
|
const authData = testimCustomToken.getTokenV3UserData();
|
|
85
95
|
return analytics.identify({
|
|
@@ -95,6 +105,7 @@ function analyticsIdentify(projectId) {
|
|
|
95
105
|
});
|
|
96
106
|
}
|
|
97
107
|
|
|
108
|
+
/** @param {string} projectId */
|
|
98
109
|
function initSocketServices(projectId, { disableResults = false, disableRemoteStep = false }) {
|
|
99
110
|
if (featureFlags.flags.useNewWSCLI.isEnabled() && !disableResults && !disableRemoteStep) {
|
|
100
111
|
return socketService.connect(projectId);
|
|
@@ -110,6 +121,10 @@ function initSocketServices(projectId, { disableResults = false, disableRemoteSt
|
|
|
110
121
|
return undefined;
|
|
111
122
|
}
|
|
112
123
|
|
|
124
|
+
/**
|
|
125
|
+
* @param {import('./runOptions').RunnerOptions} options
|
|
126
|
+
* @param {Awaited<ReturnType<initializeUserWithAuth>>['branchName']} branchInfoFromServer
|
|
127
|
+
*/
|
|
113
128
|
function setBranch(options, branchInfoFromServer) {
|
|
114
129
|
const { branch, autoDetect } = options;
|
|
115
130
|
branchService.setCurrentBranch(branchInfoFromServer, autoDetect);
|
|
@@ -118,14 +133,19 @@ function setBranch(options, branchInfoFromServer) {
|
|
|
118
133
|
}
|
|
119
134
|
}
|
|
120
135
|
|
|
136
|
+
/** @param {import('./runOptions').RunnerOptions} options */
|
|
121
137
|
async function setSfdcCredential(options) {
|
|
122
|
-
const { projectData: { projectId } } = options;
|
|
138
|
+
const { projectData: { projectId } = {} } = options;
|
|
123
139
|
const branch = branchService.getCurrentBranch();
|
|
124
140
|
if (_.get(options, 'company.activePlan.premiumFeatures.ttaForSalesforce')) {
|
|
125
141
|
options.sfdcCredential = await servicesApi.loadSfdcCredential({ projectId, branch });
|
|
126
142
|
}
|
|
127
143
|
}
|
|
128
144
|
|
|
145
|
+
/**
|
|
146
|
+
* @param {import('./runOptions').RunnerOptions} options
|
|
147
|
+
* @param {Awaited<ReturnType<initializeUserWithAuth>>['companyByProjectId']} company
|
|
148
|
+
*/
|
|
129
149
|
function setCompany(options, company) {
|
|
130
150
|
const { onprem, id, storageBaseUrl, storageType, name, activePlan = {} } = company;
|
|
131
151
|
if (onprem) {
|
|
@@ -161,6 +181,10 @@ function setCompany(options, company) {
|
|
|
161
181
|
};
|
|
162
182
|
}
|
|
163
183
|
|
|
184
|
+
/**
|
|
185
|
+
* @param {import('./runOptions').RunnerOptions} options
|
|
186
|
+
* @param {Awaited<ReturnType<initializeUserWithAuth>>['editorConfig']} editorConfig
|
|
187
|
+
*/
|
|
164
188
|
function setSystemInfo(options, editorConfig) {
|
|
165
189
|
if (EDITOR_URL) {
|
|
166
190
|
options.editorUrl = EDITOR_URL;
|
|
@@ -169,14 +193,26 @@ function setSystemInfo(options, editorConfig) {
|
|
|
169
193
|
options.editorUrl = editorConfig.editorUrl;
|
|
170
194
|
}
|
|
171
195
|
|
|
196
|
+
/**
|
|
197
|
+
* @param {import('./runOptions').RunnerOptions} options
|
|
198
|
+
* @param {Awaited<ReturnType<initializeUserWithAuth>>['allGrids']} allGrids
|
|
199
|
+
*/
|
|
172
200
|
function setAllGrids(options, allGrids) {
|
|
173
201
|
options.allGrids = allGrids;
|
|
174
202
|
}
|
|
175
203
|
|
|
204
|
+
/**
|
|
205
|
+
* @param {import('./runOptions').RunnerOptions} options
|
|
206
|
+
* @param {Awaited<ReturnType<initializeUserWithAuth>>['authData']} authData
|
|
207
|
+
*/
|
|
176
208
|
function setAuthData(options, authData) {
|
|
177
209
|
options.authData = authData;
|
|
178
210
|
}
|
|
179
211
|
|
|
212
|
+
/**
|
|
213
|
+
* @param {import('./runOptions').RunnerOptions} options
|
|
214
|
+
* @param {Awaited<ReturnType<initializeUserWithAuth>>['projectById']} project
|
|
215
|
+
*/
|
|
180
216
|
function setProject(options, project) {
|
|
181
217
|
const { id, name, type, defaults } = project;
|
|
182
218
|
featureFlags.setProjectId(id);
|
|
@@ -189,10 +225,12 @@ function setProject(options, project) {
|
|
|
189
225
|
};
|
|
190
226
|
}
|
|
191
227
|
|
|
228
|
+
/** @param {import('./runOptions').RunnerOptions} options */
|
|
192
229
|
async function setGrid(options) {
|
|
193
230
|
options.gridData = await gridService.getGridData(options);
|
|
194
231
|
}
|
|
195
232
|
|
|
233
|
+
/** @param {import('./runOptions').RunnerOptions} options */
|
|
196
234
|
async function setMockNetworkRules(options) {
|
|
197
235
|
const { project } = options;
|
|
198
236
|
const props = { projectId: project };
|
|
@@ -236,8 +274,9 @@ async function runRunner(options, customExtensionLocalLocation) {
|
|
|
236
274
|
return results;
|
|
237
275
|
}
|
|
238
276
|
|
|
277
|
+
/** @param {import('./runOptions').RunnerOptions} options */
|
|
239
278
|
function showFreeGridRunWarningIfNeeded(options) {
|
|
240
|
-
if (featureAvailabilityService.shouldShowFreeGridRunWarning(options.gridData
|
|
279
|
+
if (featureAvailabilityService.shouldShowFreeGridRunWarning(options.gridData?.type)) {
|
|
241
280
|
const CYAN = '\x1b[36m';
|
|
242
281
|
const UNDERSCORE = '\x1b[4m';
|
|
243
282
|
const RESET = '\x1b[0m';
|
|
@@ -252,15 +291,15 @@ function showFreeGridRunWarningIfNeeded(options) {
|
|
|
252
291
|
* - Reporting the user to analytics
|
|
253
292
|
* - Authenticating the user and exchanging their token for a jwt
|
|
254
293
|
* - Sets the grids for the company and validates the user has permission to run the CLI
|
|
255
|
-
* @param {
|
|
294
|
+
* @param {import('./runOptions').RunnerOptions} options - the run options passed to the CLI, namely the project and token
|
|
256
295
|
*/
|
|
257
296
|
async function init(options) {
|
|
258
297
|
perf.log('start runner init');
|
|
259
298
|
const { project, lightweightMode, useChromeLauncher, mode, disableSockets } = options;
|
|
260
299
|
const featureFlagsReady = featureFlags.fetch();
|
|
261
300
|
const socketConnected = initSocketServices(project, {
|
|
262
|
-
disableResults: disableSockets || Boolean(lightweightMode
|
|
263
|
-
disableRemoteStep: disableSockets || Boolean(lightweightMode
|
|
301
|
+
disableResults: disableSockets || Boolean(lightweightMode?.disableResults && (useChromeLauncher || mode !== 'extension')),
|
|
302
|
+
disableRemoteStep: disableSockets || Boolean(lightweightMode?.disableRemoteStep),
|
|
264
303
|
});
|
|
265
304
|
|
|
266
305
|
featureFlagsReady.catch(() => {}); // suppress unhandled rejection
|
|
@@ -280,7 +319,7 @@ async function init(options) {
|
|
|
280
319
|
setAuthData(options, authData);
|
|
281
320
|
await setSfdcCredential(options);
|
|
282
321
|
|
|
283
|
-
if (!
|
|
322
|
+
if (!options.lightweightMode?.disableLabs) {
|
|
284
323
|
await labFeaturesService.loadLabFeatures(projectById.id, companyByProjectId.activePlan);
|
|
285
324
|
}
|
|
286
325
|
|