@testsmith/testblocks 0.1.0 → 0.2.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/dist/cli/executor.d.ts +1 -0
- package/dist/cli/executor.js +32 -1
- package/dist/cli/index.js +1 -1
- package/dist/cli/reporters.js +324 -43
- package/dist/client/assets/index-DbvW1Eh_.css +1 -0
- package/dist/client/assets/index-q27f2_ju.js +2193 -0
- package/dist/client/assets/index-q27f2_ju.js.map +1 -0
- package/dist/client/index.html +2 -2
- package/dist/core/types.d.ts +7 -0
- package/dist/server/executor.d.ts +1 -0
- package/dist/server/executor.js +32 -1
- package/dist/server/index.js +64 -6
- package/package.json +1 -1
- package/dist/client/assets/index-Cq84-VIf.js +0 -2137
- package/dist/client/assets/index-Cq84-VIf.js.map +0 -1
- package/dist/client/assets/index-Dnk1ti7l.css +0 -1
package/dist/cli/executor.d.ts
CHANGED
|
@@ -16,6 +16,7 @@ export declare class TestExecutor {
|
|
|
16
16
|
constructor(options?: ExecutorOptions);
|
|
17
17
|
initialize(): Promise<void>;
|
|
18
18
|
cleanup(): Promise<void>;
|
|
19
|
+
private requiresBrowser;
|
|
19
20
|
runTestFile(testFile: TestFile): Promise<TestResult[]>;
|
|
20
21
|
private createBaseContext;
|
|
21
22
|
runTestWithData(test: TestCase, testFile: TestFile, dataSet: TestDataSet, dataIndex: number): Promise<TestResult>;
|
package/dist/cli/executor.js
CHANGED
|
@@ -42,9 +42,40 @@ class TestExecutor {
|
|
|
42
42
|
this.browserContext = null;
|
|
43
43
|
this.browser = null;
|
|
44
44
|
}
|
|
45
|
+
requiresBrowser(testFile) {
|
|
46
|
+
const hasWebStep = (steps) => {
|
|
47
|
+
return steps.some(step => step.type.startsWith('web_'));
|
|
48
|
+
};
|
|
49
|
+
const hasWebStepInState = (state) => {
|
|
50
|
+
const steps = this.extractStepsFromBlocklyState(state);
|
|
51
|
+
return hasWebStep(steps);
|
|
52
|
+
};
|
|
53
|
+
// Check beforeAll/afterAll hooks
|
|
54
|
+
if (testFile.beforeAll && hasWebStepInState(testFile.beforeAll))
|
|
55
|
+
return true;
|
|
56
|
+
if (testFile.afterAll && hasWebStepInState(testFile.afterAll))
|
|
57
|
+
return true;
|
|
58
|
+
if (testFile.beforeEach && hasWebStepInState(testFile.beforeEach))
|
|
59
|
+
return true;
|
|
60
|
+
if (testFile.afterEach && hasWebStepInState(testFile.afterEach))
|
|
61
|
+
return true;
|
|
62
|
+
// Check all tests
|
|
63
|
+
for (const test of testFile.tests) {
|
|
64
|
+
if (hasWebStepInState(test.steps))
|
|
65
|
+
return true;
|
|
66
|
+
if (test.beforeEach && hasWebStepInState(test.beforeEach))
|
|
67
|
+
return true;
|
|
68
|
+
if (test.afterEach && hasWebStepInState(test.afterEach))
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
45
73
|
async runTestFile(testFile) {
|
|
46
74
|
const results = [];
|
|
47
|
-
|
|
75
|
+
// Only initialize browser if test file contains web steps
|
|
76
|
+
if (this.requiresBrowser(testFile)) {
|
|
77
|
+
await this.initialize();
|
|
78
|
+
}
|
|
48
79
|
// Register procedures from the test file
|
|
49
80
|
if (testFile.procedures) {
|
|
50
81
|
for (const [name, procedure] of Object.entries(testFile.procedures)) {
|
package/dist/cli/index.js
CHANGED
|
@@ -230,7 +230,7 @@ program
|
|
|
230
230
|
'test:junit': 'testblocks run tests/**/*.testblocks.json -r junit -o reports',
|
|
231
231
|
},
|
|
232
232
|
devDependencies: {
|
|
233
|
-
'@testsmith/testblocks': '^0.
|
|
233
|
+
'@testsmith/testblocks': '^0.2.0',
|
|
234
234
|
},
|
|
235
235
|
};
|
|
236
236
|
fs.writeFileSync(packagePath, JSON.stringify(packageJson, null, 2));
|
package/dist/cli/reporters.js
CHANGED
|
@@ -205,10 +205,114 @@ class HTMLReporter {
|
|
|
205
205
|
}
|
|
206
206
|
}
|
|
207
207
|
exports.HTMLReporter = HTMLReporter;
|
|
208
|
+
// Format step type for display (same as StepResultItem.tsx)
|
|
209
|
+
function formatStepType(type) {
|
|
210
|
+
const displayNames = {
|
|
211
|
+
'logic_log': 'Log',
|
|
212
|
+
'logic_set_variable': 'Set Variable',
|
|
213
|
+
'logic_get_variable': 'Get Variable',
|
|
214
|
+
'api_set_header': 'Set Header',
|
|
215
|
+
'api_set_headers': 'Set Headers',
|
|
216
|
+
'api_clear_headers': 'Clear Headers',
|
|
217
|
+
'api_assert_status': 'Assert Status',
|
|
218
|
+
'api_assert_body_contains': 'Assert Body',
|
|
219
|
+
'api_extract': 'Extract',
|
|
220
|
+
'api_extract_jsonpath': 'Extract (JSONPath)',
|
|
221
|
+
'api_extract_xpath': 'Extract (XPath)',
|
|
222
|
+
'web_navigate': 'Navigate',
|
|
223
|
+
'web_click': 'Click',
|
|
224
|
+
'web_fill': 'Fill',
|
|
225
|
+
'web_type': 'Type',
|
|
226
|
+
'web_press_key': 'Press Key',
|
|
227
|
+
'web_select': 'Select',
|
|
228
|
+
'web_checkbox': 'Checkbox',
|
|
229
|
+
'web_hover': 'Hover',
|
|
230
|
+
'web_wait_for_element': 'Wait for Element',
|
|
231
|
+
'web_wait_for_url': 'Wait for URL',
|
|
232
|
+
'web_wait': 'Wait',
|
|
233
|
+
'web_screenshot': 'Screenshot',
|
|
234
|
+
'web_get_text': 'Get Text',
|
|
235
|
+
'web_get_attribute': 'Get Attribute',
|
|
236
|
+
'web_get_input_value': 'Get Input Value',
|
|
237
|
+
'web_get_title': 'Get Title',
|
|
238
|
+
'web_get_url': 'Get URL',
|
|
239
|
+
'web_assert_visible': 'Assert Visible',
|
|
240
|
+
'web_assert_not_visible': 'Assert Not Visible',
|
|
241
|
+
'web_assert_text_contains': 'Assert Text Contains',
|
|
242
|
+
'web_assert_text_equals': 'Assert Text Equals',
|
|
243
|
+
'web_assert_url_contains': 'Assert URL Contains',
|
|
244
|
+
'web_assert_title_contains': 'Assert Title Contains',
|
|
245
|
+
'web_assert_enabled': 'Assert Enabled',
|
|
246
|
+
'web_assert_checked': 'Assert Checked',
|
|
247
|
+
'totp_generate': 'Generate TOTP',
|
|
248
|
+
'totp_generate_from_numeric': 'Generate TOTP (Numeric)',
|
|
249
|
+
'totp_setup': 'Setup TOTP',
|
|
250
|
+
'totp_code': 'TOTP Code',
|
|
251
|
+
'totp_time_remaining': 'TOTP Time Remaining',
|
|
252
|
+
'totp_wait_for_new_code': 'Wait for New TOTP',
|
|
253
|
+
};
|
|
254
|
+
if (displayNames[type]) {
|
|
255
|
+
return displayNames[type];
|
|
256
|
+
}
|
|
257
|
+
return type
|
|
258
|
+
.split('_')
|
|
259
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
260
|
+
.join(' ');
|
|
261
|
+
}
|
|
262
|
+
// Get step summary from output
|
|
263
|
+
function getStepSummary(stepType, output) {
|
|
264
|
+
if (!output || typeof output !== 'object')
|
|
265
|
+
return null;
|
|
266
|
+
const out = output;
|
|
267
|
+
return out._summary || null;
|
|
268
|
+
}
|
|
269
|
+
// Check if step is an API request
|
|
270
|
+
function isApiRequestStep(stepType) {
|
|
271
|
+
return ['api_get', 'api_post', 'api_put', 'api_patch', 'api_delete'].includes(stepType);
|
|
272
|
+
}
|
|
273
|
+
// Format step output for display
|
|
274
|
+
function formatStepOutput(output, stepType) {
|
|
275
|
+
if (output === undefined || output === null)
|
|
276
|
+
return '';
|
|
277
|
+
if (stepType === 'logic_log' && typeof output === 'object' && output !== null) {
|
|
278
|
+
const logOutput = output;
|
|
279
|
+
if (logOutput._message) {
|
|
280
|
+
return String(logOutput._message);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
if (typeof output === 'string')
|
|
284
|
+
return output;
|
|
285
|
+
if (typeof output === 'number' || typeof output === 'boolean')
|
|
286
|
+
return String(output);
|
|
287
|
+
if (typeof output === 'object') {
|
|
288
|
+
const filtered = Object.fromEntries(Object.entries(output)
|
|
289
|
+
.filter(([key]) => !key.startsWith('_')));
|
|
290
|
+
if (Object.keys(filtered).length === 0) {
|
|
291
|
+
return '';
|
|
292
|
+
}
|
|
293
|
+
return JSON.stringify(filtered, null, 2);
|
|
294
|
+
}
|
|
295
|
+
return JSON.stringify(output, null, 2);
|
|
296
|
+
}
|
|
208
297
|
function generateHTMLReport(data) {
|
|
209
298
|
const { timestamp, summary, testFiles } = data;
|
|
210
|
-
|
|
211
|
-
|
|
299
|
+
// Separate lifecycle hooks from actual tests for counting
|
|
300
|
+
let actualTests = 0;
|
|
301
|
+
let actualPassed = 0;
|
|
302
|
+
let actualFailed = 0;
|
|
303
|
+
for (const { results } of testFiles) {
|
|
304
|
+
for (const result of results) {
|
|
305
|
+
if (!result.isLifecycle) {
|
|
306
|
+
actualTests++;
|
|
307
|
+
if (result.status === 'passed')
|
|
308
|
+
actualPassed++;
|
|
309
|
+
else
|
|
310
|
+
actualFailed++;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
const passRate = actualTests > 0
|
|
315
|
+
? ((actualPassed / actualTests) * 100).toFixed(1)
|
|
212
316
|
: '0';
|
|
213
317
|
let html = `<!DOCTYPE html>
|
|
214
318
|
<html lang="en">
|
|
@@ -271,17 +375,39 @@ function generateHTMLReport(data) {
|
|
|
271
375
|
align-items: center;
|
|
272
376
|
}
|
|
273
377
|
.test-file-path { font-size: 12px; color: var(--color-text-secondary); font-weight: normal; }
|
|
274
|
-
.
|
|
275
|
-
|
|
378
|
+
.lifecycle-section {
|
|
379
|
+
background: #f1f5f9;
|
|
380
|
+
padding: 8px 16px;
|
|
276
381
|
border-bottom: 1px solid var(--color-border);
|
|
382
|
+
}
|
|
383
|
+
.lifecycle-header {
|
|
384
|
+
font-size: 12px;
|
|
385
|
+
font-weight: 600;
|
|
386
|
+
color: var(--color-text-secondary);
|
|
387
|
+
text-transform: uppercase;
|
|
388
|
+
letter-spacing: 0.5px;
|
|
389
|
+
margin-bottom: 4px;
|
|
390
|
+
}
|
|
391
|
+
.lifecycle-item {
|
|
277
392
|
display: flex;
|
|
278
393
|
align-items: center;
|
|
279
|
-
gap:
|
|
394
|
+
gap: 8px;
|
|
395
|
+
padding: 4px 0;
|
|
396
|
+
font-size: 13px;
|
|
397
|
+
}
|
|
398
|
+
.test-case {
|
|
399
|
+
padding: 12px 16px;
|
|
400
|
+
border-bottom: 1px solid var(--color-border);
|
|
280
401
|
}
|
|
281
402
|
.test-case:last-child { border-bottom: none; }
|
|
282
403
|
.test-case.passed { border-left: 3px solid var(--color-passed); }
|
|
283
404
|
.test-case.failed { border-left: 3px solid var(--color-failed); }
|
|
284
405
|
.test-case.skipped { border-left: 3px solid var(--color-skipped); }
|
|
406
|
+
.test-case-header {
|
|
407
|
+
display: flex;
|
|
408
|
+
align-items: center;
|
|
409
|
+
gap: 12px;
|
|
410
|
+
}
|
|
285
411
|
.status-icon {
|
|
286
412
|
width: 20px;
|
|
287
413
|
height: 20px;
|
|
@@ -296,18 +422,15 @@ function generateHTMLReport(data) {
|
|
|
296
422
|
.status-icon.passed { background: var(--color-passed); }
|
|
297
423
|
.status-icon.failed { background: var(--color-failed); }
|
|
298
424
|
.status-icon.skipped { background: var(--color-skipped); }
|
|
299
|
-
.test-name { flex: 1; }
|
|
425
|
+
.test-name { flex: 1; font-weight: 500; }
|
|
300
426
|
.test-duration { color: var(--color-text-secondary); font-size: 14px; }
|
|
301
|
-
.error-
|
|
427
|
+
.error-message {
|
|
302
428
|
background: #fef2f2;
|
|
303
429
|
border: 1px solid #fecaca;
|
|
304
430
|
border-radius: 4px;
|
|
305
|
-
padding: 12px;
|
|
306
|
-
margin: 8px
|
|
307
|
-
font-family: monospace;
|
|
431
|
+
padding: 8px 12px;
|
|
432
|
+
margin: 8px 0 0 32px;
|
|
308
433
|
font-size: 13px;
|
|
309
|
-
white-space: pre-wrap;
|
|
310
|
-
word-break: break-word;
|
|
311
434
|
color: #991b1b;
|
|
312
435
|
}
|
|
313
436
|
.steps-toggle {
|
|
@@ -321,27 +444,87 @@ function generateHTMLReport(data) {
|
|
|
321
444
|
.steps-toggle:hover { text-decoration: underline; }
|
|
322
445
|
.steps-list {
|
|
323
446
|
display: none;
|
|
324
|
-
|
|
447
|
+
margin: 12px 0 0 32px;
|
|
448
|
+
border-left: 2px solid var(--color-border);
|
|
449
|
+
padding-left: 16px;
|
|
325
450
|
}
|
|
326
451
|
.steps-list.open { display: block; }
|
|
327
452
|
.step {
|
|
453
|
+
padding: 8px 0;
|
|
454
|
+
border-bottom: 1px solid #f1f5f9;
|
|
455
|
+
}
|
|
456
|
+
.step:last-child { border-bottom: none; }
|
|
457
|
+
.step-header {
|
|
328
458
|
display: flex;
|
|
329
459
|
align-items: center;
|
|
330
460
|
gap: 8px;
|
|
331
|
-
padding: 4px 0;
|
|
332
461
|
font-size: 13px;
|
|
333
462
|
}
|
|
334
|
-
.step-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
463
|
+
.step-dot {
|
|
464
|
+
width: 8px;
|
|
465
|
+
height: 8px;
|
|
466
|
+
border-radius: 50%;
|
|
467
|
+
flex-shrink: 0;
|
|
468
|
+
}
|
|
469
|
+
.step-dot.passed { background: var(--color-passed); }
|
|
470
|
+
.step-dot.failed { background: var(--color-failed); }
|
|
471
|
+
.step-type { font-weight: 500; }
|
|
472
|
+
.step-summary { color: var(--color-text-secondary); }
|
|
473
|
+
.step-duration { color: var(--color-text-secondary); font-size: 12px; margin-left: auto; }
|
|
474
|
+
.step-details {
|
|
475
|
+
margin-top: 8px;
|
|
476
|
+
padding: 8px;
|
|
477
|
+
background: #f8fafc;
|
|
478
|
+
border-radius: 4px;
|
|
479
|
+
font-size: 12px;
|
|
480
|
+
}
|
|
481
|
+
.step-error {
|
|
482
|
+
color: #991b1b;
|
|
483
|
+
margin-top: 4px;
|
|
484
|
+
}
|
|
485
|
+
.response-status {
|
|
486
|
+
display: flex;
|
|
487
|
+
align-items: center;
|
|
488
|
+
gap: 8px;
|
|
489
|
+
margin-bottom: 8px;
|
|
490
|
+
}
|
|
491
|
+
.response-label { font-weight: 500; }
|
|
492
|
+
.status-code { font-family: monospace; padding: 2px 6px; border-radius: 4px; }
|
|
493
|
+
.status-code.success { background: #dcfce7; color: #166534; }
|
|
494
|
+
.status-code.client-error { background: #fef3c7; color: #92400e; }
|
|
495
|
+
.status-code.server-error { background: #fef2f2; color: #991b1b; }
|
|
496
|
+
.response-section {
|
|
497
|
+
margin-top: 8px;
|
|
498
|
+
}
|
|
499
|
+
.response-section summary {
|
|
500
|
+
cursor: pointer;
|
|
501
|
+
font-weight: 500;
|
|
502
|
+
font-size: 12px;
|
|
503
|
+
color: var(--color-text-secondary);
|
|
504
|
+
}
|
|
505
|
+
.response-pre {
|
|
506
|
+
background: #1e293b;
|
|
507
|
+
color: #e2e8f0;
|
|
508
|
+
padding: 12px;
|
|
509
|
+
border-radius: 4px;
|
|
510
|
+
overflow-x: auto;
|
|
511
|
+
font-family: monospace;
|
|
512
|
+
font-size: 12px;
|
|
513
|
+
margin-top: 4px;
|
|
514
|
+
white-space: pre-wrap;
|
|
515
|
+
word-break: break-word;
|
|
516
|
+
}
|
|
517
|
+
.stack-trace {
|
|
518
|
+
background: #fef2f2;
|
|
519
|
+
color: #991b1b;
|
|
520
|
+
}
|
|
339
521
|
.screenshot {
|
|
340
522
|
max-width: 100%;
|
|
341
523
|
max-height: 300px;
|
|
342
524
|
border: 1px solid var(--color-border);
|
|
343
525
|
border-radius: 4px;
|
|
344
526
|
margin-top: 8px;
|
|
527
|
+
cursor: pointer;
|
|
345
528
|
}
|
|
346
529
|
</style>
|
|
347
530
|
</head>
|
|
@@ -352,15 +535,15 @@ function generateHTMLReport(data) {
|
|
|
352
535
|
|
|
353
536
|
<div class="summary">
|
|
354
537
|
<div class="summary-card">
|
|
355
|
-
<div class="value">${
|
|
538
|
+
<div class="value">${actualTests}</div>
|
|
356
539
|
<div class="label">Total Tests</div>
|
|
357
540
|
</div>
|
|
358
541
|
<div class="summary-card passed">
|
|
359
|
-
<div class="value">${
|
|
542
|
+
<div class="value">${actualPassed}</div>
|
|
360
543
|
<div class="label">Passed</div>
|
|
361
544
|
</div>
|
|
362
545
|
<div class="summary-card failed">
|
|
363
|
-
<div class="value">${
|
|
546
|
+
<div class="value">${actualFailed}</div>
|
|
364
547
|
<div class="label">Failed</div>
|
|
365
548
|
</div>
|
|
366
549
|
<div class="summary-card">
|
|
@@ -374,8 +557,11 @@ function generateHTMLReport(data) {
|
|
|
374
557
|
</div>
|
|
375
558
|
`;
|
|
376
559
|
for (const { file, testFile, results } of testFiles) {
|
|
377
|
-
|
|
378
|
-
const
|
|
560
|
+
// Separate lifecycle hooks from tests
|
|
561
|
+
const lifecycleResults = results.filter(r => r.isLifecycle);
|
|
562
|
+
const testResults = results.filter(r => !r.isLifecycle);
|
|
563
|
+
const filePassed = testResults.filter(r => r.status === 'passed').length;
|
|
564
|
+
const fileFailed = testResults.filter(r => r.status !== 'passed').length;
|
|
379
565
|
html += `
|
|
380
566
|
<div class="test-file">
|
|
381
567
|
<div class="test-file-header">
|
|
@@ -383,36 +569,131 @@ function generateHTMLReport(data) {
|
|
|
383
569
|
<span class="test-file-path">${escapeHtml(file)} • ${filePassed} passed, ${fileFailed} failed</span>
|
|
384
570
|
</div>
|
|
385
571
|
`;
|
|
386
|
-
|
|
572
|
+
// Render lifecycle hooks (beforeAll)
|
|
573
|
+
const beforeAllHooks = lifecycleResults.filter(r => r.lifecycleType === 'beforeAll');
|
|
574
|
+
if (beforeAllHooks.length > 0) {
|
|
575
|
+
html += ` <div class="lifecycle-section">\n`;
|
|
576
|
+
html += ` <div class="lifecycle-header">Before All</div>\n`;
|
|
577
|
+
for (const hook of beforeAllHooks) {
|
|
578
|
+
const icon = hook.status === 'passed' ? '✓' : '✗';
|
|
579
|
+
const iconClass = hook.status === 'passed' ? 'passed' : 'failed';
|
|
580
|
+
html += ` <div class="lifecycle-item">
|
|
581
|
+
<span class="step-dot ${iconClass}"></span>
|
|
582
|
+
<span>${hook.steps.length} steps</span>
|
|
583
|
+
<span style="color: var(--color-text-secondary); font-size: 12px;">${hook.duration}ms</span>
|
|
584
|
+
</div>\n`;
|
|
585
|
+
if (hook.error) {
|
|
586
|
+
html += ` <div class="error-message" style="margin: 4px 0 0 16px;">${escapeHtml(hook.error.message)}</div>\n`;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
html += ` </div>\n`;
|
|
590
|
+
}
|
|
591
|
+
// Render actual tests
|
|
592
|
+
for (const result of testResults) {
|
|
387
593
|
const statusIcon = result.status === 'passed' ? '✓' : result.status === 'failed' ? '✗' : '○';
|
|
594
|
+
const testId = `test-${Math.random().toString(36).substr(2, 9)}`;
|
|
388
595
|
html += `
|
|
389
596
|
<div class="test-case ${result.status}">
|
|
390
|
-
<div class="
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
${result.steps.length} steps
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
597
|
+
<div class="test-case-header">
|
|
598
|
+
<div class="status-icon ${result.status}">${statusIcon}</div>
|
|
599
|
+
<div class="test-name">${escapeHtml(result.testName)}</div>
|
|
600
|
+
<div class="test-duration">${result.duration}ms</div>
|
|
601
|
+
<button class="steps-toggle" onclick="document.getElementById('${testId}').classList.toggle('open'); this.textContent = this.textContent.includes('▶') ? '▼ ${result.steps.length} steps' : '▶ ${result.steps.length} steps'">
|
|
602
|
+
▶ ${result.steps.length} steps
|
|
603
|
+
</button>
|
|
604
|
+
</div>
|
|
398
605
|
`;
|
|
606
|
+
if (result.error) {
|
|
607
|
+
html += ` <div class="error-message">${escapeHtml(result.error.message)}</div>\n`;
|
|
608
|
+
}
|
|
609
|
+
html += ` <div class="steps-list" id="${testId}">\n`;
|
|
399
610
|
for (const step of result.steps) {
|
|
400
|
-
const
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
<
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
611
|
+
const summary = getStepSummary(step.stepType, step.output);
|
|
612
|
+
const isApiRequest = isApiRequestStep(step.stepType);
|
|
613
|
+
const response = step.output;
|
|
614
|
+
html += ` <div class="step">
|
|
615
|
+
<div class="step-header">
|
|
616
|
+
<span class="step-dot ${step.status}"></span>
|
|
617
|
+
<span class="step-type">${escapeHtml(formatStepType(step.stepType))}</span>
|
|
618
|
+
${summary ? `<span class="step-summary">${escapeHtml(summary)}</span>` : ''}
|
|
619
|
+
<span class="step-duration">${step.duration}ms</span>
|
|
620
|
+
</div>
|
|
407
621
|
`;
|
|
622
|
+
// Show step error
|
|
623
|
+
if (step.error) {
|
|
624
|
+
html += ` <div class="step-error">${escapeHtml(step.error.message)}</div>\n`;
|
|
625
|
+
}
|
|
626
|
+
// Show API response details
|
|
627
|
+
if (isApiRequest && response) {
|
|
628
|
+
html += ` <div class="step-details">\n`;
|
|
629
|
+
if (response.status) {
|
|
630
|
+
const statusClass = response.status >= 200 && response.status < 300 ? 'success'
|
|
631
|
+
: response.status >= 400 && response.status < 500 ? 'client-error'
|
|
632
|
+
: response.status >= 500 ? 'server-error' : '';
|
|
633
|
+
html += ` <div class="response-status">
|
|
634
|
+
<span class="response-label">Status:</span>
|
|
635
|
+
<span class="status-code ${statusClass}">${response.status}</span>
|
|
636
|
+
</div>\n`;
|
|
637
|
+
}
|
|
638
|
+
if (response.headers && Object.keys(response.headers).length > 0) {
|
|
639
|
+
html += ` <details class="response-section">
|
|
640
|
+
<summary>Headers</summary>
|
|
641
|
+
<pre class="response-pre">${escapeHtml(JSON.stringify(response.headers, null, 2))}</pre>
|
|
642
|
+
</details>\n`;
|
|
643
|
+
}
|
|
644
|
+
if (response.body !== undefined) {
|
|
645
|
+
const bodyStr = typeof response.body === 'string'
|
|
646
|
+
? response.body
|
|
647
|
+
: JSON.stringify(response.body, null, 2);
|
|
648
|
+
html += ` <details class="response-section">
|
|
649
|
+
<summary>Body</summary>
|
|
650
|
+
<pre class="response-pre">${escapeHtml(bodyStr)}</pre>
|
|
651
|
+
</details>\n`;
|
|
652
|
+
}
|
|
653
|
+
html += ` </div>\n`;
|
|
654
|
+
}
|
|
655
|
+
else if (step.output !== undefined && step.output !== null && !step.screenshot) {
|
|
656
|
+
// Show generic output for non-API steps
|
|
657
|
+
const outputStr = formatStepOutput(step.output, step.stepType);
|
|
658
|
+
if (outputStr) {
|
|
659
|
+
html += ` <div class="step-details">
|
|
660
|
+
<pre class="response-pre">${escapeHtml(outputStr)}</pre>
|
|
661
|
+
</div>\n`;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
// Show stack trace for errors
|
|
665
|
+
if (step.error?.stack) {
|
|
666
|
+
html += ` <details class="response-section">
|
|
667
|
+
<summary>Stack Trace</summary>
|
|
668
|
+
<pre class="response-pre stack-trace">${escapeHtml(step.error.stack)}</pre>
|
|
669
|
+
</details>\n`;
|
|
670
|
+
}
|
|
671
|
+
// Show screenshot
|
|
408
672
|
if (step.screenshot) {
|
|
409
|
-
html += `
|
|
673
|
+
html += ` <img class="screenshot" src="${step.screenshot}" alt="Screenshot at failure" onclick="window.open(this.src, '_blank')">\n`;
|
|
410
674
|
}
|
|
675
|
+
html += ` </div>\n`;
|
|
411
676
|
}
|
|
677
|
+
html += ` </div>\n`;
|
|
412
678
|
html += ` </div>\n`;
|
|
413
|
-
|
|
414
|
-
|
|
679
|
+
}
|
|
680
|
+
// Render lifecycle hooks (afterAll)
|
|
681
|
+
const afterAllHooks = lifecycleResults.filter(r => r.lifecycleType === 'afterAll');
|
|
682
|
+
if (afterAllHooks.length > 0) {
|
|
683
|
+
html += ` <div class="lifecycle-section">\n`;
|
|
684
|
+
html += ` <div class="lifecycle-header">After All</div>\n`;
|
|
685
|
+
for (const hook of afterAllHooks) {
|
|
686
|
+
const iconClass = hook.status === 'passed' ? 'passed' : 'failed';
|
|
687
|
+
html += ` <div class="lifecycle-item">
|
|
688
|
+
<span class="step-dot ${iconClass}"></span>
|
|
689
|
+
<span>${hook.steps.length} steps</span>
|
|
690
|
+
<span style="color: var(--color-text-secondary); font-size: 12px;">${hook.duration}ms</span>
|
|
691
|
+
</div>\n`;
|
|
692
|
+
if (hook.error) {
|
|
693
|
+
html += ` <div class="error-message" style="margin: 4px 0 0 16px;">${escapeHtml(hook.error.message)}</div>\n`;
|
|
694
|
+
}
|
|
415
695
|
}
|
|
696
|
+
html += ` </div>\n`;
|
|
416
697
|
}
|
|
417
698
|
html += ` </div>\n`;
|
|
418
699
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
:root{--ts-blue: #205d96;--ts-blue-light: #e0f2fe;--ts-blue-dark: #1a4a78;--ts-green: #9fc93c;--ts-green-light: #ecfccb;--ts-green-dark: #5c7a1f;--ts-gray: #59575d;--ts-gray-light: #6b7280;--ts-gray-bg: #f8fafc;--primary-color: var(--ts-blue);--primary-dark: var(--ts-blue-dark);--primary-light: var(--ts-blue-light);--secondary-color: var(--ts-gray);--background: var(--ts-gray-bg);--surface: #ffffff;--error: #d32f2f;--success: var(--ts-green-dark);--success-light: var(--ts-green-light);--warning: #e67e00;--text-primary: var(--ts-gray);--text-secondary: var(--ts-gray-light);--border-color: #e0e0e0}*{margin:0;padding:0;box-sizing:border-box}body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,sans-serif;background-color:var(--background);color:var(--text-primary)}.app{display:flex;flex-direction:column;height:100vh;overflow:hidden}.header{display:flex;align-items:center;justify-content:space-between;padding:0 16px;height:56px;background:var(--primary-color);color:#fff;box-shadow:0 2px 4px #0000001a;z-index:100}.header h1{font-size:20px;font-weight:500;display:flex;align-items:center;gap:8px}.header-actions{display:flex;gap:8px}.btn{display:inline-flex;align-items:center;gap:6px;padding:8px 16px;border:none;border-radius:4px;font-size:14px;font-weight:500;cursor:pointer;transition:background-color .2s,opacity .2s}.btn:disabled{opacity:.6;cursor:not-allowed}.btn-primary{background:#fff;color:var(--primary-color)}.btn-primary:hover:not(:disabled){background:#ffffffe6}.btn-secondary{background:#ffffff26;color:#fff}.btn-secondary:hover:not(:disabled){background:#ffffff40}.btn-success{background:var(--ts-green);color:#fff}.btn-success:hover:not(:disabled){background:var(--ts-green-dark)}.btn-danger{background:var(--error);color:#fff}.btn-danger:hover:not(:disabled){background:#c62828}.main-content{display:flex;flex:1;overflow:hidden}.sidebar{width:280px;background:var(--surface);border-right:1px solid var(--border-color);display:flex;flex-direction:column;overflow:hidden;transition:width .2s ease}.sidebar.collapsed{width:40px}.sidebar-toggle-header{display:flex;align-items:center;justify-content:flex-end;padding:8px;border-bottom:1px solid var(--border-color)}.sidebar.collapsed .sidebar-toggle-header{justify-content:center}.sidebar-header{padding:16px;border-bottom:1px solid var(--border-color)}.sidebar-header h2{font-size:14px;font-weight:600;color:var(--text-secondary);text-transform:uppercase;letter-spacing:.5px}.test-list{flex:1;overflow-y:auto;padding:8px}.test-item{padding:12px;border-radius:4px;cursor:pointer;border:1px solid transparent;margin-bottom:4px;transition:background-color .2s}.test-item:hover{background:var(--background)}.test-item.active{background:#1976d214;border-color:var(--primary-color)}.test-item-name{font-weight:500;font-size:14px;margin-bottom:4px}.test-item-steps{font-size:12px;color:var(--text-secondary)}.add-test-btn{margin:8px;padding:12px;border:2px dashed var(--border-color);border-radius:4px;background:transparent;color:var(--text-secondary);cursor:pointer;font-size:14px;transition:border-color .2s,color .2s}.add-test-btn:hover{border-color:var(--primary-color);color:var(--primary-color)}.editor-area{flex:1;display:flex;flex-direction:column;overflow:hidden}.editor-toolbar{display:flex;align-items:center;justify-content:space-between;padding:8px 16px;background:var(--surface);border-bottom:1px solid var(--border-color)}.test-name-input{font-size:16px;font-weight:500;padding:8px 12px;border:1px solid var(--border-color);border-radius:4px;width:300px}.test-name-input:focus{outline:none;border-color:var(--primary-color)}.blockly-container{flex:1;position:relative}#blockly-div{position:absolute;top:0;left:0;right:0;bottom:0}.blocklyTreeSeparator{height:1px!important;margin:12px 8px!important;background-color:#0006!important;border:none!important}.blocklyWidgetDiv .blocklyHtmlInput{max-width:350px;font-family:monospace;font-size:12px;line-height:1.4}.results-panel{width:350px;background:var(--surface);border-left:1px solid var(--border-color);display:flex;flex-direction:column;overflow:hidden;transition:width .2s ease}.results-panel.collapsed{width:40px}.results-header{display:flex;align-items:center;justify-content:space-between;padding:12px 16px;border-bottom:1px solid var(--border-color);gap:12px}.results-panel.collapsed .results-header{justify-content:center;padding:12px 8px}.panel-toggle-btn{background:none;border:none;color:var(--text-secondary);cursor:pointer;font-size:12px;padding:4px 8px;border-radius:4px;flex-shrink:0}.panel-toggle-btn:hover{background:var(--hover-bg);color:var(--text-primary)}.results-header h2{font-size:14px;font-weight:600;color:var(--text-secondary);text-transform:uppercase;letter-spacing:.5px;flex:1}.results-actions{display:flex;gap:8px}.btn-report{padding:4px 8px;font-size:11px;font-weight:500;border:1px solid var(--border-color);border-radius:4px;background:var(--surface);color:var(--text-secondary);cursor:pointer;transition:all .2s}.btn-report:hover{background:var(--primary-light);border-color:var(--primary-color);color:var(--primary-color)}.results-content{flex:1;overflow-y:auto;padding:16px}.result-item{padding:12px;border-radius:4px;margin-bottom:8px;border-left:4px solid}.result-item.passed{background:#388e3c14;border-left-color:var(--success)}.result-item.failed{background:#d32f2f14;border-left-color:var(--error)}.result-item.running{background:#1976d214;border-left-color:var(--primary-color)}.result-step{font-size:13px;margin-bottom:4px}.result-error{font-size:12px;color:var(--error);font-family:monospace;white-space:pre-wrap;margin-top:8px}.result-duration{font-size:11px;color:var(--text-secondary)}.modal-overlay{position:fixed;top:0;left:0;right:0;bottom:0;background:#00000080;display:flex;align-items:center;justify-content:center;z-index:1000}.modal{background:var(--surface);border-radius:8px;padding:24px;min-width:400px;max-width:600px;max-height:80vh;overflow-y:auto;box-shadow:0 4px 20px #00000026}.modal h2{margin-bottom:16px}.modal-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:24px}.folder-hooks-modal{background:var(--surface);border-radius:8px;width:90vw;max-width:1200px;height:80vh;display:flex;flex-direction:column;box-shadow:0 4px 20px #00000026}.folder-hooks-header{padding:16px 24px;border-bottom:1px solid var(--border-color);position:relative}.folder-hooks-header h2{margin:0 0 4px;font-size:18px}.folder-hooks-description{font-size:13px;color:var(--text-secondary);margin:0}.folder-hooks-tabs{display:flex;border-bottom:1px solid var(--border-color);padding:0 16px;background:var(--background)}.folder-hooks-tab{padding:12px 16px;border:none;background:transparent;color:var(--text-secondary);font-size:13px;font-weight:500;cursor:pointer;border-bottom:2px solid transparent;display:flex;align-items:center;gap:8px}.folder-hooks-tab:hover{color:var(--text-primary)}.folder-hooks-tab.active{color:var(--primary-color);border-bottom-color:var(--primary-color)}.folder-hooks-tab .tab-badge{font-size:10px;padding:2px 6px;border-radius:10px;background:var(--primary-color);color:#fff}.folder-hooks-workspace{flex:1;min-height:0}.folder-hooks-footer{padding:16px 24px;border-top:1px solid var(--border-color);display:flex;justify-content:flex-end;gap:8px}.form-group{margin-bottom:16px}.form-group label{display:block;font-size:14px;font-weight:500;margin-bottom:6px}.form-group input,.form-group textarea{width:100%;padding:10px 12px;border:1px solid var(--border-color);border-radius:4px;font-size:14px}.form-group input:focus,.form-group textarea:focus{outline:none;border-color:var(--primary-color)}.empty-state{display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;color:var(--text-secondary);text-align:center;padding:40px}.empty-state h3{margin-bottom:8px;color:var(--text-primary)}::-webkit-scrollbar{width:8px;height:8px}::-webkit-scrollbar-track{background:var(--background)}::-webkit-scrollbar-thumb{background:#bdbdbd;border-radius:4px}::-webkit-scrollbar-thumb:hover{background:#9e9e9e}.status-indicator{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:6px}.status-indicator.running{background:var(--primary-color);animation:pulse 1s infinite}.status-indicator.passed{background:var(--success)}.status-indicator.failed{background:var(--error)}.sidebar-section{border-bottom:1px solid var(--border-color)}.sidebar-section:last-child{border-bottom:none;flex:1;display:flex;flex-direction:column;overflow:hidden}.sidebar-header.clickable{cursor:pointer;-webkit-user-select:none;user-select:none}.sidebar-header.clickable:hover{background:var(--background)}.variables-list{padding:8px}.variable-item{display:flex;align-items:center;gap:8px;padding:6px 0}.variable-name{font-family:monospace;font-size:12px;color:var(--primary-color);min-width:80px;flex-shrink:0}.variable-value{flex:1;padding:4px 8px;border:1px solid var(--border-color);border-radius:4px;font-size:12px}.variable-value:focus{outline:none;border-color:var(--primary-color)}.global-variables{background:linear-gradient(to right,rgba(32,93,150,.05),transparent)}.variable-item.global{padding:4px 0}.variable-item.global .variable-name{color:var(--ts-blue);font-size:11px;word-break:break-all}.variable-value.readonly{background:var(--background);color:var(--text-secondary);font-family:monospace;font-size:11px;padding:2px 6px;border:none;border-radius:3px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.variable-value.global-input{flex:1;padding:4px 6px;border:1px solid rgba(32,93,150,.3);border-radius:4px;font-size:11px;font-family:monospace;background:#205d960d;color:var(--text-primary)}.variable-value.global-input:focus{outline:none;border-color:var(--ts-blue);background:#205d961a}.variable-group{margin-bottom:8px}.variable-group-header{font-weight:600;font-size:11px;color:var(--text-secondary);text-transform:uppercase;padding:4px 0;border-bottom:1px solid var(--border-color);margin-bottom:4px}.variable-group-content{padding-left:12px;border-left:2px solid rgba(32,93,150,.3)}.global-badge{font-size:12px;margin-left:8px;opacity:.7}.btn-icon{background:none;border:none;color:var(--text-secondary);cursor:pointer;padding:4px 8px;font-size:16px;line-height:1;border-radius:4px}.btn-icon:hover{background:var(--background);color:var(--error)}.add-variable-btn{width:100%;margin-top:8px;padding:8px;border:1px dashed var(--border-color);border-radius:4px;background:transparent;color:var(--text-secondary);cursor:pointer;font-size:12px}.add-variable-btn:hover{border-color:var(--primary-color);color:var(--primary-color)}.test-item{display:flex;align-items:center;justify-content:space-between}.test-item-content{flex:1;min-width:0}.test-item-name{display:flex;align-items:center;gap:6px}.data-driven-badge{font-size:10px;font-weight:600;background:#1565c0;color:#fff;padding:1px 5px;border-radius:8px;margin-left:4px}.btn-run-test{background:none;border:1px solid var(--border-color);border-radius:4px;padding:4px 8px;cursor:pointer;font-size:10px;color:var(--text-secondary);flex-shrink:0;margin-left:8px}.btn-run-test:hover:not(:disabled){background:var(--primary-color);border-color:var(--primary-color);color:#fff}.btn-run-test:disabled{opacity:.5;cursor:not-allowed}.status-dot{display:inline-block;width:8px;height:8px;border-radius:50%;flex-shrink:0}.status-dot.passed{background:var(--success)}.status-dot.failed{background:var(--error)}.test-item.passed{background:#388e3c0d}.test-item.failed{background:#d32f2f0d}.test-item.passed.active{background:#388e3c1f}.test-item.failed.active{background:#d32f2f1f}.test-id-indicator{display:flex;align-items:center;gap:6px;color:#ffffffe6;font-size:13px;padding:6px 12px;border-radius:4px;background:#ffffff1a}.test-id-indicator code{font-family:Monaco,Menlo,monospace;background:#fff3;padding:2px 6px;border-radius:3px;font-size:12px}.headless-toggle{display:flex;align-items:center;gap:6px;color:#fff;font-size:14px;cursor:pointer;padding:8px 12px;border-radius:4px;background:#ffffff1a}.headless-toggle:hover{background:#fff3}.headless-toggle input[type=checkbox]{width:16px;height:16px;cursor:pointer}.create-block-modal{min-width:500px;max-width:600px}.create-block-modal h2{color:var(--text-primary);margin-bottom:20px}.helper-text{font-size:12px;color:var(--text-secondary);margin-bottom:8px}.color-picker{display:flex;gap:8px}.color-option{width:32px;height:32px;border-radius:4px;border:2px solid transparent;cursor:pointer;transition:transform .1s,border-color .1s}.color-option:hover{transform:scale(1.1)}.color-option.selected{border-color:var(--text-primary)}.param-list{max-height:200px;overflow-y:auto;border:1px solid var(--border-color);border-radius:4px;padding:8px}.empty-params{color:var(--text-secondary);font-size:13px;padding:12px;text-align:center}.param-item{display:flex;align-items:center;gap:8px;padding:6px 8px;border-radius:4px;cursor:pointer;font-size:13px}.param-item:hover{background:var(--background)}.param-item input[type=checkbox]{flex-shrink:0}.param-name{font-weight:500;color:var(--primary-color)}.param-source{color:var(--text-secondary);font-size:11px}.param-default{color:var(--text-secondary);font-size:11px;margin-left:auto;font-family:monospace}.steps-preview{max-height:150px;overflow-y:auto;border:1px solid var(--border-color);border-radius:4px;padding:8px}.step-preview-item{display:flex;align-items:center;gap:8px;padding:4px 8px;font-size:13px}.step-number{color:var(--text-secondary);font-size:11px;width:20px}.step-type{color:var(--text-primary)}.create-block-modal .btn-primary{background:var(--primary-color);color:#fff}.create-block-modal .btn-primary:hover{background:var(--primary-dark)}.create-block-modal .btn-secondary{background:var(--background);color:var(--text-primary)}.result-test-header{display:flex;align-items:center;gap:8px;margin-bottom:8px}.result-test-name{font-weight:500;flex:1}.result-steps{margin-top:8px;border-top:1px solid var(--border-color);padding-top:8px}.step-result-item{margin-bottom:6px;border-radius:4px;background:#00000005}.step-result-item.passed{border-left:3px solid var(--success)}.step-result-item.failed{border-left:3px solid var(--error)}.step-result-header{display:flex;align-items:center;gap:8px;padding:6px 8px;cursor:pointer;-webkit-user-select:none;user-select:none}.step-result-header:hover{background:#0000000a}.step-result-type{font-size:12px;color:var(--text-primary);white-space:nowrap}.step-result-summary{flex:1;font-size:11px;color:var(--text-secondary);font-family:monospace;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:300px}.step-result-duration{font-size:11px;color:var(--text-secondary);white-space:nowrap}.step-result-expand{font-size:10px;color:var(--text-secondary);width:16px;text-align:center}.step-result-header.expandable{cursor:pointer}.step-result-header.expandable:hover{background:#0000000a}.step-result-details{padding:8px;background:var(--background);border-radius:0 0 4px 4px}.step-output{background:var(--surface);border-radius:4px;padding:8px}.step-result-error{padding:6px 8px;font-size:11px;color:var(--error);background:#d32f2f0d;font-family:monospace}.step-screenshot{margin-bottom:12px}.screenshot-label{font-size:11px;font-weight:600;color:var(--text-secondary);margin-bottom:6px;text-transform:uppercase;letter-spacing:.5px}.failure-screenshot{max-width:100%;max-height:300px;border:2px solid var(--error);border-radius:6px;cursor:pointer;transition:transform .2s,box-shadow .2s;display:block}.failure-screenshot:hover{transform:scale(1.02);box-shadow:0 4px 12px #d32f2f4d}.stack-trace{font-size:10px;line-height:1.4;color:var(--error);max-height:200px;overflow-y:auto}.step-result-response{padding:8px;background:var(--background);border-radius:0 0 4px 4px}.response-status{display:flex;align-items:center;gap:8px;margin-bottom:8px}.response-label{font-size:11px;color:var(--text-secondary)}.response-status-code{font-family:monospace;font-size:12px;font-weight:600;padding:2px 6px;border-radius:3px;background:var(--surface)}.response-status-code.status-success{color:var(--success);background:#388e3c1a}.response-status-code.status-client-error{color:var(--warning);background:#f57c001a}.response-status-code.status-server-error{color:var(--error);background:#d32f2f1a}.response-section{margin-top:8px}.response-section summary{font-size:11px;color:var(--text-secondary);cursor:pointer;padding:4px 0;-webkit-user-select:none;user-select:none}.response-section summary:hover{color:var(--primary-color)}.response-pre{margin:4px 0 0;padding:8px;background:var(--surface);border:1px solid var(--border-color);border-radius:4px;font-size:11px;font-family:monospace;overflow-x:auto;max-height:200px;overflow-y:auto;white-space:pre-wrap;word-break:break-all}.lifecycle-badge{display:inline-block;font-size:10px;font-weight:600;text-transform:uppercase;padding:2px 6px;border-radius:3px;background:var(--ts-gray);color:#fff;letter-spacing:.5px}.result-item.lifecycle,.result-item.lifecycle.passed{border-left-color:var(--ts-gray);background:#59575d14}.result-item.lifecycle.failed{border-left-color:#d32f2f;background:#d32f2f14}.editor-tabs{display:flex;background:var(--surface);border-bottom:1px solid var(--border-color);padding:0 8px}.editor-tab{padding:10px 16px;border:none;background:transparent;color:var(--text-secondary);font-size:13px;font-weight:500;cursor:pointer;border-bottom:2px solid transparent;transition:all .2s;display:flex;align-items:center;gap:6px}.editor-tab:hover{color:var(--text-primary);background:var(--background)}.editor-tab.active{color:var(--primary-color);border-bottom-color:var(--primary-color)}.editor-tab.test-tab{margin:0 auto 0 0;font-weight:600}.editor-tab.test-tab.active{color:var(--primary-color)}.tab-badge{display:inline-flex;align-items:center;justify-content:center;min-width:18px;height:18px;padding:0 5px;font-size:11px;font-weight:600;background:var(--ts-gray);color:#fff;border-radius:9px}.editor-tab.active .tab-badge{background:var(--primary-color)}.lifecycle-toolbar-info{display:flex;align-items:center;gap:8px;color:var(--text-primary);font-size:14px;font-weight:500}.lifecycle-icon{font-size:16px}.lifecycle-hint{color:var(--text-secondary);font-weight:400;font-size:13px}.sidebar-tabs{display:flex;border-bottom:1px solid var(--border-color)}.sidebar-tab{flex:1;padding:10px;border:none;background:transparent;color:var(--text-secondary);font-size:13px;font-weight:500;cursor:pointer;border-bottom:2px solid transparent;transition:all .2s}.sidebar-tab:hover{color:var(--text-primary);background:var(--background)}.sidebar-tab.active{color:var(--primary-color);border-bottom-color:var(--primary-color)}.file-tree-section,.file-tree{flex:1;overflow:hidden;display:flex;flex-direction:column}.file-tree-header{display:flex;align-items:center;justify-content:space-between;padding:8px 12px;border-bottom:1px solid var(--border-color);background:var(--background)}.file-tree-root-name{font-weight:600;font-size:13px;color:var(--text-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.file-tree-refresh{background:none;border:none;color:var(--text-secondary);cursor:pointer;font-size:16px;padding:4px;border-radius:4px}.file-tree-refresh:hover{background:var(--surface);color:var(--primary-color)}.file-tree-content{flex:1;overflow-y:auto;padding:4px 0}.file-tree-empty{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:32px 16px;color:var(--text-secondary);text-align:center}.file-tree-empty p{margin:4px 0;font-size:13px}.file-tree-hint{font-size:12px;opacity:.7}.file-tree-item{display:flex;align-items:center;gap:6px;padding:6px 8px;cursor:pointer;font-size:13px;color:var(--text-primary);border-left:2px solid transparent;transition:all .15s}.file-tree-item:hover{background:var(--background)}.file-tree-item.selected{background:#1976d214;border-left-color:var(--primary-color)}.file-tree-item.folder{color:var(--text-secondary)}.file-tree-icon{font-size:14px;flex-shrink:0}.file-tree-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}.file-tree-badge{font-size:10px;font-weight:600;padding:2px 6px;border-radius:10px;background:var(--primary-color);color:#fff;flex-shrink:0}.folder-hooks-btn{background:none;border:none;color:var(--text-secondary);cursor:pointer;font-size:12px;padding:2px 4px;opacity:0;transition:opacity .2s;flex-shrink:0}.file-tree-item.folder:hover .folder-hooks-btn{opacity:1}.folder-hooks-btn:hover{color:var(--primary-color)}.folder-hooks-indicator{font-size:10px;color:var(--primary-color);margin-left:4px;flex-shrink:0}.header-file-path{margin-left:12px;font-size:12px;font-weight:400;color:#ffffffb3;max-width:400px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:inline-flex;align-items:center;gap:8px}.auto-save-indicator{font-size:11px;padding:2px 8px;border-radius:4px;flex-shrink:0}.auto-save-indicator.saving{background:#ffc10733;color:#ffc107;animation:pulse 1s ease-in-out infinite}.auto-save-indicator.saved{background:#4caf5033;color:#4caf50}@keyframes pulse{0%,to{opacity:1}50%{opacity:.5}}.folder-hooks-label{background:rgba(var(--ts-blue),.1);background:#e0f2fe;color:var(--ts-blue);padding:2px 8px;border-radius:4px;font-size:11px;font-weight:500;margin-right:6px}.help-modal{width:950px;max-width:95vw;height:85vh;max-height:85vh;display:flex;flex-direction:column;padding:0;overflow:hidden}.help-header{display:flex;align-items:center;justify-content:space-between;padding:16px 24px;border-bottom:1px solid var(--border-color);flex-shrink:0}.help-header h2{margin:0;font-size:20px;font-weight:500}.btn-close{background:none;border:none;font-size:24px;cursor:pointer;color:var(--text-secondary);padding:4px 8px;border-radius:4px;line-height:1}.btn-close:hover{background:var(--background);color:var(--text-primary)}.help-layout{display:flex;flex:1;overflow:hidden}.help-nav{width:200px;flex-shrink:0;background:var(--background);border-right:1px solid var(--border-color);padding:12px 0;overflow-y:auto}.help-nav-item{display:block;width:100%;padding:10px 20px;border:none;background:none;text-align:left;cursor:pointer;font-size:14px;color:var(--text-secondary);transition:background .2s,color .2s}.help-nav-item:hover{background:#0000000a;color:var(--text-primary)}.help-nav-item.active{background:#1976d21a;color:var(--primary-color);font-weight:500;border-left:3px solid var(--primary-color);padding-left:17px}.help-content{flex:1;overflow-y:auto;padding:24px 32px}.help-content h3{margin:0 0 8px;font-size:22px;font-weight:600;color:var(--text-primary)}.help-content>p{margin:0 0 24px;color:var(--text-secondary);font-size:15px;line-height:1.5}.help-feature{margin-bottom:24px;padding:16px 20px;background:var(--background);border-radius:8px}.help-feature h4{margin:0 0 12px;font-size:15px;font-weight:600;color:var(--text-primary)}.help-feature ol,.help-feature ul{margin:0;padding-left:20px}.help-feature li{margin-bottom:8px;line-height:1.5;color:var(--text-primary)}.help-feature li:last-child{margin-bottom:0}.help-feature p{margin:0 0 12px;line-height:1.5}.help-feature code{background:#0000000f;padding:2px 6px;border-radius:4px;font-family:SF Mono,Monaco,Courier New,monospace;font-size:13px;color:var(--primary-dark)}.help-tip{margin-bottom:24px;padding:14px 18px;background:#1976d214;border-left:3px solid var(--primary-color);border-radius:0 8px 8px 0;font-size:14px;line-height:1.5}.help-tip code{background:#0000000f;padding:2px 6px;border-radius:4px;font-family:SF Mono,Monaco,Courier New,monospace;font-size:13px}.help-code{margin:12px 0 0;padding:12px 16px;background:#1e1e1e;color:#d4d4d4;border-radius:6px;font-family:SF Mono,Monaco,Courier New,monospace;font-size:13px;line-height:1.5;overflow-x:auto;white-space:pre}.scan-project-btn{margin-top:8px}.scan-project-btn:disabled{opacity:.6;cursor:not-allowed}.matches-section{margin-top:12px}.matches-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;font-size:13px;color:var(--text-secondary)}.matches-actions{display:flex;gap:12px}.btn-link{background:none;border:none;color:var(--primary-color);cursor:pointer;font-size:12px;padding:0}.btn-link:hover{text-decoration:underline}.matches-list{max-height:200px;overflow-y:auto;border:1px solid var(--border-color);border-radius:6px;background:var(--surface)}.match-file-group{border-bottom:1px solid var(--border-color)}.match-file-group:last-child{border-bottom:none}.match-file-name{font-size:12px;font-weight:600;color:var(--text-primary);padding:8px 12px 4px;background:var(--background)}.match-item{display:flex;align-items:center;gap:8px;padding:6px 12px;cursor:pointer;transition:background .15s}.match-item:hover{background:#00000008}.match-item input[type=checkbox]{flex-shrink:0}.match-location{font-size:13px;color:var(--text-primary);flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.match-detail{font-size:12px;color:var(--text-secondary);margin-left:4px}.record-dialog{min-width:500px;max-width:600px}.record-dialog .modal-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;padding-bottom:12px;border-bottom:1px solid var(--border-color)}.record-dialog .modal-header h2{margin:0;font-size:18px}.record-dialog .modal-body{min-height:150px}.record-url-input{display:flex;flex-direction:column;gap:12px}.record-url-input p{margin:0;color:var(--text-primary)}.url-input{width:100%;padding:10px 12px;border:1px solid var(--border-color);border-radius:4px;font-size:14px}.url-input:focus{outline:none;border-color:var(--primary-color)}.record-hint{font-size:13px;color:var(--text-secondary);margin-top:4px}.record-status{display:flex;align-items:center;gap:16px;padding:20px;background:var(--background);border-radius:8px;margin:16px 0}.record-status.recording{border-left:4px solid var(--error);background:#d32f2f0d}.record-status.processing{border-left:4px solid var(--warning);background:#f57c000d;justify-content:center}.recording-indicator{width:16px;height:16px;background:var(--error);border-radius:50%;animation:pulse 1.5s ease-in-out infinite;flex-shrink:0}@keyframes pulse{0%,to{opacity:1;transform:scale(1)}50%{opacity:.6;transform:scale(.9)}}.recording-info h3{margin:0 0 8px;font-size:16px;color:var(--text-primary)}.recording-info p{margin:0;font-size:13px;color:var(--text-secondary)}.processing-spinner{width:24px;height:24px;border:3px solid var(--border-color);border-top-color:var(--primary-color);border-radius:50%;animation:spin .8s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.record-preview h3{margin:0 0 12px;font-size:15px}.steps-preview{max-height:300px;overflow-y:auto;border:1px solid var(--border-color);border-radius:6px;background:var(--surface)}.step-preview-item{display:flex;align-items:center;gap:10px;padding:10px 12px;border-bottom:1px solid var(--border-color);font-size:13px}.step-preview-item:last-child{border-bottom:none}.step-number{font-weight:600;color:var(--text-secondary);min-width:24px}.step-description{color:var(--text-primary);font-family:Monaco,Menlo,Ubuntu Mono,monospace;font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.record-empty{text-align:center;padding:24px;color:var(--text-secondary)}.record-empty p{margin:0 0 8px}.record-error{color:var(--error);font-size:13px;padding:8px 12px;background:#d32f2f1a;border-radius:4px}.record-error-state{text-align:center;padding:20px}.record-error-state h3{margin:0 0 12px;color:var(--error)}.record-error-state .record-error{margin-bottom:16px}.btn-warning{background:var(--warning);color:#fff}.btn-warning:hover:not(:disabled){background:#e65100}.advanced-options{margin-top:16px;border-top:1px solid var(--border-color);padding-top:12px}.advanced-toggle{display:flex;align-items:center;gap:8px;background:none;border:none;padding:8px 0;font-size:13px;color:var(--text-secondary);cursor:pointer;width:100%;text-align:left}.advanced-toggle:hover{color:var(--text-primary)}.advanced-toggle .toggle-icon{font-size:10px;width:12px}.advanced-content{padding:12px 0 4px 20px}.option-row{display:flex;align-items:center;gap:12px;margin-bottom:8px}.option-row label{font-size:13px;color:var(--text-primary);white-space:nowrap;min-width:120px}.option-input{flex:1;padding:6px 10px;border:1px solid var(--border-color);border-radius:4px;font-size:13px;font-family:Monaco,Menlo,Ubuntu Mono,monospace}.option-input:focus{outline:none;border-color:var(--primary-color)}.option-hint{font-size:12px;color:var(--text-secondary);margin:4px 0 0}.json-editor-modal{width:600px;max-width:90vw;min-height:400px;max-height:80vh;display:flex;flex-direction:column;padding:0}.json-editor-header{display:flex;justify-content:space-between;align-items:center;padding:16px 20px;border-bottom:1px solid var(--border-color)}.json-editor-header h3{margin:0;font-size:16px;font-weight:600}.json-editor-toolbar{display:flex;align-items:center;gap:8px;padding:12px 20px;background:var(--background);border-bottom:1px solid var(--border-color)}.json-editor-toolbar .btn-small{padding:4px 12px;font-size:12px}.json-status{margin-left:auto;font-size:12px;font-weight:500;padding:4px 8px;border-radius:4px}.json-status.valid{color:var(--success);background:var(--success-light)}.json-status.invalid{color:var(--error);background:#fef2f2}.json-editor-content{flex:1;display:flex;flex-direction:column;padding:16px 20px;min-height:200px}.json-textarea{flex:1;width:100%;min-height:250px;padding:12px;border:1px solid var(--border-color);border-radius:4px;font-family:Monaco,Menlo,Ubuntu Mono,Consolas,monospace;font-size:13px;line-height:1.5;resize:vertical;background:#fafafa}.json-textarea:focus{outline:none;border-color:var(--primary-color);background:#fff}.json-textarea.has-error{border-color:var(--error);background:#fef8f8}.json-error{margin-top:8px;padding:8px 12px;background:#fef2f2;border:1px solid #fecaca;border-radius:4px;color:var(--error);font-size:12px;font-family:monospace}.json-editor-footer{display:flex;justify-content:flex-end;gap:8px;padding:16px 20px;border-top:1px solid var(--border-color);background:var(--background)}.json-edit-button{display:inline-flex;align-items:center;gap:4px;padding:2px 8px;margin-left:4px;background:var(--primary-light);border:1px solid var(--primary-color);border-radius:3px;color:var(--primary-color);font-size:11px;cursor:pointer;transition:background .2s}.json-edit-button:hover{background:var(--primary-color);color:#fff}.reopen-folder-prompt{display:flex;flex-direction:column;gap:8px;padding:12px;margin:8px;background:var(--primary-light);border:1px solid var(--primary-color);border-radius:6px;font-size:13px}.reopen-folder-prompt span{color:var(--text-secondary)}.reopen-folder-prompt strong{color:var(--text-primary);word-break:break-all}.reopen-folder-prompt .btn{align-self:flex-start}.toast-container{position:fixed;top:16px;right:16px;z-index:10000;display:flex;flex-direction:column;gap:8px;max-width:400px}.toast{display:flex;align-items:center;gap:10px;padding:12px 16px;border-radius:6px;background:#fff;box-shadow:0 4px 12px #00000026;animation:toast-slide-in .3s ease}@keyframes toast-slide-in{0%{transform:translate(100%);opacity:0}to{transform:translate(0);opacity:1}}.toast-icon{flex-shrink:0;width:20px;height:20px;display:flex;align-items:center;justify-content:center;border-radius:50%;font-size:12px;font-weight:700}.toast-success .toast-icon{background:var(--success-light);color:var(--success)}.toast-error .toast-icon{background:#ffebee;color:var(--error)}.toast-warning .toast-icon{background:#fff3e0;color:var(--warning)}.toast-info .toast-icon{background:var(--primary-light);color:var(--primary-color)}.toast-message{flex:1;font-size:13px;color:var(--text-primary)}.toast-dismiss{flex-shrink:0;background:none;border:none;font-size:18px;color:var(--text-secondary);cursor:pointer;padding:0;line-height:1}.toast-dismiss:hover{color:var(--text-primary)}.variables-editor{display:flex;flex-direction:column;gap:8px}.variable-editor-item{display:flex;align-items:flex-start;gap:8px;padding:8px;background:var(--background);border-radius:4px}.variable-editor-item .var-name{flex:0 0 120px;position:relative}.variable-editor-item .var-value{flex:1;min-width:0}.variable-editor-item input,.variable-editor-item textarea{width:100%;padding:6px 8px;border:1px solid var(--border-color);border-radius:4px;font-size:13px;font-family:monospace}.variable-editor-item input:focus,.variable-editor-item textarea:focus{outline:none;border-color:var(--primary-color)}.variable-editor-item .var-actions{display:flex;gap:4px}.variable-editor-item .btn-icon{padding:4px 8px;background:none;border:1px solid var(--border-color);border-radius:4px;cursor:pointer;font-size:12px}.variable-editor-item .btn-icon:hover{background:var(--border-color)}.variable-editor-item .btn-icon.delete:hover{background:#ffebee;border-color:var(--error);color:var(--error)}.variable-editor-item.duplicate{background:#fff3e0}.variable-editor-item .duplicate-warning{position:absolute;right:6px;top:50%;transform:translateY(-50%);color:#f57c00;font-size:14px;cursor:help}.add-variable-btn{align-self:flex-start;padding:6px 12px;background:var(--primary-light);border:1px dashed var(--primary-color);border-radius:4px;color:var(--primary-color);font-size:13px;cursor:pointer}.add-variable-btn:hover{background:var(--primary-color);color:#fff;border-style:solid}.variables-section-header{display:flex;align-items:center;justify-content:space-between;padding:8px 0;border-bottom:1px solid var(--border-color);margin-bottom:8px}.variables-section-header h4{margin:0;font-size:13px;font-weight:600;color:var(--text-primary)}.variables-empty{padding:16px;text-align:center;color:var(--text-secondary);font-size:13px;background:var(--background);border-radius:4px}
|