testdriverai 7.2.21 → 7.2.23
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/docs/v7/_drafts/plugin-migration.mdx +3 -5
- package/lib/vitest/hooks.mjs +26 -14
- package/package.json +1 -1
- package/test/testdriver/hover-image.test.mjs +19 -1
- package/test/testdriver/hover-text-with-description.test.mjs +19 -1
- package/test/testdriver/match-image.test.mjs +19 -1
- package/test/testdriver/scroll-until-text.test.mjs +19 -1
- package/docs/v7/_drafts/implementation-plan.mdx +0 -994
- package/docs/v7/_drafts/optimal-sdk-design.mdx +0 -1348
- package/docs/v7/_drafts/performance.mdx +0 -517
- package/docs/v7/_drafts/platforms/linux.mdx +0 -308
- package/docs/v7/_drafts/platforms/macos.mdx +0 -433
- package/docs/v7/_drafts/platforms/windows.mdx +0 -430
- package/docs/v7/_drafts/sdk-logging.mdx +0 -222
- package/test/testdriver/setup/globalTeardown.mjs +0 -11
- package/test/testdriver/setup/lifecycleHelpers.mjs +0 -357
- package/test/testdriver/setup/testHelpers.mjs +0 -541
- package/test/testdriver/setup/vitestSetup.mjs +0 -40
|
@@ -1,517 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: "Performance Optimization"
|
|
3
|
-
description: "Speed up your TestDriver tests"
|
|
4
|
-
icon: "gauge"
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
## Overview
|
|
8
|
-
|
|
9
|
-
TestDriver tests run in cloud sandboxes, which adds latency. This guide shows you how to minimize overhead and maximize test speed.
|
|
10
|
-
|
|
11
|
-
## Baseline Performance
|
|
12
|
-
|
|
13
|
-
Typical test execution:
|
|
14
|
-
|
|
15
|
-
| Phase | Time | What's Happening |
|
|
16
|
-
|-------|------|------------------|
|
|
17
|
-
| Sandbox startup | 20-60s | First test creates VM |
|
|
18
|
-
| Sandbox reuse | 0s | Subsequent tests reuse VM |
|
|
19
|
-
| Page load | 1-5s | Browser navigates to URL |
|
|
20
|
-
| Element finding | 0.5-3s | AI locates element |
|
|
21
|
-
| Element finding (cached) | 0.1-0.5s | Cache hit |
|
|
22
|
-
| Command execution | 0.1-1s | Click, type, etc. |
|
|
23
|
-
| AI command | 2-5s | Natural language parsing |
|
|
24
|
-
| AI command (cached) | 0.1-0.5s | Cache hit |
|
|
25
|
-
|
|
26
|
-
**First test**: 60-90s
|
|
27
|
-
**Subsequent tests**: 5-30s
|
|
28
|
-
**Optimized tests**: 2-10s
|
|
29
|
-
|
|
30
|
-
## Optimization Strategies
|
|
31
|
-
|
|
32
|
-
### 1. Reuse Sandboxes
|
|
33
|
-
|
|
34
|
-
The biggest performance win is reusing sandboxes across tests.
|
|
35
|
-
|
|
36
|
-
#### ❌ Slow - Create new sandbox per test
|
|
37
|
-
|
|
38
|
-
```javascript
|
|
39
|
-
test('test 1', async () => {
|
|
40
|
-
const testdriver = await TestDriver.create({
|
|
41
|
-
apiKey: process.env.TD_API_KEY
|
|
42
|
-
});
|
|
43
|
-
// Test logic
|
|
44
|
-
await testdriver.cleanup();
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
test('test 2', async () => {
|
|
48
|
-
const testdriver = await TestDriver.create({
|
|
49
|
-
apiKey: process.env.TD_API_KEY
|
|
50
|
-
});
|
|
51
|
-
// Test logic
|
|
52
|
-
await testdriver.cleanup();
|
|
53
|
-
});
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
**Time**: 60s + 60s = 120s
|
|
57
|
-
|
|
58
|
-
#### ✅ Fast - Reuse sandbox with context
|
|
59
|
-
|
|
60
|
-
```javascript
|
|
61
|
-
import { chrome } from '../setup/lifecycleHelpers.mjs';
|
|
62
|
-
|
|
63
|
-
test('test 1', async (context) => {
|
|
64
|
-
const { testdriver } = await chrome(context, { url: 'https://app.com' });
|
|
65
|
-
// Test logic
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
test('test 2', async (context) => {
|
|
69
|
-
const { testdriver } = await chrome(context, { url: 'https://app.com' });
|
|
70
|
-
// Test logic
|
|
71
|
-
});
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
**Time**: 60s + 5s = 65s (54% faster)
|
|
75
|
-
|
|
76
|
-
### 2. Enable Caching
|
|
77
|
-
|
|
78
|
-
TestDriver has two cache layers:
|
|
79
|
-
|
|
80
|
-
#### AI Prompt Cache
|
|
81
|
-
|
|
82
|
-
Caches AI-generated commands locally:
|
|
83
|
-
|
|
84
|
-
```yaml
|
|
85
|
-
# testdriver.yaml
|
|
86
|
-
cache:
|
|
87
|
-
ai: true # Enable prompt caching
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
**Impact**: 2-5s → 0.1-0.5s per AI command
|
|
91
|
-
|
|
92
|
-
#### Selector Cache
|
|
93
|
-
|
|
94
|
-
Caches element locations on server:
|
|
95
|
-
|
|
96
|
-
```yaml
|
|
97
|
-
# testdriver.yaml
|
|
98
|
-
cache:
|
|
99
|
-
selectors: true
|
|
100
|
-
selectorThreshold: 0.95 # 95% similarity required
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
**Impact**: 0.5-3s → 0.1-0.5s per element find
|
|
104
|
-
|
|
105
|
-
**Combined**: Tests run 3-5x faster on subsequent runs.
|
|
106
|
-
|
|
107
|
-
### 3. Parallel Test Execution
|
|
108
|
-
|
|
109
|
-
Run multiple tests simultaneously:
|
|
110
|
-
|
|
111
|
-
```javascript
|
|
112
|
-
// vitest.config.mjs
|
|
113
|
-
export default defineConfig({
|
|
114
|
-
test: {
|
|
115
|
-
maxConcurrency: 5, // Run 5 tests at once
|
|
116
|
-
fileParallelism: true
|
|
117
|
-
}
|
|
118
|
-
});
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
**Impact**: 5 tests in 300s → 5 tests in 90s (70% faster)
|
|
122
|
-
|
|
123
|
-
**Tradeoffs**:
|
|
124
|
-
- Uses more test minutes
|
|
125
|
-
- May hit API rate limits
|
|
126
|
-
- Requires adequate plan limits
|
|
127
|
-
|
|
128
|
-
### 4. Reduce AI Commands
|
|
129
|
-
|
|
130
|
-
AI commands are slower than direct commands.
|
|
131
|
-
|
|
132
|
-
#### ❌ Slow - AI for everything
|
|
133
|
-
|
|
134
|
-
```javascript
|
|
135
|
-
await testdriver.ai('click the login button');
|
|
136
|
-
await testdriver.ai('type email@example.com');
|
|
137
|
-
await testdriver.ai('type mypassword');
|
|
138
|
-
await testdriver.ai('press enter');
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
**Time**: ~15s
|
|
142
|
-
|
|
143
|
-
#### ✅ Fast - AI only for finding
|
|
144
|
-
|
|
145
|
-
```javascript
|
|
146
|
-
await testdriver.find('email field').then(el => el.click());
|
|
147
|
-
await testdriver.type('email@example.com');
|
|
148
|
-
|
|
149
|
-
await testdriver.find('password field').then(el => el.click());
|
|
150
|
-
await testdriver.type('mypassword');
|
|
151
|
-
|
|
152
|
-
await testdriver.find('login button').then(el => el.click());
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
**Time**: ~5s (66% faster)
|
|
156
|
-
|
|
157
|
-
### 5. Batch Operations
|
|
158
|
-
|
|
159
|
-
Minimize round trips to sandbox:
|
|
160
|
-
|
|
161
|
-
#### ❌ Slow - Sequential operations
|
|
162
|
-
|
|
163
|
-
```javascript
|
|
164
|
-
await testdriver.find('first name').then(el => el.click());
|
|
165
|
-
await testdriver.type('John');
|
|
166
|
-
|
|
167
|
-
await testdriver.find('last name').then(el => el.click());
|
|
168
|
-
await testdriver.type('Doe');
|
|
169
|
-
|
|
170
|
-
await testdriver.find('email').then(el => el.click());
|
|
171
|
-
await testdriver.type('john@example.com');
|
|
172
|
-
```
|
|
173
|
-
|
|
174
|
-
**Time**: 6 round trips
|
|
175
|
-
|
|
176
|
-
#### ✅ Fast - Batch with keyboard navigation
|
|
177
|
-
|
|
178
|
-
```javascript
|
|
179
|
-
await testdriver.find('first name').then(el => el.click());
|
|
180
|
-
await testdriver.type('John');
|
|
181
|
-
|
|
182
|
-
await testdriver.pressKeys(['tab']);
|
|
183
|
-
await testdriver.type('Doe');
|
|
184
|
-
|
|
185
|
-
await testdriver.pressKeys(['tab']);
|
|
186
|
-
await testdriver.type('john@example.com');
|
|
187
|
-
```
|
|
188
|
-
|
|
189
|
-
**Time**: 4 round trips (33% faster)
|
|
190
|
-
|
|
191
|
-
### 6. Smart Waiting
|
|
192
|
-
|
|
193
|
-
Avoid unnecessary delays:
|
|
194
|
-
|
|
195
|
-
#### ❌ Slow - Fixed delays
|
|
196
|
-
|
|
197
|
-
```javascript
|
|
198
|
-
await testdriver.find('button').then(el => el.click());
|
|
199
|
-
await new Promise(r => setTimeout(r, 5000)); // Always wait 5s
|
|
200
|
-
await testdriver.find('success message');
|
|
201
|
-
```
|
|
202
|
-
|
|
203
|
-
#### ✅ Fast - Poll until found
|
|
204
|
-
|
|
205
|
-
```javascript
|
|
206
|
-
await testdriver.find('button').then(el => el.click());
|
|
207
|
-
|
|
208
|
-
// Poll for success message
|
|
209
|
-
let element;
|
|
210
|
-
for (let i = 0; i < 30; i++) {
|
|
211
|
-
element = await testdriver.find('success message');
|
|
212
|
-
if (element.found()) break;
|
|
213
|
-
await new Promise(r => setTimeout(r, 500));
|
|
214
|
-
}
|
|
215
|
-
```
|
|
216
|
-
|
|
217
|
-
#### ✅ Better - Use assertions
|
|
218
|
-
|
|
219
|
-
```javascript
|
|
220
|
-
await testdriver.find('button').then(el => el.click());
|
|
221
|
-
await testdriver.assert('success message appeared'); // Waits intelligently
|
|
222
|
-
```
|
|
223
|
-
|
|
224
|
-
### 7. Optimize Element Descriptions
|
|
225
|
-
|
|
226
|
-
More specific = faster finding:
|
|
227
|
-
|
|
228
|
-
#### ❌ Slow - Vague description
|
|
229
|
-
|
|
230
|
-
```javascript
|
|
231
|
-
await testdriver.find('button'); // Many buttons to check
|
|
232
|
-
```
|
|
233
|
-
|
|
234
|
-
**Time**: 2-3s (checks all buttons)
|
|
235
|
-
|
|
236
|
-
#### ✅ Fast - Specific description
|
|
237
|
-
|
|
238
|
-
```javascript
|
|
239
|
-
await testdriver.find('blue submit button at bottom right');
|
|
240
|
-
```
|
|
241
|
-
|
|
242
|
-
**Time**: 0.5-1s (narrows search area)
|
|
243
|
-
|
|
244
|
-
### 8. Preload Resources
|
|
245
|
-
|
|
246
|
-
Speed up page load:
|
|
247
|
-
|
|
248
|
-
#### ❌ Slow - Load on demand
|
|
249
|
-
|
|
250
|
-
```javascript
|
|
251
|
-
test('test 1', async (context) => {
|
|
252
|
-
const { testdriver } = await chrome(context, { url: 'https://app.com' });
|
|
253
|
-
// First test loads everything
|
|
254
|
-
});
|
|
255
|
-
```
|
|
256
|
-
|
|
257
|
-
#### ✅ Fast - Preload in beforeAll
|
|
258
|
-
|
|
259
|
-
```javascript
|
|
260
|
-
import { beforeAll, test } from 'vitest';
|
|
261
|
-
|
|
262
|
-
beforeAll(async (context) => {
|
|
263
|
-
const { testdriver } = await chrome(context, { url: 'https://app.com' });
|
|
264
|
-
// Preload app, login, navigate to test area
|
|
265
|
-
await testdriver.find('email').then(el => el.click());
|
|
266
|
-
await testdriver.type(process.env.TEST_EMAIL);
|
|
267
|
-
await testdriver.find('password').then(el => el.click());
|
|
268
|
-
await testdriver.type(process.env.TEST_PASSWORD);
|
|
269
|
-
await testdriver.find('login').then(el => el.click());
|
|
270
|
-
}, 120000);
|
|
271
|
-
```
|
|
272
|
-
|
|
273
|
-
### 9. Clean Cache Strategically
|
|
274
|
-
|
|
275
|
-
Don't clear cache unnecessarily:
|
|
276
|
-
|
|
277
|
-
#### ❌ Slow - Clear cache every run
|
|
278
|
-
|
|
279
|
-
```bash
|
|
280
|
-
# package.json
|
|
281
|
-
{
|
|
282
|
-
"scripts": {
|
|
283
|
-
"test": "rm -rf .testdriver/.cache && vitest"
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
```
|
|
287
|
-
|
|
288
|
-
#### ✅ Fast - Clear cache when needed
|
|
289
|
-
|
|
290
|
-
```bash
|
|
291
|
-
# Only clear when UI changes
|
|
292
|
-
npm run test:clean # Separate script
|
|
293
|
-
|
|
294
|
-
# Normal runs use cache
|
|
295
|
-
npm test
|
|
296
|
-
```
|
|
297
|
-
|
|
298
|
-
### 10. Monitor Performance
|
|
299
|
-
|
|
300
|
-
Track test execution time:
|
|
301
|
-
|
|
302
|
-
```javascript
|
|
303
|
-
import { test } from 'vitest';
|
|
304
|
-
|
|
305
|
-
test('slow test detector', async (context) => {
|
|
306
|
-
const start = Date.now();
|
|
307
|
-
|
|
308
|
-
// Test logic
|
|
309
|
-
const { testdriver } = await chrome(context, { url });
|
|
310
|
-
await testdriver.find('button').then(el => el.click());
|
|
311
|
-
|
|
312
|
-
const duration = Date.now() - start;
|
|
313
|
-
console.log(`Test took ${duration}ms`);
|
|
314
|
-
|
|
315
|
-
if (duration > 30000) {
|
|
316
|
-
console.warn('⚠️ Test exceeded 30s threshold');
|
|
317
|
-
}
|
|
318
|
-
});
|
|
319
|
-
```
|
|
320
|
-
|
|
321
|
-
## Configuration Tuning
|
|
322
|
-
|
|
323
|
-
### Timeout Settings
|
|
324
|
-
|
|
325
|
-
Balance reliability vs speed:
|
|
326
|
-
|
|
327
|
-
```javascript
|
|
328
|
-
// vitest.config.mjs
|
|
329
|
-
export default defineConfig({
|
|
330
|
-
test: {
|
|
331
|
-
testTimeout: 60000, // Default: 60s
|
|
332
|
-
hookTimeout: 30000 // Setup/teardown: 30s
|
|
333
|
-
}
|
|
334
|
-
});
|
|
335
|
-
```
|
|
336
|
-
|
|
337
|
-
For fast, stable tests:
|
|
338
|
-
```javascript
|
|
339
|
-
testTimeout: 30000 // 30s
|
|
340
|
-
```
|
|
341
|
-
|
|
342
|
-
For slower, complex tests:
|
|
343
|
-
```javascript
|
|
344
|
-
testTimeout: 120000 // 2 minutes
|
|
345
|
-
```
|
|
346
|
-
|
|
347
|
-
### Verbosity Level
|
|
348
|
-
|
|
349
|
-
Lower verbosity = less overhead:
|
|
350
|
-
|
|
351
|
-
```javascript
|
|
352
|
-
const testdriver = await TestDriver.create({
|
|
353
|
-
apiKey: process.env.TD_API_KEY,
|
|
354
|
-
verbosity: 0 // 0=silent, 1=normal, 2=debug
|
|
355
|
-
});
|
|
356
|
-
```
|
|
357
|
-
|
|
358
|
-
**Impact**: Small (~100ms per test), but adds up.
|
|
359
|
-
|
|
360
|
-
### Resolution
|
|
361
|
-
|
|
362
|
-
Lower resolution = faster screenshots:
|
|
363
|
-
|
|
364
|
-
```javascript
|
|
365
|
-
const testdriver = await TestDriver.create({
|
|
366
|
-
apiKey: process.env.TD_API_KEY,
|
|
367
|
-
resolution: '1280x720' // Default: 1920x1080
|
|
368
|
-
});
|
|
369
|
-
```
|
|
370
|
-
|
|
371
|
-
**Impact**: 10-20% faster element finding.
|
|
372
|
-
|
|
373
|
-
## Advanced Techniques
|
|
374
|
-
|
|
375
|
-
### Connection Pooling
|
|
376
|
-
|
|
377
|
-
Reuse connections across test suites:
|
|
378
|
-
|
|
379
|
-
```javascript
|
|
380
|
-
// setup/pool.mjs
|
|
381
|
-
const clients = new Map();
|
|
382
|
-
|
|
383
|
-
export async function getClient(context) {
|
|
384
|
-
const key = `${context.task.file.name}`;
|
|
385
|
-
|
|
386
|
-
if (!clients.has(key)) {
|
|
387
|
-
clients.set(key, await TestDriver.create({
|
|
388
|
-
apiKey: process.env.TD_API_KEY
|
|
389
|
-
}));
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
return clients.get(key);
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
export async function cleanup() {
|
|
396
|
-
for (const client of clients.values()) {
|
|
397
|
-
await client.cleanup();
|
|
398
|
-
}
|
|
399
|
-
clients.clear();
|
|
400
|
-
}
|
|
401
|
-
```
|
|
402
|
-
|
|
403
|
-
### Lazy Loading
|
|
404
|
-
|
|
405
|
-
Only load what you need:
|
|
406
|
-
|
|
407
|
-
```javascript
|
|
408
|
-
// ❌ Load everything upfront
|
|
409
|
-
import { chrome, firefox, electron } from './setup/lifecycleHelpers.mjs';
|
|
410
|
-
|
|
411
|
-
// ✅ Import only what you use
|
|
412
|
-
import { chrome } from './setup/lifecycleHelpers.mjs';
|
|
413
|
-
```
|
|
414
|
-
|
|
415
|
-
### Snapshot Testing
|
|
416
|
-
|
|
417
|
-
Compare screenshots instead of re-running:
|
|
418
|
-
|
|
419
|
-
```javascript
|
|
420
|
-
test('visual regression', async (context) => {
|
|
421
|
-
const { testdriver } = await chrome(context, { url });
|
|
422
|
-
|
|
423
|
-
const element = await testdriver.find('hero section');
|
|
424
|
-
|
|
425
|
-
// First run: saves screenshot
|
|
426
|
-
// Subsequent runs: compares screenshot
|
|
427
|
-
expect(element.screenshot).toMatchImageSnapshot();
|
|
428
|
-
});
|
|
429
|
-
```
|
|
430
|
-
|
|
431
|
-
## Profiling
|
|
432
|
-
|
|
433
|
-
### Find Slow Tests
|
|
434
|
-
|
|
435
|
-
```bash
|
|
436
|
-
# Run with timing report
|
|
437
|
-
npm test -- --reporter=verbose
|
|
438
|
-
|
|
439
|
-
# Output shows duration per test
|
|
440
|
-
✓ fast test (1.2s)
|
|
441
|
-
✓ medium test (5.4s)
|
|
442
|
-
✗ slow test (45.2s)
|
|
443
|
-
```
|
|
444
|
-
|
|
445
|
-
### Measure Operations
|
|
446
|
-
|
|
447
|
-
```javascript
|
|
448
|
-
async function timedOperation(name, fn) {
|
|
449
|
-
const start = Date.now();
|
|
450
|
-
const result = await fn();
|
|
451
|
-
console.log(`${name}: ${Date.now() - start}ms`);
|
|
452
|
-
return result;
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
test('profiled test', async (context) => {
|
|
456
|
-
const { testdriver } = await timedOperation('Setup', async () => {
|
|
457
|
-
return await chrome(context, { url });
|
|
458
|
-
});
|
|
459
|
-
|
|
460
|
-
await timedOperation('Find button', async () => {
|
|
461
|
-
return await testdriver.find('button');
|
|
462
|
-
});
|
|
463
|
-
|
|
464
|
-
await timedOperation('Click button', async () => {
|
|
465
|
-
const el = await testdriver.find('button');
|
|
466
|
-
return await el.click();
|
|
467
|
-
});
|
|
468
|
-
});
|
|
469
|
-
```
|
|
470
|
-
|
|
471
|
-
### Identify Bottlenecks
|
|
472
|
-
|
|
473
|
-
Common slow operations:
|
|
474
|
-
|
|
475
|
-
1. **Sandbox creation** (20-60s) - Reuse sandboxes
|
|
476
|
-
2. **Page navigation** (1-5s) - Minimize navigation
|
|
477
|
-
3. **AI commands** (2-5s) - Use direct commands
|
|
478
|
-
4. **Element finding** (0.5-3s) - Enable caching
|
|
479
|
-
5. **Fixed delays** (varies) - Replace with smart waiting
|
|
480
|
-
|
|
481
|
-
## Production Checklist
|
|
482
|
-
|
|
483
|
-
- [ ] Reuse sandboxes via context
|
|
484
|
-
- [ ] Enable AI and selector caching
|
|
485
|
-
- [ ] Use parallel execution (maxConcurrency: 5)
|
|
486
|
-
- [ ] Minimize AI commands
|
|
487
|
-
- [ ] Batch operations
|
|
488
|
-
- [ ] Use smart waiting (assertions)
|
|
489
|
-
- [ ] Specific element descriptions
|
|
490
|
-
- [ ] Appropriate timeout settings
|
|
491
|
-
- [ ] Monitor test duration
|
|
492
|
-
- [ ] Profile slow tests
|
|
493
|
-
|
|
494
|
-
Expected performance:
|
|
495
|
-
- **First run**: 60-90s for suite
|
|
496
|
-
- **Cached runs**: 10-30s for suite
|
|
497
|
-
- **Per test**: 2-10s (cached)
|
|
498
|
-
|
|
499
|
-
## See Also
|
|
500
|
-
|
|
501
|
-
<CardGroup cols={2}>
|
|
502
|
-
<Card title="Caching (AI)" icon="brain" href="/v7/guides/caching-ai">
|
|
503
|
-
AI prompt caching
|
|
504
|
-
</Card>
|
|
505
|
-
|
|
506
|
-
<Card title="Caching (Selectors)" icon="bullseye" href="/v7/guides/caching-selectors">
|
|
507
|
-
Selector caching
|
|
508
|
-
</Card>
|
|
509
|
-
|
|
510
|
-
<Card title="Best Practices" icon="star" href="/v7/guides/best-practices">
|
|
511
|
-
Testing patterns
|
|
512
|
-
</Card>
|
|
513
|
-
|
|
514
|
-
<Card title="CI/CD Integration" icon="arrows-spin" href="/v7/guides/ci-cd">
|
|
515
|
-
Optimize CI pipelines
|
|
516
|
-
</Card>
|
|
517
|
-
</CardGroup>
|