@tvlabs/wdio-service 0.1.4 → 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/README.md +31 -17
- package/cjs/channels/base.d.ts +20 -0
- package/cjs/channels/base.d.ts.map +1 -0
- package/cjs/channels/build.d.ts +16 -0
- package/cjs/channels/build.d.ts.map +1 -0
- package/cjs/{channel.d.ts → channels/session.d.ts} +4 -14
- package/cjs/channels/session.d.ts.map +1 -0
- package/cjs/index.js +252 -72
- package/cjs/logger.d.ts +1 -0
- package/cjs/logger.d.ts.map +1 -1
- package/cjs/service.d.ts +4 -1
- package/cjs/service.d.ts.map +1 -1
- package/cjs/types.d.ts +17 -1
- package/cjs/types.d.ts.map +1 -1
- package/esm/channels/base.d.ts +20 -0
- package/esm/channels/base.d.ts.map +1 -0
- package/esm/channels/build.d.ts +16 -0
- package/esm/channels/build.d.ts.map +1 -0
- package/esm/{channel.d.ts → channels/session.d.ts} +4 -14
- package/esm/channels/session.d.ts.map +1 -0
- package/esm/index.js +249 -72
- package/esm/logger.d.ts +1 -0
- package/esm/logger.d.ts.map +1 -1
- package/esm/service.d.ts +4 -1
- package/esm/service.d.ts.map +1 -1
- package/esm/types.d.ts +17 -1
- package/esm/types.d.ts.map +1 -1
- package/package.json +9 -8
- package/src/channels/base.ts +110 -0
- package/src/channels/build.ts +174 -0
- package/src/{channel.ts → channels/session.ts} +18 -102
- package/src/logger.ts +60 -3
- package/src/service.ts +41 -8
- package/src/types.ts +20 -1
- package/cjs/channel.d.ts.map +0 -1
- package/esm/channel.d.ts.map +0 -1
package/README.md
CHANGED
|
@@ -10,9 +10,11 @@
|
|
|
10
10
|
|
|
11
11
|
## Introduction
|
|
12
12
|
|
|
13
|
-
The `@tvlabs/wdio-service` package uses a websocket to connect to the TV Labs platform before an Appium session begins, logging events relating to
|
|
13
|
+
The `@tvlabs/wdio-service` package uses a websocket to connect to the TV Labs platform before an Appium session begins, logging events relating to build upload and session creation as they occur. This offloads the responsibility of creating the TV Labs session from the `POST /session` Webdriver endpoint, leading to more reliable session requests and creation.
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
If a build path is provided, the service first makes a build upload request to the TV Labs platform, and then sets the `tvlabs:build` capability to the newly created build ID.
|
|
16
|
+
|
|
17
|
+
The service then makes a session request and then subscribes to events for that request. Once the session has been filled and is ready for the Webdriver script to begin, the service receives a ready event with the TV Labs session ID. This session ID is injected into the capabilities as `tvlabs:session_id` on the Webdriver session create request.
|
|
16
18
|
|
|
17
19
|
Additionally, the service adds a unique request ID for each request made. The service will generate and attach an `x-request-id` header before each request to the TV Labs platform. This can be used to correlate requests in the client side logs to the Appium server logs.
|
|
18
20
|
|
|
@@ -56,22 +58,22 @@ To use this with WebdriverIO remote but without the test runner, call the before
|
|
|
56
58
|
import { remote } from 'webdriverio';
|
|
57
59
|
import { TVLabsService } from '@tvlabs/wdio-service';
|
|
58
60
|
|
|
59
|
-
const capabilities = { ... };
|
|
60
|
-
|
|
61
|
-
const wdOpts = {
|
|
62
|
-
capabilities,
|
|
63
|
-
hostname: 'appium.tvlabs.ai',
|
|
64
|
-
port: 4723,
|
|
65
|
-
headers: {
|
|
66
|
-
Authorization: `Bearer ${process.env.TVLABS_API_TOKEN}`,
|
|
67
|
-
},
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
const serviceOpts = {
|
|
71
|
-
apiKey: process.env.TVLABS_API_TOKEN,
|
|
72
|
-
}
|
|
73
|
-
|
|
74
61
|
async function run() {
|
|
62
|
+
const capabilities = { ... };
|
|
63
|
+
|
|
64
|
+
const wdOpts = {
|
|
65
|
+
capabilities,
|
|
66
|
+
hostname: 'appium.tvlabs.ai',
|
|
67
|
+
port: 4723,
|
|
68
|
+
headers: {
|
|
69
|
+
Authorization: `Bearer ${process.env.TVLABS_API_TOKEN}`,
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const serviceOpts = {
|
|
74
|
+
apiKey: process.env.TVLABS_API_TOKEN,
|
|
75
|
+
}
|
|
76
|
+
|
|
75
77
|
const service = new TVLabsService(serviceOpts, capabilities, {})
|
|
76
78
|
|
|
77
79
|
// The TV Labs service does not use specs or a cid, pass default values.
|
|
@@ -100,6 +102,18 @@ run();
|
|
|
100
102
|
- **Required:** Yes
|
|
101
103
|
- **Description:** TV Labs API key used for authentication to the platform
|
|
102
104
|
|
|
105
|
+
### `buildPath`
|
|
106
|
+
|
|
107
|
+
- **Type:** `string`
|
|
108
|
+
- **Required:** No
|
|
109
|
+
- **Description:** Path to the packaged build to use for the session. When provided, this will perform a build upload before the session is created, and sets the `tvlabs:build` capability to the newly created build ID. The build is uploaded under the organizations default App unless the `app` option is provided
|
|
110
|
+
|
|
111
|
+
### `app`
|
|
112
|
+
|
|
113
|
+
- **Type:** `string`
|
|
114
|
+
- **Required:** No
|
|
115
|
+
- **Description:** Slug of the App for build uploads. When provided in combination with `buildPath`, the build is uploaded under this specified App.
|
|
116
|
+
|
|
103
117
|
### `retries`
|
|
104
118
|
|
|
105
119
|
- **Type:** `number`
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Socket, type Channel } from 'phoenix';
|
|
2
|
+
import { Logger } from '../logger.js';
|
|
3
|
+
import type { LogLevel } from '../types.js';
|
|
4
|
+
export declare abstract class BaseChannel {
|
|
5
|
+
protected endpoint: string;
|
|
6
|
+
protected maxReconnectRetries: number;
|
|
7
|
+
protected key: string;
|
|
8
|
+
protected logLevel: LogLevel;
|
|
9
|
+
protected socket: Socket;
|
|
10
|
+
protected log: Logger;
|
|
11
|
+
constructor(endpoint: string, maxReconnectRetries: number, key: string, logLevel: LogLevel | undefined, loggerName: string);
|
|
12
|
+
abstract connect(): Promise<void>;
|
|
13
|
+
abstract disconnect(): Promise<void>;
|
|
14
|
+
protected join(topic: Channel): Promise<void>;
|
|
15
|
+
protected push<T>(topic: Channel, event: string, payload: object): Promise<T>;
|
|
16
|
+
private params;
|
|
17
|
+
private reconnectAfterMs;
|
|
18
|
+
private static logSocketError;
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=base.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"base.d.ts","sourceRoot":"","sources":["../../src/channels/base.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,KAAK,OAAO,EAAE,MAAM,SAAS,CAAC;AAE/C,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAGtC,OAAO,KAAK,EAAsB,QAAQ,EAAE,MAAM,aAAa,CAAC;AAGhE,8BAAsB,WAAW;IAK7B,SAAS,CAAC,QAAQ,EAAE,MAAM;IAC1B,SAAS,CAAC,mBAAmB,EAAE,MAAM;IACrC,SAAS,CAAC,GAAG,EAAE,MAAM;IACrB,SAAS,CAAC,QAAQ,EAAE,QAAQ;IAP9B,SAAS,CAAC,MAAM,EAAE,MAAM,CAAC;IACzB,SAAS,CAAC,GAAG,EAAE,MAAM,CAAC;gBAGV,QAAQ,EAAE,MAAM,EAChB,mBAAmB,EAAE,MAAM,EAC3B,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,QAAQ,YAAS,EACrC,UAAU,EAAE,MAAM;IAepB,QAAQ,CAAC,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IACjC,QAAQ,CAAC,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;cAEpB,IAAI,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC;cAgBnC,IAAI,CAAC,CAAC,EACpB,KAAK,EAAE,OAAO,EACd,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,CAAC,CAAC;IAgBb,OAAO,CAAC,MAAM;IAWd,OAAO,CAAC,gBAAgB;IAgBxB,OAAO,CAAC,MAAM,CAAC,cAAc;CAU9B"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { BaseChannel } from './base.js';
|
|
2
|
+
import type { LogLevel } from '../types.js';
|
|
3
|
+
export declare class BuildChannel extends BaseChannel {
|
|
4
|
+
private lobbyTopic;
|
|
5
|
+
constructor(endpoint: string, maxReconnectRetries: number, key: string, logLevel?: LogLevel);
|
|
6
|
+
disconnect(): Promise<void>;
|
|
7
|
+
connect(): Promise<void>;
|
|
8
|
+
uploadBuild(buildPath: string, appSlug?: string): Promise<string>;
|
|
9
|
+
private requestUploadUrl;
|
|
10
|
+
private uploadToUrl;
|
|
11
|
+
private extractBuildInfo;
|
|
12
|
+
private getFileMetadata;
|
|
13
|
+
private computeSha256;
|
|
14
|
+
private detectMimeType;
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=build.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../src/channels/build.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAExC,OAAO,KAAK,EAIV,QAAQ,EACT,MAAM,aAAa,CAAC;AAErB,qBAAa,YAAa,SAAQ,WAAW;IAC3C,OAAO,CAAC,UAAU,CAAU;gBAG1B,QAAQ,EAAE,MAAM,EAChB,mBAAmB,EAAE,MAAM,EAC3B,GAAG,EAAE,MAAM,EACX,QAAQ,GAAE,QAAiB;IAYvB,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAO3B,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAiBxB,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;YAoBzD,gBAAgB;YAgBhB,WAAW;YA8BX,gBAAgB;YAehB,eAAe;YAWf,aAAa;IAW3B,OAAO,CAAC,cAAc;CAYvB"}
|
|
@@ -1,13 +1,8 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
private maxReconnectRetries;
|
|
5
|
-
private key;
|
|
6
|
-
private logLevel;
|
|
7
|
-
private socket;
|
|
1
|
+
import { BaseChannel } from './base.js';
|
|
2
|
+
import type { TVLabsCapabilities, LogLevel } from '../types.js';
|
|
3
|
+
export declare class SessionChannel extends BaseChannel {
|
|
8
4
|
private lobbyTopic;
|
|
9
5
|
private requestTopic?;
|
|
10
|
-
private log;
|
|
11
6
|
private readonly events;
|
|
12
7
|
constructor(endpoint: string, maxReconnectRetries: number, key: string, logLevel?: LogLevel);
|
|
13
8
|
disconnect(): Promise<void>;
|
|
@@ -17,11 +12,6 @@ export declare class TVLabsChannel {
|
|
|
17
12
|
private observeRequest;
|
|
18
13
|
private unobserveRequest;
|
|
19
14
|
private requestSession;
|
|
20
|
-
private join;
|
|
21
|
-
private push;
|
|
22
|
-
private params;
|
|
23
|
-
private reconnectAfterMs;
|
|
24
15
|
private tvlabsSessionLink;
|
|
25
|
-
private static logSocketError;
|
|
26
16
|
}
|
|
27
|
-
//# sourceMappingURL=
|
|
17
|
+
//# sourceMappingURL=session.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../../src/channels/session.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAExC,OAAO,KAAK,EACV,kBAAkB,EAGlB,QAAQ,EACT,MAAM,aAAa,CAAC;AAErB,qBAAa,cAAe,SAAQ,WAAW;IAC7C,OAAO,CAAC,UAAU,CAAU;IAC5B,OAAO,CAAC,YAAY,CAAC,CAAU;IAE/B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAOZ;gBAGT,QAAQ,EAAE,MAAM,EAChB,mBAAmB,EAAE,MAAM,EAC3B,GAAG,EAAE,MAAM,EACX,QAAQ,GAAE,QAAiB;IAYvB,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;IAQ3B,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;IAiBxB,UAAU,CACd,YAAY,EAAE,kBAAkB,EAChC,UAAU,EAAE,MAAM,EAClB,KAAK,SAAI,GACR,OAAO,CAAC,MAAM,CAAC;YAWJ,WAAW;YAkBX,cAAc;IAsD5B,OAAO,CAAC,gBAAgB;YAUV,cAAc;IAuB5B,OAAO,CAAC,iBAAiB;CAG1B"}
|
package/cjs/index.js
CHANGED
|
@@ -3,9 +3,12 @@
|
|
|
3
3
|
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4
4
|
|
|
5
5
|
var webdriverio = require('webdriverio');
|
|
6
|
-
var crypto = require('crypto');
|
|
6
|
+
var crypto$1 = require('crypto');
|
|
7
7
|
var ws = require('ws');
|
|
8
8
|
var phoenix = require('phoenix');
|
|
9
|
+
var fs = require('node:fs');
|
|
10
|
+
var path = require('node:path');
|
|
11
|
+
var crypto = require('node:crypto');
|
|
9
12
|
|
|
10
13
|
function _interopNamespaceDefault(e) {
|
|
11
14
|
var n = Object.create(null);
|
|
@@ -24,6 +27,9 @@ function _interopNamespaceDefault(e) {
|
|
|
24
27
|
return Object.freeze(n);
|
|
25
28
|
}
|
|
26
29
|
|
|
30
|
+
var crypto__namespace$1 = /*#__PURE__*/_interopNamespaceDefault(crypto$1);
|
|
31
|
+
var fs__namespace = /*#__PURE__*/_interopNamespaceDefault(fs);
|
|
32
|
+
var path__namespace = /*#__PURE__*/_interopNamespaceDefault(path);
|
|
27
33
|
var crypto__namespace = /*#__PURE__*/_interopNamespaceDefault(crypto);
|
|
28
34
|
|
|
29
35
|
const LOG_LEVELS = {
|
|
@@ -50,9 +56,58 @@ class Logger {
|
|
|
50
56
|
formatMessage(level, ...args) {
|
|
51
57
|
const timestamp = new Date().toISOString();
|
|
52
58
|
return `${timestamp} ${level.toUpperCase()} ${this.name}: ${args
|
|
53
|
-
.map((arg) =>
|
|
59
|
+
.map((arg) => this.serializeArg(arg))
|
|
54
60
|
.join(' ')}`;
|
|
55
61
|
}
|
|
62
|
+
serializeArg(arg) {
|
|
63
|
+
if (typeof arg === 'string' ||
|
|
64
|
+
typeof arg === 'number' ||
|
|
65
|
+
typeof arg === 'boolean') {
|
|
66
|
+
return String(arg);
|
|
67
|
+
}
|
|
68
|
+
if (arg === null || arg === undefined) {
|
|
69
|
+
return String(arg);
|
|
70
|
+
}
|
|
71
|
+
if (arg instanceof Error) {
|
|
72
|
+
return arg.stack || `${arg.name}: ${arg.message}`;
|
|
73
|
+
}
|
|
74
|
+
if (typeof arg === 'object') {
|
|
75
|
+
try {
|
|
76
|
+
const stringified = JSON.stringify(arg, (key, value) => {
|
|
77
|
+
if (value instanceof Error) {
|
|
78
|
+
return `${value.name}: ${value.message}`;
|
|
79
|
+
}
|
|
80
|
+
return value;
|
|
81
|
+
});
|
|
82
|
+
if (stringified === '{}') {
|
|
83
|
+
const keys = Object.getOwnPropertyNames(arg);
|
|
84
|
+
if (keys.length > 0) {
|
|
85
|
+
const props = {};
|
|
86
|
+
keys.forEach((key) => {
|
|
87
|
+
try {
|
|
88
|
+
const value = arg[key];
|
|
89
|
+
if (value instanceof Error) {
|
|
90
|
+
props[key] = `${value.name}: ${value.message}`;
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
props[key] = value;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
props[key] = '[unable to access]';
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
return JSON.stringify(props);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return stringified;
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return String(arg);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return String(arg);
|
|
110
|
+
}
|
|
56
111
|
debug(...args) {
|
|
57
112
|
if (this.shouldLog('debug')) {
|
|
58
113
|
console.log(this.formatMessage('debug', ...args));
|
|
@@ -81,7 +136,7 @@ class Logger {
|
|
|
81
136
|
}
|
|
82
137
|
|
|
83
138
|
var name = "@tvlabs/wdio-service";
|
|
84
|
-
var version = "0.1.
|
|
139
|
+
var version = "0.1.6";
|
|
85
140
|
var packageJson = {
|
|
86
141
|
name: name,
|
|
87
142
|
version: version};
|
|
@@ -99,15 +154,81 @@ function getServiceName() {
|
|
|
99
154
|
return packageJson.name;
|
|
100
155
|
}
|
|
101
156
|
|
|
102
|
-
class
|
|
157
|
+
class BaseChannel {
|
|
103
158
|
endpoint;
|
|
104
159
|
maxReconnectRetries;
|
|
105
160
|
key;
|
|
106
161
|
logLevel;
|
|
107
162
|
socket;
|
|
163
|
+
log;
|
|
164
|
+
constructor(endpoint, maxReconnectRetries, key, logLevel = 'info', loggerName) {
|
|
165
|
+
this.endpoint = endpoint;
|
|
166
|
+
this.maxReconnectRetries = maxReconnectRetries;
|
|
167
|
+
this.key = key;
|
|
168
|
+
this.logLevel = logLevel;
|
|
169
|
+
this.log = new Logger(loggerName, this.logLevel);
|
|
170
|
+
this.socket = new phoenix.Socket(this.endpoint, {
|
|
171
|
+
transport: ws.WebSocket,
|
|
172
|
+
params: this.params(),
|
|
173
|
+
reconnectAfterMs: this.reconnectAfterMs.bind(this),
|
|
174
|
+
});
|
|
175
|
+
this.socket.onError((...args) => BaseChannel.logSocketError(this.log, ...args));
|
|
176
|
+
}
|
|
177
|
+
async join(topic) {
|
|
178
|
+
return new Promise((res, rej) => {
|
|
179
|
+
topic
|
|
180
|
+
.join()
|
|
181
|
+
.receive('ok', (_resp) => {
|
|
182
|
+
res();
|
|
183
|
+
})
|
|
184
|
+
.receive('error', ({ response }) => {
|
|
185
|
+
rej('Failed to join topic: ' + response);
|
|
186
|
+
})
|
|
187
|
+
.receive('timeout', () => {
|
|
188
|
+
rej('timeout');
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
async push(topic, event, payload) {
|
|
193
|
+
return new Promise((res, rej) => {
|
|
194
|
+
topic
|
|
195
|
+
.push(event, payload)
|
|
196
|
+
.receive('ok', (msg) => {
|
|
197
|
+
res(msg);
|
|
198
|
+
})
|
|
199
|
+
.receive('error', (reason) => {
|
|
200
|
+
rej(reason);
|
|
201
|
+
})
|
|
202
|
+
.receive('timeout', () => {
|
|
203
|
+
rej('timeout');
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
params() {
|
|
208
|
+
const serviceInfo = getServiceInfo();
|
|
209
|
+
this.log.debug('Info:', serviceInfo);
|
|
210
|
+
return {
|
|
211
|
+
...serviceInfo,
|
|
212
|
+
api_key: this.key,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
reconnectAfterMs(tries) {
|
|
216
|
+
if (tries > this.maxReconnectRetries) {
|
|
217
|
+
throw new webdriverio.SevereServiceError('Could not connect to TV Labs, please check your connection.');
|
|
218
|
+
}
|
|
219
|
+
const wait = [0, 1000, 3000, 5000][tries] || 10000;
|
|
220
|
+
this.log.info(`[${tries}/${this.maxReconnectRetries}] Waiting ${wait}ms before re-attempting to connect...`);
|
|
221
|
+
return wait;
|
|
222
|
+
}
|
|
223
|
+
static logSocketError(log, event, _transport, _establishedConnections) {
|
|
224
|
+
const error = event.error;
|
|
225
|
+
log.error('Socket error:', error || event);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
class SessionChannel extends BaseChannel {
|
|
108
230
|
lobbyTopic;
|
|
109
231
|
requestTopic;
|
|
110
|
-
log;
|
|
111
232
|
events = {
|
|
112
233
|
SESSION_READY: 'session:ready',
|
|
113
234
|
SESSION_FAILED: 'session:failed',
|
|
@@ -117,17 +238,7 @@ class TVLabsChannel {
|
|
|
117
238
|
REQUEST_MATCHING: 'request:matching',
|
|
118
239
|
};
|
|
119
240
|
constructor(endpoint, maxReconnectRetries, key, logLevel = 'info') {
|
|
120
|
-
|
|
121
|
-
this.maxReconnectRetries = maxReconnectRetries;
|
|
122
|
-
this.key = key;
|
|
123
|
-
this.logLevel = logLevel;
|
|
124
|
-
this.log = new Logger('@tvlabs/wdio-channel', this.logLevel);
|
|
125
|
-
this.socket = new phoenix.Socket(this.endpoint, {
|
|
126
|
-
transport: ws.WebSocket,
|
|
127
|
-
params: this.params(),
|
|
128
|
-
reconnectAfterMs: this.reconnectAfterMs.bind(this),
|
|
129
|
-
});
|
|
130
|
-
this.socket.onError((...args) => TVLabsChannel.logSocketError(this.log, ...args));
|
|
241
|
+
super(endpoint, maxReconnectRetries, key, logLevel, '@tvlabs/session-channel');
|
|
131
242
|
this.lobbyTopic = this.socket.channel('requests:lobby');
|
|
132
243
|
}
|
|
133
244
|
async disconnect() {
|
|
@@ -139,14 +250,14 @@ class TVLabsChannel {
|
|
|
139
250
|
}
|
|
140
251
|
async connect() {
|
|
141
252
|
try {
|
|
142
|
-
this.log.debug('Connecting to
|
|
253
|
+
this.log.debug('Connecting to session channel...');
|
|
143
254
|
this.socket.connect();
|
|
144
255
|
await this.join(this.lobbyTopic);
|
|
145
|
-
this.log.debug('Connected to
|
|
256
|
+
this.log.debug('Connected to session channel!');
|
|
146
257
|
}
|
|
147
258
|
catch (error) {
|
|
148
|
-
this.log.error('Error connecting to
|
|
149
|
-
throw new webdriverio.SevereServiceError('Could not connect to
|
|
259
|
+
this.log.error('Error connecting to session channel:', error);
|
|
260
|
+
throw new webdriverio.SevereServiceError('Could not connect to session channel, please check your connection.');
|
|
150
261
|
}
|
|
151
262
|
}
|
|
152
263
|
async newSession(capabilities, maxRetries, retry = 0) {
|
|
@@ -224,58 +335,111 @@ class TVLabsChannel {
|
|
|
224
335
|
throw error;
|
|
225
336
|
}
|
|
226
337
|
}
|
|
227
|
-
|
|
228
|
-
return
|
|
229
|
-
topic
|
|
230
|
-
.join()
|
|
231
|
-
.receive('ok', (_resp) => {
|
|
232
|
-
res();
|
|
233
|
-
})
|
|
234
|
-
.receive('error', ({ response }) => {
|
|
235
|
-
rej('Failed to join topic: ' + response);
|
|
236
|
-
})
|
|
237
|
-
.receive('timeout', () => {
|
|
238
|
-
rej('timeout');
|
|
239
|
-
});
|
|
240
|
-
});
|
|
338
|
+
tvlabsSessionLink(sessionId) {
|
|
339
|
+
return `https://tvlabs.ai/app/sessions/${sessionId}`;
|
|
241
340
|
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
});
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
class BuildChannel extends BaseChannel {
|
|
344
|
+
lobbyTopic;
|
|
345
|
+
constructor(endpoint, maxReconnectRetries, key, logLevel = 'info') {
|
|
346
|
+
super(endpoint, maxReconnectRetries, key, logLevel, '@tvlabs/build-channel');
|
|
347
|
+
this.lobbyTopic = this.socket.channel('upload:lobby');
|
|
348
|
+
}
|
|
349
|
+
async disconnect() {
|
|
350
|
+
return new Promise((res, _rej) => {
|
|
351
|
+
this.lobbyTopic.leave();
|
|
352
|
+
this.socket.disconnect(() => res());
|
|
255
353
|
});
|
|
256
354
|
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
}
|
|
355
|
+
async connect() {
|
|
356
|
+
try {
|
|
357
|
+
this.log.debug('Connecting to build channel...');
|
|
358
|
+
this.socket.connect();
|
|
359
|
+
await this.join(this.lobbyTopic);
|
|
360
|
+
this.log.debug('Connected to build channel!');
|
|
361
|
+
}
|
|
362
|
+
catch (error) {
|
|
363
|
+
this.log.error('Error connecting to build channel:', error);
|
|
364
|
+
throw new webdriverio.SevereServiceError('Could not connect to build channel, please check your connection.');
|
|
365
|
+
}
|
|
264
366
|
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
367
|
+
async uploadBuild(buildPath, appSlug) {
|
|
368
|
+
const metadata = await this.getFileMetadata(buildPath);
|
|
369
|
+
this.log.info(`Requesting upload for build ${metadata.filename} (${metadata.type}, ${metadata.size} bytes)`);
|
|
370
|
+
const { url, build_id } = await this.requestUploadUrl(metadata, appSlug);
|
|
371
|
+
this.log.info('Uploading build...');
|
|
372
|
+
await this.uploadToUrl(url, buildPath, metadata);
|
|
373
|
+
const { application_id } = await this.extractBuildInfo();
|
|
374
|
+
this.log.info(`Build "${application_id}" processed successfully`);
|
|
375
|
+
return build_id;
|
|
376
|
+
}
|
|
377
|
+
async requestUploadUrl(metadata, appSlug) {
|
|
378
|
+
try {
|
|
379
|
+
return await this.push(this.lobbyTopic, 'request_upload_url', { metadata, application_slug: appSlug });
|
|
380
|
+
}
|
|
381
|
+
catch (error) {
|
|
382
|
+
this.log.error('Error requesting upload URL:', error);
|
|
383
|
+
throw error;
|
|
268
384
|
}
|
|
269
|
-
const wait = [0, 1000, 3000, 5000][tries] || 10000;
|
|
270
|
-
this.log.info(`[${tries}/${this.maxReconnectRetries}] Waiting ${wait}ms before re-attempting to connect...`);
|
|
271
|
-
return wait;
|
|
272
385
|
}
|
|
273
|
-
|
|
274
|
-
|
|
386
|
+
async uploadToUrl(url, filePath, metadata) {
|
|
387
|
+
try {
|
|
388
|
+
const response = await fetch(url, {
|
|
389
|
+
method: 'PUT',
|
|
390
|
+
headers: {
|
|
391
|
+
'Content-Type': metadata.type,
|
|
392
|
+
'Content-Length': String(metadata.size),
|
|
393
|
+
},
|
|
394
|
+
body: fs__namespace.createReadStream(filePath),
|
|
395
|
+
duplex: 'half',
|
|
396
|
+
});
|
|
397
|
+
if (!response.ok) {
|
|
398
|
+
throw new webdriverio.SevereServiceError(`Failed to upload build to storage, got ${response.status}`);
|
|
399
|
+
}
|
|
400
|
+
this.log.info('Upload complete');
|
|
401
|
+
}
|
|
402
|
+
catch (error) {
|
|
403
|
+
this.log.error('Error uploading build:', error);
|
|
404
|
+
throw error;
|
|
405
|
+
}
|
|
275
406
|
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
407
|
+
async extractBuildInfo() {
|
|
408
|
+
this.log.info('Processing uploaded build...');
|
|
409
|
+
try {
|
|
410
|
+
return await this.push(this.lobbyTopic, 'extract_build_info', {});
|
|
411
|
+
}
|
|
412
|
+
catch (error) {
|
|
413
|
+
this.log.error('Error processing build:', error);
|
|
414
|
+
throw error;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
async getFileMetadata(buildPath) {
|
|
418
|
+
const filename = path__namespace.basename(buildPath);
|
|
419
|
+
const size = fs__namespace.statSync(buildPath).size;
|
|
420
|
+
const type = this.detectMimeType(filename);
|
|
421
|
+
const sha256 = await this.computeSha256(buildPath);
|
|
422
|
+
return { filename, type, size, sha256 };
|
|
423
|
+
}
|
|
424
|
+
async computeSha256(buildPath) {
|
|
425
|
+
return new Promise((resolve, reject) => {
|
|
426
|
+
const hash = crypto__namespace.createHash('sha256');
|
|
427
|
+
const stream = fs__namespace.createReadStream(buildPath);
|
|
428
|
+
stream.on('data', (chunk) => hash.update(chunk));
|
|
429
|
+
stream.on('end', () => resolve(hash.digest('hex')));
|
|
430
|
+
stream.on('error', (err) => reject(err));
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
detectMimeType(filename) {
|
|
434
|
+
const fileExtension = path__namespace.extname(filename).toLowerCase();
|
|
435
|
+
switch (fileExtension) {
|
|
436
|
+
case '.apk':
|
|
437
|
+
return 'application/vnd.android.package-archive';
|
|
438
|
+
case '.zip':
|
|
439
|
+
return 'application/zip';
|
|
440
|
+
default:
|
|
441
|
+
return 'application/octet-stream';
|
|
442
|
+
}
|
|
279
443
|
}
|
|
280
444
|
}
|
|
281
445
|
|
|
@@ -299,15 +463,22 @@ class TVLabsService {
|
|
|
299
463
|
}
|
|
300
464
|
}
|
|
301
465
|
async beforeSession(_config, capabilities, _specs, _cid) {
|
|
302
|
-
const
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
466
|
+
const buildPath = this.buildPath();
|
|
467
|
+
if (buildPath) {
|
|
468
|
+
const buildChannel = new BuildChannel(this.buildEndpoint(), this.reconnectRetries(), this.apiKey(), this.logLevel());
|
|
469
|
+
await buildChannel.connect();
|
|
470
|
+
capabilities['tvlabs:build'] = await buildChannel.uploadBuild(buildPath, this.appSlug());
|
|
471
|
+
await buildChannel.disconnect();
|
|
472
|
+
}
|
|
473
|
+
const sessionChannel = new SessionChannel(this.sessionEndpoint(), this.reconnectRetries(), this.apiKey(), this.logLevel());
|
|
474
|
+
await sessionChannel.connect();
|
|
475
|
+
capabilities['tvlabs:session_id'] = await sessionChannel.newSession(capabilities, this.retries());
|
|
476
|
+
await sessionChannel.disconnect();
|
|
306
477
|
}
|
|
307
478
|
setupRequestId() {
|
|
308
479
|
const originalTransformRequest = this._config.transformRequest;
|
|
309
480
|
this._config.transformRequest = (requestOptions) => {
|
|
310
|
-
const requestId = crypto__namespace.randomUUID();
|
|
481
|
+
const requestId = crypto__namespace$1.randomUUID();
|
|
311
482
|
const originalRequestOptions = typeof originalTransformRequest === 'function'
|
|
312
483
|
? originalTransformRequest(requestOptions)
|
|
313
484
|
: requestOptions;
|
|
@@ -332,8 +503,17 @@ class TVLabsService {
|
|
|
332
503
|
}
|
|
333
504
|
}
|
|
334
505
|
}
|
|
335
|
-
|
|
336
|
-
return this._options.
|
|
506
|
+
buildPath() {
|
|
507
|
+
return this._options.buildPath;
|
|
508
|
+
}
|
|
509
|
+
appSlug() {
|
|
510
|
+
return this._options.app;
|
|
511
|
+
}
|
|
512
|
+
sessionEndpoint() {
|
|
513
|
+
return this._options.sessionEndpoint ?? 'wss://tvlabs.ai/appium';
|
|
514
|
+
}
|
|
515
|
+
buildEndpoint() {
|
|
516
|
+
return this._options.buildEndpoint ?? 'wss://tvlabs.ai/cli';
|
|
337
517
|
}
|
|
338
518
|
retries() {
|
|
339
519
|
return this._options.retries ?? 3;
|
package/cjs/logger.d.ts
CHANGED
package/cjs/logger.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAc3C,qBAAa,MAAM;IAEf,OAAO,CAAC,IAAI;IACZ,OAAO,CAAC,QAAQ;gBADR,IAAI,EAAE,MAAM,EACZ,QAAQ,GAAE,QAAiB;IAGrC,OAAO,CAAC,SAAS;IAOjB,OAAO,CAAC,aAAa;
|
|
1
|
+
{"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../src/logger.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAc3C,qBAAa,MAAM;IAEf,OAAO,CAAC,IAAI;IACZ,OAAO,CAAC,QAAQ;gBADR,IAAI,EAAE,MAAM,EACZ,QAAQ,GAAE,QAAiB;IAGrC,OAAO,CAAC,SAAS;IAOjB,OAAO,CAAC,aAAa;IAOrB,OAAO,CAAC,YAAY;IA2DpB,KAAK,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI;IAM/B,IAAI,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI;IAM9B,IAAI,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI;IAM9B,KAAK,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI;IAM/B,KAAK,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI;CAKhC"}
|
package/cjs/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
|
|
13
|
+
private buildPath;
|
|
14
|
+
private appSlug;
|
|
15
|
+
private sessionEndpoint;
|
|
16
|
+
private buildEndpoint;
|
|
14
17
|
private retries;
|
|
15
18
|
private apiKey;
|
|
16
19
|
private logLevel;
|
package/cjs/service.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../src/service.ts"],"names":[],"mappings":"
|
|
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/cjs/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
|
-
|
|
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
|
package/cjs/types.d.ts.map
CHANGED
|
@@ -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,
|
|
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"}
|