test-proxy-recorder 0.1.0
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/LICENSE +21 -0
- package/README.md +363 -0
- package/dist/ProxyServer.d.ts +39 -0
- package/dist/ProxyServer.d.ts.map +1 -0
- package/dist/ProxyServer.js +464 -0
- package/dist/cli.d.ts +8 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +32 -0
- package/dist/constants.d.ts +7 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +7 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/playwright/index.d.ts +48 -0
- package/dist/playwright/index.d.ts.map +1 -0
- package/dist/playwright/index.js +92 -0
- package/dist/proxy.d.ts +2 -0
- package/dist/proxy.d.ts.map +1 -0
- package/dist/proxy.js +8 -0
- package/dist/types.d.ts +46 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/utils/fileUtils.d.ts +5 -0
- package/dist/utils/fileUtils.d.ts.map +1 -0
- package/dist/utils/fileUtils.js +16 -0
- package/dist/utils/httpHelpers.d.ts +4 -0
- package/dist/utils/httpHelpers.d.ts.map +1 -0
- package/dist/utils/httpHelpers.js +13 -0
- package/dist/utils/index.d.ts +4 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +4 -0
- package/dist/utils/requestKeyGenerator.d.ts +3 -0
- package/dist/utils/requestKeyGenerator.d.ts.map +1 -0
- package/dist/utils/requestKeyGenerator.js +24 -0
- package/package.json +100 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 asmyshlyaev177
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
# test-proxy-recorder
|
|
2
|
+
|
|
3
|
+
HTTP proxy server for recording and replaying network requests in testing. Works seamlessly with Playwright and other testing frameworks.
|
|
4
|
+
|
|
5
|
+
### BETA VERSION, NOT STABLE FOR PRODUCTION USE
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Record Mode**: Capture HTTP/HTTPS requests and responses, including WebSocket connections
|
|
10
|
+
- **Replay Mode**: Replay captured requests from disk without hitting real endpoints
|
|
11
|
+
- **Transparent Mode**: Act as a simple proxy without recording or replaying
|
|
12
|
+
- **Playwright Integration**: Built-in fixture for easy integration with Playwright tests
|
|
13
|
+
- **WebSocket Support**: Full support for recording and replaying WebSocket connections
|
|
14
|
+
- **Multiple Targets**: Load balance between multiple backend targets
|
|
15
|
+
- **Timeout Control**: Automatic mode switching after configurable timeouts
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install test-proxy-recorder
|
|
21
|
+
# or
|
|
22
|
+
pnpm add test-proxy-recorder
|
|
23
|
+
# or
|
|
24
|
+
yarn add test-proxy-recorder
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Quick Start
|
|
28
|
+
|
|
29
|
+
### Standalone Usage
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
import { ProxyServer } from 'test-proxy-recorder';
|
|
33
|
+
|
|
34
|
+
const proxy = new ProxyServer(
|
|
35
|
+
['http://localhost:3000'], // backend targets
|
|
36
|
+
'./recordings' // directory to store recordings
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
await proxy.init();
|
|
40
|
+
const server = proxy.listen(8080);
|
|
41
|
+
console.log('Proxy running on http://localhost:8080');
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### With Playwright
|
|
45
|
+
|
|
46
|
+
The proxy runs continuously in the background. Tests control the recording/replay mode using `playwrightProxy.before()` and `playwrightProxy.after()`:
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
import { test } from '@playwright/test';
|
|
50
|
+
import { playwrightProxy } from 'test-proxy-recorder';
|
|
51
|
+
|
|
52
|
+
test('record API responses', async ({ page }, testInfo) => {
|
|
53
|
+
// Set proxy to recording mode for this test
|
|
54
|
+
await playwrightProxy.before(testInfo, 'recording');
|
|
55
|
+
|
|
56
|
+
// Make requests - they will be recorded
|
|
57
|
+
await page.goto('http://localhost:8080/api/data');
|
|
58
|
+
|
|
59
|
+
// Your test assertions here
|
|
60
|
+
await expect(page.getByText('Data loaded')).toBeVisible();
|
|
61
|
+
|
|
62
|
+
// Clean up - return to transparent mode
|
|
63
|
+
await playwrightProxy.after(testInfo);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('replay recorded responses', async ({ page }, testInfo) => {
|
|
67
|
+
// Set proxy to replay mode - uses recording from test above
|
|
68
|
+
await playwrightProxy.before(testInfo, 'replay');
|
|
69
|
+
|
|
70
|
+
// This will use recorded responses instead of hitting the real API
|
|
71
|
+
await page.goto('http://localhost:8080/api/data');
|
|
72
|
+
|
|
73
|
+
await expect(page.getByText('Data loaded')).toBeVisible();
|
|
74
|
+
|
|
75
|
+
await playwrightProxy.after(testInfo);
|
|
76
|
+
});
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
You can also use standalone functions for more control:
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
import { test } from '@playwright/test';
|
|
83
|
+
import { setProxyMode, generateSessionId } from 'test-proxy-recorder';
|
|
84
|
+
|
|
85
|
+
test('custom proxy control', async ({ page }, testInfo) => {
|
|
86
|
+
const sessionId = generateSessionId(testInfo);
|
|
87
|
+
|
|
88
|
+
// Start recording with a 30s timeout
|
|
89
|
+
await setProxyMode('recording', sessionId, 30000);
|
|
90
|
+
|
|
91
|
+
await page.goto('http://localhost:8080/api/data');
|
|
92
|
+
|
|
93
|
+
// Switch to transparent mode
|
|
94
|
+
await setProxyMode('transparent', sessionId);
|
|
95
|
+
});
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## How It Works
|
|
99
|
+
|
|
100
|
+
The proxy server runs continuously and can switch between three modes:
|
|
101
|
+
|
|
102
|
+
### 1. Transparent Mode (Default)
|
|
103
|
+
|
|
104
|
+
Simply proxies requests to the backend without recording or replaying.
|
|
105
|
+
|
|
106
|
+
### 2. Record Mode
|
|
107
|
+
|
|
108
|
+
Captures all HTTP requests/responses and WebSocket messages to disk. Each test gets its own recording file based on the test name.
|
|
109
|
+
|
|
110
|
+
### 3. Replay Mode
|
|
111
|
+
|
|
112
|
+
Replays previously recorded responses from disk instead of hitting the real API. Perfect for fast, deterministic tests.
|
|
113
|
+
|
|
114
|
+
## Modes Control
|
|
115
|
+
|
|
116
|
+
### Via Playwright (Recommended)
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
// Recording mode
|
|
120
|
+
await playwrightProxy.before(testInfo, 'recording');
|
|
121
|
+
// ... test code ...
|
|
122
|
+
await playwrightProxy.after(testInfo);
|
|
123
|
+
|
|
124
|
+
// Replay mode
|
|
125
|
+
await playwrightProxy.before(testInfo, 'replay');
|
|
126
|
+
// ... test code ...
|
|
127
|
+
await playwrightProxy.after(testInfo);
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Via HTTP Control Endpoint
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
# Switch to record mode
|
|
134
|
+
curl -X POST http://localhost:8080/__proxy_control__ \
|
|
135
|
+
-H "Content-Type: application/json" \
|
|
136
|
+
-d '{"mode": "record", "id": "test-session-1", "timeout": 30000}'
|
|
137
|
+
|
|
138
|
+
# Switch to replay mode
|
|
139
|
+
curl -X POST http://localhost:8080/__proxy_control__ \
|
|
140
|
+
-H "Content-Type: application/json" \
|
|
141
|
+
-d '{"mode": "replay", "id": "test-session-1"}'
|
|
142
|
+
|
|
143
|
+
# Switch to transparent mode
|
|
144
|
+
curl -X POST http://localhost:8080/__proxy_control__ \
|
|
145
|
+
-H "Content-Type: application/json" \
|
|
146
|
+
-d '{"mode": "transparent", "id": "test-session-1"}'
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## CLI Usage
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
# Start proxy server
|
|
153
|
+
test-proxy-recorder --port 8080 --target http://localhost:3000 --recordings ./recordings
|
|
154
|
+
|
|
155
|
+
# With multiple targets for load balancing
|
|
156
|
+
test-proxy-recorder --port 8080 \
|
|
157
|
+
--target http://localhost:3000 \
|
|
158
|
+
--target http://localhost:3001 \
|
|
159
|
+
--recordings ./recordings
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### CLI Options
|
|
163
|
+
|
|
164
|
+
- `--port, -p`: Port to listen on (default: 8080)
|
|
165
|
+
- `--target, -t`: Backend target URL (can be specified multiple times)
|
|
166
|
+
- `--recordings, -r`: Directory to store recordings (default: ./recordings)
|
|
167
|
+
|
|
168
|
+
## API
|
|
169
|
+
|
|
170
|
+
### ProxyServer
|
|
171
|
+
|
|
172
|
+
```typescript
|
|
173
|
+
class ProxyServer {
|
|
174
|
+
constructor(targets: string[], recordingsDir: string);
|
|
175
|
+
|
|
176
|
+
async init(): Promise<void>;
|
|
177
|
+
listen(port: number): http.Server;
|
|
178
|
+
}
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Control Endpoint
|
|
182
|
+
|
|
183
|
+
Send POST requests to `/__proxy_control__` with JSON body:
|
|
184
|
+
|
|
185
|
+
```typescript
|
|
186
|
+
interface ControlRequest {
|
|
187
|
+
mode: 'transparent' | 'record' | 'replay';
|
|
188
|
+
id?: string; // Required for record/replay modes
|
|
189
|
+
timeout?: number; // Auto-switch to transparent mode after timeout (ms)
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Playwright Integration API
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
import { playwrightProxy } from 'test-proxy-recorder';
|
|
197
|
+
|
|
198
|
+
// Main helper object for use with Playwright tests
|
|
199
|
+
const playwrightProxy = {
|
|
200
|
+
// Set proxy mode before test
|
|
201
|
+
async before(testInfo: PlaywrightTestInfo, mode: 'recording' | 'replay' | 'transparent'): Promise<void>;
|
|
202
|
+
|
|
203
|
+
// Reset to transparent mode after test
|
|
204
|
+
async after(testInfo: PlaywrightTestInfo): Promise<void>;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
// Standalone functions for custom control:
|
|
208
|
+
import {
|
|
209
|
+
setProxyMode, // Set mode with custom session ID
|
|
210
|
+
generateSessionId, // Generate session ID from test info
|
|
211
|
+
startRecording, // Helper to start recording
|
|
212
|
+
startReplay, // Helper to start replay
|
|
213
|
+
stopProxy // Helper to stop recording/replay
|
|
214
|
+
} from 'test-proxy-recorder';
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### Usage Pattern
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
test('your test', async ({ page }, testInfo) => {
|
|
221
|
+
// Setup: Set proxy mode at start of test
|
|
222
|
+
await playwrightProxy.before(testInfo, 'replay');
|
|
223
|
+
|
|
224
|
+
// Your test code here
|
|
225
|
+
await page.goto('/your-page');
|
|
226
|
+
await expect(page.getByText('Something')).toBeVisible();
|
|
227
|
+
|
|
228
|
+
// Cleanup: Return to transparent mode
|
|
229
|
+
await playwrightProxy.after(testInfo);
|
|
230
|
+
});
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
## Recording Format
|
|
234
|
+
|
|
235
|
+
Recordings are stored as JSON files in the recordings directory:
|
|
236
|
+
|
|
237
|
+
```
|
|
238
|
+
recordings/
|
|
239
|
+
├── test-session-1.json
|
|
240
|
+
├── test-session-2.json
|
|
241
|
+
└── ...
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Each recording contains:
|
|
245
|
+
|
|
246
|
+
- Request/response pairs with headers and bodies
|
|
247
|
+
- WebSocket messages with timestamps
|
|
248
|
+
- Unique keys for request matching during replay
|
|
249
|
+
|
|
250
|
+
## Typical Workflow
|
|
251
|
+
|
|
252
|
+
1. **Start the proxy server** (runs continuously):
|
|
253
|
+
|
|
254
|
+
```bash
|
|
255
|
+
test-proxy-recorder http://localhost:3000 --port 8080
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
2. **Configure your app** to use the proxy:
|
|
259
|
+
|
|
260
|
+
```bash
|
|
261
|
+
export EXTERNAL_API_URL=http://localhost:8080
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
3. **Record responses** (first run):
|
|
265
|
+
|
|
266
|
+
```typescript
|
|
267
|
+
test('my test', async ({ page }, testInfo) => {
|
|
268
|
+
await playwrightProxy.before(testInfo, 'recording');
|
|
269
|
+
// Test interacts with real API through proxy
|
|
270
|
+
await page.goto('/my-page');
|
|
271
|
+
await playwrightProxy.after(testInfo);
|
|
272
|
+
});
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
4. **Replay responses** (subsequent runs):
|
|
276
|
+
|
|
277
|
+
```typescript
|
|
278
|
+
test('my test', async ({ page }, testInfo) => {
|
|
279
|
+
await playwrightProxy.before(testInfo, 'replay');
|
|
280
|
+
// Test uses recorded responses - no real API calls
|
|
281
|
+
await page.goto('/my-page');
|
|
282
|
+
await playwrightProxy.after(testInfo);
|
|
283
|
+
});
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
## Use Cases
|
|
287
|
+
|
|
288
|
+
- **Fast CI/CD Tests**: Record API responses once, replay them for instant test execution
|
|
289
|
+
- **Deterministic Tests**: Same responses every time, no flaky network issues
|
|
290
|
+
- **API Contract Testing**: Verify frontend handles all API scenarios
|
|
291
|
+
- **WebSocket Testing**: Record and replay complex WebSocket message sequences
|
|
292
|
+
|
|
293
|
+
## Configuration Examples
|
|
294
|
+
|
|
295
|
+
### With Environment Variables
|
|
296
|
+
|
|
297
|
+
```typescript
|
|
298
|
+
const proxy = new ProxyServer(
|
|
299
|
+
[process.env.BACKEND_URL || 'http://localhost:3000'],
|
|
300
|
+
process.env.RECORDINGS_DIR || './recordings'
|
|
301
|
+
);
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
### Multiple Backend Targets
|
|
305
|
+
|
|
306
|
+
```typescript
|
|
307
|
+
// Round-robin load balancing between targets
|
|
308
|
+
const proxy = new ProxyServer(
|
|
309
|
+
[
|
|
310
|
+
'http://backend-1:3000',
|
|
311
|
+
'http://backend-2:3000',
|
|
312
|
+
'http://backend-3:3000'
|
|
313
|
+
],
|
|
314
|
+
'./recordings'
|
|
315
|
+
);
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
## WebSocket Support
|
|
319
|
+
|
|
320
|
+
WebSocket connections are automatically detected and recorded/replayed:
|
|
321
|
+
|
|
322
|
+
```typescript
|
|
323
|
+
test('websocket test', async ({ page }, testInfo) => {
|
|
324
|
+
// Recording WebSocket messages
|
|
325
|
+
await playwrightProxy.before(testInfo, 'recording');
|
|
326
|
+
|
|
327
|
+
await page.goto('/websocket-page');
|
|
328
|
+
// WebSocket connections and messages are recorded
|
|
329
|
+
|
|
330
|
+
await playwrightProxy.after(testInfo);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
test('websocket replay', async ({ page }, testInfo) => {
|
|
334
|
+
// Replay WebSocket messages
|
|
335
|
+
await playwrightProxy.before(testInfo, 'replay');
|
|
336
|
+
|
|
337
|
+
await page.goto('/websocket-page');
|
|
338
|
+
// Previously recorded WebSocket messages are replayed
|
|
339
|
+
|
|
340
|
+
await playwrightProxy.after(testInfo);
|
|
341
|
+
});
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
## Requirements
|
|
345
|
+
|
|
346
|
+
- Node.js >= 22.0.0
|
|
347
|
+
- For Playwright integration: @playwright/test >= 1.0.0
|
|
348
|
+
|
|
349
|
+
## License
|
|
350
|
+
|
|
351
|
+
MIT
|
|
352
|
+
|
|
353
|
+
## Contributing
|
|
354
|
+
|
|
355
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
356
|
+
|
|
357
|
+
## Author
|
|
358
|
+
|
|
359
|
+
asmyshlyaev177
|
|
360
|
+
|
|
361
|
+
## Repository
|
|
362
|
+
|
|
363
|
+
[https://github.com/asmyshlyaev177/test-proxy-recorder](https://github.com/asmyshlyaev177/test-proxy-recorder)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import http from 'node:http';
|
|
2
|
+
export declare class ProxyServer {
|
|
3
|
+
private targets;
|
|
4
|
+
private currentTargetIndex;
|
|
5
|
+
private mode;
|
|
6
|
+
private recordingId;
|
|
7
|
+
private replayId;
|
|
8
|
+
private modeTimeout;
|
|
9
|
+
private proxy;
|
|
10
|
+
private currentSession;
|
|
11
|
+
private recordingsDir;
|
|
12
|
+
constructor(targets: string[], recordingsDir: string);
|
|
13
|
+
init(): Promise<void>;
|
|
14
|
+
listen(port: number): http.Server;
|
|
15
|
+
private setupProxyEventHandlers;
|
|
16
|
+
private handleProxyError;
|
|
17
|
+
private handleProxyResponse;
|
|
18
|
+
private getTarget;
|
|
19
|
+
private handleControlRequest;
|
|
20
|
+
private clearModeTimeout;
|
|
21
|
+
private switchMode;
|
|
22
|
+
private switchToTransparentMode;
|
|
23
|
+
private switchToRecordMode;
|
|
24
|
+
private switchToReplayMode;
|
|
25
|
+
private setupModeTimeout;
|
|
26
|
+
private saveCurrentSession;
|
|
27
|
+
private saveRequestRecord;
|
|
28
|
+
private recordResponse;
|
|
29
|
+
private handleReplayRequest;
|
|
30
|
+
private handleReplayError;
|
|
31
|
+
private handleRequest;
|
|
32
|
+
private handleProxyRequest;
|
|
33
|
+
private bufferRequestForRecord;
|
|
34
|
+
private handleUpgrade;
|
|
35
|
+
private handleRecordWebSocket;
|
|
36
|
+
private handleReplayWebSocket;
|
|
37
|
+
private logServerStartup;
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=ProxyServer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ProxyServer.d.ts","sourceRoot":"","sources":["../src/ProxyServer.ts"],"names":[],"mappings":"AACA,OAAO,IAAI,MAAM,WAAW,CAAC;AA8B7B,qBAAa,WAAW;IACtB,OAAO,CAAC,OAAO,CAAW;IAC1B,OAAO,CAAC,kBAAkB,CAAS;IACnC,OAAO,CAAC,IAAI,CAAO;IACnB,OAAO,CAAC,WAAW,CAAgB;IACnC,OAAO,CAAC,QAAQ,CAAgB;IAChC,OAAO,CAAC,WAAW,CAAwB;IAC3C,OAAO,CAAC,KAAK,CAAY;IACzB,OAAO,CAAC,cAAc,CAA0B;IAChD,OAAO,CAAC,aAAa,CAAS;gBAElB,OAAO,EAAE,MAAM,EAAE,EAAE,aAAa,EAAE,MAAM;IAiB9C,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAI3B,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC,MAAM;IAiBjC,OAAO,CAAC,uBAAuB;IAK/B,OAAO,CAAC,gBAAgB;IAoBxB,OAAO,CAAC,mBAAmB;IAS3B,OAAO,CAAC,SAAS;YAOH,oBAAoB;IA8BlC,OAAO,CAAC,gBAAgB;YAOV,UAAU;IA8BxB,OAAO,CAAC,uBAAuB;IAQ/B,OAAO,CAAC,kBAAkB;IAW1B,OAAO,CAAC,kBAAkB;IAW1B,OAAO,CAAC,gBAAgB;YAWV,kBAAkB;YAoBlB,iBAAiB;YAuBjB,cAAc;YAmCd,mBAAmB;IA6BjC,OAAO,CAAC,iBAAiB;YAoBX,aAAa;YAeb,kBAAkB;YAclB,sBAAsB;IAepC,OAAO,CAAC,aAAa;IAqBrB,OAAO,CAAC,qBAAqB;IA2G7B,OAAO,CAAC,qBAAqB;IAqG7B,OAAO,CAAC,gBAAgB;CAQzB"}
|