@tvlabs/wdio-service 0.1.5 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -2
- 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 +202 -70
- 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 +199 -70
- 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 +5 -4
- package/src/channels/base.ts +110 -0
- package/src/channels/build.ts +174 -0
- package/src/{channel.ts → channels/session.ts} +18 -100
- 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
|
|
|
@@ -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 = {
|
|
@@ -130,7 +136,7 @@ class Logger {
|
|
|
130
136
|
}
|
|
131
137
|
|
|
132
138
|
var name = "@tvlabs/wdio-service";
|
|
133
|
-
var version = "0.1.
|
|
139
|
+
var version = "0.1.6";
|
|
134
140
|
var packageJson = {
|
|
135
141
|
name: name,
|
|
136
142
|
version: version};
|
|
@@ -148,15 +154,81 @@ function getServiceName() {
|
|
|
148
154
|
return packageJson.name;
|
|
149
155
|
}
|
|
150
156
|
|
|
151
|
-
class
|
|
157
|
+
class BaseChannel {
|
|
152
158
|
endpoint;
|
|
153
159
|
maxReconnectRetries;
|
|
154
160
|
key;
|
|
155
161
|
logLevel;
|
|
156
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 {
|
|
157
230
|
lobbyTopic;
|
|
158
231
|
requestTopic;
|
|
159
|
-
log;
|
|
160
232
|
events = {
|
|
161
233
|
SESSION_READY: 'session:ready',
|
|
162
234
|
SESSION_FAILED: 'session:failed',
|
|
@@ -166,17 +238,7 @@ class TVLabsChannel {
|
|
|
166
238
|
REQUEST_MATCHING: 'request:matching',
|
|
167
239
|
};
|
|
168
240
|
constructor(endpoint, maxReconnectRetries, key, logLevel = 'info') {
|
|
169
|
-
|
|
170
|
-
this.maxReconnectRetries = maxReconnectRetries;
|
|
171
|
-
this.key = key;
|
|
172
|
-
this.logLevel = logLevel;
|
|
173
|
-
this.log = new Logger('@tvlabs/wdio-channel', this.logLevel);
|
|
174
|
-
this.socket = new phoenix.Socket(this.endpoint, {
|
|
175
|
-
transport: ws.WebSocket,
|
|
176
|
-
params: this.params(),
|
|
177
|
-
reconnectAfterMs: this.reconnectAfterMs.bind(this),
|
|
178
|
-
});
|
|
179
|
-
this.socket.onError((...args) => TVLabsChannel.logSocketError(this.log, ...args));
|
|
241
|
+
super(endpoint, maxReconnectRetries, key, logLevel, '@tvlabs/session-channel');
|
|
180
242
|
this.lobbyTopic = this.socket.channel('requests:lobby');
|
|
181
243
|
}
|
|
182
244
|
async disconnect() {
|
|
@@ -188,14 +250,14 @@ class TVLabsChannel {
|
|
|
188
250
|
}
|
|
189
251
|
async connect() {
|
|
190
252
|
try {
|
|
191
|
-
this.log.debug('Connecting to
|
|
253
|
+
this.log.debug('Connecting to session channel...');
|
|
192
254
|
this.socket.connect();
|
|
193
255
|
await this.join(this.lobbyTopic);
|
|
194
|
-
this.log.debug('Connected to
|
|
256
|
+
this.log.debug('Connected to session channel!');
|
|
195
257
|
}
|
|
196
258
|
catch (error) {
|
|
197
|
-
this.log.error('Error connecting to
|
|
198
|
-
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.');
|
|
199
261
|
}
|
|
200
262
|
}
|
|
201
263
|
async newSession(capabilities, maxRetries, retry = 0) {
|
|
@@ -273,57 +335,111 @@ class TVLabsChannel {
|
|
|
273
335
|
throw error;
|
|
274
336
|
}
|
|
275
337
|
}
|
|
276
|
-
|
|
277
|
-
return
|
|
278
|
-
topic
|
|
279
|
-
.join()
|
|
280
|
-
.receive('ok', (_resp) => {
|
|
281
|
-
res();
|
|
282
|
-
})
|
|
283
|
-
.receive('error', ({ response }) => {
|
|
284
|
-
rej('Failed to join topic: ' + response);
|
|
285
|
-
})
|
|
286
|
-
.receive('timeout', () => {
|
|
287
|
-
rej('timeout');
|
|
288
|
-
});
|
|
289
|
-
});
|
|
338
|
+
tvlabsSessionLink(sessionId) {
|
|
339
|
+
return `https://tvlabs.ai/app/sessions/${sessionId}`;
|
|
290
340
|
}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
});
|
|
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());
|
|
304
353
|
});
|
|
305
354
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
}
|
|
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
|
+
}
|
|
313
366
|
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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;
|
|
317
384
|
}
|
|
318
|
-
const wait = [0, 1000, 3000, 5000][tries] || 10000;
|
|
319
|
-
this.log.info(`[${tries}/${this.maxReconnectRetries}] Waiting ${wait}ms before re-attempting to connect...`);
|
|
320
|
-
return wait;
|
|
321
385
|
}
|
|
322
|
-
|
|
323
|
-
|
|
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
|
+
}
|
|
324
406
|
}
|
|
325
|
-
|
|
326
|
-
log.
|
|
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
|
+
}
|
|
327
443
|
}
|
|
328
444
|
}
|
|
329
445
|
|
|
@@ -347,15 +463,22 @@ class TVLabsService {
|
|
|
347
463
|
}
|
|
348
464
|
}
|
|
349
465
|
async beforeSession(_config, capabilities, _specs, _cid) {
|
|
350
|
-
const
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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();
|
|
354
477
|
}
|
|
355
478
|
setupRequestId() {
|
|
356
479
|
const originalTransformRequest = this._config.transformRequest;
|
|
357
480
|
this._config.transformRequest = (requestOptions) => {
|
|
358
|
-
const requestId = crypto__namespace.randomUUID();
|
|
481
|
+
const requestId = crypto__namespace$1.randomUUID();
|
|
359
482
|
const originalRequestOptions = typeof originalTransformRequest === 'function'
|
|
360
483
|
? originalTransformRequest(requestOptions)
|
|
361
484
|
: requestOptions;
|
|
@@ -380,8 +503,17 @@ class TVLabsService {
|
|
|
380
503
|
}
|
|
381
504
|
}
|
|
382
505
|
}
|
|
383
|
-
|
|
384
|
-
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';
|
|
385
517
|
}
|
|
386
518
|
retries() {
|
|
387
519
|
return this._options.retries ?? 3;
|
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"}
|
|
@@ -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"}
|