@tvlabs/wdio-service 0.1.5 → 0.1.6

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/esm/index.js CHANGED
@@ -1,7 +1,10 @@
1
1
  import { SevereServiceError } from 'webdriverio';
2
- import * as crypto from 'crypto';
2
+ import * as crypto$1 from 'crypto';
3
3
  import { WebSocket } from 'ws';
4
4
  import { Socket } from 'phoenix';
5
+ import * as fs from 'node:fs';
6
+ import * as path from 'node:path';
7
+ import * as crypto from 'node:crypto';
5
8
 
6
9
  const LOG_LEVELS = {
7
10
  error: 0,
@@ -107,7 +110,7 @@ class Logger {
107
110
  }
108
111
 
109
112
  var name = "@tvlabs/wdio-service";
110
- var version = "0.1.5";
113
+ var version = "0.1.6";
111
114
  var packageJson = {
112
115
  name: name,
113
116
  version: version};
@@ -125,15 +128,81 @@ function getServiceName() {
125
128
  return packageJson.name;
126
129
  }
127
130
 
128
- class TVLabsChannel {
131
+ class BaseChannel {
129
132
  endpoint;
130
133
  maxReconnectRetries;
131
134
  key;
132
135
  logLevel;
133
136
  socket;
137
+ log;
138
+ constructor(endpoint, maxReconnectRetries, key, logLevel = 'info', loggerName) {
139
+ this.endpoint = endpoint;
140
+ this.maxReconnectRetries = maxReconnectRetries;
141
+ this.key = key;
142
+ this.logLevel = logLevel;
143
+ this.log = new Logger(loggerName, this.logLevel);
144
+ this.socket = new Socket(this.endpoint, {
145
+ transport: WebSocket,
146
+ params: this.params(),
147
+ reconnectAfterMs: this.reconnectAfterMs.bind(this),
148
+ });
149
+ this.socket.onError((...args) => BaseChannel.logSocketError(this.log, ...args));
150
+ }
151
+ async join(topic) {
152
+ return new Promise((res, rej) => {
153
+ topic
154
+ .join()
155
+ .receive('ok', (_resp) => {
156
+ res();
157
+ })
158
+ .receive('error', ({ response }) => {
159
+ rej('Failed to join topic: ' + response);
160
+ })
161
+ .receive('timeout', () => {
162
+ rej('timeout');
163
+ });
164
+ });
165
+ }
166
+ async push(topic, event, payload) {
167
+ return new Promise((res, rej) => {
168
+ topic
169
+ .push(event, payload)
170
+ .receive('ok', (msg) => {
171
+ res(msg);
172
+ })
173
+ .receive('error', (reason) => {
174
+ rej(reason);
175
+ })
176
+ .receive('timeout', () => {
177
+ rej('timeout');
178
+ });
179
+ });
180
+ }
181
+ params() {
182
+ const serviceInfo = getServiceInfo();
183
+ this.log.debug('Info:', serviceInfo);
184
+ return {
185
+ ...serviceInfo,
186
+ api_key: this.key,
187
+ };
188
+ }
189
+ reconnectAfterMs(tries) {
190
+ if (tries > this.maxReconnectRetries) {
191
+ throw new SevereServiceError('Could not connect to TV Labs, please check your connection.');
192
+ }
193
+ const wait = [0, 1000, 3000, 5000][tries] || 10000;
194
+ this.log.info(`[${tries}/${this.maxReconnectRetries}] Waiting ${wait}ms before re-attempting to connect...`);
195
+ return wait;
196
+ }
197
+ static logSocketError(log, event, _transport, _establishedConnections) {
198
+ const error = event.error;
199
+ log.error('Socket error:', error || event);
200
+ }
201
+ }
202
+
203
+ class SessionChannel extends BaseChannel {
134
204
  lobbyTopic;
135
205
  requestTopic;
136
- log;
137
206
  events = {
138
207
  SESSION_READY: 'session:ready',
139
208
  SESSION_FAILED: 'session:failed',
@@ -143,17 +212,7 @@ class TVLabsChannel {
143
212
  REQUEST_MATCHING: 'request:matching',
144
213
  };
145
214
  constructor(endpoint, maxReconnectRetries, key, logLevel = 'info') {
146
- this.endpoint = endpoint;
147
- this.maxReconnectRetries = maxReconnectRetries;
148
- this.key = key;
149
- this.logLevel = logLevel;
150
- this.log = new Logger('@tvlabs/wdio-channel', this.logLevel);
151
- this.socket = new Socket(this.endpoint, {
152
- transport: WebSocket,
153
- params: this.params(),
154
- reconnectAfterMs: this.reconnectAfterMs.bind(this),
155
- });
156
- this.socket.onError((...args) => TVLabsChannel.logSocketError(this.log, ...args));
215
+ super(endpoint, maxReconnectRetries, key, logLevel, '@tvlabs/session-channel');
157
216
  this.lobbyTopic = this.socket.channel('requests:lobby');
158
217
  }
159
218
  async disconnect() {
@@ -165,14 +224,14 @@ class TVLabsChannel {
165
224
  }
166
225
  async connect() {
167
226
  try {
168
- this.log.debug('Connecting to TV Labs...');
227
+ this.log.debug('Connecting to session channel...');
169
228
  this.socket.connect();
170
229
  await this.join(this.lobbyTopic);
171
- this.log.debug('Connected to TV Labs!');
230
+ this.log.debug('Connected to session channel!');
172
231
  }
173
232
  catch (error) {
174
- this.log.error('Error connecting to TV Labs:', error);
175
- throw new SevereServiceError('Could not connect to TV Labs, please check your connection.');
233
+ this.log.error('Error connecting to session channel:', error);
234
+ throw new SevereServiceError('Could not connect to session channel, please check your connection.');
176
235
  }
177
236
  }
178
237
  async newSession(capabilities, maxRetries, retry = 0) {
@@ -250,57 +309,111 @@ class TVLabsChannel {
250
309
  throw error;
251
310
  }
252
311
  }
253
- async join(topic) {
254
- return new Promise((res, rej) => {
255
- topic
256
- .join()
257
- .receive('ok', (_resp) => {
258
- res();
259
- })
260
- .receive('error', ({ response }) => {
261
- rej('Failed to join topic: ' + response);
262
- })
263
- .receive('timeout', () => {
264
- rej('timeout');
265
- });
266
- });
312
+ tvlabsSessionLink(sessionId) {
313
+ return `https://tvlabs.ai/app/sessions/${sessionId}`;
267
314
  }
268
- async push(topic, event, payload) {
269
- return new Promise((res, rej) => {
270
- topic
271
- .push(event, payload)
272
- .receive('ok', (msg) => {
273
- res(msg);
274
- })
275
- .receive('error', (reason) => {
276
- rej(reason);
277
- })
278
- .receive('timeout', () => {
279
- rej('timeout');
280
- });
315
+ }
316
+
317
+ class BuildChannel extends BaseChannel {
318
+ lobbyTopic;
319
+ constructor(endpoint, maxReconnectRetries, key, logLevel = 'info') {
320
+ super(endpoint, maxReconnectRetries, key, logLevel, '@tvlabs/build-channel');
321
+ this.lobbyTopic = this.socket.channel('upload:lobby');
322
+ }
323
+ async disconnect() {
324
+ return new Promise((res, _rej) => {
325
+ this.lobbyTopic.leave();
326
+ this.socket.disconnect(() => res());
281
327
  });
282
328
  }
283
- params() {
284
- const serviceInfo = getServiceInfo();
285
- this.log.debug('Info:', serviceInfo);
286
- return {
287
- ...serviceInfo,
288
- api_key: this.key,
289
- };
329
+ async connect() {
330
+ try {
331
+ this.log.debug('Connecting to build channel...');
332
+ this.socket.connect();
333
+ await this.join(this.lobbyTopic);
334
+ this.log.debug('Connected to build channel!');
335
+ }
336
+ catch (error) {
337
+ this.log.error('Error connecting to build channel:', error);
338
+ throw new SevereServiceError('Could not connect to build channel, please check your connection.');
339
+ }
290
340
  }
291
- reconnectAfterMs(tries) {
292
- if (tries > this.maxReconnectRetries) {
293
- throw new SevereServiceError('Could not connect to TV Labs, please check your connection.');
341
+ async uploadBuild(buildPath, appSlug) {
342
+ const metadata = await this.getFileMetadata(buildPath);
343
+ this.log.info(`Requesting upload for build ${metadata.filename} (${metadata.type}, ${metadata.size} bytes)`);
344
+ const { url, build_id } = await this.requestUploadUrl(metadata, appSlug);
345
+ this.log.info('Uploading build...');
346
+ await this.uploadToUrl(url, buildPath, metadata);
347
+ const { application_id } = await this.extractBuildInfo();
348
+ this.log.info(`Build "${application_id}" processed successfully`);
349
+ return build_id;
350
+ }
351
+ async requestUploadUrl(metadata, appSlug) {
352
+ try {
353
+ return await this.push(this.lobbyTopic, 'request_upload_url', { metadata, application_slug: appSlug });
354
+ }
355
+ catch (error) {
356
+ this.log.error('Error requesting upload URL:', error);
357
+ throw error;
294
358
  }
295
- const wait = [0, 1000, 3000, 5000][tries] || 10000;
296
- this.log.info(`[${tries}/${this.maxReconnectRetries}] Waiting ${wait}ms before re-attempting to connect...`);
297
- return wait;
298
359
  }
299
- tvlabsSessionLink(sessionId) {
300
- return `https://tvlabs.ai/app/sessions/${sessionId}`;
360
+ async uploadToUrl(url, filePath, metadata) {
361
+ try {
362
+ const response = await fetch(url, {
363
+ method: 'PUT',
364
+ headers: {
365
+ 'Content-Type': metadata.type,
366
+ 'Content-Length': String(metadata.size),
367
+ },
368
+ body: fs.createReadStream(filePath),
369
+ duplex: 'half',
370
+ });
371
+ if (!response.ok) {
372
+ throw new SevereServiceError(`Failed to upload build to storage, got ${response.status}`);
373
+ }
374
+ this.log.info('Upload complete');
375
+ }
376
+ catch (error) {
377
+ this.log.error('Error uploading build:', error);
378
+ throw error;
379
+ }
301
380
  }
302
- static logSocketError(log, event, _transport, _establishedConnections) {
303
- log.error('Socket error:', event.error);
381
+ async extractBuildInfo() {
382
+ this.log.info('Processing uploaded build...');
383
+ try {
384
+ return await this.push(this.lobbyTopic, 'extract_build_info', {});
385
+ }
386
+ catch (error) {
387
+ this.log.error('Error processing build:', error);
388
+ throw error;
389
+ }
390
+ }
391
+ async getFileMetadata(buildPath) {
392
+ const filename = path.basename(buildPath);
393
+ const size = fs.statSync(buildPath).size;
394
+ const type = this.detectMimeType(filename);
395
+ const sha256 = await this.computeSha256(buildPath);
396
+ return { filename, type, size, sha256 };
397
+ }
398
+ async computeSha256(buildPath) {
399
+ return new Promise((resolve, reject) => {
400
+ const hash = crypto.createHash('sha256');
401
+ const stream = fs.createReadStream(buildPath);
402
+ stream.on('data', (chunk) => hash.update(chunk));
403
+ stream.on('end', () => resolve(hash.digest('hex')));
404
+ stream.on('error', (err) => reject(err));
405
+ });
406
+ }
407
+ detectMimeType(filename) {
408
+ const fileExtension = path.extname(filename).toLowerCase();
409
+ switch (fileExtension) {
410
+ case '.apk':
411
+ return 'application/vnd.android.package-archive';
412
+ case '.zip':
413
+ return 'application/zip';
414
+ default:
415
+ return 'application/octet-stream';
416
+ }
304
417
  }
305
418
  }
306
419
 
@@ -324,15 +437,22 @@ class TVLabsService {
324
437
  }
325
438
  }
326
439
  async beforeSession(_config, capabilities, _specs, _cid) {
327
- const channel = new TVLabsChannel(this.endpoint(), this.reconnectRetries(), this.apiKey(), this.logLevel());
328
- await channel.connect();
329
- capabilities['tvlabs:session_id'] = await channel.newSession(capabilities, this.retries());
330
- await channel.disconnect();
440
+ const buildPath = this.buildPath();
441
+ if (buildPath) {
442
+ const buildChannel = new BuildChannel(this.buildEndpoint(), this.reconnectRetries(), this.apiKey(), this.logLevel());
443
+ await buildChannel.connect();
444
+ capabilities['tvlabs:build'] = await buildChannel.uploadBuild(buildPath, this.appSlug());
445
+ await buildChannel.disconnect();
446
+ }
447
+ const sessionChannel = new SessionChannel(this.sessionEndpoint(), this.reconnectRetries(), this.apiKey(), this.logLevel());
448
+ await sessionChannel.connect();
449
+ capabilities['tvlabs:session_id'] = await sessionChannel.newSession(capabilities, this.retries());
450
+ await sessionChannel.disconnect();
331
451
  }
332
452
  setupRequestId() {
333
453
  const originalTransformRequest = this._config.transformRequest;
334
454
  this._config.transformRequest = (requestOptions) => {
335
- const requestId = crypto.randomUUID();
455
+ const requestId = crypto$1.randomUUID();
336
456
  const originalRequestOptions = typeof originalTransformRequest === 'function'
337
457
  ? originalTransformRequest(requestOptions)
338
458
  : requestOptions;
@@ -357,8 +477,17 @@ class TVLabsService {
357
477
  }
358
478
  }
359
479
  }
360
- endpoint() {
361
- return this._options.endpoint ?? 'wss://tvlabs.ai/appium';
480
+ buildPath() {
481
+ return this._options.buildPath;
482
+ }
483
+ appSlug() {
484
+ return this._options.app;
485
+ }
486
+ sessionEndpoint() {
487
+ return this._options.sessionEndpoint ?? 'wss://tvlabs.ai/appium';
488
+ }
489
+ buildEndpoint() {
490
+ return this._options.buildEndpoint ?? 'wss://tvlabs.ai/cli';
362
491
  }
363
492
  retries() {
364
493
  return this._options.retries ?? 3;
package/esm/service.d.ts CHANGED
@@ -10,7 +10,10 @@ export default class TVLabsService implements Services.ServiceInstance {
10
10
  beforeSession(_config: Omit<Options.Testrunner, 'capabilities'>, capabilities: TVLabsCapabilities, _specs: string[], _cid: string): Promise<void>;
11
11
  private setupRequestId;
12
12
  private setRequestHeader;
13
- private endpoint;
13
+ private buildPath;
14
+ private appSlug;
15
+ private sessionEndpoint;
16
+ private buildEndpoint;
14
17
  private retries;
15
18
  private apiKey;
16
19
  private logLevel;
@@ -1 +1 @@
1
- {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,QAAQ,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AACnE,OAAO,KAAK,EACV,kBAAkB,EAClB,oBAAoB,EAErB,MAAM,YAAY,CAAC;AAEpB,MAAM,CAAC,OAAO,OAAO,aAAc,YAAW,QAAQ,CAAC,eAAe;IAIlE,OAAO,CAAC,QAAQ;IAChB,OAAO,CAAC,aAAa;IACrB,OAAO,CAAC,OAAO;IALjB,OAAO,CAAC,GAAG,CAAS;gBAGV,QAAQ,EAAE,oBAAoB,EAC9B,aAAa,EAAE,YAAY,CAAC,8BAA8B,EAC1D,OAAO,EAAE,OAAO,CAAC,WAAW;IAQtC,SAAS,CACP,OAAO,EAAE,OAAO,CAAC,UAAU,EAC3B,KAAK,EAAE,YAAY,CAAC,sBAAsB;IAStC,aAAa,CACjB,OAAO,EAAE,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,cAAc,CAAC,EACjD,YAAY,EAAE,kBAAkB,EAChC,MAAM,EAAE,MAAM,EAAE,EAChB,IAAI,EAAE,MAAM;IAmBd,OAAO,CAAC,cAAc;IA0BtB,OAAO,CAAC,gBAAgB;IAgBxB,OAAO,CAAC,QAAQ;IAIhB,OAAO,CAAC,OAAO;IAIf,OAAO,CAAC,MAAM;IAId,OAAO,CAAC,QAAQ;IAIhB,OAAO,CAAC,eAAe;IAIvB,OAAO,CAAC,gBAAgB;CAGzB"}
1
+ {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,QAAQ,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AACnE,OAAO,KAAK,EACV,kBAAkB,EAClB,oBAAoB,EAErB,MAAM,YAAY,CAAC;AAEpB,MAAM,CAAC,OAAO,OAAO,aAAc,YAAW,QAAQ,CAAC,eAAe;IAIlE,OAAO,CAAC,QAAQ;IAChB,OAAO,CAAC,aAAa;IACrB,OAAO,CAAC,OAAO;IALjB,OAAO,CAAC,GAAG,CAAS;gBAGV,QAAQ,EAAE,oBAAoB,EAC9B,aAAa,EAAE,YAAY,CAAC,8BAA8B,EAC1D,OAAO,EAAE,OAAO,CAAC,WAAW;IAQtC,SAAS,CACP,OAAO,EAAE,OAAO,CAAC,UAAU,EAC3B,KAAK,EAAE,YAAY,CAAC,sBAAsB;IAStC,aAAa,CACjB,OAAO,EAAE,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE,cAAc,CAAC,EACjD,YAAY,EAAE,kBAAkB,EAChC,MAAM,EAAE,MAAM,EAAE,EAChB,IAAI,EAAE,MAAM;IAuCd,OAAO,CAAC,cAAc;IA0BtB,OAAO,CAAC,gBAAgB;IAgBxB,OAAO,CAAC,SAAS;IAIjB,OAAO,CAAC,OAAO;IAIf,OAAO,CAAC,eAAe;IAIvB,OAAO,CAAC,aAAa;IAIrB,OAAO,CAAC,OAAO;IAIf,OAAO,CAAC,MAAM;IAId,OAAO,CAAC,QAAQ;IAIhB,OAAO,CAAC,eAAe;IAIvB,OAAO,CAAC,gBAAgB;CAGzB"}
package/esm/types.d.ts CHANGED
@@ -2,8 +2,11 @@ import type { Capabilities } from '@wdio/types';
2
2
  export type LogLevel = 'error' | 'warn' | 'info' | 'debug' | 'trace' | 'silent';
3
3
  export type TVLabsServiceOptions = {
4
4
  apiKey: string;
5
- endpoint?: string;
5
+ sessionEndpoint?: string;
6
+ buildEndpoint?: string;
6
7
  retries?: number;
8
+ buildPath?: string;
9
+ app?: string;
7
10
  reconnectRetries?: number;
8
11
  attachRequestId?: boolean;
9
12
  };
@@ -38,4 +41,17 @@ export type TVLabsServiceInfo = {
38
41
  service_version: string;
39
42
  service_name: string;
40
43
  };
44
+ export type TVLabsRequestUploadUrlResponse = {
45
+ url: string;
46
+ build_id: string;
47
+ };
48
+ export type TVLabsExtractBuildInfoResponse = {
49
+ application_id: string;
50
+ };
51
+ export type TVLabsBuildMetadata = {
52
+ filename: string;
53
+ type: string;
54
+ size: number;
55
+ sha256: string;
56
+ };
41
57
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,OAAO,GAAG,QAAQ,CAAC;AAEhF,MAAM,MAAM,oBAAoB,GAAG;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAC5B,YAAY,CAAC,+BAA+B,GAAG;IAC7C,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,oBAAoB,CAAC,EAAE;QACrB,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,kCAAkC,CAAC,EAAE,MAAM,CAAC;QAC5C,qBAAqB,CAAC,EAAE,OAAO,CAAC;KACjC,CAAC;IACF,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,uBAAuB,CAAC,EAAE,MAAM,CAAC;CAClC,CAAC;AAEJ,MAAM,MAAM,gCAAgC,GAAG,CAC7C,QAAQ,EAAE,0BAA0B,KACjC,IAAI,CAAC;AAEV,MAAM,MAAM,0BAA0B,GAAG;IACvC,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,4BAA4B,GAAG;IACzC,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG,iBAAiB,GAAG;IACnD,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,eAAe,EAAE,MAAM,CAAC;IACxB,YAAY,EAAE,MAAM,CAAC;CACtB,CAAC"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,MAAM,MAAM,QAAQ,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,OAAO,GAAG,QAAQ,CAAC;AAEhF,MAAM,MAAM,oBAAoB,GAAG;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAC5B,YAAY,CAAC,+BAA+B,GAAG;IAC7C,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,oBAAoB,CAAC,EAAE;QACrB,YAAY,CAAC,EAAE,MAAM,CAAC;QACtB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,kCAAkC,CAAC,EAAE,MAAM,CAAC;QAC5C,qBAAqB,CAAC,EAAE,OAAO,CAAC;KACjC,CAAC;IACF,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,uBAAuB,CAAC,EAAE,MAAM,CAAC;CAClC,CAAC;AAEJ,MAAM,MAAM,gCAAgC,GAAG,CAC7C,QAAQ,EAAE,0BAA0B,KACjC,IAAI,CAAC;AAEV,MAAM,MAAM,0BAA0B,GAAG;IACvC,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,4BAA4B,GAAG;IACzC,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG,iBAAiB,GAAG;IACnD,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,eAAe,EAAE,MAAM,CAAC;IACxB,YAAY,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,8BAA8B,GAAG;IAC3C,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,8BAA8B,GAAG;IAC3C,cAAc,EAAE,MAAM,CAAC;CACxB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tvlabs/wdio-service",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "WebdriverIO service that provides a better integration into TV Labs",
5
5
  "author": "Regan Karlewicz <regan@tvlabs.ai>",
6
6
  "license": "Apache-2.0",
@@ -9,7 +9,7 @@
9
9
  },
10
10
  "repository": {
11
11
  "type": "git",
12
- "url": "git+https://github.com/tv-labs/wdio-tvlabs-service.git"
12
+ "url": "git+https://github.com/tv-labs/wdio-service.git"
13
13
  },
14
14
  "keywords": [
15
15
  "wdio-plugin",
@@ -19,7 +19,7 @@
19
19
  "appium"
20
20
  ],
21
21
  "bugs": {
22
- "url": "https://github.com/tv-labs/wdio-tvlabs-service/issues"
22
+ "url": "https://github.com/tv-labs/wdio-service/issues"
23
23
  },
24
24
  "scripts": {
25
25
  "build": "npm run clean && rollup -c",
@@ -29,7 +29,8 @@
29
29
  "format": "prettier --write .",
30
30
  "format:check": "prettier --check .",
31
31
  "lint": "eslint",
32
- "test": "vitest --config vitest.config.ts --coverage",
32
+ "test": "vitest --config vitest.config.ts --coverage --run",
33
+ "test:watch": "vitest --config vitest.config.ts --coverage",
33
34
  "publish:dry": "npm publish --access public --provenance --dry-run"
34
35
  },
35
36
  "type": "module",
@@ -0,0 +1,110 @@
1
+ import { WebSocket } from 'ws';
2
+ import { Socket, type Channel } from 'phoenix';
3
+ import { SevereServiceError } from 'webdriverio';
4
+ import { Logger } from '../logger.js';
5
+ import { getServiceInfo } from '../utils.js';
6
+
7
+ import type { TVLabsSocketParams, LogLevel } from '../types.js';
8
+ import type { PhoenixChannelJoinResponse } from '../phoenix.js';
9
+
10
+ export abstract class BaseChannel {
11
+ protected socket: Socket;
12
+ protected log: Logger;
13
+
14
+ constructor(
15
+ protected endpoint: string,
16
+ protected maxReconnectRetries: number,
17
+ protected key: string,
18
+ protected logLevel: LogLevel = 'info',
19
+ loggerName: string,
20
+ ) {
21
+ this.log = new Logger(loggerName, this.logLevel);
22
+
23
+ this.socket = new Socket(this.endpoint, {
24
+ transport: WebSocket,
25
+ params: this.params(),
26
+ reconnectAfterMs: this.reconnectAfterMs.bind(this),
27
+ });
28
+
29
+ this.socket.onError((...args) =>
30
+ BaseChannel.logSocketError(this.log, ...args),
31
+ );
32
+ }
33
+
34
+ abstract connect(): Promise<void>;
35
+ abstract disconnect(): Promise<void>;
36
+
37
+ protected async join(topic: Channel): Promise<void> {
38
+ return new Promise((res, rej) => {
39
+ topic
40
+ .join()
41
+ .receive('ok', (_resp: PhoenixChannelJoinResponse) => {
42
+ res();
43
+ })
44
+ .receive('error', ({ response }: PhoenixChannelJoinResponse) => {
45
+ rej('Failed to join topic: ' + response);
46
+ })
47
+ .receive('timeout', () => {
48
+ rej('timeout');
49
+ });
50
+ });
51
+ }
52
+
53
+ protected async push<T>(
54
+ topic: Channel,
55
+ event: string,
56
+ payload: object,
57
+ ): Promise<T> {
58
+ return new Promise((res, rej) => {
59
+ topic
60
+ .push(event, payload)
61
+ .receive('ok', (msg: T) => {
62
+ res(msg);
63
+ })
64
+ .receive('error', (reason: string) => {
65
+ rej(reason);
66
+ })
67
+ .receive('timeout', () => {
68
+ rej('timeout');
69
+ });
70
+ });
71
+ }
72
+
73
+ private params(): TVLabsSocketParams {
74
+ const serviceInfo = getServiceInfo();
75
+
76
+ this.log.debug('Info:', serviceInfo);
77
+
78
+ return {
79
+ ...serviceInfo,
80
+ api_key: this.key,
81
+ };
82
+ }
83
+
84
+ private reconnectAfterMs(tries: number) {
85
+ if (tries > this.maxReconnectRetries) {
86
+ throw new SevereServiceError(
87
+ 'Could not connect to TV Labs, please check your connection.',
88
+ );
89
+ }
90
+
91
+ const wait = [0, 1000, 3000, 5000][tries] || 10000;
92
+
93
+ this.log.info(
94
+ `[${tries}/${this.maxReconnectRetries}] Waiting ${wait}ms before re-attempting to connect...`,
95
+ );
96
+
97
+ return wait;
98
+ }
99
+
100
+ private static logSocketError(
101
+ log: Logger,
102
+ event: ErrorEvent,
103
+ _transport: new (endpoint: string) => object,
104
+ _establishedConnections: number,
105
+ ) {
106
+ const error = event.error;
107
+
108
+ log.error('Socket error:', error || event);
109
+ }
110
+ }