@wdio/sauce-service 7.17.0 → 7.18.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/build/constants.d.ts +3 -0
- package/build/constants.d.ts.map +1 -0
- package/build/constants.js +6 -0
- package/build/index.d.ts +13 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +25 -0
- package/build/launcher.d.ts +21 -0
- package/build/launcher.d.ts.map +1 -0
- package/build/launcher.js +115 -0
- package/build/service.d.ts +70 -0
- package/build/service.d.ts.map +1 -0
- package/build/service.js +350 -0
- package/build/types.d.ts +46 -0
- package/build/types.d.ts.map +1 -0
- package/build/types.js +2 -0
- package/build/utils.d.ts +58 -0
- package/build/utils.d.ts.map +1 -0
- package/build/utils.js +99 -0
- package/package.json +6 -6
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAA;AAEjD,eAAO,MAAM,eAAe,EAAE,OAAO,CAAC,kBAAkB,CAEvD,CAAA"}
|
package/build/index.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import SauceLauncher from './launcher';
|
|
2
|
+
import SauceService from './service';
|
|
3
|
+
import { SauceServiceConfig } from './types';
|
|
4
|
+
export default SauceService;
|
|
5
|
+
export declare const launcher: typeof SauceLauncher;
|
|
6
|
+
export * from './types';
|
|
7
|
+
declare global {
|
|
8
|
+
namespace WebdriverIO {
|
|
9
|
+
interface ServiceOption extends SauceServiceConfig {
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,MAAM,YAAY,CAAA;AACtC,OAAO,YAAY,MAAM,WAAW,CAAA;AACpC,OAAO,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAA;AAE5C,eAAe,YAAY,CAAA;AAC3B,eAAO,MAAM,QAAQ,sBAAgB,CAAA;AACrC,cAAc,SAAS,CAAA;AAEvB,OAAO,CAAC,MAAM,CAAC;IACX,UAAU,WAAW,CAAC;QAClB,UAAU,aAAc,SAAQ,kBAAkB;SAAG;KACxD;CACJ"}
|
package/build/index.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
17
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
18
|
+
};
|
|
19
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
+
exports.launcher = void 0;
|
|
21
|
+
const launcher_1 = __importDefault(require("./launcher"));
|
|
22
|
+
const service_1 = __importDefault(require("./service"));
|
|
23
|
+
exports.default = service_1.default;
|
|
24
|
+
exports.launcher = launcher_1.default;
|
|
25
|
+
__exportStar(require("./types"), exports);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { SauceConnectOptions, SauceConnectInstance } from 'saucelabs';
|
|
2
|
+
import type { Services, Capabilities, Options } from '@wdio/types';
|
|
3
|
+
import type { SauceServiceConfig } from './types';
|
|
4
|
+
export default class SauceLauncher implements Services.ServiceInstance {
|
|
5
|
+
private _options;
|
|
6
|
+
private _capabilities;
|
|
7
|
+
private _config;
|
|
8
|
+
private _api;
|
|
9
|
+
private _sauceConnectProcess?;
|
|
10
|
+
constructor(_options: SauceServiceConfig, _capabilities: unknown, _config: Options.Testrunner);
|
|
11
|
+
/**
|
|
12
|
+
* modify config and launch sauce connect
|
|
13
|
+
*/
|
|
14
|
+
onPrepare(config: Options.Testrunner, capabilities: Capabilities.RemoteCapabilities): Promise<void>;
|
|
15
|
+
startTunnel(sauceConnectOpts: SauceConnectOptions, retryCount?: number): Promise<SauceConnectInstance>;
|
|
16
|
+
/**
|
|
17
|
+
* shut down sauce connect
|
|
18
|
+
*/
|
|
19
|
+
onComplete(): Promise<undefined> | undefined;
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=launcher.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"launcher.d.ts","sourceRoot":"","sources":["../src/launcher.ts"],"names":[],"mappings":"AACA,OAAkB,EAAoB,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,WAAW,CAAA;AAGlG,OAAO,KAAK,EAAE,QAAQ,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,aAAa,CAAA;AAGlE,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAA;AAUjD,MAAM,CAAC,OAAO,OAAO,aAAc,YAAW,QAAQ,CAAC,eAAe;IAK9D,OAAO,CAAC,QAAQ;IAChB,OAAO,CAAC,aAAa;IACrB,OAAO,CAAC,OAAO;IANnB,OAAO,CAAC,IAAI,CAAW;IACvB,OAAO,CAAC,oBAAoB,CAAC,CAAsB;gBAGvC,QAAQ,EAAE,kBAAkB,EAC5B,aAAa,EAAE,OAAO,EACtB,OAAO,EAAE,OAAO,CAAC,UAAU;IAKvC;;OAEG;IACG,SAAS,CACX,MAAM,EAAE,OAAO,CAAC,UAAU,EAC1B,YAAY,EAAE,YAAY,CAAC,kBAAkB;IA4D3C,WAAW,CAAE,gBAAgB,EAAE,mBAAmB,EAAE,UAAU,SAAI,GAAG,OAAO,CAAC,oBAAoB,CAAC;IA4BxG;;OAEG;IACH,UAAU;CAOb"}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const perf_hooks_1 = require("perf_hooks");
|
|
7
|
+
const saucelabs_1 = __importDefault(require("saucelabs"));
|
|
8
|
+
const logger_1 = __importDefault(require("@wdio/logger"));
|
|
9
|
+
const utils_1 = require("./utils");
|
|
10
|
+
const SC_RELAY_DEPCRECATION_WARNING = [
|
|
11
|
+
'The "scRelay" option is depcrecated and will be removed',
|
|
12
|
+
'with the upcoming versions of @wdio/sauce-service. Please',
|
|
13
|
+
'remove the option as tests should work identically without it.'
|
|
14
|
+
].join(' ');
|
|
15
|
+
const MAX_SC_START_TRIALS = 3;
|
|
16
|
+
const log = (0, logger_1.default)('@wdio/sauce-service');
|
|
17
|
+
class SauceLauncher {
|
|
18
|
+
constructor(_options, _capabilities, _config) {
|
|
19
|
+
this._options = _options;
|
|
20
|
+
this._capabilities = _capabilities;
|
|
21
|
+
this._config = _config;
|
|
22
|
+
this._api = new saucelabs_1.default(this._config);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* modify config and launch sauce connect
|
|
26
|
+
*/
|
|
27
|
+
async onPrepare(config, capabilities) {
|
|
28
|
+
var _a;
|
|
29
|
+
if (!this._options.sauceConnect) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const sauceConnectTunnelIdentifier = (((_a = this._options.sauceConnectOpts) === null || _a === void 0 ? void 0 : _a.tunnelIdentifier) ||
|
|
33
|
+
/**
|
|
34
|
+
* generate random identifier if not provided
|
|
35
|
+
*/
|
|
36
|
+
`SC-tunnel-${Math.random().toString().slice(2)}`);
|
|
37
|
+
const sauceConnectOpts = {
|
|
38
|
+
noAutodetect: true,
|
|
39
|
+
tunnelIdentifier: sauceConnectTunnelIdentifier,
|
|
40
|
+
...this._options.sauceConnectOpts
|
|
41
|
+
};
|
|
42
|
+
let endpointConfigurations = {};
|
|
43
|
+
if (this._options.scRelay) {
|
|
44
|
+
log.warn(SC_RELAY_DEPCRECATION_WARNING);
|
|
45
|
+
const scRelayPort = sauceConnectOpts.sePort || 4445;
|
|
46
|
+
sauceConnectOpts.sePort = scRelayPort;
|
|
47
|
+
endpointConfigurations = {
|
|
48
|
+
protocol: 'http',
|
|
49
|
+
hostname: 'localhost',
|
|
50
|
+
port: scRelayPort
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
const prepareCapability = (0, utils_1.makeCapabilityFactory)(sauceConnectTunnelIdentifier, endpointConfigurations);
|
|
54
|
+
if (Array.isArray(capabilities)) {
|
|
55
|
+
for (const capability of capabilities) {
|
|
56
|
+
prepareCapability(capability);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
for (const browserName of Object.keys(capabilities)) {
|
|
61
|
+
const caps = capabilities[browserName].capabilities;
|
|
62
|
+
prepareCapability(caps.alwaysMatch || caps);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* measure SC boot time
|
|
67
|
+
*/
|
|
68
|
+
const obs = new perf_hooks_1.PerformanceObserver((list) => {
|
|
69
|
+
const entry = list.getEntries()[0];
|
|
70
|
+
log.info(`Sauce Connect successfully started after ${entry.duration}ms`);
|
|
71
|
+
});
|
|
72
|
+
obs.observe({ entryTypes: ['measure'] });
|
|
73
|
+
perf_hooks_1.performance.mark('sauceConnectStart');
|
|
74
|
+
this._sauceConnectProcess = await this.startTunnel(sauceConnectOpts);
|
|
75
|
+
perf_hooks_1.performance.mark('sauceConnectEnd');
|
|
76
|
+
perf_hooks_1.performance.measure('bootTime', 'sauceConnectStart', 'sauceConnectEnd');
|
|
77
|
+
}
|
|
78
|
+
async startTunnel(sauceConnectOpts, retryCount = 0) {
|
|
79
|
+
try {
|
|
80
|
+
const scProcess = await this._api.startSauceConnect(sauceConnectOpts);
|
|
81
|
+
return scProcess;
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
++retryCount;
|
|
85
|
+
/**
|
|
86
|
+
* fail starting Sauce Connect eventually
|
|
87
|
+
*/
|
|
88
|
+
if (
|
|
89
|
+
/**
|
|
90
|
+
* only fail for ENOENT errors due to racing condition
|
|
91
|
+
* see: https://github.com/saucelabs/node-saucelabs/issues/86
|
|
92
|
+
*/
|
|
93
|
+
!err.message.includes('ENOENT') ||
|
|
94
|
+
/**
|
|
95
|
+
* or if we reached the maximum rety count
|
|
96
|
+
*/
|
|
97
|
+
retryCount >= MAX_SC_START_TRIALS) {
|
|
98
|
+
throw err;
|
|
99
|
+
}
|
|
100
|
+
log.debug(`Failed to start Sauce Connect Proxy due to ${err.stack}`);
|
|
101
|
+
log.debug(`Retrying ${retryCount}/${MAX_SC_START_TRIALS}`);
|
|
102
|
+
return this.startTunnel(sauceConnectOpts, retryCount);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* shut down sauce connect
|
|
107
|
+
*/
|
|
108
|
+
onComplete() {
|
|
109
|
+
if (!this._sauceConnectProcess) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
return this._sauceConnectProcess.close();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
exports.default = SauceLauncher;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { Job } from 'saucelabs';
|
|
2
|
+
import type { Services, Capabilities, Options, Frameworks } from '@wdio/types';
|
|
3
|
+
import type { Browser, MultiRemoteBrowser } from 'webdriverio';
|
|
4
|
+
import { SauceServiceConfig } from './types';
|
|
5
|
+
export default class SauceService implements Services.ServiceInstance {
|
|
6
|
+
private _capabilities;
|
|
7
|
+
private _config;
|
|
8
|
+
private _testCnt;
|
|
9
|
+
private _maxErrorStackLength;
|
|
10
|
+
private _failures;
|
|
11
|
+
private _isServiceEnabled;
|
|
12
|
+
private _isJobNameSet;
|
|
13
|
+
private _options;
|
|
14
|
+
private _api;
|
|
15
|
+
private _browser?;
|
|
16
|
+
private _isRDC?;
|
|
17
|
+
private _suiteTitle?;
|
|
18
|
+
private _cid;
|
|
19
|
+
constructor(options: SauceServiceConfig, _capabilities: Capabilities.RemoteCapability, _config: Options.Testrunner);
|
|
20
|
+
/**
|
|
21
|
+
* gather information about runner
|
|
22
|
+
*/
|
|
23
|
+
beforeSession(_: never, __: never, ___: never, cid: string): void;
|
|
24
|
+
before(caps: unknown, specs: string[], browser: Browser<'async'> | MultiRemoteBrowser<'async'>): void;
|
|
25
|
+
beforeSuite(suite: Frameworks.Suite): Promise<void>;
|
|
26
|
+
beforeTest(test: Frameworks.Test): Promise<unknown>;
|
|
27
|
+
afterSuite(suite: Frameworks.Suite): void;
|
|
28
|
+
private _reportErrorLog;
|
|
29
|
+
afterTest(test: Frameworks.Test, context: unknown, results: Frameworks.TestResult): void;
|
|
30
|
+
afterHook(test: never, context: never, results: Frameworks.TestResult): void;
|
|
31
|
+
/**
|
|
32
|
+
* For CucumberJS
|
|
33
|
+
*/
|
|
34
|
+
beforeFeature(uri: unknown, feature: {
|
|
35
|
+
name: string;
|
|
36
|
+
}): Promise<unknown>;
|
|
37
|
+
beforeScenario(world: Frameworks.World): Promise<unknown> | undefined;
|
|
38
|
+
beforeStep(step: Frameworks.PickleStep): Promise<unknown>;
|
|
39
|
+
/**
|
|
40
|
+
*
|
|
41
|
+
* Runs before a Cucumber Scenario.
|
|
42
|
+
* @param world world object containing information on pickle and test step
|
|
43
|
+
* @param result result object containing
|
|
44
|
+
* @param result.passed true if scenario has passed
|
|
45
|
+
* @param result.error error stack if scenario failed
|
|
46
|
+
* @param result.duration duration of scenario in milliseconds
|
|
47
|
+
*/
|
|
48
|
+
afterScenario(world: Frameworks.World, result: Frameworks.PickleResult): void;
|
|
49
|
+
/**
|
|
50
|
+
* update Sauce Labs job
|
|
51
|
+
*/
|
|
52
|
+
after(result: number): Promise<unknown>;
|
|
53
|
+
/**
|
|
54
|
+
* upload files to Sauce Labs platform
|
|
55
|
+
* @param jobId id of the job
|
|
56
|
+
* @returns a promise that is resolved once all files got uploaded
|
|
57
|
+
*/
|
|
58
|
+
private _uploadLogs;
|
|
59
|
+
onReload(oldSessionId: string, newSessionId: string): Promise<void> | undefined;
|
|
60
|
+
updateJob(sessionId: string, failures: number, calledOnReload?: boolean, browserName?: string): Promise<void>;
|
|
61
|
+
/**
|
|
62
|
+
* VM message data
|
|
63
|
+
*/
|
|
64
|
+
getBody(failures: number, calledOnReload?: boolean, browserName?: string): Partial<Job>;
|
|
65
|
+
/**
|
|
66
|
+
* Update the running Sauce Labs Job with an annotation
|
|
67
|
+
*/
|
|
68
|
+
setAnnotation(annotation: string): Promise<unknown>;
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=service.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAGA,OAAkB,EAAoB,GAAG,EAAE,MAAM,WAAW,CAAA;AAE5D,OAAO,KAAK,EAAE,QAAQ,EAAE,YAAY,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AAC9E,OAAO,KAAK,EAAE,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAA;AAG9D,OAAO,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAA;AAO5C,MAAM,CAAC,OAAO,OAAO,YAAa,YAAW,QAAQ,CAAC,eAAe;IAgB7D,OAAO,CAAC,aAAa;IACrB,OAAO,CAAC,OAAO;IAhBnB,OAAO,CAAC,QAAQ,CAAI;IACpB,OAAO,CAAC,oBAAoB,CAAI;IAChC,OAAO,CAAC,SAAS,CAAI;IACrB,OAAO,CAAC,iBAAiB,CAAO;IAChC,OAAO,CAAC,aAAa,CAAQ;IAE7B,OAAO,CAAC,QAAQ,CAAoB;IACpC,OAAO,CAAC,IAAI,CAAW;IACvB,OAAO,CAAC,QAAQ,CAAC,CAAgD;IACjE,OAAO,CAAC,MAAM,CAAC,CAAS;IACxB,OAAO,CAAC,WAAW,CAAC,CAAQ;IAC5B,OAAO,CAAC,IAAI,CAAK;gBAGb,OAAO,EAAE,kBAAkB,EACnB,aAAa,EAAE,YAAY,CAAC,gBAAgB,EAC5C,OAAO,EAAE,OAAO,CAAC,UAAU;IAOvC;;OAEG;IACH,aAAa,CAAE,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM;IAkB3D,MAAM,CAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,kBAAkB,CAAC,OAAO,CAAC;IAazF,WAAW,CAAE,KAAK,EAAE,UAAU,CAAC,KAAK;IAepC,UAAU,CAAE,IAAI,EAAE,UAAU,CAAC,IAAI;IAqDvC,UAAU,CAAE,KAAK,EAAE,UAAU,CAAC,KAAK;IAMnC,OAAO,CAAC,eAAe;IAKvB,SAAS,CAAE,IAAI,EAAE,UAAU,CAAC,IAAI,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,UAAU,CAAC,UAAU;IAyClF,SAAS,CAAE,IAAI,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,UAAU,CAAC,UAAU;IAetE;;OAEG;IACG,aAAa,CAAE,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE;IAuB5D,cAAc,CAAE,KAAK,EAAE,UAAU,CAAC,KAAK;IAajC,UAAU,CAAE,IAAI,EAAE,UAAU,CAAC,UAAU;IAY7C;;;;;;;;OAQG;IACH,aAAa,CAAC,KAAK,EAAE,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,CAAC,YAAY;IAOtE;;OAEG;IACG,KAAK,CAAE,MAAM,EAAE,MAAM;IAsC3B;;;;OAIG;YACW,WAAW;IAezB,QAAQ,CAAE,YAAY,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM;IAmB9C,SAAS,CAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,cAAc,UAAQ,EAAE,WAAW,CAAC,EAAE,MAAM;IAMlG;;OAEG;IACH,OAAO,CAAE,QAAQ,EAAE,MAAM,EAAE,cAAc,UAAQ,EAAE,WAAW,CAAC,EAAE,MAAM;IAgDvE;;OAEG;IACG,aAAa,CAAE,UAAU,EAAE,MAAM;CAkB1C"}
|
package/build/service.js
ADDED
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const fs_1 = __importDefault(require("fs"));
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const saucelabs_1 = __importDefault(require("saucelabs"));
|
|
9
|
+
const logger_1 = __importDefault(require("@wdio/logger"));
|
|
10
|
+
const utils_1 = require("./utils");
|
|
11
|
+
const constants_1 = require("./constants");
|
|
12
|
+
const jobDataProperties = ['name', 'tags', 'public', 'build', 'custom-data'];
|
|
13
|
+
const log = (0, logger_1.default)('@wdio/sauce-service');
|
|
14
|
+
class SauceService {
|
|
15
|
+
constructor(options, _capabilities, _config) {
|
|
16
|
+
this._capabilities = _capabilities;
|
|
17
|
+
this._config = _config;
|
|
18
|
+
this._testCnt = 0;
|
|
19
|
+
this._maxErrorStackLength = 5;
|
|
20
|
+
this._failures = 0; // counts failures between reloads
|
|
21
|
+
this._isServiceEnabled = true;
|
|
22
|
+
this._isJobNameSet = false;
|
|
23
|
+
this._cid = '';
|
|
24
|
+
this._options = { ...constants_1.DEFAULT_OPTIONS, ...options };
|
|
25
|
+
this._api = new saucelabs_1.default(this._config);
|
|
26
|
+
this._maxErrorStackLength = this._options.maxErrorStackLength || this._maxErrorStackLength;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* gather information about runner
|
|
30
|
+
*/
|
|
31
|
+
beforeSession(_, __, ___, cid) {
|
|
32
|
+
this._cid = cid;
|
|
33
|
+
/**
|
|
34
|
+
* if no user and key is specified even though a sauce service was
|
|
35
|
+
* provided set user and key with values so that the session request
|
|
36
|
+
* will fail
|
|
37
|
+
*/
|
|
38
|
+
if (!this._config.user) {
|
|
39
|
+
this._isServiceEnabled = false;
|
|
40
|
+
this._config.user = 'unknown_user';
|
|
41
|
+
}
|
|
42
|
+
if (!this._config.key) {
|
|
43
|
+
this._isServiceEnabled = false;
|
|
44
|
+
this._config.key = 'unknown_key';
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
before(caps, specs, browser) {
|
|
48
|
+
this._browser = browser;
|
|
49
|
+
// Ensure capabilities are not null in case of multiremote
|
|
50
|
+
// Changed from `this._browser.capabilities` to this to get the correct
|
|
51
|
+
// capabilities for EMUSIM (with the postfix) to determine ff the string
|
|
52
|
+
// contains `simulator` or `emulator` it's an EMU/SIM session
|
|
53
|
+
// `this._browser.capabilities` returns the process data from Sauce which is without
|
|
54
|
+
// the postfix
|
|
55
|
+
const capabilities = this._browser.requestedCapabilities || {};
|
|
56
|
+
this._isRDC = (0, utils_1.isRDC)(capabilities);
|
|
57
|
+
}
|
|
58
|
+
async beforeSuite(suite) {
|
|
59
|
+
this._suiteTitle = suite.title;
|
|
60
|
+
/**
|
|
61
|
+
* Make sure we account for the cases where there is a long running `before` function for a
|
|
62
|
+
* suite or one that can fail so we set the default job name at the suite level
|
|
63
|
+
* Don't do this for Jasmine because the `suiteTitle` is `Jasmine__TopLevel__Suite` and the
|
|
64
|
+
* `fullName` is `null`, so no alternative
|
|
65
|
+
**/
|
|
66
|
+
if (this._browser && !this._isRDC && !this._isJobNameSet && this._suiteTitle !== 'Jasmine__TopLevel__Suite') {
|
|
67
|
+
await this.setAnnotation('sauce:job-name=' + this._suiteTitle);
|
|
68
|
+
this._isJobNameSet = true;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
async beforeTest(test) {
|
|
72
|
+
if (!this._isServiceEnabled || !this._browser) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* in jasmine we get Jasmine__TopLevel__Suite as title since service using test
|
|
77
|
+
* framework hooks in order to execute async functions.
|
|
78
|
+
* This tweak allows us to set the real suite name for jasmine jobs.
|
|
79
|
+
*/
|
|
80
|
+
/* istanbul ignore if */
|
|
81
|
+
if (this._suiteTitle === 'Jasmine__TopLevel__Suite') {
|
|
82
|
+
this._suiteTitle = test.fullName.slice(0, test.fullName.indexOf(test.description || '') - 1);
|
|
83
|
+
}
|
|
84
|
+
if (this._browser && !this._isJobNameSet) {
|
|
85
|
+
let jobName = this._suiteTitle;
|
|
86
|
+
if (this._options.setJobName) {
|
|
87
|
+
jobName = this._options.setJobName(this._config, this._capabilities, this._suiteTitle);
|
|
88
|
+
await this.setAnnotation(`sauce:job-name=${jobName}`);
|
|
89
|
+
this._isJobNameSet = true;
|
|
90
|
+
}
|
|
91
|
+
if (!this._isJobNameSet) {
|
|
92
|
+
await this.setAnnotation(`sauce:job-name=${jobName}`);
|
|
93
|
+
this._isJobNameSet = true;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Date: 20200714
|
|
98
|
+
* Remark: Sauce Unified Platform doesn't support updating the context yet.
|
|
99
|
+
*/
|
|
100
|
+
if (this._isRDC) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const fullTitle = (
|
|
104
|
+
/**
|
|
105
|
+
* Jasmine
|
|
106
|
+
*/
|
|
107
|
+
test.fullName ||
|
|
108
|
+
/**
|
|
109
|
+
* Mocha
|
|
110
|
+
*/
|
|
111
|
+
`${test.parent} - ${test.title}`);
|
|
112
|
+
return this.setAnnotation(`sauce:context=${fullTitle}`);
|
|
113
|
+
}
|
|
114
|
+
afterSuite(suite) {
|
|
115
|
+
if (Object.prototype.hasOwnProperty.call(suite, 'error')) {
|
|
116
|
+
++this._failures;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
_reportErrorLog(error) {
|
|
120
|
+
const lines = (error.stack || '').split(/\r?\n/).slice(0, this._maxErrorStackLength);
|
|
121
|
+
lines.forEach((line) => this.setAnnotation(`sauce:context=${line.replace((0, utils_1.ansiRegex)(), '')}`));
|
|
122
|
+
}
|
|
123
|
+
afterTest(test, context, results) {
|
|
124
|
+
/**
|
|
125
|
+
* If the test failed push the stack to Sauce Labs in separate lines
|
|
126
|
+
* This should not be done for UP because it's not supported yet and
|
|
127
|
+
* should be removed when UP supports `sauce:context`
|
|
128
|
+
*/
|
|
129
|
+
if (results.error && results.error.stack && !this._isRDC) {
|
|
130
|
+
this._reportErrorLog(results.error);
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* remove failure if test was retried and passed
|
|
134
|
+
* > Mocha only
|
|
135
|
+
*/
|
|
136
|
+
if (test._retriedTest && results.passed) {
|
|
137
|
+
--this._failures;
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* don't bump failure number if test was retried and still failed
|
|
142
|
+
* > Mocha only
|
|
143
|
+
*/
|
|
144
|
+
if (test._retriedTest &&
|
|
145
|
+
!results.passed &&
|
|
146
|
+
(typeof test._currentRetry === 'number' &&
|
|
147
|
+
typeof test._retries === 'number' &&
|
|
148
|
+
test._currentRetry < test._retries)) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
const isJasminePendingError = typeof results.error === 'string' && results.error.includes('marked Pending');
|
|
152
|
+
if (!results.passed && !isJasminePendingError) {
|
|
153
|
+
++this._failures;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
afterHook(test, context, results) {
|
|
157
|
+
/**
|
|
158
|
+
* If the test failed push the stack to Sauce Labs in separate lines
|
|
159
|
+
* This should not be done for UP because it's not supported yet and
|
|
160
|
+
* should be removed when UP supports `sauce:context`
|
|
161
|
+
*/
|
|
162
|
+
if (results.error && !this._isRDC) {
|
|
163
|
+
this._reportErrorLog(results.error);
|
|
164
|
+
}
|
|
165
|
+
if (!results.passed) {
|
|
166
|
+
++this._failures;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* For CucumberJS
|
|
171
|
+
*/
|
|
172
|
+
async beforeFeature(uri, feature) {
|
|
173
|
+
if (!this._isServiceEnabled || !this._browser) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
this._suiteTitle = feature.name;
|
|
177
|
+
if (this._browser && !this._isJobNameSet) {
|
|
178
|
+
await this.setAnnotation(`sauce:job-name=${this._suiteTitle}`);
|
|
179
|
+
this._isJobNameSet = true;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Date: 20200714
|
|
183
|
+
* Remark: Sauce Unified Platform doesn't support updating the context yet.
|
|
184
|
+
*/
|
|
185
|
+
if (this._isRDC) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
return this.setAnnotation(`sauce:context=Feature: ${this._suiteTitle}`);
|
|
189
|
+
}
|
|
190
|
+
beforeScenario(world) {
|
|
191
|
+
/**
|
|
192
|
+
* Date: 20200714
|
|
193
|
+
* Remark: Sauce Unified Platform doesn't support updating the context yet.
|
|
194
|
+
*/
|
|
195
|
+
if (!this._isServiceEnabled || this._isRDC || !this._browser) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
const scenarioName = world.pickle.name || 'unknown scenario';
|
|
199
|
+
return this.setAnnotation(`sauce:context=-Scenario: ${scenarioName}`);
|
|
200
|
+
}
|
|
201
|
+
async beforeStep(step) {
|
|
202
|
+
/**
|
|
203
|
+
* Remark: Sauce Unified Platform doesn't support updating the context yet.
|
|
204
|
+
*/
|
|
205
|
+
if (!this._isServiceEnabled || this._isRDC || !this._browser) {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
const { keyword, text } = step;
|
|
209
|
+
return this.setAnnotation(`sauce:context=--Step: ${keyword}${text}`);
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
*
|
|
213
|
+
* Runs before a Cucumber Scenario.
|
|
214
|
+
* @param world world object containing information on pickle and test step
|
|
215
|
+
* @param result result object containing
|
|
216
|
+
* @param result.passed true if scenario has passed
|
|
217
|
+
* @param result.error error stack if scenario failed
|
|
218
|
+
* @param result.duration duration of scenario in milliseconds
|
|
219
|
+
*/
|
|
220
|
+
afterScenario(world, result) {
|
|
221
|
+
// check if scenario has failed
|
|
222
|
+
if (!result.passed) {
|
|
223
|
+
++this._failures;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* update Sauce Labs job
|
|
228
|
+
*/
|
|
229
|
+
async after(result) {
|
|
230
|
+
if (!this._browser || !this._isServiceEnabled) {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
let failures = this._failures;
|
|
234
|
+
/**
|
|
235
|
+
* set failures if user has bail option set in which case afterTest and
|
|
236
|
+
* afterSuite aren't executed before after hook
|
|
237
|
+
*/
|
|
238
|
+
if (this._config.mochaOpts && this._config.mochaOpts.bail && Boolean(result)) {
|
|
239
|
+
failures = 1;
|
|
240
|
+
}
|
|
241
|
+
const status = 'status: ' + (failures > 0 ? 'failing' : 'passing');
|
|
242
|
+
if (!this._browser.isMultiremote) {
|
|
243
|
+
await this._uploadLogs(this._browser.sessionId);
|
|
244
|
+
log.info(`Update job with sessionId ${this._browser.sessionId}, ${status}`);
|
|
245
|
+
return this._isRDC ?
|
|
246
|
+
this.setAnnotation(`sauce:job-result=${failures === 0}`) :
|
|
247
|
+
this.updateJob(this._browser.sessionId, failures);
|
|
248
|
+
}
|
|
249
|
+
const multiRemoteBrowser = this._browser;
|
|
250
|
+
return Promise.all(Object.keys(this._capabilities).map(async (browserName) => {
|
|
251
|
+
const isMultiRemoteRDC = (0, utils_1.isRDC)(multiRemoteBrowser[browserName].capabilities);
|
|
252
|
+
log.info(`Update multiRemote job for browser "${browserName}" and sessionId ${multiRemoteBrowser[browserName].sessionId}, ${status}`);
|
|
253
|
+
// Sauce Unified Platform (RDC) can not be updated with an API.
|
|
254
|
+
// The logs can also not be uploaded
|
|
255
|
+
if (isMultiRemoteRDC) {
|
|
256
|
+
return this.setAnnotation(`sauce:job-result=${failures === 0}`);
|
|
257
|
+
}
|
|
258
|
+
await this._uploadLogs(multiRemoteBrowser[browserName].sessionId);
|
|
259
|
+
return this.updateJob(multiRemoteBrowser[browserName].sessionId, failures, false, browserName);
|
|
260
|
+
}));
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* upload files to Sauce Labs platform
|
|
264
|
+
* @param jobId id of the job
|
|
265
|
+
* @returns a promise that is resolved once all files got uploaded
|
|
266
|
+
*/
|
|
267
|
+
async _uploadLogs(jobId) {
|
|
268
|
+
if (!this._options.uploadLogs || !this._config.outputDir) {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
const files = (await fs_1.default.promises.readdir(this._config.outputDir))
|
|
272
|
+
.filter((file) => file.startsWith(`wdio-${this._cid}`) && file.endsWith('.log'));
|
|
273
|
+
log.info(`Uploading WebdriverIO logs (${files.join(', ')}) to Sauce Labs`);
|
|
274
|
+
return this._api.uploadJobAssets(jobId, { files: files.map((file) => path_1.default.join(this._config.outputDir, file)) }).catch((err) => log.error(`Couldn't upload log files to Sauce Labs: ${err.message}`));
|
|
275
|
+
}
|
|
276
|
+
onReload(oldSessionId, newSessionId) {
|
|
277
|
+
if (!this._browser || !this._isServiceEnabled) {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
const status = 'status: ' + (this._failures > 0 ? 'failing' : 'passing');
|
|
281
|
+
if (!this._browser.isMultiremote) {
|
|
282
|
+
log.info(`Update (reloaded) job with sessionId ${oldSessionId}, ${status}`);
|
|
283
|
+
return this.updateJob(oldSessionId, this._failures, true);
|
|
284
|
+
}
|
|
285
|
+
const mulitremoteBrowser = this._browser;
|
|
286
|
+
const browserName = mulitremoteBrowser.instances.filter((browserName) => mulitremoteBrowser[browserName].sessionId === newSessionId)[0];
|
|
287
|
+
log.info(`Update (reloaded) multiremote job for browser "${browserName}" and sessionId ${oldSessionId}, ${status}`);
|
|
288
|
+
return this.updateJob(oldSessionId, this._failures, true, browserName);
|
|
289
|
+
}
|
|
290
|
+
async updateJob(sessionId, failures, calledOnReload = false, browserName) {
|
|
291
|
+
const body = this.getBody(failures, calledOnReload, browserName);
|
|
292
|
+
await this._api.updateJob(this._config.user, sessionId, body);
|
|
293
|
+
this._failures = 0;
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* VM message data
|
|
297
|
+
*/
|
|
298
|
+
getBody(failures, calledOnReload = false, browserName) {
|
|
299
|
+
let body = {};
|
|
300
|
+
/**
|
|
301
|
+
* add reload count to title if reload is used
|
|
302
|
+
*/
|
|
303
|
+
if (calledOnReload || this._testCnt) {
|
|
304
|
+
/**
|
|
305
|
+
* set default values
|
|
306
|
+
*/
|
|
307
|
+
body.name = this._suiteTitle;
|
|
308
|
+
if (browserName) {
|
|
309
|
+
body.name = `${browserName}: ${body.name}`;
|
|
310
|
+
}
|
|
311
|
+
let testCnt = ++this._testCnt;
|
|
312
|
+
const mulitremoteBrowser = this._browser;
|
|
313
|
+
if (this._browser && this._browser.isMultiremote) {
|
|
314
|
+
testCnt = Math.ceil(testCnt / mulitremoteBrowser.instances.length);
|
|
315
|
+
}
|
|
316
|
+
body.name += ` (${testCnt})`;
|
|
317
|
+
}
|
|
318
|
+
let caps = this._capabilities['sauce:options'] || this._capabilities;
|
|
319
|
+
for (let prop of jobDataProperties) {
|
|
320
|
+
if (!caps[prop]) {
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
body[prop] = caps[prop];
|
|
324
|
+
}
|
|
325
|
+
if (this._options.setJobName) {
|
|
326
|
+
body.name = this._options.setJobName(this._config, this._capabilities, this._suiteTitle);
|
|
327
|
+
}
|
|
328
|
+
body.passed = failures === 0;
|
|
329
|
+
return body;
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Update the running Sauce Labs Job with an annotation
|
|
333
|
+
*/
|
|
334
|
+
async setAnnotation(annotation) {
|
|
335
|
+
if (!this._browser) {
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
if (this._browser.isMultiremote) {
|
|
339
|
+
const multiRemoteBrowser = this._browser;
|
|
340
|
+
return Promise.all(Object.keys(this._capabilities).map(async (browserName) => {
|
|
341
|
+
const isMultiRemoteRDC = (0, utils_1.isRDC)(multiRemoteBrowser[browserName].capabilities);
|
|
342
|
+
if ((isMultiRemoteRDC && !annotation.includes('sauce:context')) || !isMultiRemoteRDC) {
|
|
343
|
+
return this._browser.execute(annotation);
|
|
344
|
+
}
|
|
345
|
+
}));
|
|
346
|
+
}
|
|
347
|
+
return this._browser.execute(annotation);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
exports.default = SauceService;
|
package/build/types.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { SauceConnectOptions } from 'saucelabs';
|
|
2
|
+
import type { Capabilities, Options } from '@wdio/types';
|
|
3
|
+
export interface SauceServiceConfig {
|
|
4
|
+
/**
|
|
5
|
+
* Specify the max error stack length represents the amount of error stack lines that will be
|
|
6
|
+
* pushed to Sauce Labs when a test fails
|
|
7
|
+
*/
|
|
8
|
+
maxErrorStackLength?: number;
|
|
9
|
+
/**
|
|
10
|
+
* Specify tunnel identifier for Sauce Connect tunnel
|
|
11
|
+
*/
|
|
12
|
+
tunnelIdentifier?: string;
|
|
13
|
+
/**
|
|
14
|
+
* Specify tunnel identifier for Sauce Connect parent tunnel
|
|
15
|
+
*/
|
|
16
|
+
parentTunnel?: string;
|
|
17
|
+
/**
|
|
18
|
+
* If true it runs Sauce Connect and opens a secure connection between a Sauce Labs virtual
|
|
19
|
+
* machine running your browser tests.
|
|
20
|
+
*
|
|
21
|
+
* @default false
|
|
22
|
+
*/
|
|
23
|
+
sauceConnect?: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Apply Sauce Connect options (e.g. to change port number or logFile settings). See this
|
|
26
|
+
* list for more information: https://github.com/bermi/sauce-connect-launcher#advanced-usage
|
|
27
|
+
*
|
|
28
|
+
* @default {}
|
|
29
|
+
*/
|
|
30
|
+
sauceConnectOpts?: SauceConnectOptions;
|
|
31
|
+
/**
|
|
32
|
+
* Upload WebdriverIO logs to the Sauce Labs platform.
|
|
33
|
+
* @default true
|
|
34
|
+
*/
|
|
35
|
+
uploadLogs?: boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Use Sauce Connect as a Selenium Relay. See more [here](https://wiki.saucelabs.com/display/DOCS/Using+the+Selenium+Relay+with+Sauce+Connect+Proxy).
|
|
38
|
+
* @deprecated
|
|
39
|
+
*/
|
|
40
|
+
scRelay?: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Dynamically control the name of the job
|
|
43
|
+
*/
|
|
44
|
+
setJobName?: (config: Options.Testrunner, capabilities: Capabilities.RemoteCapability, suiteTitle: string) => string;
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,WAAW,CAAA;AACpD,OAAO,KAAK,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,aAAa,CAAA;AAExD,MAAM,WAAW,kBAAkB;IAC/B;;;OAGG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAA;IAE5B;;OAEG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAA;IAEzB;;OAEG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;IAErB;;;;;OAKG;IACH,YAAY,CAAC,EAAE,OAAO,CAAA;IAEtB;;;;;OAKG;IACH,gBAAgB,CAAC,EAAE,mBAAmB,CAAA;IAEtC;;;OAGG;IACH,UAAU,CAAC,EAAE,OAAO,CAAA;IAEpB;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAA;IAEjB;;OAEG;IACH,UAAU,CAAC,EAAE,CACT,MAAM,EAAE,OAAO,CAAC,UAAU,EAC1B,YAAY,EAAE,YAAY,CAAC,gBAAgB,EAC3C,UAAU,EAAE,MAAM,KACjB,MAAM,CAAA;CACd"}
|
package/build/types.js
ADDED
package/build/utils.d.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { Capabilities } from '@wdio/types';
|
|
2
|
+
/**
|
|
3
|
+
* Determine if the current instance is a RDC instance. RDC tests are Real Device tests
|
|
4
|
+
* that can be started with different sets of capabilities. A deviceName is not mandatory, the only mandatory cap for
|
|
5
|
+
* RDC is the platformName. Downside of the platformName is that is can also be EMUSIM. EMUSIM can be distinguished by
|
|
6
|
+
* the `Emulator|Simulator` postfix
|
|
7
|
+
*
|
|
8
|
+
* @param {object} caps
|
|
9
|
+
* @returns {boolean}
|
|
10
|
+
*
|
|
11
|
+
* This is what we get back from the UP (for now)
|
|
12
|
+
*
|
|
13
|
+
* capabilities = {
|
|
14
|
+
* webStorageEnabled: false,
|
|
15
|
+
* locationContextEnabled: false,
|
|
16
|
+
* browserName: 'safari',
|
|
17
|
+
* platform: 'MAC',
|
|
18
|
+
* javascriptEnabled: true,
|
|
19
|
+
* databaseEnabled: false,
|
|
20
|
+
* takesScreenshot: true,
|
|
21
|
+
* networkConnectionEnabled: false,
|
|
22
|
+
* platformVersion: '12.1.2',
|
|
23
|
+
* webDriverAgentUrl: 'http://127.0.0.1:5700',
|
|
24
|
+
* testobject_platform_name: 'iOS',
|
|
25
|
+
* orientation: 'PORTRAIT',
|
|
26
|
+
* realDevice: true,
|
|
27
|
+
* build: 'Sauce Real Device browser iOS - 1594732389756',
|
|
28
|
+
* commandTimeouts: { default: 60000 },
|
|
29
|
+
* testobject_device: 'iPhone_XS_ws',
|
|
30
|
+
* automationName: 'XCUITest',
|
|
31
|
+
* platformName: 'iOS',
|
|
32
|
+
* udid: '',
|
|
33
|
+
* deviceName: '',
|
|
34
|
+
* testobject_test_report_api_url: '',
|
|
35
|
+
* testobject_test_report_url: '',
|
|
36
|
+
* testobject_user_id: 'wim.selles',
|
|
37
|
+
* testobject_project_id: 'saucelabs-default',
|
|
38
|
+
* testobject_test_report_id: 51,
|
|
39
|
+
* testobject_device_name: 'iPhone XS',
|
|
40
|
+
* testobject_device_session_id: '',
|
|
41
|
+
* deviceContextId: ''
|
|
42
|
+
* }
|
|
43
|
+
*/
|
|
44
|
+
export declare function isRDC(caps: Capabilities.DesiredCapabilities): boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Determine if this is an EMUSIM session
|
|
47
|
+
* @param {object} caps
|
|
48
|
+
* @returns {boolean}
|
|
49
|
+
*/
|
|
50
|
+
export declare function isEmuSim(caps: Capabilities.DesiredCapabilities): boolean;
|
|
51
|
+
/** Ensure capabilities are in the correct format for Sauce Labs
|
|
52
|
+
* @param {string} tunnelIdentifier - The default Sauce Connect tunnel identifier
|
|
53
|
+
* @param {object} options - Additional options to set on the capability
|
|
54
|
+
* @returns {function(object): void} - A function that mutates a single capability
|
|
55
|
+
*/
|
|
56
|
+
export declare function makeCapabilityFactory(tunnelIdentifier: string, options: any): (capability: Capabilities.DesiredCapabilities) => void;
|
|
57
|
+
export declare function ansiRegex(): RegExp;
|
|
58
|
+
//# sourceMappingURL=utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAI/C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AACH,wBAAgB,KAAK,CAAE,IAAI,EAAE,YAAY,CAAC,mBAAmB,WAM5D;AAED;;;;GAIG;AACH,wBAAgB,QAAQ,CAAE,IAAI,EAAE,YAAY,CAAC,mBAAmB,WAM/D;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,gBAAgB,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,gBACpD,aAAa,mBAAmB,UA2BvD;AAED,wBAAgB,SAAS,WAOxB"}
|
package/build/utils.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ansiRegex = exports.makeCapabilityFactory = exports.isEmuSim = exports.isRDC = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Determine if the current instance is a RDC instance. RDC tests are Real Device tests
|
|
6
|
+
* that can be started with different sets of capabilities. A deviceName is not mandatory, the only mandatory cap for
|
|
7
|
+
* RDC is the platformName. Downside of the platformName is that is can also be EMUSIM. EMUSIM can be distinguished by
|
|
8
|
+
* the `Emulator|Simulator` postfix
|
|
9
|
+
*
|
|
10
|
+
* @param {object} caps
|
|
11
|
+
* @returns {boolean}
|
|
12
|
+
*
|
|
13
|
+
* This is what we get back from the UP (for now)
|
|
14
|
+
*
|
|
15
|
+
* capabilities = {
|
|
16
|
+
* webStorageEnabled: false,
|
|
17
|
+
* locationContextEnabled: false,
|
|
18
|
+
* browserName: 'safari',
|
|
19
|
+
* platform: 'MAC',
|
|
20
|
+
* javascriptEnabled: true,
|
|
21
|
+
* databaseEnabled: false,
|
|
22
|
+
* takesScreenshot: true,
|
|
23
|
+
* networkConnectionEnabled: false,
|
|
24
|
+
* platformVersion: '12.1.2',
|
|
25
|
+
* webDriverAgentUrl: 'http://127.0.0.1:5700',
|
|
26
|
+
* testobject_platform_name: 'iOS',
|
|
27
|
+
* orientation: 'PORTRAIT',
|
|
28
|
+
* realDevice: true,
|
|
29
|
+
* build: 'Sauce Real Device browser iOS - 1594732389756',
|
|
30
|
+
* commandTimeouts: { default: 60000 },
|
|
31
|
+
* testobject_device: 'iPhone_XS_ws',
|
|
32
|
+
* automationName: 'XCUITest',
|
|
33
|
+
* platformName: 'iOS',
|
|
34
|
+
* udid: '',
|
|
35
|
+
* deviceName: '',
|
|
36
|
+
* testobject_test_report_api_url: '',
|
|
37
|
+
* testobject_test_report_url: '',
|
|
38
|
+
* testobject_user_id: 'wim.selles',
|
|
39
|
+
* testobject_project_id: 'saucelabs-default',
|
|
40
|
+
* testobject_test_report_id: 51,
|
|
41
|
+
* testobject_device_name: 'iPhone XS',
|
|
42
|
+
* testobject_device_session_id: '',
|
|
43
|
+
* deviceContextId: ''
|
|
44
|
+
* }
|
|
45
|
+
*/
|
|
46
|
+
function isRDC(caps) {
|
|
47
|
+
const { 'appium:deviceName': appiumDeviceName = '', deviceName = '', platformName = '' } = caps;
|
|
48
|
+
const name = appiumDeviceName || deviceName;
|
|
49
|
+
// If the string contains `simulator` or `emulator` it's an EMU/SIM session
|
|
50
|
+
return !name.match(/(simulator)|(emulator)/gi) && !!platformName.match(/(ios)|(android)/gi);
|
|
51
|
+
}
|
|
52
|
+
exports.isRDC = isRDC;
|
|
53
|
+
/**
|
|
54
|
+
* Determine if this is an EMUSIM session
|
|
55
|
+
* @param {object} caps
|
|
56
|
+
* @returns {boolean}
|
|
57
|
+
*/
|
|
58
|
+
function isEmuSim(caps) {
|
|
59
|
+
const { 'appium:deviceName': appiumDeviceName = '', deviceName = '', platformName = '' } = caps;
|
|
60
|
+
const name = appiumDeviceName || deviceName;
|
|
61
|
+
// If the string contains `simulator` or `emulator` it's an EMU/SIM session
|
|
62
|
+
return !!name.match(/(simulator)|(emulator)/gi) && !!platformName.match(/(ios)|(android)/gi);
|
|
63
|
+
}
|
|
64
|
+
exports.isEmuSim = isEmuSim;
|
|
65
|
+
/** Ensure capabilities are in the correct format for Sauce Labs
|
|
66
|
+
* @param {string} tunnelIdentifier - The default Sauce Connect tunnel identifier
|
|
67
|
+
* @param {object} options - Additional options to set on the capability
|
|
68
|
+
* @returns {function(object): void} - A function that mutates a single capability
|
|
69
|
+
*/
|
|
70
|
+
function makeCapabilityFactory(tunnelIdentifier, options) {
|
|
71
|
+
return (capability) => {
|
|
72
|
+
// Check if this is a 'valid' W3C request, this is done with a simple check
|
|
73
|
+
// where we assume that if only one cap has `:` it's W3C, even if the request
|
|
74
|
+
// is a mix of JWP and W3C. This is hard to check
|
|
75
|
+
const isW3CRequest = Boolean(Object.keys(capability).find((cap) => cap.includes(':')));
|
|
76
|
+
// If the `sauce:options` are not provided and it is a W3C session
|
|
77
|
+
// then add it
|
|
78
|
+
if (!capability['sauce:options'] && isW3CRequest) {
|
|
79
|
+
capability['sauce:options'] = {};
|
|
80
|
+
}
|
|
81
|
+
Object.assign(capability, options);
|
|
82
|
+
const sauceOptions = (isW3CRequest ? capability['sauce:options'] : capability);
|
|
83
|
+
sauceOptions.tunnelIdentifier = (capability.tunnelIdentifier ||
|
|
84
|
+
sauceOptions.tunnelIdentifier ||
|
|
85
|
+
tunnelIdentifier);
|
|
86
|
+
if (isW3CRequest) {
|
|
87
|
+
delete capability.tunnelIdentifier;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
exports.makeCapabilityFactory = makeCapabilityFactory;
|
|
92
|
+
function ansiRegex() {
|
|
93
|
+
const pattern = [
|
|
94
|
+
'[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)',
|
|
95
|
+
'(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))'
|
|
96
|
+
].join('|');
|
|
97
|
+
return new RegExp(pattern, 'g');
|
|
98
|
+
}
|
|
99
|
+
exports.ansiRegex = ansiRegex;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wdio/sauce-service",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.18.0",
|
|
4
4
|
"description": "WebdriverIO service that provides a better integration into Sauce Labs",
|
|
5
5
|
"author": "Christian Bromann <christian@saucelabs.com>",
|
|
6
6
|
"homepage": "https://github.com/webdriverio/webdriverio/tree/main/packages/wdio-sauce-service",
|
|
@@ -24,11 +24,11 @@
|
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@types/node": "^17.0.4",
|
|
27
|
-
"@wdio/logger": "7.
|
|
28
|
-
"@wdio/types": "7.
|
|
29
|
-
"@wdio/utils": "7.
|
|
27
|
+
"@wdio/logger": "7.17.3",
|
|
28
|
+
"@wdio/types": "7.18.0",
|
|
29
|
+
"@wdio/utils": "7.18.0",
|
|
30
30
|
"saucelabs": "^7.1.3",
|
|
31
|
-
"webdriverio": "7.
|
|
31
|
+
"webdriverio": "7.18.0"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
34
|
"@wdio/cli": "^7.0.0"
|
|
@@ -37,5 +37,5 @@
|
|
|
37
37
|
"access": "public"
|
|
38
38
|
},
|
|
39
39
|
"types": "./build/index.d.ts",
|
|
40
|
-
"gitHead": "
|
|
40
|
+
"gitHead": "44729cdd585af3ad69f2093f32377f297f5ac344"
|
|
41
41
|
}
|