@wdio/browserstack-service 9.0.0-alpha.78 → 9.0.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/README.md +1 -1
- package/build/Percy/Percy-Handler.d.ts +1 -1
- package/build/Percy/Percy-Handler.d.ts.map +1 -1
- package/build/Percy/PercyHelper.d.ts +1 -1
- package/build/Percy/PercyHelper.d.ts.map +1 -1
- package/build/accessibility-handler.d.ts +2 -2
- package/build/accessibility-handler.d.ts.map +1 -1
- package/build/ai-handler.d.ts +23 -0
- package/build/ai-handler.d.ts.map +1 -0
- package/build/constants.d.ts +10 -1
- package/build/constants.d.ts.map +1 -1
- package/build/crash-reporter.d.ts +2 -2
- package/build/crash-reporter.d.ts.map +1 -1
- package/build/fileStream.d.ts +0 -2
- package/build/fileStream.d.ts.map +1 -1
- package/build/index.js +5822 -12
- package/build/insights-handler.d.ts +5 -1
- package/build/insights-handler.d.ts.map +1 -1
- package/build/instrumentation/funnelInstrumentation.d.ts +2 -0
- package/build/instrumentation/funnelInstrumentation.d.ts.map +1 -1
- package/build/launcher.d.ts +6 -6
- package/build/launcher.d.ts.map +1 -1
- package/build/performance-tester.d.ts +0 -1
- package/build/performance-tester.d.ts.map +1 -1
- package/build/reporter.d.ts.map +1 -1
- package/build/service.d.ts +4 -4
- package/build/service.d.ts.map +1 -1
- package/build/testOps/listener.d.ts +1 -0
- package/build/testOps/listener.d.ts.map +1 -1
- package/build/types.d.ts +9 -3
- package/build/types.d.ts.map +1 -1
- package/build/util.d.ts +43 -30
- package/build/util.d.ts.map +1 -1
- package/package.json +13 -10
- package/build/Percy/Percy-Handler.js +0 -156
- package/build/Percy/Percy.js +0 -123
- package/build/Percy/PercyBinary.js +0 -142
- package/build/Percy/PercyCaptureMap.js +0 -35
- package/build/Percy/PercyHelper.js +0 -67
- package/build/Percy/PercyLogger.js +0 -67
- package/build/Percy/PercySDK.js +0 -39
- package/build/accessibility-handler.js +0 -287
- package/build/bstackLogger.js +0 -70
- package/build/cleanup.js +0 -89
- package/build/config.js +0 -39
- package/build/constants.js +0 -73
- package/build/crash-reporter.js +0 -138
- package/build/cucumber-types.js +0 -1
- package/build/data-store.js +0 -41
- package/build/exitHandler.js +0 -29
- package/build/fetchWrapper.js +0 -14
- package/build/fileStream.js +0 -12
- package/build/insights-handler.js +0 -719
- package/build/instrumentation/funnelInstrumentation.js +0 -120
- package/build/launcher.js +0 -741
- package/build/log4jsAppender.js +0 -19
- package/build/logPatcher.js +0 -38
- package/build/logReportingAPI.js +0 -56
- package/build/performance-tester.js +0 -94
- package/build/reporter.js +0 -251
- package/build/request-handler.js +0 -74
- package/build/scripts/accessibility-scripts.js +0 -61
- package/build/service.js +0 -428
- package/build/testOps/featureStats.js +0 -116
- package/build/testOps/featureUsage.js +0 -47
- package/build/testOps/listener.js +0 -222
- package/build/testOps/requestUtils.js +0 -39
- package/build/testOps/testOpsConfig.js +0 -19
- package/build/testOps/usageStats.js +0 -114
- package/build/types.js +0 -1
- package/build/util.js +0 -1132
- /package/{LICENSE-MIT → LICENSE} +0 -0
package/build/launcher.js
DELETED
|
@@ -1,741 +0,0 @@
|
|
|
1
|
-
import { FormData } from 'formdata-node';
|
|
2
|
-
import { v4 as uuidv4 } from 'uuid';
|
|
3
|
-
import fs from 'node:fs';
|
|
4
|
-
import path from 'node:path';
|
|
5
|
-
import { promisify, format } from 'node:util';
|
|
6
|
-
import { performance, PerformanceObserver } from 'node:perf_hooks';
|
|
7
|
-
import os from 'node:os';
|
|
8
|
-
import { SevereServiceError } from 'webdriverio';
|
|
9
|
-
import * as BrowserstackLocalLauncher from 'browserstack-local';
|
|
10
|
-
import PerformanceTester from './performance-tester.js';
|
|
11
|
-
import { startPercy, stopPercy, getBestPlatformForPercySnapshot } from './Percy/PercyHelper.js';
|
|
12
|
-
import { BSTACK_SERVICE_VERSION, NOT_ALLOWED_KEYS_IN_CAPS, PERF_MEASUREMENT_ENV, RERUN_ENV, RERUN_TESTS_ENV, TESTOPS_BUILD_ID_ENV, VALID_APP_EXTENSION } from './constants.js';
|
|
13
|
-
import { launchTestSession, createAccessibilityTestRun, shouldAddServiceVersion, stopBuildUpstream, getCiInfo, isBStackSession, isUndefined, isAccessibilityAutomationSession, stopAccessibilityTestRun, isTrue, getBrowserStackUser, getBrowserStackKey, uploadLogs, ObjectsAreEqual, getBasicAuthHeader, } from './util.js';
|
|
14
|
-
import CrashReporter from './crash-reporter.js';
|
|
15
|
-
import { BStackLogger } from './bstackLogger.js';
|
|
16
|
-
import { PercyLogger } from './Percy/PercyLogger.js';
|
|
17
|
-
import { FileStream } from './fileStream.js';
|
|
18
|
-
import BrowserStackConfig from './config.js';
|
|
19
|
-
import { setupExitHandlers } from './exitHandler.js';
|
|
20
|
-
import { sendFinish, sendStart } from './instrumentation/funnelInstrumentation.js';
|
|
21
|
-
export default class BrowserstackLauncherService {
|
|
22
|
-
_options;
|
|
23
|
-
_config;
|
|
24
|
-
browserstackLocal;
|
|
25
|
-
_buildName;
|
|
26
|
-
_projectName;
|
|
27
|
-
_buildTag;
|
|
28
|
-
_buildIdentifier;
|
|
29
|
-
_accessibilityAutomation;
|
|
30
|
-
_percy;
|
|
31
|
-
_percyBestPlatformCaps;
|
|
32
|
-
browserStackConfig;
|
|
33
|
-
constructor(_options, capabilities, _config) {
|
|
34
|
-
this._options = _options;
|
|
35
|
-
this._config = _config;
|
|
36
|
-
BStackLogger.clearLogFile();
|
|
37
|
-
PercyLogger.clearLogFile();
|
|
38
|
-
setupExitHandlers();
|
|
39
|
-
// added to maintain backward compatibility with webdriverIO v5
|
|
40
|
-
this._config || (this._config = _options);
|
|
41
|
-
this.browserStackConfig = BrowserStackConfig.getInstance(_options, _config);
|
|
42
|
-
if (Array.isArray(capabilities)) {
|
|
43
|
-
capabilities
|
|
44
|
-
.flatMap((c) => {
|
|
45
|
-
if (Object.values(c).length > 0 && Object.values(c).every(c => typeof c === 'object' && c.capabilities)) {
|
|
46
|
-
return Object.values(c).map((o) => o.capabilities);
|
|
47
|
-
}
|
|
48
|
-
return c;
|
|
49
|
-
})
|
|
50
|
-
.forEach((capability) => {
|
|
51
|
-
if (!capability['bstack:options']) {
|
|
52
|
-
// Skipping adding of service version if session is not of browserstack
|
|
53
|
-
if (isBStackSession(this._config)) {
|
|
54
|
-
const extensionCaps = Object.keys(capability).filter((cap) => cap.includes(':'));
|
|
55
|
-
if (extensionCaps.length) {
|
|
56
|
-
capability['bstack:options'] = { wdioService: BSTACK_SERVICE_VERSION };
|
|
57
|
-
if (!isUndefined(capability['browserstack.accessibility'])) {
|
|
58
|
-
this._accessibilityAutomation ||= isTrue(capability['browserstack.accessibility']);
|
|
59
|
-
}
|
|
60
|
-
else if (isTrue(this._options.accessibility)) {
|
|
61
|
-
capability['bstack:options'].accessibility = true;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
else if (shouldAddServiceVersion(this._config, this._options.testObservability)) {
|
|
65
|
-
capability['browserstack.wdioService'] = BSTACK_SERVICE_VERSION;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
// Need this details for sending data to Observability
|
|
69
|
-
this._buildIdentifier = capability['browserstack.buildIdentifier']?.toString();
|
|
70
|
-
this._buildName = capability.build?.toString();
|
|
71
|
-
}
|
|
72
|
-
else {
|
|
73
|
-
capability['bstack:options'].wdioService = BSTACK_SERVICE_VERSION;
|
|
74
|
-
this._buildName = capability['bstack:options'].buildName;
|
|
75
|
-
this._projectName = capability['bstack:options'].projectName;
|
|
76
|
-
this._buildTag = capability['bstack:options'].buildTag;
|
|
77
|
-
this._buildIdentifier = capability['bstack:options'].buildIdentifier;
|
|
78
|
-
if (!isUndefined(capability['bstack:options'].accessibility)) {
|
|
79
|
-
this._accessibilityAutomation ||= isTrue(capability['bstack:options'].accessibility);
|
|
80
|
-
}
|
|
81
|
-
else if (isTrue(this._options.accessibility)) {
|
|
82
|
-
capability['bstack:options'].accessibility = (isTrue(this._options.accessibility));
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
else if (typeof capabilities === 'object') {
|
|
88
|
-
Object.entries(capabilities).forEach(([, caps]) => {
|
|
89
|
-
if (!caps.capabilities['bstack:options']) {
|
|
90
|
-
if (isBStackSession(this._config)) {
|
|
91
|
-
const extensionCaps = Object.keys(caps.capabilities).filter((cap) => cap.includes(':'));
|
|
92
|
-
if (extensionCaps.length) {
|
|
93
|
-
caps.capabilities['bstack:options'] = { wdioService: BSTACK_SERVICE_VERSION };
|
|
94
|
-
if (!isUndefined(caps.capabilities['browserstack.accessibility'])) {
|
|
95
|
-
this._accessibilityAutomation ||= isTrue(caps.capabilities['browserstack.accessibility']);
|
|
96
|
-
}
|
|
97
|
-
else if (isTrue(this._options.accessibility)) {
|
|
98
|
-
caps.capabilities['bstack:options'] = { wdioService: BSTACK_SERVICE_VERSION, accessibility: (isTrue(this._options.accessibility)) };
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
else if (shouldAddServiceVersion(this._config, this._options.testObservability)) {
|
|
102
|
-
caps.capabilities['browserstack.wdioService'] = BSTACK_SERVICE_VERSION;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
this._buildIdentifier = caps.capabilities['browserstack.buildIdentifier'];
|
|
106
|
-
}
|
|
107
|
-
else {
|
|
108
|
-
const bstackOptions = caps.capabilities['bstack:options'];
|
|
109
|
-
bstackOptions.wdioService = BSTACK_SERVICE_VERSION;
|
|
110
|
-
this._buildName = bstackOptions.buildName;
|
|
111
|
-
this._projectName = bstackOptions.projectName;
|
|
112
|
-
this._buildTag = bstackOptions.buildTag;
|
|
113
|
-
this._buildIdentifier = bstackOptions.buildIdentifier;
|
|
114
|
-
if (!isUndefined(bstackOptions.accessibility)) {
|
|
115
|
-
this._accessibilityAutomation ||= isTrue(bstackOptions.accessibility);
|
|
116
|
-
}
|
|
117
|
-
else if (isTrue(this._options.accessibility)) {
|
|
118
|
-
bstackOptions.accessibility = isTrue(this._options.accessibility);
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
this.browserStackConfig.buildIdentifier = this._buildIdentifier;
|
|
124
|
-
this.browserStackConfig.buildName = this._buildName;
|
|
125
|
-
if (process.env[PERF_MEASUREMENT_ENV]) {
|
|
126
|
-
PerformanceTester.startMonitoring('performance-report-launcher.csv');
|
|
127
|
-
}
|
|
128
|
-
this._accessibilityAutomation ||= isTrue(this._options.accessibility);
|
|
129
|
-
this._options.accessibility = this._accessibilityAutomation;
|
|
130
|
-
// by default observability will be true unless specified as false
|
|
131
|
-
this._options.testObservability = this._options.testObservability !== false;
|
|
132
|
-
if (this._options.testObservability
|
|
133
|
-
&&
|
|
134
|
-
// update files to run if it's a rerun
|
|
135
|
-
process.env[RERUN_ENV] && process.env[RERUN_TESTS_ENV]) {
|
|
136
|
-
this._config.specs = process.env[RERUN_TESTS_ENV].split(',');
|
|
137
|
-
}
|
|
138
|
-
try {
|
|
139
|
-
CrashReporter.setConfigDetails(this._config, capabilities, this._options);
|
|
140
|
-
}
|
|
141
|
-
catch (error) {
|
|
142
|
-
BStackLogger.error(`[Crash_Report_Upload] Config processing failed due to ${error}`);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
async onWorkerStart(cid, caps) {
|
|
146
|
-
try {
|
|
147
|
-
if (this._options.percy && this._percyBestPlatformCaps) {
|
|
148
|
-
const isThisBestPercyPlatform = ObjectsAreEqual(caps, this._percyBestPlatformCaps);
|
|
149
|
-
if (isThisBestPercyPlatform) {
|
|
150
|
-
process.env.BEST_PLATFORM_CID = cid;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
catch (err) {
|
|
155
|
-
PercyLogger.error(`Error while setting best platform for Percy snapshot at worker start ${err}`);
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
async onPrepare(config, capabilities) {
|
|
159
|
-
// Send Funnel start request
|
|
160
|
-
await sendStart(this.browserStackConfig);
|
|
161
|
-
/**
|
|
162
|
-
* Upload app to BrowserStack if valid file path to app is given.
|
|
163
|
-
* Update app value of capability directly if app_url, custom_id, shareable_id is given
|
|
164
|
-
*/
|
|
165
|
-
if (!this._options.app) {
|
|
166
|
-
BStackLogger.info('app is not defined in browserstack-service config, skipping ...');
|
|
167
|
-
}
|
|
168
|
-
else {
|
|
169
|
-
let app = {};
|
|
170
|
-
const appConfig = this._options.app;
|
|
171
|
-
try {
|
|
172
|
-
app = await this._validateApp(appConfig);
|
|
173
|
-
}
|
|
174
|
-
catch (error) {
|
|
175
|
-
throw new SevereServiceError(error);
|
|
176
|
-
}
|
|
177
|
-
if (VALID_APP_EXTENSION.includes(path.extname(app.app))) {
|
|
178
|
-
if (fs.existsSync(app.app)) {
|
|
179
|
-
const data = await this._uploadApp(app);
|
|
180
|
-
BStackLogger.info(`app upload completed: ${JSON.stringify(data)}`);
|
|
181
|
-
app.app = data.app_url;
|
|
182
|
-
}
|
|
183
|
-
else if (app.customId) {
|
|
184
|
-
app.app = app.customId;
|
|
185
|
-
}
|
|
186
|
-
else {
|
|
187
|
-
throw new SevereServiceError(`[Invalid app path] app path ${app.app} is not correct, Provide correct path to app under test`);
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
BStackLogger.info(`Using app: ${app.app}`);
|
|
191
|
-
this._updateCaps(capabilities, 'app', app.app);
|
|
192
|
-
}
|
|
193
|
-
/**
|
|
194
|
-
* buildIdentifier in service options will take precedence over specified in capabilities
|
|
195
|
-
*/
|
|
196
|
-
if (this._options.buildIdentifier) {
|
|
197
|
-
this._buildIdentifier = this._options.buildIdentifier;
|
|
198
|
-
this._updateCaps(capabilities, 'buildIdentifier', this._buildIdentifier);
|
|
199
|
-
}
|
|
200
|
-
/**
|
|
201
|
-
* evaluate buildIdentifier in case unique execution identifiers are present
|
|
202
|
-
* e.g., ${BUILD_NUMBER} and ${DATE_TIME}
|
|
203
|
-
*/
|
|
204
|
-
this._handleBuildIdentifier(capabilities);
|
|
205
|
-
// remove accessibilityOptions from the capabilities if present
|
|
206
|
-
this._updateObjectTypeCaps(capabilities, 'accessibilityOptions');
|
|
207
|
-
if (this._accessibilityAutomation) {
|
|
208
|
-
const scannerVersion = await createAccessibilityTestRun(this._options, this._config, {
|
|
209
|
-
projectName: this._projectName,
|
|
210
|
-
buildName: this._buildName,
|
|
211
|
-
buildTag: this._buildTag,
|
|
212
|
-
bstackServiceVersion: BSTACK_SERVICE_VERSION,
|
|
213
|
-
buildIdentifier: this._buildIdentifier,
|
|
214
|
-
accessibilityOptions: this._options.accessibilityOptions
|
|
215
|
-
});
|
|
216
|
-
if (scannerVersion) {
|
|
217
|
-
process.env.BSTACK_A11Y_SCANNER_VERSION = scannerVersion;
|
|
218
|
-
}
|
|
219
|
-
BStackLogger.debug(`Accessibility scannerVersion ${scannerVersion}`);
|
|
220
|
-
}
|
|
221
|
-
if (this._options.accessibilityOptions) {
|
|
222
|
-
const filteredOpts = Object.keys(this._options.accessibilityOptions)
|
|
223
|
-
.filter(key => !NOT_ALLOWED_KEYS_IN_CAPS.includes(key))
|
|
224
|
-
.reduce((opts, key) => {
|
|
225
|
-
return {
|
|
226
|
-
...opts,
|
|
227
|
-
[key]: this._options.accessibilityOptions?.[key]
|
|
228
|
-
};
|
|
229
|
-
}, {});
|
|
230
|
-
this._updateObjectTypeCaps(capabilities, 'accessibilityOptions', filteredOpts);
|
|
231
|
-
}
|
|
232
|
-
else if (isAccessibilityAutomationSession(this._accessibilityAutomation)) {
|
|
233
|
-
this._updateObjectTypeCaps(capabilities, 'accessibilityOptions', {});
|
|
234
|
-
}
|
|
235
|
-
if (this._options.testObservability) {
|
|
236
|
-
BStackLogger.debug('Sending launch start event');
|
|
237
|
-
await launchTestSession(this._options, this._config, {
|
|
238
|
-
projectName: this._projectName,
|
|
239
|
-
buildName: this._buildName,
|
|
240
|
-
buildTag: this._buildTag,
|
|
241
|
-
bstackServiceVersion: BSTACK_SERVICE_VERSION,
|
|
242
|
-
buildIdentifier: this._buildIdentifier
|
|
243
|
-
});
|
|
244
|
-
}
|
|
245
|
-
if (this._options.percy) {
|
|
246
|
-
try {
|
|
247
|
-
const bestPlatformPercyCaps = getBestPlatformForPercySnapshot(capabilities);
|
|
248
|
-
this._percyBestPlatformCaps = bestPlatformPercyCaps;
|
|
249
|
-
await this.setupPercy(this._options, this._config, {
|
|
250
|
-
projectName: this._projectName
|
|
251
|
-
});
|
|
252
|
-
}
|
|
253
|
-
catch (err) {
|
|
254
|
-
PercyLogger.error(`Error while setting up Percy ${err}`);
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
if (!this._options.browserstackLocal) {
|
|
258
|
-
return BStackLogger.info('browserstackLocal is not enabled - skipping...');
|
|
259
|
-
}
|
|
260
|
-
const opts = {
|
|
261
|
-
key: this._config.key,
|
|
262
|
-
...this._options.opts
|
|
263
|
-
};
|
|
264
|
-
this.browserstackLocal = new BrowserstackLocalLauncher.Local();
|
|
265
|
-
this._updateCaps(capabilities, 'local');
|
|
266
|
-
if (opts.localIdentifier) {
|
|
267
|
-
this._updateCaps(capabilities, 'localIdentifier', opts.localIdentifier);
|
|
268
|
-
}
|
|
269
|
-
/**
|
|
270
|
-
* measure BrowserStack tunnel boot time
|
|
271
|
-
*/
|
|
272
|
-
const obs = new PerformanceObserver((list) => {
|
|
273
|
-
const entry = list.getEntries()[0];
|
|
274
|
-
BStackLogger.info(`Browserstack Local successfully started after ${entry.duration}ms`);
|
|
275
|
-
});
|
|
276
|
-
obs.observe({ entryTypes: ['measure'] });
|
|
277
|
-
let timer;
|
|
278
|
-
performance.mark('tbTunnelStart');
|
|
279
|
-
return Promise.race([
|
|
280
|
-
promisify(this.browserstackLocal.start.bind(this.browserstackLocal))(opts),
|
|
281
|
-
new Promise((resolve, reject) => {
|
|
282
|
-
/* istanbul ignore next */
|
|
283
|
-
timer = setTimeout(function () {
|
|
284
|
-
reject('Browserstack Local failed to start within 60 seconds!');
|
|
285
|
-
}, 60000);
|
|
286
|
-
})
|
|
287
|
-
]).then(function (result) {
|
|
288
|
-
clearTimeout(timer);
|
|
289
|
-
performance.mark('tbTunnelEnd');
|
|
290
|
-
performance.measure('bootTime', 'tbTunnelStart', 'tbTunnelEnd');
|
|
291
|
-
return Promise.resolve(result);
|
|
292
|
-
}, function (err) {
|
|
293
|
-
clearTimeout(timer);
|
|
294
|
-
return Promise.reject(err);
|
|
295
|
-
});
|
|
296
|
-
}
|
|
297
|
-
async onComplete() {
|
|
298
|
-
BStackLogger.debug('Inside OnComplete hook..');
|
|
299
|
-
if (isAccessibilityAutomationSession(this._accessibilityAutomation)) {
|
|
300
|
-
await stopAccessibilityTestRun().catch((error) => {
|
|
301
|
-
BStackLogger.error(`Exception in stop accessibility test run: ${error}`);
|
|
302
|
-
});
|
|
303
|
-
}
|
|
304
|
-
if (this._options.testObservability) {
|
|
305
|
-
BStackLogger.debug('Sending stop launch event');
|
|
306
|
-
await stopBuildUpstream();
|
|
307
|
-
if (process.env[TESTOPS_BUILD_ID_ENV]) {
|
|
308
|
-
console.log(`\nVisit https://observability.browserstack.com/builds/${process.env[TESTOPS_BUILD_ID_ENV]} to view build report, insights, and many more debugging information all at one place!\n`);
|
|
309
|
-
}
|
|
310
|
-
this.browserStackConfig.testObservability.buildStopped = true;
|
|
311
|
-
if (process.env[PERF_MEASUREMENT_ENV]) {
|
|
312
|
-
await PerformanceTester.stopAndGenerate('performance-launcher.html');
|
|
313
|
-
PerformanceTester.calculateTimes(['launchTestSession', 'stopBuildUpstream']);
|
|
314
|
-
if (!process.env.START_TIME) {
|
|
315
|
-
return;
|
|
316
|
-
}
|
|
317
|
-
const duration = (new Date()).getTime() - (new Date(process.env.START_TIME)).getTime();
|
|
318
|
-
BStackLogger.info(`Total duration is ${duration / 1000} s`);
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
try {
|
|
322
|
-
await this._uploadServiceLogs();
|
|
323
|
-
}
|
|
324
|
-
catch (error) {
|
|
325
|
-
BStackLogger.debug(`Failed to upload BrowserStack WDIO Service logs ${error}`);
|
|
326
|
-
}
|
|
327
|
-
BStackLogger.clearLogger();
|
|
328
|
-
if (this._options.percy) {
|
|
329
|
-
await this.stopPercy();
|
|
330
|
-
}
|
|
331
|
-
PercyLogger.clearLogger();
|
|
332
|
-
await sendFinish(this.browserStackConfig);
|
|
333
|
-
if (!this.browserstackLocal || !this.browserstackLocal.isRunning()) {
|
|
334
|
-
return;
|
|
335
|
-
}
|
|
336
|
-
if (this._options.forcedStop) {
|
|
337
|
-
return process.kill(this.browserstackLocal.pid);
|
|
338
|
-
}
|
|
339
|
-
let timer;
|
|
340
|
-
return Promise.race([
|
|
341
|
-
new Promise((resolve, reject) => {
|
|
342
|
-
this.browserstackLocal?.stop((err) => {
|
|
343
|
-
if (err) {
|
|
344
|
-
return reject(err);
|
|
345
|
-
}
|
|
346
|
-
resolve();
|
|
347
|
-
});
|
|
348
|
-
}),
|
|
349
|
-
new Promise((resolve, reject) => {
|
|
350
|
-
/* istanbul ignore next */
|
|
351
|
-
timer = setTimeout(() => reject(new Error('Browserstack Local failed to stop within 60 seconds!')), 60000);
|
|
352
|
-
})
|
|
353
|
-
]).then(function (result) {
|
|
354
|
-
clearTimeout(timer);
|
|
355
|
-
return Promise.resolve(result);
|
|
356
|
-
}, function (err) {
|
|
357
|
-
clearTimeout(timer);
|
|
358
|
-
return Promise.reject(err);
|
|
359
|
-
});
|
|
360
|
-
}
|
|
361
|
-
async setupPercy(options, config, bsConfig) {
|
|
362
|
-
if (this._percy?.isRunning()) {
|
|
363
|
-
return;
|
|
364
|
-
}
|
|
365
|
-
try {
|
|
366
|
-
this._percy = await startPercy(options, config, bsConfig);
|
|
367
|
-
if (!this._percy) {
|
|
368
|
-
throw new Error('Could not start percy, check percy logs for info.');
|
|
369
|
-
}
|
|
370
|
-
PercyLogger.info('Percy started successfully');
|
|
371
|
-
let signal = 0;
|
|
372
|
-
const handler = async () => {
|
|
373
|
-
signal++;
|
|
374
|
-
signal === 1 && await this.stopPercy();
|
|
375
|
-
};
|
|
376
|
-
process.on('beforeExit', handler);
|
|
377
|
-
process.on('SIGINT', handler);
|
|
378
|
-
process.on('SIGTERM', handler);
|
|
379
|
-
}
|
|
380
|
-
catch (err) {
|
|
381
|
-
PercyLogger.debug(`Error in percy setup ${err}`);
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
async stopPercy() {
|
|
385
|
-
if (!this._percy || !this._percy.isRunning()) {
|
|
386
|
-
return;
|
|
387
|
-
}
|
|
388
|
-
try {
|
|
389
|
-
await stopPercy(this._percy);
|
|
390
|
-
PercyLogger.info('Percy stopped');
|
|
391
|
-
}
|
|
392
|
-
catch (err) {
|
|
393
|
-
PercyLogger.error('Error occured while stopping percy : ' + err);
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
async _uploadApp(app) {
|
|
397
|
-
BStackLogger.info(`uploading app ${app.app} ${app.customId ? `and custom_id: ${app.customId}` : ''} to browserstack`);
|
|
398
|
-
const form = new FormData();
|
|
399
|
-
if (app.app) {
|
|
400
|
-
const fileName = path.basename(app.app);
|
|
401
|
-
form.append('file', new FileStream(fs.createReadStream(app.app)), fileName);
|
|
402
|
-
}
|
|
403
|
-
if (app.customId) {
|
|
404
|
-
form.append('custom_id', app.customId);
|
|
405
|
-
}
|
|
406
|
-
const headers = {
|
|
407
|
-
'Content-Type': 'multipart/form-data',
|
|
408
|
-
Authorization: getBasicAuthHeader(this._config.user, this._config.key),
|
|
409
|
-
};
|
|
410
|
-
const res = await fetch('https://api-cloud.browserstack.com/app-automate/upload', {
|
|
411
|
-
method: 'POST',
|
|
412
|
-
body: form,
|
|
413
|
-
headers
|
|
414
|
-
});
|
|
415
|
-
if (!res.ok) {
|
|
416
|
-
throw new SevereServiceError(`app upload failed ${res.body}`);
|
|
417
|
-
}
|
|
418
|
-
return await res.json();
|
|
419
|
-
}
|
|
420
|
-
/**
|
|
421
|
-
* @param {String | AppConfig} appConfig <string>: should be "app file path" or "app_url" or "custom_id" or "shareable_id".
|
|
422
|
-
* <object>: only "path" and "custom_id" should coexist as multiple properties.
|
|
423
|
-
*/
|
|
424
|
-
async _validateApp(appConfig) {
|
|
425
|
-
const app = {};
|
|
426
|
-
if (typeof appConfig === 'string') {
|
|
427
|
-
app.app = appConfig;
|
|
428
|
-
}
|
|
429
|
-
else if (typeof appConfig === 'object' && Object.keys(appConfig).length) {
|
|
430
|
-
if (Object.keys(appConfig).length > 2 || (Object.keys(appConfig).length === 2 && (!appConfig.path || !appConfig.custom_id))) {
|
|
431
|
-
throw new SevereServiceError(`keys ${Object.keys(appConfig)} can't co-exist as app values, use any one property from
|
|
432
|
-
{id<string>, path<string>, custom_id<string>, shareable_id<string>}, only "path" and "custom_id" can co-exist.`);
|
|
433
|
-
}
|
|
434
|
-
app.app = appConfig.id || appConfig.path || appConfig.custom_id || appConfig.shareable_id;
|
|
435
|
-
app.customId = appConfig.custom_id;
|
|
436
|
-
}
|
|
437
|
-
else {
|
|
438
|
-
throw new SevereServiceError('[Invalid format] app should be string or an object');
|
|
439
|
-
}
|
|
440
|
-
if (!app.app) {
|
|
441
|
-
throw new SevereServiceError(`[Invalid app property] supported properties are {id<string>, path<string>, custom_id<string>, shareable_id<string>}.
|
|
442
|
-
For more details please visit https://www.browserstack.com/docs/app-automate/appium/set-up-tests/specify-app ')`);
|
|
443
|
-
}
|
|
444
|
-
return app;
|
|
445
|
-
}
|
|
446
|
-
async _uploadServiceLogs() {
|
|
447
|
-
const clientBuildUuid = this._getClientBuildUuid();
|
|
448
|
-
const response = await uploadLogs(getBrowserStackUser(this._config), getBrowserStackKey(this._config), clientBuildUuid);
|
|
449
|
-
BStackLogger.logToFile(`Response - ${format(response)}`, 'debug');
|
|
450
|
-
}
|
|
451
|
-
_updateObjectTypeCaps(capabilities, capType, value) {
|
|
452
|
-
try {
|
|
453
|
-
if (Array.isArray(capabilities)) {
|
|
454
|
-
capabilities
|
|
455
|
-
.flatMap((c) => {
|
|
456
|
-
if (Object.values(c).length > 0 && Object.values(c).every(c => typeof c === 'object' && c.capabilities)) {
|
|
457
|
-
return Object.values(c).map((o) => o.capabilities);
|
|
458
|
-
}
|
|
459
|
-
return c;
|
|
460
|
-
})
|
|
461
|
-
.forEach((capability) => {
|
|
462
|
-
if (!capability['bstack:options']) {
|
|
463
|
-
const extensionCaps = Object.keys(capability).filter((cap) => cap.includes(':'));
|
|
464
|
-
if (extensionCaps.length) {
|
|
465
|
-
if (capType === 'accessibilityOptions' && value) {
|
|
466
|
-
capability['bstack:options'] = { accessibilityOptions: value };
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
else if (capType === 'accessibilityOptions') {
|
|
470
|
-
if (value) {
|
|
471
|
-
const accessibilityOpts = { ...value };
|
|
472
|
-
if (capability?.accessibility) {
|
|
473
|
-
accessibilityOpts.authToken = process.env.BSTACK_A11Y_JWT;
|
|
474
|
-
accessibilityOpts.scannerVersion = process.env.BSTACK_A11Y_SCANNER_VERSION;
|
|
475
|
-
}
|
|
476
|
-
capability['browserstack.accessibilityOptions'] = accessibilityOpts;
|
|
477
|
-
}
|
|
478
|
-
else {
|
|
479
|
-
delete capability['browserstack.accessibilityOptions'];
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
else if (capType === 'accessibilityOptions') {
|
|
484
|
-
if (value) {
|
|
485
|
-
const accessibilityOpts = { ...value };
|
|
486
|
-
if (capability['bstack:options'].accessibility) {
|
|
487
|
-
accessibilityOpts.authToken = process.env.BSTACK_A11Y_JWT;
|
|
488
|
-
accessibilityOpts.scannerVersion = process.env.BSTACK_A11Y_SCANNER_VERSION;
|
|
489
|
-
}
|
|
490
|
-
capability['bstack:options'].accessibilityOptions = accessibilityOpts;
|
|
491
|
-
}
|
|
492
|
-
else {
|
|
493
|
-
delete capability['bstack:options'].accessibilityOptions;
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
});
|
|
497
|
-
}
|
|
498
|
-
else if (typeof capabilities === 'object') {
|
|
499
|
-
Object.entries(capabilities).forEach(([, caps]) => {
|
|
500
|
-
if (!caps.capabilities['bstack:options']) {
|
|
501
|
-
const extensionCaps = Object.keys(caps.capabilities).filter((cap) => cap.includes(':'));
|
|
502
|
-
if (extensionCaps.length) {
|
|
503
|
-
if (capType === 'accessibilityOptions' && value) {
|
|
504
|
-
caps.capabilities['bstack:options'] = { accessibilityOptions: value };
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
else if (capType === 'accessibilityOptions') {
|
|
508
|
-
if (value) {
|
|
509
|
-
const accessibilityOpts = { ...value };
|
|
510
|
-
if (caps.capabilities['browserstack.accessibility']) {
|
|
511
|
-
accessibilityOpts.authToken = process.env.BSTACK_A11Y_JWT;
|
|
512
|
-
accessibilityOpts.scannerVersion = process.env.BSTACK_A11Y_SCANNER_VERSION;
|
|
513
|
-
}
|
|
514
|
-
caps.capabilities['browserstack.accessibilityOptions'] = accessibilityOpts;
|
|
515
|
-
}
|
|
516
|
-
else {
|
|
517
|
-
delete caps.capabilities['browserstack.accessibilityOptions'];
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
else if (capType === 'accessibilityOptions') {
|
|
522
|
-
if (value) {
|
|
523
|
-
const accessibilityOpts = { ...value };
|
|
524
|
-
if (caps.capabilities['bstack:options'].accessibility) {
|
|
525
|
-
accessibilityOpts.authToken = process.env.BSTACK_A11Y_JWT;
|
|
526
|
-
accessibilityOpts.scannerVersion = process.env.BSTACK_A11Y_SCANNER_VERSION;
|
|
527
|
-
}
|
|
528
|
-
caps.capabilities['bstack:options'].accessibilityOptions = accessibilityOpts;
|
|
529
|
-
}
|
|
530
|
-
else {
|
|
531
|
-
delete caps.capabilities['bstack:options'].accessibilityOptions;
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
});
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
catch (error) {
|
|
538
|
-
BStackLogger.debug(`Exception while retrieving capability value. Error - ${error}`);
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
_updateCaps(capabilities, capType, value) {
|
|
542
|
-
if (Array.isArray(capabilities)) {
|
|
543
|
-
capabilities
|
|
544
|
-
.flatMap((c) => {
|
|
545
|
-
if (Object.values(c).length > 0 && Object.values(c).every(c => typeof c === 'object' && c.capabilities)) {
|
|
546
|
-
return Object.values(c).map((o) => o.capabilities);
|
|
547
|
-
}
|
|
548
|
-
return c;
|
|
549
|
-
})
|
|
550
|
-
.forEach((capability) => {
|
|
551
|
-
if (!capability['bstack:options']) {
|
|
552
|
-
const extensionCaps = Object.keys(capability).filter((cap) => cap.includes(':'));
|
|
553
|
-
if (extensionCaps.length) {
|
|
554
|
-
if (capType === 'local') {
|
|
555
|
-
capability['bstack:options'] = { local: true };
|
|
556
|
-
}
|
|
557
|
-
else if (capType === 'app') {
|
|
558
|
-
capability['appium:app'] = value;
|
|
559
|
-
}
|
|
560
|
-
else if (capType === 'buildIdentifier' && value) {
|
|
561
|
-
capability['bstack:options'] = { buildIdentifier: value };
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
else if (capType === 'local') {
|
|
565
|
-
capability['browserstack.local'] = true;
|
|
566
|
-
}
|
|
567
|
-
else if (capType === 'app') {
|
|
568
|
-
capability.app = value;
|
|
569
|
-
}
|
|
570
|
-
else if (capType === 'buildIdentifier') {
|
|
571
|
-
if (value) {
|
|
572
|
-
capability['browserstack.buildIdentifier'] = value;
|
|
573
|
-
}
|
|
574
|
-
else {
|
|
575
|
-
delete capability['browserstack.buildIdentifier'];
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
else if (capType === 'localIdentifier') {
|
|
579
|
-
capability['browserstack.localIdentifier'] = value;
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
else if (capType === 'local') {
|
|
583
|
-
capability['bstack:options'].local = true;
|
|
584
|
-
}
|
|
585
|
-
else if (capType === 'app') {
|
|
586
|
-
capability['appium:app'] = value;
|
|
587
|
-
}
|
|
588
|
-
else if (capType === 'buildIdentifier') {
|
|
589
|
-
if (value) {
|
|
590
|
-
capability['bstack:options'].buildIdentifier = value;
|
|
591
|
-
}
|
|
592
|
-
else {
|
|
593
|
-
delete capability['bstack:options'].buildIdentifier;
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
else if (capType === 'localIdentifier') {
|
|
597
|
-
capability['bstack:options'].localIdentifier = value;
|
|
598
|
-
}
|
|
599
|
-
});
|
|
600
|
-
}
|
|
601
|
-
else if (typeof capabilities === 'object') {
|
|
602
|
-
Object.entries(capabilities).forEach(([, caps]) => {
|
|
603
|
-
if (!caps.capabilities['bstack:options']) {
|
|
604
|
-
const extensionCaps = Object.keys(caps.capabilities).filter((cap) => cap.includes(':'));
|
|
605
|
-
if (extensionCaps.length) {
|
|
606
|
-
if (capType === 'local') {
|
|
607
|
-
caps.capabilities['bstack:options'] = { local: true };
|
|
608
|
-
}
|
|
609
|
-
else if (capType === 'app') {
|
|
610
|
-
caps.capabilities['appium:app'] = value;
|
|
611
|
-
}
|
|
612
|
-
else if (capType === 'buildIdentifier' && value) {
|
|
613
|
-
caps.capabilities['bstack:options'] = { buildIdentifier: value };
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
else if (capType === 'local') {
|
|
617
|
-
caps.capabilities['browserstack.local'] = true;
|
|
618
|
-
}
|
|
619
|
-
else if (capType === 'app') {
|
|
620
|
-
caps.capabilities['appium:app'] = value;
|
|
621
|
-
}
|
|
622
|
-
else if (capType === 'buildIdentifier') {
|
|
623
|
-
if (value) {
|
|
624
|
-
caps.capabilities['browserstack.buildIdentifier'] = value;
|
|
625
|
-
}
|
|
626
|
-
else {
|
|
627
|
-
delete caps.capabilities['browserstack.buildIdentifier'];
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
else if (capType === 'localIdentifier') {
|
|
631
|
-
caps.capabilities['browserstack.localIdentifier'] = value;
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
else if (capType === 'local') {
|
|
635
|
-
caps.capabilities['bstack:options'].local = true;
|
|
636
|
-
}
|
|
637
|
-
else if (capType === 'app') {
|
|
638
|
-
caps.capabilities['appium:app'] = value;
|
|
639
|
-
}
|
|
640
|
-
else if (capType === 'buildIdentifier') {
|
|
641
|
-
if (value) {
|
|
642
|
-
caps.capabilities['bstack:options'].buildIdentifier = value;
|
|
643
|
-
}
|
|
644
|
-
else {
|
|
645
|
-
delete caps.capabilities['bstack:options'].buildIdentifier;
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
|
-
else if (capType === 'localIdentifier') {
|
|
649
|
-
caps.capabilities['bstack:options'].localIdentifier = value;
|
|
650
|
-
}
|
|
651
|
-
});
|
|
652
|
-
}
|
|
653
|
-
else {
|
|
654
|
-
throw new SevereServiceError('Capabilities should be an object or Array!');
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
_handleBuildIdentifier(capabilities) {
|
|
658
|
-
if (!this._buildIdentifier) {
|
|
659
|
-
return;
|
|
660
|
-
}
|
|
661
|
-
if ((!this._buildName || process.env.BROWSERSTACK_BUILD_NAME) && this._buildIdentifier) {
|
|
662
|
-
this._updateCaps(capabilities, 'buildIdentifier');
|
|
663
|
-
BStackLogger.warn('Skipping buildIdentifier as buildName is not passed.');
|
|
664
|
-
return;
|
|
665
|
-
}
|
|
666
|
-
if (this._buildIdentifier && this._buildIdentifier.includes('${DATE_TIME}')) {
|
|
667
|
-
const formattedDate = new Intl.DateTimeFormat('en-GB', {
|
|
668
|
-
month: 'short',
|
|
669
|
-
day: '2-digit',
|
|
670
|
-
hour: '2-digit',
|
|
671
|
-
minute: '2-digit',
|
|
672
|
-
hour12: false
|
|
673
|
-
})
|
|
674
|
-
.format(new Date())
|
|
675
|
-
.replace(/ |, /g, '-');
|
|
676
|
-
this._buildIdentifier = this._buildIdentifier.replace('${DATE_TIME}', formattedDate);
|
|
677
|
-
this._updateCaps(capabilities, 'buildIdentifier', this._buildIdentifier);
|
|
678
|
-
}
|
|
679
|
-
if (!this._buildIdentifier.includes('${BUILD_NUMBER}')) {
|
|
680
|
-
return;
|
|
681
|
-
}
|
|
682
|
-
const ciInfo = getCiInfo();
|
|
683
|
-
if (ciInfo !== null && ciInfo.build_number) {
|
|
684
|
-
this._buildIdentifier = this._buildIdentifier.replace('${BUILD_NUMBER}', 'CI ' + ciInfo.build_number);
|
|
685
|
-
this._updateCaps(capabilities, 'buildIdentifier', this._buildIdentifier);
|
|
686
|
-
}
|
|
687
|
-
else {
|
|
688
|
-
const localBuildNumber = this._getLocalBuildNumber();
|
|
689
|
-
if (localBuildNumber) {
|
|
690
|
-
this._buildIdentifier = this._buildIdentifier.replace('${BUILD_NUMBER}', localBuildNumber);
|
|
691
|
-
this._updateCaps(capabilities, 'buildIdentifier', this._buildIdentifier);
|
|
692
|
-
}
|
|
693
|
-
}
|
|
694
|
-
}
|
|
695
|
-
/**
|
|
696
|
-
* @return {string} if buildName doesn't exist in json file, it will return 1
|
|
697
|
-
* else returns corresponding value in json file (e.g. { "wdio-build": { "identifier" : 2 } } => 2 in this case)
|
|
698
|
-
*/
|
|
699
|
-
_getLocalBuildNumber() {
|
|
700
|
-
const browserstackFolderPath = path.join(os.homedir(), '.browserstack');
|
|
701
|
-
try {
|
|
702
|
-
if (!fs.existsSync(browserstackFolderPath)) {
|
|
703
|
-
fs.mkdirSync(browserstackFolderPath);
|
|
704
|
-
}
|
|
705
|
-
const filePath = path.join(browserstackFolderPath, '.build-name-cache.json');
|
|
706
|
-
if (!fs.existsSync(filePath)) {
|
|
707
|
-
fs.appendFileSync(filePath, JSON.stringify({}));
|
|
708
|
-
}
|
|
709
|
-
const buildCacheFileData = fs.readFileSync(filePath);
|
|
710
|
-
const parsedBuildCacheFileData = JSON.parse(buildCacheFileData.toString());
|
|
711
|
-
if (this._buildName && this._buildName in parsedBuildCacheFileData) {
|
|
712
|
-
const prevIdentifier = parseInt((parsedBuildCacheFileData[this._buildName].identifier));
|
|
713
|
-
const newIdentifier = prevIdentifier + 1;
|
|
714
|
-
this._updateLocalBuildCache(filePath, this._buildName, newIdentifier);
|
|
715
|
-
return newIdentifier.toString();
|
|
716
|
-
}
|
|
717
|
-
const newIdentifier = 1;
|
|
718
|
-
this._updateLocalBuildCache(filePath, this._buildName, 1);
|
|
719
|
-
return newIdentifier.toString();
|
|
720
|
-
}
|
|
721
|
-
catch (error) {
|
|
722
|
-
return null;
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
_updateLocalBuildCache(filePath, buildName, buildIdentifier) {
|
|
726
|
-
if (!buildName || !filePath) {
|
|
727
|
-
return;
|
|
728
|
-
}
|
|
729
|
-
const jsonContent = JSON.parse(fs.readFileSync(filePath).toString());
|
|
730
|
-
jsonContent[buildName] = { 'identifier': buildIdentifier };
|
|
731
|
-
fs.writeFileSync(filePath, JSON.stringify(jsonContent));
|
|
732
|
-
}
|
|
733
|
-
_getClientBuildUuid() {
|
|
734
|
-
if (process.env[TESTOPS_BUILD_ID_ENV]) {
|
|
735
|
-
return process.env[TESTOPS_BUILD_ID_ENV];
|
|
736
|
-
}
|
|
737
|
-
const uuid = uuidv4();
|
|
738
|
-
BStackLogger.logToFile(`If facing any issues, please contact BrowserStack support with the Build Run Id - ${uuid}`, 'info');
|
|
739
|
-
return uuid;
|
|
740
|
-
}
|
|
741
|
-
}
|