@wdio/sauce-service 7.17.0 → 7.17.3

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.
@@ -0,0 +1,3 @@
1
+ import type { SauceServiceConfig } from './types';
2
+ export declare const DEFAULT_OPTIONS: Partial<SauceServiceConfig>;
3
+ //# sourceMappingURL=constants.d.ts.map
@@ -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"}
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEFAULT_OPTIONS = void 0;
4
+ exports.DEFAULT_OPTIONS = {
5
+ uploadLogs: true
6
+ };
@@ -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"}
@@ -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;
@@ -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
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -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.17.0",
3
+ "version": "7.17.3",
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.16.0",
28
- "@wdio/types": "7.16.14",
29
- "@wdio/utils": "7.17.0",
27
+ "@wdio/logger": "7.17.3",
28
+ "@wdio/types": "7.17.3",
29
+ "@wdio/utils": "7.17.3",
30
30
  "saucelabs": "^7.1.3",
31
- "webdriverio": "7.17.0"
31
+ "webdriverio": "7.17.3"
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": "e18d2cde6ff979758830bdff4c3bc82ca9818b24"
40
+ "gitHead": "2bcb589dbdd10ca181f301a269b4dd158faab257"
41
41
  }