@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
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { type Channel } from 'phoenix';
|
|
2
|
+
import { SevereServiceError } from 'webdriverio';
|
|
3
|
+
import * as fs from 'node:fs';
|
|
4
|
+
import * as path from 'node:path';
|
|
5
|
+
import * as crypto from 'node:crypto';
|
|
6
|
+
|
|
7
|
+
import { BaseChannel } from './base.js';
|
|
8
|
+
|
|
9
|
+
import type {
|
|
10
|
+
TVLabsExtractBuildInfoResponse,
|
|
11
|
+
TVLabsRequestUploadUrlResponse,
|
|
12
|
+
TVLabsBuildMetadata,
|
|
13
|
+
LogLevel,
|
|
14
|
+
} from '../types.js';
|
|
15
|
+
|
|
16
|
+
export class BuildChannel extends BaseChannel {
|
|
17
|
+
private lobbyTopic: Channel;
|
|
18
|
+
|
|
19
|
+
constructor(
|
|
20
|
+
endpoint: string,
|
|
21
|
+
maxReconnectRetries: number,
|
|
22
|
+
key: string,
|
|
23
|
+
logLevel: LogLevel = 'info',
|
|
24
|
+
) {
|
|
25
|
+
super(
|
|
26
|
+
endpoint,
|
|
27
|
+
maxReconnectRetries,
|
|
28
|
+
key,
|
|
29
|
+
logLevel,
|
|
30
|
+
'@tvlabs/build-channel',
|
|
31
|
+
);
|
|
32
|
+
this.lobbyTopic = this.socket.channel('upload:lobby');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async disconnect(): Promise<void> {
|
|
36
|
+
return new Promise((res, _rej) => {
|
|
37
|
+
this.lobbyTopic.leave();
|
|
38
|
+
this.socket.disconnect(() => res());
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async connect(): Promise<void> {
|
|
43
|
+
try {
|
|
44
|
+
this.log.debug('Connecting to build channel...');
|
|
45
|
+
|
|
46
|
+
this.socket.connect();
|
|
47
|
+
|
|
48
|
+
await this.join(this.lobbyTopic);
|
|
49
|
+
|
|
50
|
+
this.log.debug('Connected to build channel!');
|
|
51
|
+
} catch (error) {
|
|
52
|
+
this.log.error('Error connecting to build channel:', error);
|
|
53
|
+
throw new SevereServiceError(
|
|
54
|
+
'Could not connect to build channel, please check your connection.',
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async uploadBuild(buildPath: string, appSlug?: string): Promise<string> {
|
|
60
|
+
const metadata = await this.getFileMetadata(buildPath);
|
|
61
|
+
|
|
62
|
+
this.log.info(
|
|
63
|
+
`Requesting upload for build ${metadata.filename} (${metadata.type}, ${metadata.size} bytes)`,
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
const { url, build_id } = await this.requestUploadUrl(metadata, appSlug);
|
|
67
|
+
|
|
68
|
+
this.log.info('Uploading build...');
|
|
69
|
+
|
|
70
|
+
await this.uploadToUrl(url, buildPath, metadata);
|
|
71
|
+
|
|
72
|
+
const { application_id } = await this.extractBuildInfo();
|
|
73
|
+
|
|
74
|
+
this.log.info(`Build "${application_id}" processed successfully`);
|
|
75
|
+
|
|
76
|
+
return build_id;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
private async requestUploadUrl(
|
|
80
|
+
metadata: TVLabsBuildMetadata,
|
|
81
|
+
appSlug: string | undefined,
|
|
82
|
+
) {
|
|
83
|
+
try {
|
|
84
|
+
return await this.push<TVLabsRequestUploadUrlResponse>(
|
|
85
|
+
this.lobbyTopic,
|
|
86
|
+
'request_upload_url',
|
|
87
|
+
{ metadata, application_slug: appSlug },
|
|
88
|
+
);
|
|
89
|
+
} catch (error) {
|
|
90
|
+
this.log.error('Error requesting upload URL:', error);
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private async uploadToUrl(
|
|
96
|
+
url: string,
|
|
97
|
+
filePath: string,
|
|
98
|
+
metadata: TVLabsBuildMetadata,
|
|
99
|
+
): Promise<void> {
|
|
100
|
+
try {
|
|
101
|
+
const response = await fetch(url, {
|
|
102
|
+
method: 'PUT',
|
|
103
|
+
headers: {
|
|
104
|
+
'Content-Type': metadata.type,
|
|
105
|
+
'Content-Length': String(metadata.size),
|
|
106
|
+
},
|
|
107
|
+
// @ts-expect-error - FIXME: fetch types are incorrect
|
|
108
|
+
body: fs.createReadStream(filePath),
|
|
109
|
+
duplex: 'half',
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
if (!response.ok) {
|
|
113
|
+
throw new SevereServiceError(
|
|
114
|
+
`Failed to upload build to storage, got ${response.status}`,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
this.log.info('Upload complete');
|
|
119
|
+
} catch (error) {
|
|
120
|
+
this.log.error('Error uploading build:', error);
|
|
121
|
+
throw error;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private async extractBuildInfo() {
|
|
126
|
+
this.log.info('Processing uploaded build...');
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
return await this.push<TVLabsExtractBuildInfoResponse>(
|
|
130
|
+
this.lobbyTopic,
|
|
131
|
+
'extract_build_info',
|
|
132
|
+
{},
|
|
133
|
+
);
|
|
134
|
+
} catch (error) {
|
|
135
|
+
this.log.error('Error processing build:', error);
|
|
136
|
+
throw error;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private async getFileMetadata(
|
|
141
|
+
buildPath: string,
|
|
142
|
+
): Promise<TVLabsBuildMetadata> {
|
|
143
|
+
const filename = path.basename(buildPath);
|
|
144
|
+
const size = fs.statSync(buildPath).size;
|
|
145
|
+
const type = this.detectMimeType(filename);
|
|
146
|
+
const sha256 = await this.computeSha256(buildPath);
|
|
147
|
+
|
|
148
|
+
return { filename, type, size, sha256 };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private async computeSha256(buildPath: string): Promise<string> {
|
|
152
|
+
return new Promise((resolve, reject) => {
|
|
153
|
+
const hash = crypto.createHash('sha256');
|
|
154
|
+
const stream = fs.createReadStream(buildPath);
|
|
155
|
+
|
|
156
|
+
stream.on('data', (chunk) => hash.update(chunk));
|
|
157
|
+
stream.on('end', () => resolve(hash.digest('hex')));
|
|
158
|
+
stream.on('error', (err) => reject(err));
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private detectMimeType(filename: string): string {
|
|
163
|
+
const fileExtension = path.extname(filename).toLowerCase();
|
|
164
|
+
|
|
165
|
+
switch (fileExtension) {
|
|
166
|
+
case '.apk':
|
|
167
|
+
return 'application/vnd.android.package-archive';
|
|
168
|
+
case '.zip':
|
|
169
|
+
return 'application/zip';
|
|
170
|
+
default:
|
|
171
|
+
return 'application/octet-stream';
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -1,23 +1,17 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { Socket, type Channel } from 'phoenix';
|
|
1
|
+
import { type Channel } from 'phoenix';
|
|
3
2
|
import { SevereServiceError } from 'webdriverio';
|
|
4
|
-
import {
|
|
5
|
-
import { getServiceInfo } from './utils.js';
|
|
3
|
+
import { BaseChannel } from './base.js';
|
|
6
4
|
|
|
7
5
|
import type {
|
|
8
6
|
TVLabsCapabilities,
|
|
9
|
-
TVLabsSocketParams,
|
|
10
7
|
TVLabsSessionRequestEventHandler,
|
|
11
8
|
TVLabsSessionRequestResponse,
|
|
12
9
|
LogLevel,
|
|
13
|
-
} from '
|
|
14
|
-
import type { PhoenixChannelJoinResponse } from './phoenix.js';
|
|
10
|
+
} from '../types.js';
|
|
15
11
|
|
|
16
|
-
export class
|
|
17
|
-
private socket: Socket;
|
|
12
|
+
export class SessionChannel extends BaseChannel {
|
|
18
13
|
private lobbyTopic: Channel;
|
|
19
14
|
private requestTopic?: Channel;
|
|
20
|
-
private log: Logger;
|
|
21
15
|
|
|
22
16
|
private readonly events = {
|
|
23
17
|
SESSION_READY: 'session:ready',
|
|
@@ -29,22 +23,18 @@ export class TVLabsChannel {
|
|
|
29
23
|
} as const;
|
|
30
24
|
|
|
31
25
|
constructor(
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
26
|
+
endpoint: string,
|
|
27
|
+
maxReconnectRetries: number,
|
|
28
|
+
key: string,
|
|
29
|
+
logLevel: LogLevel = 'info',
|
|
36
30
|
) {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
this.socket.onError((...args) =>
|
|
45
|
-
TVLabsChannel.logSocketError(this.log, ...args),
|
|
31
|
+
super(
|
|
32
|
+
endpoint,
|
|
33
|
+
maxReconnectRetries,
|
|
34
|
+
key,
|
|
35
|
+
logLevel,
|
|
36
|
+
'@tvlabs/session-channel',
|
|
46
37
|
);
|
|
47
|
-
|
|
48
38
|
this.lobbyTopic = this.socket.channel('requests:lobby');
|
|
49
39
|
}
|
|
50
40
|
|
|
@@ -58,17 +48,17 @@ export class TVLabsChannel {
|
|
|
58
48
|
|
|
59
49
|
async connect(): Promise<void> {
|
|
60
50
|
try {
|
|
61
|
-
this.log.debug('Connecting to
|
|
51
|
+
this.log.debug('Connecting to session channel...');
|
|
62
52
|
|
|
63
53
|
this.socket.connect();
|
|
64
54
|
|
|
65
55
|
await this.join(this.lobbyTopic);
|
|
66
56
|
|
|
67
|
-
this.log.debug('Connected to
|
|
57
|
+
this.log.debug('Connected to session channel!');
|
|
68
58
|
} catch (error) {
|
|
69
|
-
this.log.error('Error connecting to
|
|
59
|
+
this.log.error('Error connecting to session channel:', error);
|
|
70
60
|
throw new SevereServiceError(
|
|
71
|
-
'Could not connect to
|
|
61
|
+
'Could not connect to session channel, please check your connection.',
|
|
72
62
|
);
|
|
73
63
|
}
|
|
74
64
|
}
|
|
@@ -193,81 +183,7 @@ export class TVLabsChannel {
|
|
|
193
183
|
}
|
|
194
184
|
}
|
|
195
185
|
|
|
196
|
-
private async join(topic: Channel): Promise<void> {
|
|
197
|
-
return new Promise((res, rej) => {
|
|
198
|
-
topic
|
|
199
|
-
.join()
|
|
200
|
-
.receive('ok', (_resp: PhoenixChannelJoinResponse) => {
|
|
201
|
-
res();
|
|
202
|
-
})
|
|
203
|
-
.receive('error', ({ response }: PhoenixChannelJoinResponse) => {
|
|
204
|
-
rej('Failed to join topic: ' + response);
|
|
205
|
-
})
|
|
206
|
-
.receive('timeout', () => {
|
|
207
|
-
rej('timeout');
|
|
208
|
-
});
|
|
209
|
-
});
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
private async push<T>(
|
|
213
|
-
topic: Channel,
|
|
214
|
-
event: string,
|
|
215
|
-
payload: object,
|
|
216
|
-
): Promise<T> {
|
|
217
|
-
return new Promise((res, rej) => {
|
|
218
|
-
topic
|
|
219
|
-
.push(event, payload)
|
|
220
|
-
.receive('ok', (msg: T) => {
|
|
221
|
-
res(msg);
|
|
222
|
-
})
|
|
223
|
-
.receive('error', (reason: string) => {
|
|
224
|
-
rej(reason);
|
|
225
|
-
})
|
|
226
|
-
.receive('timeout', () => {
|
|
227
|
-
rej('timeout');
|
|
228
|
-
});
|
|
229
|
-
});
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
private params(): TVLabsSocketParams {
|
|
233
|
-
const serviceInfo = getServiceInfo();
|
|
234
|
-
|
|
235
|
-
this.log.debug('Info:', serviceInfo);
|
|
236
|
-
|
|
237
|
-
return {
|
|
238
|
-
...serviceInfo,
|
|
239
|
-
api_key: this.key,
|
|
240
|
-
};
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
private reconnectAfterMs(tries: number) {
|
|
244
|
-
if (tries > this.maxReconnectRetries) {
|
|
245
|
-
throw new SevereServiceError(
|
|
246
|
-
'Could not connect to TV Labs, please check your connection.',
|
|
247
|
-
);
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
const wait = [0, 1000, 3000, 5000][tries] || 10000;
|
|
251
|
-
|
|
252
|
-
this.log.info(
|
|
253
|
-
`[${tries}/${this.maxReconnectRetries}] Waiting ${wait}ms before re-attempting to connect...`,
|
|
254
|
-
);
|
|
255
|
-
|
|
256
|
-
return wait;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
186
|
private tvlabsSessionLink(sessionId: string) {
|
|
260
187
|
return `https://tvlabs.ai/app/sessions/${sessionId}`;
|
|
261
188
|
}
|
|
262
|
-
|
|
263
|
-
private static logSocketError(
|
|
264
|
-
log: Logger,
|
|
265
|
-
event: ErrorEvent,
|
|
266
|
-
_transport: new (endpoint: string) => object,
|
|
267
|
-
_establishedConnections: number,
|
|
268
|
-
) {
|
|
269
|
-
const error = event.error;
|
|
270
|
-
|
|
271
|
-
log.error('Socket error:', error || event);
|
|
272
|
-
}
|
|
273
189
|
}
|
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/service.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { SevereServiceError } from 'webdriverio';
|
|
2
2
|
import * as crypto from 'crypto';
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { SessionChannel } from './channels/session.js';
|
|
5
|
+
import { BuildChannel } from './channels/build.js';
|
|
5
6
|
import { Logger } from './logger.js';
|
|
6
7
|
|
|
7
8
|
import type { Services, Capabilities, Options } from '@wdio/types';
|
|
@@ -42,21 +43,41 @@ export default class TVLabsService implements Services.ServiceInstance {
|
|
|
42
43
|
_specs: string[],
|
|
43
44
|
_cid: string,
|
|
44
45
|
) {
|
|
45
|
-
const
|
|
46
|
-
|
|
46
|
+
const buildPath = this.buildPath();
|
|
47
|
+
|
|
48
|
+
if (buildPath) {
|
|
49
|
+
const buildChannel = new BuildChannel(
|
|
50
|
+
this.buildEndpoint(),
|
|
51
|
+
this.reconnectRetries(),
|
|
52
|
+
this.apiKey(),
|
|
53
|
+
this.logLevel(),
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
await buildChannel.connect();
|
|
57
|
+
|
|
58
|
+
capabilities['tvlabs:build'] = await buildChannel.uploadBuild(
|
|
59
|
+
buildPath,
|
|
60
|
+
this.appSlug(),
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
await buildChannel.disconnect();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const sessionChannel = new SessionChannel(
|
|
67
|
+
this.sessionEndpoint(),
|
|
47
68
|
this.reconnectRetries(),
|
|
48
69
|
this.apiKey(),
|
|
49
70
|
this.logLevel(),
|
|
50
71
|
);
|
|
51
72
|
|
|
52
|
-
await
|
|
73
|
+
await sessionChannel.connect();
|
|
53
74
|
|
|
54
|
-
capabilities['tvlabs:session_id'] = await
|
|
75
|
+
capabilities['tvlabs:session_id'] = await sessionChannel.newSession(
|
|
55
76
|
capabilities,
|
|
56
77
|
this.retries(),
|
|
57
78
|
);
|
|
58
79
|
|
|
59
|
-
await
|
|
80
|
+
await sessionChannel.disconnect();
|
|
60
81
|
}
|
|
61
82
|
|
|
62
83
|
private setupRequestId() {
|
|
@@ -101,8 +122,20 @@ export default class TVLabsService implements Services.ServiceInstance {
|
|
|
101
122
|
}
|
|
102
123
|
}
|
|
103
124
|
|
|
104
|
-
private
|
|
105
|
-
return this._options.
|
|
125
|
+
private buildPath(): string | undefined {
|
|
126
|
+
return this._options.buildPath;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private appSlug(): string | undefined {
|
|
130
|
+
return this._options.app;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private sessionEndpoint(): string {
|
|
134
|
+
return this._options.sessionEndpoint ?? 'wss://tvlabs.ai/appium';
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private buildEndpoint(): string {
|
|
138
|
+
return this._options.buildEndpoint ?? 'wss://tvlabs.ai/cli';
|
|
106
139
|
}
|
|
107
140
|
|
|
108
141
|
private retries(): number {
|
package/src/types.ts
CHANGED
|
@@ -4,8 +4,11 @@ export type LogLevel = 'error' | 'warn' | 'info' | 'debug' | 'trace' | 'silent';
|
|
|
4
4
|
|
|
5
5
|
export type TVLabsServiceOptions = {
|
|
6
6
|
apiKey: string;
|
|
7
|
-
|
|
7
|
+
sessionEndpoint?: string;
|
|
8
|
+
buildEndpoint?: string;
|
|
8
9
|
retries?: number;
|
|
10
|
+
buildPath?: string;
|
|
11
|
+
app?: string;
|
|
9
12
|
reconnectRetries?: number;
|
|
10
13
|
attachRequestId?: boolean;
|
|
11
14
|
};
|
|
@@ -49,3 +52,19 @@ export type TVLabsServiceInfo = {
|
|
|
49
52
|
service_version: string;
|
|
50
53
|
service_name: string;
|
|
51
54
|
};
|
|
55
|
+
|
|
56
|
+
export type TVLabsRequestUploadUrlResponse = {
|
|
57
|
+
url: string;
|
|
58
|
+
build_id: string;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export type TVLabsExtractBuildInfoResponse = {
|
|
62
|
+
application_id: string;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export type TVLabsBuildMetadata = {
|
|
66
|
+
filename: string;
|
|
67
|
+
type: string;
|
|
68
|
+
size: number;
|
|
69
|
+
sha256: string;
|
|
70
|
+
};
|
package/cjs/channel.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"channel.d.ts","sourceRoot":"","sources":["../src/channel.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EACV,kBAAkB,EAIlB,QAAQ,EACT,MAAM,YAAY,CAAC;AAGpB,qBAAa,aAAa;IAgBtB,OAAO,CAAC,QAAQ;IAChB,OAAO,CAAC,mBAAmB;IAC3B,OAAO,CAAC,GAAG;IACX,OAAO,CAAC,QAAQ;IAlBlB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,UAAU,CAAU;IAC5B,OAAO,CAAC,YAAY,CAAC,CAAU;IAC/B,OAAO,CAAC,GAAG,CAAS;IAEpB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAOZ;gBAGD,QAAQ,EAAE,MAAM,EAChB,mBAAmB,EAAE,MAAM,EAC3B,GAAG,EAAE,MAAM,EACX,QAAQ,GAAE,QAAiB;IAgB/B,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;YAuBd,IAAI;YAgBJ,IAAI;IAoBlB,OAAO,CAAC,MAAM;IAWd,OAAO,CAAC,gBAAgB;IAgBxB,OAAO,CAAC,iBAAiB;IAIzB,OAAO,CAAC,MAAM,CAAC,cAAc;CAU9B"}
|
package/esm/channel.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"channel.d.ts","sourceRoot":"","sources":["../src/channel.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EACV,kBAAkB,EAIlB,QAAQ,EACT,MAAM,YAAY,CAAC;AAGpB,qBAAa,aAAa;IAgBtB,OAAO,CAAC,QAAQ;IAChB,OAAO,CAAC,mBAAmB;IAC3B,OAAO,CAAC,GAAG;IACX,OAAO,CAAC,QAAQ;IAlBlB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,UAAU,CAAU;IAC5B,OAAO,CAAC,YAAY,CAAC,CAAU;IAC/B,OAAO,CAAC,GAAG,CAAS;IAEpB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAOZ;gBAGD,QAAQ,EAAE,MAAM,EAChB,mBAAmB,EAAE,MAAM,EAC3B,GAAG,EAAE,MAAM,EACX,QAAQ,GAAE,QAAiB;IAgB/B,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;YAuBd,IAAI;YAgBJ,IAAI;IAoBlB,OAAO,CAAC,MAAM;IAWd,OAAO,CAAC,gBAAgB;IAgBxB,OAAO,CAAC,iBAAiB;IAIzB,OAAO,CAAC,MAAM,CAAC,cAAc;CAU9B"}
|