@tvlabs/wdio-service 0.1.5 → 0.1.7

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.7";
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,116 @@ 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 { existing, build_id, url } = await this.requestUploadUrl(metadata, appSlug);
345
+ if (existing) {
346
+ this.log.info('Build is pre-existing, skipping upload');
294
347
  }
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;
348
+ else {
349
+ this.log.info('Uploading build...');
350
+ await this.uploadToUrl(url, buildPath, metadata);
351
+ const { application_id } = await this.extractBuildInfo();
352
+ this.log.info(`Build "${application_id}" processed successfully`);
353
+ }
354
+ return build_id;
298
355
  }
299
- tvlabsSessionLink(sessionId) {
300
- return `https://tvlabs.ai/app/sessions/${sessionId}`;
356
+ async requestUploadUrl(metadata, appSlug) {
357
+ try {
358
+ return await this.push(this.lobbyTopic, 'request_upload_url', { metadata, application_slug: appSlug });
359
+ }
360
+ catch (error) {
361
+ this.log.error('Error requesting upload URL:', error);
362
+ throw error;
363
+ }
301
364
  }
302
- static logSocketError(log, event, _transport, _establishedConnections) {
303
- log.error('Socket error:', event.error);
365
+ async uploadToUrl(url, filePath, metadata) {
366
+ try {
367
+ const response = await fetch(url, {
368
+ method: 'PUT',
369
+ headers: {
370
+ 'Content-Type': metadata.type,
371
+ 'Content-Length': String(metadata.size),
372
+ },
373
+ body: fs.createReadStream(filePath),
374
+ duplex: 'half',
375
+ });
376
+ if (!response.ok) {
377
+ throw new SevereServiceError(`Failed to upload build to storage, got ${response.status}`);
378
+ }
379
+ this.log.info('Upload complete');
380
+ }
381
+ catch (error) {
382
+ this.log.error('Error uploading build:', error);
383
+ throw error;
384
+ }
385
+ }
386
+ async extractBuildInfo() {
387
+ this.log.info('Processing uploaded build...');
388
+ try {
389
+ return await this.push(this.lobbyTopic, 'extract_build_info', {});
390
+ }
391
+ catch (error) {
392
+ this.log.error('Error processing build:', error);
393
+ throw error;
394
+ }
395
+ }
396
+ async getFileMetadata(buildPath) {
397
+ const filename = path.basename(buildPath);
398
+ const size = fs.statSync(buildPath).size;
399
+ const type = this.detectMimeType(filename);
400
+ const sha256 = await this.computeSha256(buildPath);
401
+ return { filename, type, size, sha256 };
402
+ }
403
+ async computeSha256(buildPath) {
404
+ return new Promise((resolve, reject) => {
405
+ const hash = crypto.createHash('sha256');
406
+ const stream = fs.createReadStream(buildPath);
407
+ stream.on('data', (chunk) => hash.update(chunk));
408
+ stream.on('end', () => resolve(hash.digest('hex')));
409
+ stream.on('error', (err) => reject(err));
410
+ });
411
+ }
412
+ detectMimeType(filename) {
413
+ const fileExtension = path.extname(filename).toLowerCase();
414
+ switch (fileExtension) {
415
+ case '.apk':
416
+ return 'application/vnd.android.package-archive';
417
+ case '.zip':
418
+ return 'application/zip';
419
+ default:
420
+ return 'application/octet-stream';
421
+ }
304
422
  }
305
423
  }
306
424
 
@@ -324,15 +442,22 @@ class TVLabsService {
324
442
  }
325
443
  }
326
444
  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();
445
+ const buildPath = this.buildPath();
446
+ if (buildPath) {
447
+ const buildChannel = new BuildChannel(this.buildEndpoint(), this.reconnectRetries(), this.apiKey(), this.logLevel());
448
+ await buildChannel.connect();
449
+ capabilities['tvlabs:build'] = await buildChannel.uploadBuild(buildPath, this.appSlug());
450
+ await buildChannel.disconnect();
451
+ }
452
+ const sessionChannel = new SessionChannel(this.sessionEndpoint(), this.reconnectRetries(), this.apiKey(), this.logLevel());
453
+ await sessionChannel.connect();
454
+ capabilities['tvlabs:session_id'] = await sessionChannel.newSession(capabilities, this.retries());
455
+ await sessionChannel.disconnect();
331
456
  }
332
457
  setupRequestId() {
333
458
  const originalTransformRequest = this._config.transformRequest;
334
459
  this._config.transformRequest = (requestOptions) => {
335
- const requestId = crypto.randomUUID();
460
+ const requestId = crypto$1.randomUUID();
336
461
  const originalRequestOptions = typeof originalTransformRequest === 'function'
337
462
  ? originalTransformRequest(requestOptions)
338
463
  : requestOptions;
@@ -357,8 +482,17 @@ class TVLabsService {
357
482
  }
358
483
  }
359
484
  }
360
- endpoint() {
361
- return this._options.endpoint ?? 'wss://tvlabs.ai/appium';
485
+ buildPath() {
486
+ return this._options.buildPath;
487
+ }
488
+ appSlug() {
489
+ return this._options.app;
490
+ }
491
+ sessionEndpoint() {
492
+ return this._options.sessionEndpoint ?? 'wss://tvlabs.ai/appium';
493
+ }
494
+ buildEndpoint() {
495
+ return this._options.buildEndpoint ?? 'wss://tvlabs.ai/cli';
362
496
  }
363
497
  retries() {
364
498
  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,19 @@ 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
+ existing: boolean;
48
+ application_id?: string;
49
+ };
50
+ export type TVLabsExtractBuildInfoResponse = {
51
+ application_id: string;
52
+ };
53
+ export type TVLabsBuildMetadata = {
54
+ filename: string;
55
+ type: string;
56
+ size: number;
57
+ sha256: string;
58
+ };
41
59
  //# 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;IACjB,QAAQ,EAAE,OAAO,CAAC;IAClB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,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.7",
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
+ }