@tvlabs/wdio-service 0.1.3 → 0.1.5
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 +15 -15
- package/cjs/channel.d.ts +1 -1
- package/cjs/channel.d.ts.map +1 -1
- package/cjs/index.js +403 -20
- package/cjs/logger.d.ts +1 -0
- package/cjs/logger.d.ts.map +1 -1
- package/cjs/package.json +3 -1
- package/cjs/types.d.ts +5 -1
- package/cjs/types.d.ts.map +1 -1
- package/cjs/utils.d.ts +5 -0
- package/cjs/utils.d.ts.map +1 -0
- package/esm/channel.d.ts +1 -1
- package/esm/channel.d.ts.map +1 -1
- package/esm/index.js +380 -4
- package/esm/logger.d.ts +1 -0
- package/esm/logger.d.ts.map +1 -1
- package/esm/package.json +3 -1
- package/esm/types.d.ts +5 -1
- package/esm/types.d.ts.map +1 -1
- package/esm/utils.d.ts +5 -0
- package/esm/utils.d.ts.map +1 -0
- package/package.json +11 -8
- package/rollup.config.js +53 -0
- package/src/channel.ts +10 -6
- package/src/logger.ts +60 -3
- package/src/types.ts +6 -1
- package/src/utils.ts +17 -0
- package/cjs/channel.js +0 -184
- package/cjs/logger.js +0 -57
- package/cjs/service.js +0 -79
- package/cjs/types.js +0 -2
- package/esm/channel.js +0 -180
- package/esm/logger.js +0 -53
- package/esm/service.js +0 -76
- package/esm/types.js +0 -1
package/esm/index.js
CHANGED
|
@@ -1,4 +1,380 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
import { SevereServiceError } from 'webdriverio';
|
|
2
|
+
import * as crypto from 'crypto';
|
|
3
|
+
import { WebSocket } from 'ws';
|
|
4
|
+
import { Socket } from 'phoenix';
|
|
5
|
+
|
|
6
|
+
const LOG_LEVELS = {
|
|
7
|
+
error: 0,
|
|
8
|
+
warn: 1,
|
|
9
|
+
info: 2,
|
|
10
|
+
debug: 3,
|
|
11
|
+
trace: 4,
|
|
12
|
+
silent: 5,
|
|
13
|
+
};
|
|
14
|
+
class Logger {
|
|
15
|
+
name;
|
|
16
|
+
logLevel;
|
|
17
|
+
constructor(name, logLevel = 'info') {
|
|
18
|
+
this.name = name;
|
|
19
|
+
this.logLevel = logLevel;
|
|
20
|
+
}
|
|
21
|
+
shouldLog(level) {
|
|
22
|
+
if (this.logLevel === 'silent') {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
return LOG_LEVELS[level] <= LOG_LEVELS[this.logLevel];
|
|
26
|
+
}
|
|
27
|
+
formatMessage(level, ...args) {
|
|
28
|
+
const timestamp = new Date().toISOString();
|
|
29
|
+
return `${timestamp} ${level.toUpperCase()} ${this.name}: ${args
|
|
30
|
+
.map((arg) => this.serializeArg(arg))
|
|
31
|
+
.join(' ')}`;
|
|
32
|
+
}
|
|
33
|
+
serializeArg(arg) {
|
|
34
|
+
if (typeof arg === 'string' ||
|
|
35
|
+
typeof arg === 'number' ||
|
|
36
|
+
typeof arg === 'boolean') {
|
|
37
|
+
return String(arg);
|
|
38
|
+
}
|
|
39
|
+
if (arg === null || arg === undefined) {
|
|
40
|
+
return String(arg);
|
|
41
|
+
}
|
|
42
|
+
if (arg instanceof Error) {
|
|
43
|
+
return arg.stack || `${arg.name}: ${arg.message}`;
|
|
44
|
+
}
|
|
45
|
+
if (typeof arg === 'object') {
|
|
46
|
+
try {
|
|
47
|
+
const stringified = JSON.stringify(arg, (key, value) => {
|
|
48
|
+
if (value instanceof Error) {
|
|
49
|
+
return `${value.name}: ${value.message}`;
|
|
50
|
+
}
|
|
51
|
+
return value;
|
|
52
|
+
});
|
|
53
|
+
if (stringified === '{}') {
|
|
54
|
+
const keys = Object.getOwnPropertyNames(arg);
|
|
55
|
+
if (keys.length > 0) {
|
|
56
|
+
const props = {};
|
|
57
|
+
keys.forEach((key) => {
|
|
58
|
+
try {
|
|
59
|
+
const value = arg[key];
|
|
60
|
+
if (value instanceof Error) {
|
|
61
|
+
props[key] = `${value.name}: ${value.message}`;
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
props[key] = value;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
props[key] = '[unable to access]';
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
return JSON.stringify(props);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return stringified;
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return String(arg);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return String(arg);
|
|
81
|
+
}
|
|
82
|
+
debug(...args) {
|
|
83
|
+
if (this.shouldLog('debug')) {
|
|
84
|
+
console.log(this.formatMessage('debug', ...args));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
info(...args) {
|
|
88
|
+
if (this.shouldLog('info')) {
|
|
89
|
+
console.log(this.formatMessage('info', ...args));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
warn(...args) {
|
|
93
|
+
if (this.shouldLog('warn')) {
|
|
94
|
+
console.warn(this.formatMessage('warn', ...args));
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
error(...args) {
|
|
98
|
+
if (this.shouldLog('error')) {
|
|
99
|
+
console.error(this.formatMessage('error', ...args));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
trace(...args) {
|
|
103
|
+
if (this.shouldLog('trace')) {
|
|
104
|
+
console.trace(this.formatMessage('trace', ...args));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
var name = "@tvlabs/wdio-service";
|
|
110
|
+
var version = "0.1.5";
|
|
111
|
+
var packageJson = {
|
|
112
|
+
name: name,
|
|
113
|
+
version: version};
|
|
114
|
+
|
|
115
|
+
function getServiceInfo() {
|
|
116
|
+
return {
|
|
117
|
+
service_name: getServiceName(),
|
|
118
|
+
service_version: getServiceVersion(),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
function getServiceVersion() {
|
|
122
|
+
return packageJson.version;
|
|
123
|
+
}
|
|
124
|
+
function getServiceName() {
|
|
125
|
+
return packageJson.name;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
class TVLabsChannel {
|
|
129
|
+
endpoint;
|
|
130
|
+
maxReconnectRetries;
|
|
131
|
+
key;
|
|
132
|
+
logLevel;
|
|
133
|
+
socket;
|
|
134
|
+
lobbyTopic;
|
|
135
|
+
requestTopic;
|
|
136
|
+
log;
|
|
137
|
+
events = {
|
|
138
|
+
SESSION_READY: 'session:ready',
|
|
139
|
+
SESSION_FAILED: 'session:failed',
|
|
140
|
+
REQUEST_CANCELED: 'request:canceled',
|
|
141
|
+
REQUEST_FAILED: 'request:failed',
|
|
142
|
+
REQUEST_FILLED: 'request:filled',
|
|
143
|
+
REQUEST_MATCHING: 'request:matching',
|
|
144
|
+
};
|
|
145
|
+
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));
|
|
157
|
+
this.lobbyTopic = this.socket.channel('requests:lobby');
|
|
158
|
+
}
|
|
159
|
+
async disconnect() {
|
|
160
|
+
return new Promise((res, _rej) => {
|
|
161
|
+
this.lobbyTopic.leave();
|
|
162
|
+
this.requestTopic?.leave();
|
|
163
|
+
this.socket.disconnect(() => res());
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
async connect() {
|
|
167
|
+
try {
|
|
168
|
+
this.log.debug('Connecting to TV Labs...');
|
|
169
|
+
this.socket.connect();
|
|
170
|
+
await this.join(this.lobbyTopic);
|
|
171
|
+
this.log.debug('Connected to TV Labs!');
|
|
172
|
+
}
|
|
173
|
+
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.');
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
async newSession(capabilities, maxRetries, retry = 0) {
|
|
179
|
+
try {
|
|
180
|
+
const requestId = await this.requestSession(capabilities);
|
|
181
|
+
const sessionId = await this.observeRequest(requestId);
|
|
182
|
+
return sessionId;
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
return this.handleRetry(capabilities, maxRetries, retry);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
async handleRetry(capabilities, maxRetries, retry) {
|
|
189
|
+
if (retry < maxRetries) {
|
|
190
|
+
this.log.warn(`Could not create a session, retrying (${retry + 1}/${maxRetries})`);
|
|
191
|
+
return this.newSession(capabilities, maxRetries, retry + 1);
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
throw new SevereServiceError(`Could not create a session after ${maxRetries} attempts.`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
async observeRequest(requestId) {
|
|
198
|
+
const cleanup = () => this.unobserveRequest();
|
|
199
|
+
return new Promise((res, rej) => {
|
|
200
|
+
this.requestTopic = this.socket.channel(`requests:${requestId}`);
|
|
201
|
+
const eventHandlers = {
|
|
202
|
+
[this.events.REQUEST_MATCHING]: ({ request_id }) => {
|
|
203
|
+
this.log.info(`Session request ${request_id} matching...`);
|
|
204
|
+
},
|
|
205
|
+
[this.events.REQUEST_FILLED]: ({ session_id, request_id }) => {
|
|
206
|
+
this.log.info(`Session request ${request_id} filled: ${this.tvlabsSessionLink(session_id)}`);
|
|
207
|
+
this.log.info('Waiting for device to be ready...');
|
|
208
|
+
},
|
|
209
|
+
[this.events.SESSION_FAILED]: ({ session_id, reason }) => {
|
|
210
|
+
this.log.error(`Session ${session_id} failed, reason: ${reason}`);
|
|
211
|
+
rej(reason);
|
|
212
|
+
},
|
|
213
|
+
[this.events.REQUEST_CANCELED]: ({ request_id, reason }) => {
|
|
214
|
+
this.log.info(`Session request ${request_id} canceled, reason: ${reason}`);
|
|
215
|
+
rej(reason);
|
|
216
|
+
},
|
|
217
|
+
[this.events.REQUEST_FAILED]: ({ request_id, reason }) => {
|
|
218
|
+
this.log.info(`Session request ${request_id} failed, reason: ${reason}`);
|
|
219
|
+
rej(reason);
|
|
220
|
+
},
|
|
221
|
+
[this.events.SESSION_READY]: ({ session_id }) => {
|
|
222
|
+
this.log.info(`Session ${session_id} ready!`);
|
|
223
|
+
res(session_id);
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
Object.entries(eventHandlers).forEach(([event, handler]) => {
|
|
227
|
+
this.requestTopic?.on(event, handler);
|
|
228
|
+
});
|
|
229
|
+
this.join(this.requestTopic).catch((err) => {
|
|
230
|
+
rej(err);
|
|
231
|
+
});
|
|
232
|
+
}).finally(cleanup);
|
|
233
|
+
}
|
|
234
|
+
unobserveRequest() {
|
|
235
|
+
Object.values(this.events).forEach((event) => {
|
|
236
|
+
this.requestTopic?.off(event);
|
|
237
|
+
});
|
|
238
|
+
this.requestTopic?.leave();
|
|
239
|
+
this.requestTopic = undefined;
|
|
240
|
+
}
|
|
241
|
+
async requestSession(capabilities) {
|
|
242
|
+
this.log.info('Requesting TV Labs session');
|
|
243
|
+
try {
|
|
244
|
+
const response = await this.push(this.lobbyTopic, 'requests:create', { capabilities });
|
|
245
|
+
this.log.info(`Received session request ID: ${response.request_id}. Waiting for a match...`);
|
|
246
|
+
return response.request_id;
|
|
247
|
+
}
|
|
248
|
+
catch (error) {
|
|
249
|
+
this.log.error('Error requesting session:', error);
|
|
250
|
+
throw error;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
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
|
+
});
|
|
267
|
+
}
|
|
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
|
+
});
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
params() {
|
|
284
|
+
const serviceInfo = getServiceInfo();
|
|
285
|
+
this.log.debug('Info:', serviceInfo);
|
|
286
|
+
return {
|
|
287
|
+
...serviceInfo,
|
|
288
|
+
api_key: this.key,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
reconnectAfterMs(tries) {
|
|
292
|
+
if (tries > this.maxReconnectRetries) {
|
|
293
|
+
throw new SevereServiceError('Could not connect to TV Labs, please check your connection.');
|
|
294
|
+
}
|
|
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
|
+
}
|
|
299
|
+
tvlabsSessionLink(sessionId) {
|
|
300
|
+
return `https://tvlabs.ai/app/sessions/${sessionId}`;
|
|
301
|
+
}
|
|
302
|
+
static logSocketError(log, event, _transport, _establishedConnections) {
|
|
303
|
+
log.error('Socket error:', event.error);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
class TVLabsService {
|
|
308
|
+
_options;
|
|
309
|
+
_capabilities;
|
|
310
|
+
_config;
|
|
311
|
+
log;
|
|
312
|
+
constructor(_options, _capabilities, _config) {
|
|
313
|
+
this._options = _options;
|
|
314
|
+
this._capabilities = _capabilities;
|
|
315
|
+
this._config = _config;
|
|
316
|
+
this.log = new Logger('@tvlabs/wdio-server', this._config.logLevel);
|
|
317
|
+
if (this.attachRequestId()) {
|
|
318
|
+
this.setupRequestId();
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
onPrepare(_config, param) {
|
|
322
|
+
if (!Array.isArray(param)) {
|
|
323
|
+
throw new SevereServiceError('Multi-remote capabilities are not implemented. Contact TV Labs support if you are interested in this feature.');
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
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();
|
|
331
|
+
}
|
|
332
|
+
setupRequestId() {
|
|
333
|
+
const originalTransformRequest = this._config.transformRequest;
|
|
334
|
+
this._config.transformRequest = (requestOptions) => {
|
|
335
|
+
const requestId = crypto.randomUUID();
|
|
336
|
+
const originalRequestOptions = typeof originalTransformRequest === 'function'
|
|
337
|
+
? originalTransformRequest(requestOptions)
|
|
338
|
+
: requestOptions;
|
|
339
|
+
if (typeof originalRequestOptions.headers === 'undefined') {
|
|
340
|
+
originalRequestOptions.headers = {};
|
|
341
|
+
}
|
|
342
|
+
this.setRequestHeader(originalRequestOptions.headers, 'x-request-id', requestId);
|
|
343
|
+
this.log.info('ATTACHED REQUEST ID', requestId);
|
|
344
|
+
return originalRequestOptions;
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
setRequestHeader(headers, header, value) {
|
|
348
|
+
if (headers instanceof Headers) {
|
|
349
|
+
headers.set(header, value);
|
|
350
|
+
}
|
|
351
|
+
else if (typeof headers === 'object') {
|
|
352
|
+
if (Array.isArray(headers)) {
|
|
353
|
+
headers.push([header, value]);
|
|
354
|
+
}
|
|
355
|
+
else {
|
|
356
|
+
headers[header] = value;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
endpoint() {
|
|
361
|
+
return this._options.endpoint ?? 'wss://tvlabs.ai/appium';
|
|
362
|
+
}
|
|
363
|
+
retries() {
|
|
364
|
+
return this._options.retries ?? 3;
|
|
365
|
+
}
|
|
366
|
+
apiKey() {
|
|
367
|
+
return this._options.apiKey;
|
|
368
|
+
}
|
|
369
|
+
logLevel() {
|
|
370
|
+
return this._config.logLevel ?? 'info';
|
|
371
|
+
}
|
|
372
|
+
attachRequestId() {
|
|
373
|
+
return this._options.attachRequestId ?? true;
|
|
374
|
+
}
|
|
375
|
+
reconnectRetries() {
|
|
376
|
+
return this._options.reconnectRetries ?? 5;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export { TVLabsService, TVLabsService as default };
|
package/esm/logger.d.ts
CHANGED
package/esm/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/esm/package.json
CHANGED
package/esm/types.d.ts
CHANGED
|
@@ -31,7 +31,11 @@ export type TVLabsSessionRequestUpdate = {
|
|
|
31
31
|
export type TVLabsSessionRequestResponse = {
|
|
32
32
|
request_id: string;
|
|
33
33
|
};
|
|
34
|
-
export type
|
|
34
|
+
export type TVLabsSocketParams = TVLabsServiceInfo & {
|
|
35
35
|
api_key: string;
|
|
36
36
|
};
|
|
37
|
+
export type TVLabsServiceInfo = {
|
|
38
|
+
service_version: string;
|
|
39
|
+
service_name: string;
|
|
40
|
+
};
|
|
37
41
|
//# sourceMappingURL=types.d.ts.map
|
package/esm/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,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,
|
|
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"}
|
package/esm/utils.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAG/C,wBAAgB,cAAc,IAAI,iBAAiB,CAKlD;AAED,wBAAgB,iBAAiB,IAAI,MAAM,CAE1C;AAED,wBAAgB,cAAc,IAAI,MAAM,CAEvC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tvlabs/wdio-service",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
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",
|
|
@@ -22,9 +22,7 @@
|
|
|
22
22
|
"url": "https://github.com/tv-labs/wdio-tvlabs-service/issues"
|
|
23
23
|
},
|
|
24
24
|
"scripts": {
|
|
25
|
-
"build": "npm run
|
|
26
|
-
"build:cjs": "tsc -p src --module commonjs --moduleResolution node --outDir cjs && echo '{\"type\": \"commonjs\"}' > cjs/package.json",
|
|
27
|
-
"build:esm": "tsc -p src --outDir esm && echo '{\"type\": \"module\"}' > esm/package.json",
|
|
25
|
+
"build": "npm run clean && rollup -c",
|
|
28
26
|
"start": "ts-node src/index.ts",
|
|
29
27
|
"dev": "ts-node --transpile-only src/index.ts",
|
|
30
28
|
"clean": "rm -rf cjs && rm -rf esm",
|
|
@@ -44,11 +42,15 @@
|
|
|
44
42
|
"typeScriptVersion": "5.8.2",
|
|
45
43
|
"devDependencies": {
|
|
46
44
|
"@eslint/js": "^9.22.0",
|
|
47
|
-
"@
|
|
45
|
+
"@rollup/plugin-commonjs": "^28.0.6",
|
|
46
|
+
"@rollup/plugin-json": "^6.1.0",
|
|
47
|
+
"@rollup/plugin-node-resolve": "^16.0.1",
|
|
48
|
+
"@rollup/plugin-typescript": "^12.1.4",
|
|
49
|
+
"@types/node": "^24.1.0",
|
|
48
50
|
"@types/phoenix": "^1.6.6",
|
|
49
51
|
"@types/ws": "^8.18.0",
|
|
50
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
51
|
-
"@typescript-eslint/parser": "^8.
|
|
52
|
+
"@typescript-eslint/eslint-plugin": "^8.38.0",
|
|
53
|
+
"@typescript-eslint/parser": "^8.38.0",
|
|
52
54
|
"@vitest/coverage-v8": "^3.0.9",
|
|
53
55
|
"@wdio/globals": "^9.12.1",
|
|
54
56
|
"@wdio/types": "^9.10.1",
|
|
@@ -56,8 +58,9 @@
|
|
|
56
58
|
"globals": "^16.0.0",
|
|
57
59
|
"jiti": "^2.4.2",
|
|
58
60
|
"prettier": "^3.5.3",
|
|
61
|
+
"rollup": "^4.45.1",
|
|
59
62
|
"typescript": "^5.8.2",
|
|
60
|
-
"typescript-eslint": "^8.
|
|
63
|
+
"typescript-eslint": "^8.38.0",
|
|
61
64
|
"vitest": "^3.0.9",
|
|
62
65
|
"webdriverio": "^9.12.1"
|
|
63
66
|
},
|
package/rollup.config.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import typescript from '@rollup/plugin-typescript';
|
|
2
|
+
import { nodeResolve } from '@rollup/plugin-node-resolve';
|
|
3
|
+
import commonjs from '@rollup/plugin-commonjs';
|
|
4
|
+
import json from '@rollup/plugin-json';
|
|
5
|
+
import { writeFileSync, mkdirSync } from 'fs';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
|
|
8
|
+
const createPackageJson = (dir, type) => ({
|
|
9
|
+
name: `create-package-json-${type}`,
|
|
10
|
+
generateBundle() {
|
|
11
|
+
const content = JSON.stringify({ type }, null, 2);
|
|
12
|
+
mkdirSync(dir, { recursive: true });
|
|
13
|
+
writeFileSync(path.join(dir, 'package.json'), content);
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const external = (id) => !id.startsWith('.') && !path.isAbsolute(id);
|
|
18
|
+
|
|
19
|
+
const plugins = (outDir) => [
|
|
20
|
+
nodeResolve({
|
|
21
|
+
preferBuiltins: true,
|
|
22
|
+
exportConditions: ['node'],
|
|
23
|
+
}),
|
|
24
|
+
json(),
|
|
25
|
+
commonjs(),
|
|
26
|
+
typescript({
|
|
27
|
+
tsconfig: 'src/tsconfig.json',
|
|
28
|
+
outDir,
|
|
29
|
+
}),
|
|
30
|
+
createPackageJson(outDir, outDir === 'esm' ? 'module' : 'commonjs'),
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
export default [
|
|
34
|
+
{
|
|
35
|
+
input: './src/index.ts',
|
|
36
|
+
output: {
|
|
37
|
+
dir: 'esm',
|
|
38
|
+
format: 'esm',
|
|
39
|
+
},
|
|
40
|
+
external,
|
|
41
|
+
plugins: plugins('esm'),
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
input: './src/index.ts',
|
|
45
|
+
output: {
|
|
46
|
+
dir: 'cjs',
|
|
47
|
+
format: 'cjs',
|
|
48
|
+
exports: 'named',
|
|
49
|
+
},
|
|
50
|
+
external,
|
|
51
|
+
plugins: plugins('cjs'),
|
|
52
|
+
},
|
|
53
|
+
];
|
package/src/channel.ts
CHANGED
|
@@ -2,10 +2,11 @@ import { WebSocket } from 'ws';
|
|
|
2
2
|
import { Socket, type Channel } from 'phoenix';
|
|
3
3
|
import { SevereServiceError } from 'webdriverio';
|
|
4
4
|
import { Logger } from './logger.js';
|
|
5
|
+
import { getServiceInfo } from './utils.js';
|
|
5
6
|
|
|
6
7
|
import type {
|
|
7
8
|
TVLabsCapabilities,
|
|
8
|
-
|
|
9
|
+
TVLabsSocketParams,
|
|
9
10
|
TVLabsSessionRequestEventHandler,
|
|
10
11
|
TVLabsSessionRequestResponse,
|
|
11
12
|
LogLevel,
|
|
@@ -31,7 +32,7 @@ export class TVLabsChannel {
|
|
|
31
32
|
private endpoint: string,
|
|
32
33
|
private maxReconnectRetries: number,
|
|
33
34
|
private key: string,
|
|
34
|
-
private logLevel: LogLevel,
|
|
35
|
+
private logLevel: LogLevel = 'info',
|
|
35
36
|
) {
|
|
36
37
|
this.log = new Logger('@tvlabs/wdio-channel', this.logLevel);
|
|
37
38
|
this.socket = new Socket(this.endpoint, {
|
|
@@ -228,8 +229,13 @@ export class TVLabsChannel {
|
|
|
228
229
|
});
|
|
229
230
|
}
|
|
230
231
|
|
|
231
|
-
private params():
|
|
232
|
+
private params(): TVLabsSocketParams {
|
|
233
|
+
const serviceInfo = getServiceInfo();
|
|
234
|
+
|
|
235
|
+
this.log.debug('Info:', serviceInfo);
|
|
236
|
+
|
|
232
237
|
return {
|
|
238
|
+
...serviceInfo,
|
|
233
239
|
api_key: this.key,
|
|
234
240
|
};
|
|
235
241
|
}
|
|
@@ -260,8 +266,6 @@ export class TVLabsChannel {
|
|
|
260
266
|
_transport: new (endpoint: string) => object,
|
|
261
267
|
_establishedConnections: number,
|
|
262
268
|
) {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
log.error('Socket error:', error || event);
|
|
269
|
+
log.error('Socket error:', event.error);
|
|
266
270
|
}
|
|
267
271
|
}
|
package/src/logger.ts
CHANGED
|
@@ -28,12 +28,69 @@ export class Logger {
|
|
|
28
28
|
private formatMessage(level: LogLevel, ...args: unknown[]): string {
|
|
29
29
|
const timestamp = new Date().toISOString();
|
|
30
30
|
return `${timestamp} ${level.toUpperCase()} ${this.name}: ${args
|
|
31
|
-
.map((arg) =>
|
|
32
|
-
typeof arg === 'object' ? JSON.stringify(arg) : String(arg),
|
|
33
|
-
)
|
|
31
|
+
.map((arg) => this.serializeArg(arg))
|
|
34
32
|
.join(' ')}`;
|
|
35
33
|
}
|
|
36
34
|
|
|
35
|
+
private serializeArg(arg: unknown): string {
|
|
36
|
+
if (
|
|
37
|
+
typeof arg === 'string' ||
|
|
38
|
+
typeof arg === 'number' ||
|
|
39
|
+
typeof arg === 'boolean'
|
|
40
|
+
) {
|
|
41
|
+
return String(arg);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (arg === null || arg === undefined) {
|
|
45
|
+
return String(arg);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (arg instanceof Error) {
|
|
49
|
+
// Handle Error objects specially - use stack trace which includes name and message
|
|
50
|
+
return arg.stack || `${arg.name}: ${arg.message}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (typeof arg === 'object') {
|
|
54
|
+
try {
|
|
55
|
+
// Try JSON.stringify with a custom replacer to handle nested Error objects
|
|
56
|
+
const stringified = JSON.stringify(arg, (key, value) => {
|
|
57
|
+
if (value instanceof Error) {
|
|
58
|
+
return `${value.name}: ${value.message}`;
|
|
59
|
+
}
|
|
60
|
+
return value;
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// If it's just an empty object, try to extract more info
|
|
64
|
+
if (stringified === '{}') {
|
|
65
|
+
// For objects that don't serialize well, try to extract key properties
|
|
66
|
+
const keys = Object.getOwnPropertyNames(arg);
|
|
67
|
+
if (keys.length > 0) {
|
|
68
|
+
const props: Record<string, unknown> = {};
|
|
69
|
+
keys.forEach((key) => {
|
|
70
|
+
try {
|
|
71
|
+
const value = (arg as Record<string, unknown>)[key];
|
|
72
|
+
if (value instanceof Error) {
|
|
73
|
+
props[key] = `${value.name}: ${value.message}`;
|
|
74
|
+
} else {
|
|
75
|
+
props[key] = value;
|
|
76
|
+
}
|
|
77
|
+
} catch {
|
|
78
|
+
props[key] = '[unable to access]';
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
return JSON.stringify(props);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return stringified;
|
|
85
|
+
} catch {
|
|
86
|
+
// Fallback to string representation if JSON.stringify fails
|
|
87
|
+
return String(arg);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return String(arg);
|
|
92
|
+
}
|
|
93
|
+
|
|
37
94
|
debug(...args: unknown[]): void {
|
|
38
95
|
if (this.shouldLog('debug')) {
|
|
39
96
|
console.log(this.formatMessage('debug', ...args));
|
package/src/types.ts
CHANGED
|
@@ -41,6 +41,11 @@ export type TVLabsSessionRequestResponse = {
|
|
|
41
41
|
request_id: string;
|
|
42
42
|
};
|
|
43
43
|
|
|
44
|
-
export type
|
|
44
|
+
export type TVLabsSocketParams = TVLabsServiceInfo & {
|
|
45
45
|
api_key: string;
|
|
46
46
|
};
|
|
47
|
+
|
|
48
|
+
export type TVLabsServiceInfo = {
|
|
49
|
+
service_version: string;
|
|
50
|
+
service_name: string;
|
|
51
|
+
};
|