@wdio/browserstack-service 8.0.11 → 8.1.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.
@@ -1,6 +1,6 @@
1
1
  import * as BrowserstackLocalLauncher from 'browserstack-local';
2
2
  import type { Capabilities, Services, Options } from '@wdio/types';
3
- import type { BrowserstackConfig, App, AppConfig, AppUploadResponse } from './types';
3
+ import type { BrowserstackConfig, App, AppConfig, AppUploadResponse } from './types.js';
4
4
  type BrowserstackLocal = BrowserstackLocalLauncher.Local & {
5
5
  pid?: number;
6
6
  stop(callback: (err?: any) => void): void;
@@ -9,9 +9,12 @@ export default class BrowserstackLauncherService implements Services.ServiceInst
9
9
  private _options;
10
10
  private _config;
11
11
  browserstackLocal?: BrowserstackLocal;
12
+ private _buildName?;
13
+ private _projectName?;
14
+ private _buildTag?;
12
15
  constructor(_options: BrowserstackConfig & Options.Testrunner, capabilities: Capabilities.RemoteCapability, _config: Options.Testrunner);
13
16
  onPrepare(config?: Options.Testrunner, capabilities?: Capabilities.RemoteCapabilities): Promise<unknown>;
14
- onComplete(): true | Promise<unknown> | undefined;
17
+ onComplete(): Promise<unknown>;
15
18
  _uploadApp(app: App): Promise<AppUploadResponse>;
16
19
  /**
17
20
  * @param {String | AppConfig} appConfig <string>: should be "app file path" or "app_url" or "custom_id" or "shareable_id".
@@ -1 +1 @@
1
- {"version":3,"file":"launcher.d.ts","sourceRoot":"","sources":["../src/launcher.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,yBAAyB,MAAM,oBAAoB,CAAA;AAG/D,OAAO,KAAK,EAAE,YAAY,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,aAAa,CAAA;AAElE,OAAO,KAAK,EAAE,kBAAkB,EAAE,GAAG,EAAE,SAAS,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAA;AAQpF,KAAK,iBAAiB,GAAG,yBAAyB,CAAC,KAAK,GAAG;IACvD,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,QAAQ,EAAE,CAAC,GAAG,CAAC,EAAE,GAAG,KAAK,IAAI,GAAG,IAAI,CAAC;CAC7C,CAAA;AAED,MAAM,CAAC,OAAO,OAAO,2BAA4B,YAAW,QAAQ,CAAC,eAAe;IAI5E,OAAO,CAAC,QAAQ;IAEhB,OAAO,CAAC,OAAO;IALnB,iBAAiB,CAAC,EAAE,iBAAiB,CAAA;gBAGzB,QAAQ,EAAE,kBAAkB,GAAG,OAAO,CAAC,UAAU,EACzD,YAAY,EAAE,YAAY,CAAC,gBAAgB,EACnC,OAAO,EAAE,OAAO,CAAC,UAAU;IAiCjC,SAAS,CAAE,MAAM,CAAC,EAAE,OAAO,CAAC,UAAU,EAAE,YAAY,CAAC,EAAE,YAAY,CAAC,kBAAkB;IA6E5F,UAAU;IAmCJ,UAAU,CAAC,GAAG,EAAC,GAAG,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAkBrD;;;OAGG;IACG,YAAY,CAAE,SAAS,EAAE,SAAS,GAAG,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC;IAyBhE,WAAW,CAAC,YAAY,CAAC,EAAE,YAAY,CAAC,kBAAkB,EAAE,OAAO,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,EAAC,MAAM;CA+C9F"}
1
+ {"version":3,"file":"launcher.d.ts","sourceRoot":"","sources":["../src/launcher.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,yBAAyB,MAAM,oBAAoB,CAAA;AAG/D,OAAO,KAAK,EAAE,YAAY,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,aAAa,CAAA;AAElE,OAAO,KAAK,EAAE,kBAAkB,EAAE,GAAG,EAAE,SAAS,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AASvF,KAAK,iBAAiB,GAAG,yBAAyB,CAAC,KAAK,GAAG;IACvD,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,QAAQ,EAAE,CAAC,GAAG,CAAC,EAAE,GAAG,KAAK,IAAI,GAAG,IAAI,CAAC;CAC7C,CAAA;AAED,MAAM,CAAC,OAAO,OAAO,2BAA4B,YAAW,QAAQ,CAAC,eAAe;IAO5E,OAAO,CAAC,QAAQ;IAEhB,OAAO,CAAC,OAAO;IARnB,iBAAiB,CAAC,EAAE,iBAAiB,CAAA;IACrC,OAAO,CAAC,UAAU,CAAC,CAAQ;IAC3B,OAAO,CAAC,YAAY,CAAC,CAAQ;IAC7B,OAAO,CAAC,SAAS,CAAC,CAAQ;gBAGd,QAAQ,EAAE,kBAAkB,GAAG,OAAO,CAAC,UAAU,EACzD,YAAY,EAAE,YAAY,CAAC,gBAAgB,EACnC,OAAO,EAAE,OAAO,CAAC,UAAU;IAmDjC,SAAS,CAAE,MAAM,CAAC,EAAE,OAAO,CAAC,UAAU,EAAE,YAAY,CAAC,EAAE,YAAY,CAAC,kBAAkB;IAuFtF,UAAU;IA2CV,UAAU,CAAC,GAAG,EAAC,GAAG,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAsBrD;;;OAGG;IACG,YAAY,CAAE,SAAS,EAAE,SAAS,GAAG,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC;IAyBhE,WAAW,CAAC,YAAY,CAAC,EAAE,YAAY,CAAC,kBAAkB,EAAE,OAAO,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,EAAC,MAAM;CA+C9F"}
package/build/launcher.js CHANGED
@@ -9,6 +9,7 @@ import { SevereServiceError } from 'webdriverio';
9
9
  import * as BrowserstackLocalLauncher from 'browserstack-local';
10
10
  import logger from '@wdio/logger';
11
11
  import { VALID_APP_EXTENSION } from './constants.js';
12
+ import { launchTestSession, shouldAddServiceVersion, stopBuildUpstream } from './util.js';
12
13
  const require = createRequire(import.meta.url);
13
14
  const { version: bstackServiceVersion } = require('../package.json');
14
15
  const log = logger('@wdio/browserstack-service');
@@ -16,6 +17,9 @@ export default class BrowserstackLauncherService {
16
17
  _options;
17
18
  _config;
18
19
  browserstackLocal;
20
+ _buildName;
21
+ _projectName;
22
+ _buildTag;
19
23
  constructor(_options, capabilities, _config) {
20
24
  this._options = _options;
21
25
  this._config = _config;
@@ -28,12 +32,15 @@ export default class BrowserstackLauncherService {
28
32
  if (extensionCaps.length) {
29
33
  capability['bstack:options'] = { wdioService: bstackServiceVersion };
30
34
  }
31
- else {
35
+ else if (shouldAddServiceVersion(this._config, this._options.testObservability)) {
32
36
  capability['browserstack.wdioService'] = bstackServiceVersion;
33
37
  }
34
38
  }
35
39
  else {
36
40
  capability['bstack:options'].wdioService = bstackServiceVersion;
41
+ this._buildName = capability['bstack:options'].buildName;
42
+ this._projectName = capability['bstack:options'].projectName;
43
+ this._buildTag = capability['bstack:options'].buildTag;
37
44
  }
38
45
  });
39
46
  }
@@ -44,15 +51,27 @@ export default class BrowserstackLauncherService {
44
51
  if (extensionCaps.length) {
45
52
  caps.capabilities['bstack:options'] = { wdioService: bstackServiceVersion };
46
53
  }
47
- else {
54
+ else if (shouldAddServiceVersion(this._config, this._options.testObservability)) {
48
55
  caps.capabilities['browserstack.wdioService'] = bstackServiceVersion;
49
56
  }
50
57
  }
51
58
  else {
52
- caps.capabilities['bstack:options'].wdioService = bstackServiceVersion;
59
+ const bstackOptions = caps.capabilities['bstack:options'];
60
+ bstackOptions.wdioService = bstackServiceVersion;
61
+ this._buildName = bstackOptions.buildName;
62
+ this._projectName = bstackOptions.projectName;
63
+ this._buildTag = bstackOptions.buildTag;
53
64
  }
54
65
  });
55
66
  }
67
+ // by default observability will be true unless specified as false
68
+ this._options.testObservability = this._options.testObservability === false ? false : true;
69
+ if (this._options.testObservability
70
+ &&
71
+ // update files to run if it's a rerun
72
+ process.env.BROWSERSTACK_RERUN && process.env.BROWSERSTACK_RERUN_TESTS) {
73
+ this._config.specs = process.env.BROWSERSTACK_RERUN_TESTS.split(',');
74
+ }
56
75
  }
57
76
  async onPrepare(config, capabilities) {
58
77
  /**
@@ -64,7 +83,7 @@ export default class BrowserstackLauncherService {
64
83
  }
65
84
  else {
66
85
  let app = {};
67
- let appConfig = this._options.app;
86
+ const appConfig = this._options.app;
68
87
  try {
69
88
  app = await this._validateApp(appConfig);
70
89
  }
@@ -73,8 +92,7 @@ export default class BrowserstackLauncherService {
73
92
  }
74
93
  if (VALID_APP_EXTENSION.includes(path.extname(app.app))) {
75
94
  if (fs.existsSync(app.app)) {
76
- let data;
77
- data = await this._uploadApp(app);
95
+ const data = await this._uploadApp(app);
78
96
  log.info(`app upload completed: ${JSON.stringify(data)}`);
79
97
  app.app = data.app_url;
80
98
  }
@@ -88,6 +106,15 @@ export default class BrowserstackLauncherService {
88
106
  log.info(`Using app: ${app.app}`);
89
107
  this._updateCaps(capabilities, 'app', app.app);
90
108
  }
109
+ if (this._options.testObservability) {
110
+ log.debug('Sending launch start event');
111
+ await launchTestSession(this._options, this._config, {
112
+ projectName: this._projectName,
113
+ buildName: this._buildName,
114
+ buildTag: this._buildTag,
115
+ bstackServiceVersion: bstackServiceVersion
116
+ });
117
+ }
91
118
  if (!this._options.browserstackLocal) {
92
119
  return log.info('browserstackLocal is not enabled - skipping...');
93
120
  }
@@ -125,7 +152,14 @@ export default class BrowserstackLauncherService {
125
152
  return Promise.reject(err);
126
153
  });
127
154
  }
128
- onComplete() {
155
+ async onComplete() {
156
+ if (this._options.testObservability) {
157
+ log.debug('Sending stop launch event');
158
+ await stopBuildUpstream();
159
+ if (process.env.BS_TESTOPS_BUILD_HASHED_ID) {
160
+ console.log(`\nVisit https://observability.browserstack.com/builds/${process.env.BS_TESTOPS_BUILD_HASHED_ID} to view build report, insights, and many more debugging information all at one place!\n`);
161
+ }
162
+ }
129
163
  if (!this.browserstackLocal || !this.browserstackLocal.isRunning()) {
130
164
  return;
131
165
  }
@@ -157,10 +191,12 @@ export default class BrowserstackLauncherService {
157
191
  async _uploadApp(app) {
158
192
  log.info(`uploading app ${app.app} ${app.customId ? `and custom_id: ${app.customId}` : ''} to browserstack`);
159
193
  const form = new FormData();
160
- if (app.app)
194
+ if (app.app) {
161
195
  form.append('file', fs.createReadStream(app.app));
162
- if (app.customId)
196
+ }
197
+ if (app.customId) {
163
198
  form.append('custom_id', app.customId);
199
+ }
164
200
  const res = await got.post('https://api-cloud.browserstack.com/app-automate/upload', {
165
201
  body: form,
166
202
  username: this._config.user,
@@ -175,7 +211,7 @@ export default class BrowserstackLauncherService {
175
211
  * <object>: only "path" and "custom_id" should coexist as multiple properties.
176
212
  */
177
213
  async _validateApp(appConfig) {
178
- let app = {};
214
+ const app = {};
179
215
  if (typeof appConfig === 'string') {
180
216
  app.app = appConfig;
181
217
  }
@@ -213,7 +249,7 @@ export default class BrowserstackLauncherService {
213
249
  capability['browserstack.local'] = true;
214
250
  }
215
251
  else if (capType === 'app') {
216
- capability['app'] = value;
252
+ capability.app = value;
217
253
  }
218
254
  }
219
255
  else if (capType === 'local') {
@@ -240,7 +276,7 @@ export default class BrowserstackLauncherService {
240
276
  caps.capabilities['browserstack.local'] = true;
241
277
  }
242
278
  else if (capType === 'app') {
243
- caps.capabilities['app'] = value;
279
+ caps.capabilities.app = value;
244
280
  }
245
281
  }
246
282
  else if (capType === 'local') {
@@ -0,0 +1,14 @@
1
+ import type { SuiteStats, TestStats, RunnerStats } from '@wdio/reporter';
2
+ import WDIOReporter from '@wdio/reporter';
3
+ export default class TestReporter extends WDIOReporter {
4
+ private _capabilities;
5
+ private _config?;
6
+ private _observability;
7
+ private _sessionId?;
8
+ private _suiteName?;
9
+ private _requestQueueHandler;
10
+ onRunnerStart(runnerStats: RunnerStats): void;
11
+ onSuiteStart(suiteStats: SuiteStats): void;
12
+ onTestSkip(testStats: TestStats): Promise<void>;
13
+ }
14
+ //# sourceMappingURL=reporter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reporter.d.ts","sourceRoot":"","sources":["../src/reporter.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AACxE,OAAO,YAAY,MAAM,gBAAgB,CAAA;AASzC,MAAM,CAAC,OAAO,OAAO,YAAa,SAAQ,YAAY;IAClD,OAAO,CAAC,aAAa,CAAgC;IACrD,OAAO,CAAC,OAAO,CAAC,CAAyC;IACzD,OAAO,CAAC,cAAc,CAAO;IAC7B,OAAO,CAAC,UAAU,CAAC,CAAQ;IAC3B,OAAO,CAAC,UAAU,CAAC,CAAQ;IAC3B,OAAO,CAAC,oBAAoB,CAAoC;IAEhE,aAAa,CAAE,WAAW,EAAE,WAAW;IASvC,YAAY,CAAE,UAAU,EAAE,UAAU;IAI9B,UAAU,CAAE,SAAS,EAAE,SAAS;CAgDzC"}
@@ -0,0 +1,66 @@
1
+ import WDIOReporter from '@wdio/reporter';
2
+ import { v4 as uuidv4 } from 'uuid';
3
+ import { getCloudProvider, uploadEventData, getHierarchy } from './util.js';
4
+ import RequestQueueHandler from './request-handler.js';
5
+ export default class TestReporter extends WDIOReporter {
6
+ _capabilities = {};
7
+ _config;
8
+ _observability = true;
9
+ _sessionId;
10
+ _suiteName;
11
+ _requestQueueHandler = RequestQueueHandler.getInstance();
12
+ onRunnerStart(runnerStats) {
13
+ this._capabilities = runnerStats.capabilities;
14
+ this._config = runnerStats.config;
15
+ this._sessionId = runnerStats.sessionId;
16
+ if (typeof this._config.testObservability !== 'undefined') {
17
+ this._observability = this._config.testObservability;
18
+ }
19
+ }
20
+ onSuiteStart(suiteStats) {
21
+ this._suiteName = suiteStats.file;
22
+ }
23
+ async onTestSkip(testStats) {
24
+ // cucumber steps call this method. We don't want step skipped state so skip for cucumber
25
+ const framework = this._config?.framework;
26
+ if (this._observability && framework !== 'cucumber') {
27
+ const testData = {
28
+ uuid: uuidv4(),
29
+ type: testStats.type,
30
+ name: testStats.title,
31
+ body: {
32
+ lang: 'webdriverio',
33
+ code: null
34
+ },
35
+ scope: testStats.fullTitle,
36
+ scopes: getHierarchy(testStats.fullTitle),
37
+ identifier: testStats.fullTitle,
38
+ file_name: this._suiteName,
39
+ location: this._suiteName,
40
+ started_at: (new Date()).toISOString(),
41
+ framework: framework,
42
+ finished_at: (new Date()).toISOString(),
43
+ duration_in_ms: testStats._duration,
44
+ retries: { limit: 0, attempts: 0 },
45
+ result: testStats.state,
46
+ };
47
+ const cloudProvider = getCloudProvider({ options: { hostname: this._config?.hostname } });
48
+ testData.integrations = {};
49
+ testData.integrations[cloudProvider] = {
50
+ capabilities: this._capabilities,
51
+ session_id: this._sessionId,
52
+ browser: this._capabilities?.browserName,
53
+ browser_version: this._capabilities?.browserVersion,
54
+ platform: this._capabilities?.platformName,
55
+ };
56
+ const uploadData = {
57
+ event_type: 'TestRunFinished',
58
+ test_run: testData
59
+ };
60
+ const req = this._requestQueueHandler.add(uploadData);
61
+ if (req.proceed && req.data) {
62
+ await uploadEventData(req.data, req.url);
63
+ }
64
+ }
65
+ }
66
+ }
@@ -0,0 +1,26 @@
1
+ import type { UploadType } from './types.js';
2
+ export default class RequestQueueHandler {
3
+ private queue;
4
+ private started;
5
+ private pollEventBatchInterval?;
6
+ pendingUploads: number;
7
+ static instance: RequestQueueHandler;
8
+ private constructor();
9
+ static getInstance(): RequestQueueHandler;
10
+ start(): void;
11
+ add(event: UploadType): {
12
+ proceed: boolean;
13
+ data?: undefined;
14
+ url?: undefined;
15
+ } | {
16
+ proceed: boolean;
17
+ data: UploadType[] | undefined;
18
+ url: string;
19
+ };
20
+ shutdown(): Promise<void>;
21
+ startEventBatchPolling(): void;
22
+ resetEventBatchPolling(): void;
23
+ removeEventBatchPolling(tag: string): void;
24
+ shouldProceed(): boolean;
25
+ }
26
+ //# sourceMappingURL=request-handler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"request-handler.d.ts","sourceRoot":"","sources":["../src/request-handler.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAA;AAK5C,MAAM,CAAC,OAAO,OAAO,mBAAmB;IACpC,OAAO,CAAC,KAAK,CAAmB;IAChC,OAAO,CAAC,OAAO,CAAQ;IACvB,OAAO,CAAC,sBAAsB,CAAC,CAAgC;IACxD,cAAc,SAAI;IAEzB,MAAM,CAAC,QAAQ,EAAE,mBAAmB,CAAA;IAGpC,OAAO;WAEO,WAAW,IAAI,mBAAmB;IAOhD,KAAK;IAOL,GAAG,CAAE,KAAK,EAAE,UAAU;;;;;;;;;IAuChB,QAAQ;IAQd,sBAAsB;IAUtB,sBAAsB;IAKtB,uBAAuB,CAAE,GAAG,EAAE,MAAM;IAQpC,aAAa;CAGhB"}
@@ -0,0 +1,88 @@
1
+ import logger from '@wdio/logger';
2
+ import { DATA_BATCH_SIZE, DATA_BATCH_INTERVAL, DATA_BATCH_ENDPOINT, BATCH_EVENT_TYPES, DATA_SCREENSHOT_ENDPOINT } from './constants.js';
3
+ import { batchAndPostEvents } from './util.js';
4
+ const log = logger('@wdio/browserstack-service');
5
+ export default class RequestQueueHandler {
6
+ queue = [];
7
+ started = false;
8
+ pollEventBatchInterval;
9
+ pendingUploads = 0;
10
+ static instance;
11
+ // making it private to use singleton pattern
12
+ constructor() { }
13
+ static getInstance() {
14
+ if (!RequestQueueHandler.instance) {
15
+ RequestQueueHandler.instance = new RequestQueueHandler();
16
+ }
17
+ return RequestQueueHandler.instance;
18
+ }
19
+ start() {
20
+ if (!this.started) {
21
+ this.started = true;
22
+ this.startEventBatchPolling();
23
+ }
24
+ }
25
+ add(event) {
26
+ if (!process.env.BS_TESTOPS_BUILD_COMPLETED) {
27
+ return {
28
+ proceed: false
29
+ };
30
+ }
31
+ if (!BATCH_EVENT_TYPES.includes(event.event_type)) {
32
+ return {
33
+ proceed: true
34
+ };
35
+ }
36
+ if (event.logs && event.logs[0] && event.logs[0].kind === 'TEST_SCREENSHOT') {
37
+ return {
38
+ proceed: true,
39
+ data: [event],
40
+ url: DATA_SCREENSHOT_ENDPOINT
41
+ };
42
+ }
43
+ this.queue.push(event);
44
+ log.debug(`Added data to request queue. Queue length = ${this.queue.length}`);
45
+ let data;
46
+ const shouldProceed = this.shouldProceed();
47
+ if (shouldProceed) {
48
+ data = this.queue.splice(0, DATA_BATCH_SIZE);
49
+ this.resetEventBatchPolling();
50
+ log.debug(`Sending data from request queue. Data length = ${data.length}, Queue length after removal = ${this.queue.length}`);
51
+ }
52
+ return {
53
+ proceed: shouldProceed,
54
+ data: data,
55
+ url: DATA_BATCH_ENDPOINT
56
+ };
57
+ }
58
+ async shutdown() {
59
+ this.removeEventBatchPolling('Shutting down');
60
+ while (this.queue.length > 0) {
61
+ const data = this.queue.splice(0, DATA_BATCH_SIZE);
62
+ await batchAndPostEvents(DATA_BATCH_ENDPOINT, 'SHUTDOWN_QUEUE', data);
63
+ }
64
+ }
65
+ startEventBatchPolling() {
66
+ this.pollEventBatchInterval = setInterval(async () => {
67
+ if (this.queue.length > 0) {
68
+ const data = this.queue.splice(0, DATA_BATCH_SIZE);
69
+ log.debug(`Sending data from request queue. Data length = ${data.length}, Queue length after removal = ${this.queue.length}`);
70
+ await batchAndPostEvents(DATA_BATCH_ENDPOINT, 'INTERVAL_QUEUE', data);
71
+ }
72
+ }, DATA_BATCH_INTERVAL);
73
+ }
74
+ resetEventBatchPolling() {
75
+ this.removeEventBatchPolling('Resetting');
76
+ this.startEventBatchPolling();
77
+ }
78
+ removeEventBatchPolling(tag) {
79
+ if (this.pollEventBatchInterval) {
80
+ log.debug(`${tag} request queue`);
81
+ clearInterval(this.pollEventBatchInterval);
82
+ this.started = false;
83
+ }
84
+ }
85
+ shouldProceed() {
86
+ return this.queue.length >= DATA_BATCH_SIZE;
87
+ }
88
+ }
@@ -1,6 +1,7 @@
1
1
  import type { Services, Capabilities, Options, Frameworks } from '@wdio/types';
2
2
  import type { Browser, MultiRemoteBrowser } from 'webdriverio';
3
- import type { BrowserstackConfig, MultiRemoteAction } from './types';
3
+ import type { BrowserstackConfig, MultiRemoteAction } from './types.js';
4
+ import type { Pickle, Feature, ITestCaseHookParameter } from './cucumber-types.js';
4
5
  export default class BrowserstackService implements Services.ServiceInstance {
5
6
  private _caps;
6
7
  private _config;
@@ -12,6 +13,10 @@ export default class BrowserstackService implements Services.ServiceInstance {
12
13
  private _suiteTitle?;
13
14
  private _fullTitle?;
14
15
  private _options;
16
+ private _specsRan;
17
+ private _observability;
18
+ private _currentTest?;
19
+ private _insightsHandler?;
15
20
  constructor(options: BrowserstackConfig & Options.Testrunner, _caps: Capabilities.RemoteCapability, _config: Options.Testrunner);
16
21
  _updateCaps(fn: (caps: Capabilities.Capabilities | Capabilities.DesiredCapabilities) => void): void;
17
22
  beforeSession(config: Omit<Options.Testrunner, 'capabilities'>): void;
@@ -24,33 +29,28 @@ export default class BrowserstackService implements Services.ServiceInstance {
24
29
  * and `suite.fullTitle` is `undefined`, so no alternative to use for the job name.
25
30
  */
26
31
  beforeSuite(suite: Frameworks.Suite): Promise<void>;
32
+ beforeHook(test: Frameworks.Test, context: any): Promise<void>;
33
+ afterHook(test: Frameworks.Test, context: unknown, result: Frameworks.TestResult): Promise<void>;
27
34
  beforeTest(test: Frameworks.Test): Promise<void>;
35
+ afterTest(test: Frameworks.Test, context: never, results: Frameworks.TestResult): Promise<void>;
36
+ after(result: number): Promise<void>;
28
37
  /**
29
38
  * For CucumberJS
30
39
  */
31
- beforeFeature(uri: unknown, feature: {
32
- name: string;
33
- }): Promise<void>;
40
+ beforeFeature(uri: string, feature: Feature): Promise<void>;
34
41
  /**
35
42
  * Runs before a Cucumber Scenario.
36
43
  * @param world world object containing information on pickle and test step
37
44
  */
38
- beforeScenario(world: Frameworks.World): Promise<void>;
39
- /**
40
- * For CucumberJS
41
- */
42
- beforeStep(step: Frameworks.PickleStep): Promise<void>;
43
- afterTest(test: Frameworks.Test, context: never, results: Frameworks.TestResult): void;
44
- after(result: number): Promise<void>;
45
- /**
46
- * For CucumberJS
47
- */
48
- afterScenario(world: Frameworks.World): void;
45
+ beforeScenario(world: ITestCaseHookParameter): Promise<void>;
46
+ afterScenario(world: ITestCaseHookParameter): Promise<void>;
47
+ beforeStep(step: Frameworks.PickleStep, scenario: Pickle): Promise<void>;
48
+ afterStep(step: Frameworks.PickleStep, scenario: Pickle, result: Frameworks.PickleResult): Promise<void>;
49
49
  onReload(oldSessionId: string, newSessionId: string): Promise<void>;
50
50
  _isAppAutomate(): boolean;
51
51
  _updateJob(requestBody: any): Promise<any>;
52
52
  _multiRemoteAction(action: MultiRemoteAction): Promise<any>;
53
- _update(sessionId: string, requestBody: any): import("got").CancelableRequest<import("got").Response<string>>;
53
+ _update(sessionId: string, requestBody: any): Promise<void> | import("got").CancelableRequest<import("got").Response<string>>;
54
54
  _printSessionURL(): Promise<void>;
55
55
  private _setSessionName;
56
56
  private _setAnnotation;
@@ -1 +1 @@
1
- {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAEA,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,KAAK,EAAE,kBAAkB,EAAE,iBAAiB,EAAmB,MAAM,SAAS,CAAA;AAKrF,MAAM,CAAC,OAAO,OAAO,mBAAoB,YAAW,QAAQ,CAAC,eAAe;IAYpE,OAAO,CAAC,KAAK;IACb,OAAO,CAAC,OAAO;IAZnB,OAAO,CAAC,eAAe,CAAmD;IAC1E,OAAO,CAAC,YAAY,CAAe;IACnC,OAAO,CAAC,iBAAiB,CAAe;IACxC,OAAO,CAAC,gBAAgB,CAA4D;IACpF,OAAO,CAAC,QAAQ,CAAC,CAAgD;IACjE,OAAO,CAAC,WAAW,CAAC,CAAQ;IAC5B,OAAO,CAAC,UAAU,CAAC,CAAQ;IAC3B,OAAO,CAAC,QAAQ,CAAyC;gBAGrD,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,UAAU,EACxC,KAAK,EAAE,YAAY,CAAC,gBAAgB,EACpC,OAAO,EAAE,OAAO,CAAC,UAAU;IAavC,WAAW,CAAE,EAAE,EAAE,CAAC,IAAI,EAAE,YAAY,CAAC,YAAY,GAAG,YAAY,CAAC,mBAAmB,KAAK,IAAI;IAU7F,aAAa,CAAE,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,cAAc,CAAC;IAe/D,MAAM,CAAC,IAAI,EAAE,YAAY,CAAC,gBAAgB,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,kBAAkB,CAAC,OAAO,CAAC;IAepH;;;;;;OAMG;IACG,WAAW,CAAE,KAAK,EAAE,UAAU,CAAC,KAAK;IAQpC,UAAU,CAAE,IAAI,EAAE,UAAU,CAAC,IAAI;IAkBvC;;OAEG;IACG,aAAa,CAAC,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE;IAM3D;;;OAGG;IACG,cAAc,CAAE,KAAK,EAAE,UAAU,CAAC,KAAK;IAK7C;;OAEG;IACG,UAAU,CAAE,IAAI,EAAE,UAAU,CAAC,UAAU;IAK7C,SAAS,CAAC,IAAI,EAAE,UAAU,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,UAAU,CAAC,UAAU;IAOzE,KAAK,CAAE,MAAM,EAAE,MAAM;IAkB3B;;OAEG;IACH,aAAa,CAAE,KAAK,EAAE,UAAU,CAAC,KAAK;IAmBhC,QAAQ,CAAC,YAAY,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM;IAgCzD,cAAc,IAAI,OAAO;IAOzB,UAAU,CAAE,WAAW,EAAE,GAAG;IAU5B,kBAAkB,CAAE,MAAM,EAAE,iBAAiB;IAqB7C,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,GAAG;IAUrC,gBAAgB;YAuBR,eAAe;IA0B7B,OAAO,CAAC,cAAc;YAIR,eAAe;CAqBhC"}
1
+ {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"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"}
package/build/service.js CHANGED
@@ -1,7 +1,12 @@
1
+ import path from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
1
3
  import logger from '@wdio/logger';
2
4
  import got from 'got';
3
- import { getBrowserDescription, getBrowserCapabilities, isBrowserstackCapability, getParentSuiteName } from './util.js';
5
+ import { getBrowserDescription, getBrowserCapabilities, isBrowserstackCapability, getParentSuiteName, isBrowserstackSession } from './util.js';
6
+ import InsightsHandler from './insights-handler.js';
4
7
  import { DEFAULT_OPTIONS } from './constants.js';
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = path.dirname(__filename);
5
10
  const log = logger('@wdio/browserstack-service');
6
11
  export default class BrowserstackService {
7
12
  _caps;
@@ -14,12 +19,20 @@ export default class BrowserstackService {
14
19
  _suiteTitle;
15
20
  _fullTitle;
16
21
  _options;
22
+ _specsRan = false;
23
+ _observability;
24
+ _currentTest;
25
+ _insightsHandler;
17
26
  constructor(options, _caps, _config) {
18
27
  this._caps = _caps;
19
28
  this._config = _config;
20
29
  this._options = { ...DEFAULT_OPTIONS, ...options };
21
30
  // added to maintain backward compatibility with webdriverIO v5
22
31
  this._config || (this._config = this._options);
32
+ this._observability = this._options.testObservability;
33
+ if (this._observability) {
34
+ this._config.reporters?.push(path.join(__dirname, 'reporter.js'));
35
+ }
23
36
  // Cucumber specific
24
37
  const strict = Boolean(this._config.cucumberOpts && this._config.cucumberOpts.strict);
25
38
  // See https://github.com/cucumber/cucumber-js/blob/master/src/runtime/index.ts#L136
@@ -47,7 +60,7 @@ export default class BrowserstackService {
47
60
  this._config.user = config.user;
48
61
  this._config.key = config.key;
49
62
  }
50
- before(caps, specs, browser) {
63
+ async before(caps, specs, browser) {
51
64
  // added to maintain backward compatibility with webdriverIO v5
52
65
  this._browser = browser ? browser : global.browser;
53
66
  // Ensure capabilities are not null in case of multiremote
@@ -55,7 +68,19 @@ export default class BrowserstackService {
55
68
  this._sessionBaseUrl = 'https://api-cloud.browserstack.com/app-automate/sessions';
56
69
  }
57
70
  this._scenariosThatRan = [];
58
- return this._printSessionURL();
71
+ if (this._observability && this._browser) {
72
+ this._insightsHandler = new InsightsHandler(this._browser, this._browser.capabilities, this._isAppAutomate(), this._browser.sessionId, this._config.framework);
73
+ await this._insightsHandler.before();
74
+ /**
75
+ * register command event
76
+ */
77
+ this._browser.on('command', async (command) => await this._insightsHandler?.browserCommand('client:beforeCommand', Object.assign(command, { sessionId: this._browser?.sessionId }), this._currentTest));
78
+ /**
79
+ * register result event
80
+ */
81
+ this._browser.on('result', async (result) => await this._insightsHandler?.browserCommand('client:afterCommand', Object.assign(result, { sessionId: this._browser?.sessionId }), this._currentTest));
82
+ }
83
+ return await this._printSessionURL();
59
84
  }
60
85
  /**
61
86
  * Set the default job name at the suite level to make sure we account
@@ -70,7 +95,17 @@ export default class BrowserstackService {
70
95
  await this._setSessionName(suite.title);
71
96
  }
72
97
  }
98
+ async beforeHook(test, context) {
99
+ if (this._config.framework !== 'cucumber') {
100
+ this._currentTest = test; // not update currentTest when this is called for cucumber step
101
+ }
102
+ await this._insightsHandler?.beforeHook(test, context);
103
+ }
104
+ async afterHook(test, context, result) {
105
+ await this._insightsHandler?.afterHook(test, result);
106
+ }
73
107
  async beforeTest(test) {
108
+ this._currentTest = test;
74
109
  let suiteTitle = this._suiteTitle;
75
110
  if (test.fullName) {
76
111
  // For Jasmine, `suite.title` is `Jasmine__TopLevel__Suite`.
@@ -85,35 +120,15 @@ export default class BrowserstackService {
85
120
  }
86
121
  await this._setSessionName(suiteTitle, test);
87
122
  await this._setAnnotation(`Test: ${test.fullName ?? test.title}`);
123
+ await this._insightsHandler?.beforeTest(test);
88
124
  }
89
- /**
90
- * For CucumberJS
91
- */
92
- async beforeFeature(uri, feature) {
93
- this._suiteTitle = feature.name;
94
- await this._setSessionName(feature.name);
95
- await this._setAnnotation(`Feature: ${feature.name}`);
96
- }
97
- /**
98
- * Runs before a Cucumber Scenario.
99
- * @param world world object containing information on pickle and test step
100
- */
101
- async beforeScenario(world) {
102
- const scenarioName = world.pickle.name || 'unknown scenario';
103
- await this._setAnnotation(`Scenario: ${scenarioName}`);
104
- }
105
- /**
106
- * For CucumberJS
107
- */
108
- async beforeStep(step) {
109
- const { keyword, text } = step;
110
- await this._setAnnotation(`Step: ${keyword}${text}`);
111
- }
112
- afterTest(test, context, results) {
125
+ async afterTest(test, context, results) {
126
+ this._specsRan = true;
113
127
  const { error, passed } = results;
114
128
  if (!passed) {
115
129
  this._failReasons.push((error && error.message) || 'Unknown Error');
116
130
  }
131
+ await this._insightsHandler?.afterTest(test, results);
117
132
  }
118
133
  async after(result) {
119
134
  const { preferScenarioName, setSessionName, setSessionStatus } = this._options;
@@ -125,16 +140,34 @@ export default class BrowserstackService {
125
140
  if (setSessionStatus) {
126
141
  const hasReasons = this._failReasons.length > 0;
127
142
  await this._updateJob({
128
- status: result === 0 ? 'passed' : 'failed',
143
+ status: result === 0 && this._specsRan ? 'passed' : 'failed',
129
144
  ...(setSessionName ? { name: this._fullTitle } : {}),
130
145
  ...(hasReasons ? { reason: this._failReasons.join('\n') } : {})
131
146
  });
132
147
  }
148
+ await this._insightsHandler?.uploadPending();
149
+ await this._insightsHandler?.teardown();
133
150
  }
134
151
  /**
135
152
  * For CucumberJS
136
153
  */
137
- afterScenario(world) {
154
+ async beforeFeature(uri, feature) {
155
+ this._suiteTitle = feature.name;
156
+ await this._setSessionName(feature.name);
157
+ await this._setAnnotation(`Feature: ${feature.name}`);
158
+ }
159
+ /**
160
+ * Runs before a Cucumber Scenario.
161
+ * @param world world object containing information on pickle and test step
162
+ */
163
+ async beforeScenario(world) {
164
+ this._currentTest = world;
165
+ await this._insightsHandler?.beforeScenario(world);
166
+ const scenarioName = world.pickle.name || 'unknown scenario';
167
+ await this._setAnnotation(`Scenario: ${scenarioName}`);
168
+ }
169
+ async afterScenario(world) {
170
+ this._specsRan = true;
138
171
  const status = world.result?.status.toLowerCase();
139
172
  if (status !== 'skipped') {
140
173
  this._scenariosThatRan.push(world.pickle.name || 'unknown pickle name');
@@ -146,6 +179,14 @@ export default class BrowserstackService {
146
179
  : 'Unknown Error'));
147
180
  this._failReasons.push(exception);
148
181
  }
182
+ await this._insightsHandler?.afterScenario(world);
183
+ }
184
+ async beforeStep(step, scenario) {
185
+ await this._insightsHandler?.beforeStep(step, scenario);
186
+ await this._setAnnotation(`Step: ${step.keyword}${step.text}`);
187
+ }
188
+ async afterStep(step, scenario, result) {
189
+ await this._insightsHandler?.afterStep(step, scenario, result);
149
190
  }
150
191
  async onReload(oldSessionId, newSessionId) {
151
192
  if (!this._browser) {
@@ -203,6 +244,9 @@ export default class BrowserstackService {
203
244
  .map((browserName) => (action(_browser[browserName].sessionId, browserName))));
204
245
  }
205
246
  _update(sessionId, requestBody) {
247
+ if (!isBrowserstackSession(this._browser)) {
248
+ return Promise.resolve();
249
+ }
206
250
  const sessionUrl = `${this._sessionBaseUrl}/${sessionId}.json`;
207
251
  log.debug(`Updating Browserstack session at ${sessionUrl} with request body: `, requestBody);
208
252
  return got.put(sessionUrl, {
@@ -212,7 +256,7 @@ export default class BrowserstackService {
212
256
  });
213
257
  }
214
258
  async _printSessionURL() {
215
- if (!this._browser) {
259
+ if (!this._browser || !isBrowserstackSession(this._browser)) {
216
260
  return Promise.resolve();
217
261
  }
218
262
  await this._multiRemoteAction(async (sessionId, browserName) => {
@@ -254,7 +298,7 @@ export default class BrowserstackService {
254
298
  return this._executeCommand('annotate', { data, level: 'info' });
255
299
  }
256
300
  async _executeCommand(action, args) {
257
- if (!this._browser) {
301
+ if (!this._browser || !isBrowserstackSession(this._browser)) {
258
302
  return Promise.resolve();
259
303
  }
260
304
  const cmd = { action, ...(args ? { arguments: args } : {}) };