@curl-runner/cli 1.0.2 → 1.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/dist/cli.js +7 -6
- package/package.json +2 -2
- package/src/executor/request-executor.ts +47 -1
- package/src/parser/yaml.test.ts +176 -0
- package/src/parser/yaml.ts +54 -8
- package/src/types/config.ts +30 -0
- package/src/utils/logger.ts +252 -105
- package/src/utils/response-store.test.ts +213 -0
- package/src/utils/response-store.ts +108 -0
package/src/utils/logger.ts
CHANGED
|
@@ -5,6 +5,64 @@ import type {
|
|
|
5
5
|
RequestConfig,
|
|
6
6
|
} from '../types/config';
|
|
7
7
|
|
|
8
|
+
interface TreeNode {
|
|
9
|
+
label: string;
|
|
10
|
+
value?: string;
|
|
11
|
+
children?: TreeNode[];
|
|
12
|
+
color?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
class TreeRenderer {
|
|
16
|
+
private colors: Record<string, string>;
|
|
17
|
+
|
|
18
|
+
constructor(colors: Record<string, string>) {
|
|
19
|
+
this.colors = colors;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
private color(text: string, colorName: string): string {
|
|
23
|
+
if (!colorName || !this.colors[colorName]) {
|
|
24
|
+
return text;
|
|
25
|
+
}
|
|
26
|
+
return `${this.colors[colorName]}${text}${this.colors.reset}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
render(nodes: TreeNode[], basePrefix: string = ' '): void {
|
|
30
|
+
nodes.forEach((node, index) => {
|
|
31
|
+
const isLast = index === nodes.length - 1;
|
|
32
|
+
const prefix = isLast ? `${basePrefix}└─` : `${basePrefix}├─`;
|
|
33
|
+
|
|
34
|
+
if (node.label && node.value) {
|
|
35
|
+
// Regular labeled node with value
|
|
36
|
+
const displayValue = node.color ? this.color(node.value, node.color) : node.value;
|
|
37
|
+
|
|
38
|
+
// Handle multiline values (like Response Body)
|
|
39
|
+
const lines = displayValue.split('\n');
|
|
40
|
+
if (lines.length === 1) {
|
|
41
|
+
console.log(`${prefix} ${node.label}: ${displayValue}`);
|
|
42
|
+
} else {
|
|
43
|
+
console.log(`${prefix} ${node.label}:`);
|
|
44
|
+
const contentPrefix = isLast ? `${basePrefix} ` : `${basePrefix}│ `;
|
|
45
|
+
lines.forEach((line) => {
|
|
46
|
+
console.log(`${contentPrefix}${line}`);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
} else if (node.label && !node.value) {
|
|
50
|
+
// Section header (like "Headers:" or "Metrics:")
|
|
51
|
+
console.log(`${prefix} ${node.label}:`);
|
|
52
|
+
} else if (!node.label && node.value) {
|
|
53
|
+
// Content line without label (like response body lines)
|
|
54
|
+
const continuationPrefix = isLast ? `${basePrefix} ` : `${basePrefix}│ `;
|
|
55
|
+
console.log(`${continuationPrefix}${node.value}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (node.children && node.children.length > 0) {
|
|
59
|
+
const childPrefix = isLast ? `${basePrefix} ` : `${basePrefix}│ `;
|
|
60
|
+
this.render(node.children, childPrefix);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
8
66
|
export class Logger {
|
|
9
67
|
private config: GlobalConfig['output'];
|
|
10
68
|
|
|
@@ -40,7 +98,7 @@ export class Logger {
|
|
|
40
98
|
showBody: true,
|
|
41
99
|
showMetrics: false,
|
|
42
100
|
format: 'pretty',
|
|
43
|
-
prettyLevel: '
|
|
101
|
+
prettyLevel: 'minimal',
|
|
44
102
|
...config,
|
|
45
103
|
};
|
|
46
104
|
}
|
|
@@ -239,56 +297,25 @@ export class Logger {
|
|
|
239
297
|
return `${(bytes / 1024 ** i).toFixed(2)} ${sizes[i]}`;
|
|
240
298
|
}
|
|
241
299
|
|
|
242
|
-
private printSeparator(char: string = '─', length: number = 60): void {
|
|
243
|
-
console.log(this.color(char.repeat(length), 'dim'));
|
|
244
|
-
}
|
|
245
|
-
|
|
246
300
|
logExecutionStart(count: number, mode: string): void {
|
|
247
301
|
if (!this.shouldShowOutput()) {
|
|
248
302
|
return;
|
|
249
303
|
}
|
|
250
304
|
|
|
251
305
|
if (this.shouldShowSeparators()) {
|
|
252
|
-
|
|
253
|
-
console.log(this.color(
|
|
254
|
-
console.log(
|
|
255
|
-
|
|
306
|
+
console.log(); // Add spacing before the execution header
|
|
307
|
+
console.log(this.color(`Executing ${count} request(s) in ${mode} mode`, 'dim'));
|
|
308
|
+
console.log();
|
|
309
|
+
} else {
|
|
310
|
+
// For minimal format, still add spacing after processing info
|
|
256
311
|
console.log();
|
|
257
312
|
}
|
|
258
313
|
}
|
|
259
314
|
|
|
260
|
-
logRequestStart(
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
const name = config.name || `Request #${index}`;
|
|
266
|
-
const sourceFile = config.sourceFile
|
|
267
|
-
? ` ${this.color(`[${this.getShortFilename(config.sourceFile)}]`, 'cyan')}`
|
|
268
|
-
: '';
|
|
269
|
-
console.log(this.color(`▶ ${name}`, 'bright') + sourceFile);
|
|
270
|
-
console.log(
|
|
271
|
-
` ${this.color(config.method || 'GET', 'yellow')} ${this.color(config.url, 'blue')}`,
|
|
272
|
-
);
|
|
273
|
-
|
|
274
|
-
if (
|
|
275
|
-
this.shouldShowRequestDetails() &&
|
|
276
|
-
config.headers &&
|
|
277
|
-
Object.keys(config.headers).length > 0
|
|
278
|
-
) {
|
|
279
|
-
console.log(this.color(' Headers:', 'dim'));
|
|
280
|
-
for (const [key, value] of Object.entries(config.headers)) {
|
|
281
|
-
console.log(` ${key}: ${value}`);
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
if (this.shouldShowRequestDetails() && config.body) {
|
|
286
|
-
console.log(this.color(' Body:', 'dim'));
|
|
287
|
-
const bodyStr = this.formatJson(config.body);
|
|
288
|
-
for (const line of bodyStr.split('\n')) {
|
|
289
|
-
console.log(` ${line}`);
|
|
290
|
-
}
|
|
291
|
-
}
|
|
315
|
+
logRequestStart(_config: RequestConfig, _index: number): void {
|
|
316
|
+
// In the new format, we show everything in logRequestComplete
|
|
317
|
+
// This method is kept for compatibility but simplified
|
|
318
|
+
return;
|
|
292
319
|
}
|
|
293
320
|
|
|
294
321
|
logCommand(command: string): void {
|
|
@@ -336,62 +363,173 @@ export class Logger {
|
|
|
336
363
|
return;
|
|
337
364
|
}
|
|
338
365
|
|
|
366
|
+
const level = this.config.prettyLevel || 'minimal';
|
|
339
367
|
const statusColor = result.success ? 'green' : 'red';
|
|
340
|
-
const statusIcon = result.success ? '✓' : '
|
|
368
|
+
const statusIcon = result.success ? '✓' : 'x';
|
|
369
|
+
const name = result.request.name || 'Request';
|
|
370
|
+
|
|
371
|
+
if (level === 'minimal') {
|
|
372
|
+
// Minimal format: clean tree structure but compact
|
|
373
|
+
const fileTag = result.request.sourceFile
|
|
374
|
+
? this.getShortFilename(result.request.sourceFile)
|
|
375
|
+
: 'inline';
|
|
376
|
+
console.log(
|
|
377
|
+
`${this.color(statusIcon, statusColor)} ${this.color(name, 'bright')} [${fileTag}]`,
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
const treeNodes: TreeNode[] = [];
|
|
381
|
+
const renderer = new TreeRenderer(this.colors);
|
|
382
|
+
|
|
383
|
+
treeNodes.push({
|
|
384
|
+
label: result.request.method || 'GET',
|
|
385
|
+
value: result.request.url,
|
|
386
|
+
color: 'blue',
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
const statusText = result.status ? `${result.status}` : 'ERROR';
|
|
390
|
+
treeNodes.push({
|
|
391
|
+
label: `${statusIcon} Status`,
|
|
392
|
+
value: statusText,
|
|
393
|
+
color: statusColor,
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
if (result.metrics) {
|
|
397
|
+
const durationSize = `${this.formatDuration(result.metrics.duration)} | ${this.formatSize(result.metrics.size)}`;
|
|
398
|
+
treeNodes.push({
|
|
399
|
+
label: 'Duration',
|
|
400
|
+
value: durationSize,
|
|
401
|
+
color: 'cyan',
|
|
402
|
+
});
|
|
403
|
+
}
|
|
341
404
|
|
|
342
|
-
|
|
343
|
-
` ${this.color(statusIcon, statusColor)} ` +
|
|
344
|
-
`Status: ${this.color(String(result.status || 'ERROR'), statusColor)}`,
|
|
345
|
-
);
|
|
405
|
+
renderer.render(treeNodes);
|
|
346
406
|
|
|
347
|
-
|
|
348
|
-
|
|
407
|
+
if (result.error) {
|
|
408
|
+
console.log();
|
|
409
|
+
this.logValidationErrors(result.error);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
console.log();
|
|
413
|
+
return;
|
|
349
414
|
}
|
|
350
415
|
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
const parts = [`Duration: ${this.color(this.formatDuration(metrics.duration), 'cyan')}`];
|
|
416
|
+
// Standard and detailed formats: use clean tree structure
|
|
417
|
+
console.log(`${this.color(statusIcon, statusColor)} ${this.color(name, 'bright')}`);
|
|
354
418
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
419
|
+
// Build tree structure
|
|
420
|
+
const treeNodes: TreeNode[] = [];
|
|
421
|
+
const renderer = new TreeRenderer(this.colors);
|
|
358
422
|
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
parts.push(`TLS: ${this.formatDuration(metrics.tlsHandshake)}`);
|
|
368
|
-
}
|
|
369
|
-
if (metrics.firstByte) {
|
|
370
|
-
parts.push(`TTFB: ${this.formatDuration(metrics.firstByte)}`);
|
|
371
|
-
}
|
|
372
|
-
}
|
|
423
|
+
// Main info nodes
|
|
424
|
+
treeNodes.push({ label: 'URL', value: result.request.url, color: 'blue' });
|
|
425
|
+
treeNodes.push({ label: 'Method', value: result.request.method || 'GET', color: 'yellow' });
|
|
426
|
+
treeNodes.push({
|
|
427
|
+
label: 'Status',
|
|
428
|
+
value: String(result.status || 'ERROR'),
|
|
429
|
+
color: statusColor,
|
|
430
|
+
});
|
|
373
431
|
|
|
374
|
-
|
|
432
|
+
if (result.metrics) {
|
|
433
|
+
treeNodes.push({
|
|
434
|
+
label: 'Duration',
|
|
435
|
+
value: this.formatDuration(result.metrics.duration),
|
|
436
|
+
color: 'cyan',
|
|
437
|
+
});
|
|
375
438
|
}
|
|
376
439
|
|
|
440
|
+
// Add headers section if needed
|
|
377
441
|
if (this.shouldShowHeaders() && result.headers && Object.keys(result.headers).length > 0) {
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
}
|
|
442
|
+
const headerChildren: TreeNode[] = Object.entries(result.headers).map(([key, value]) => ({
|
|
443
|
+
label: this.color(key, 'dim'),
|
|
444
|
+
value: String(value),
|
|
445
|
+
}));
|
|
446
|
+
|
|
447
|
+
treeNodes.push({
|
|
448
|
+
label: 'Headers',
|
|
449
|
+
children: headerChildren,
|
|
450
|
+
});
|
|
382
451
|
}
|
|
383
452
|
|
|
453
|
+
// Add body section if needed
|
|
384
454
|
if (this.shouldShowBody() && result.body) {
|
|
385
|
-
console.log(this.color(' Response Body:', 'dim'));
|
|
386
455
|
const bodyStr = this.formatJson(result.body);
|
|
387
456
|
const lines = bodyStr.split('\n');
|
|
388
457
|
const maxLines = this.shouldShowRequestDetails() ? Infinity : 10;
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
}
|
|
458
|
+
const bodyLines = lines.slice(0, maxLines);
|
|
459
|
+
|
|
392
460
|
if (lines.length > maxLines) {
|
|
393
|
-
|
|
461
|
+
bodyLines.push(this.color(`... (${lines.length - maxLines} more lines)`, 'dim'));
|
|
394
462
|
}
|
|
463
|
+
|
|
464
|
+
treeNodes.push({
|
|
465
|
+
label: 'Response Body',
|
|
466
|
+
value: bodyLines.join('\n'),
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Add detailed metrics section if needed
|
|
471
|
+
if (this.shouldShowMetrics() && result.metrics && level === 'detailed') {
|
|
472
|
+
const metrics = result.metrics;
|
|
473
|
+
const metricChildren: TreeNode[] = [];
|
|
474
|
+
|
|
475
|
+
metricChildren.push({
|
|
476
|
+
label: 'Request Duration',
|
|
477
|
+
value: this.formatDuration(metrics.duration),
|
|
478
|
+
color: 'cyan',
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
if (metrics.size !== undefined) {
|
|
482
|
+
metricChildren.push({
|
|
483
|
+
label: 'Response Size',
|
|
484
|
+
value: this.formatSize(metrics.size),
|
|
485
|
+
color: 'cyan',
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
if (metrics.dnsLookup) {
|
|
490
|
+
metricChildren.push({
|
|
491
|
+
label: 'DNS Lookup',
|
|
492
|
+
value: this.formatDuration(metrics.dnsLookup),
|
|
493
|
+
color: 'cyan',
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
if (metrics.tcpConnection) {
|
|
498
|
+
metricChildren.push({
|
|
499
|
+
label: 'TCP Connection',
|
|
500
|
+
value: this.formatDuration(metrics.tcpConnection),
|
|
501
|
+
color: 'cyan',
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
if (metrics.tlsHandshake) {
|
|
506
|
+
metricChildren.push({
|
|
507
|
+
label: 'TLS Handshake',
|
|
508
|
+
value: this.formatDuration(metrics.tlsHandshake),
|
|
509
|
+
color: 'cyan',
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if (metrics.firstByte) {
|
|
514
|
+
metricChildren.push({
|
|
515
|
+
label: 'Time to First Byte',
|
|
516
|
+
value: this.formatDuration(metrics.firstByte),
|
|
517
|
+
color: 'cyan',
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
treeNodes.push({
|
|
522
|
+
label: 'Metrics',
|
|
523
|
+
children: metricChildren,
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Render the tree
|
|
528
|
+
renderer.render(treeNodes);
|
|
529
|
+
|
|
530
|
+
if (result.error) {
|
|
531
|
+
console.log();
|
|
532
|
+
this.logValidationErrors(result.error);
|
|
395
533
|
}
|
|
396
534
|
|
|
397
535
|
console.log();
|
|
@@ -435,53 +573,64 @@ export class Logger {
|
|
|
435
573
|
return;
|
|
436
574
|
}
|
|
437
575
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
576
|
+
const level = this.config.prettyLevel || 'minimal';
|
|
577
|
+
|
|
578
|
+
// Add spacing for global summary
|
|
579
|
+
if (isGlobal) {
|
|
580
|
+
console.log(); // Extra spacing before global summary
|
|
443
581
|
}
|
|
444
582
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
summary.failed === 0 ? 'green' :
|
|
583
|
+
if (level === 'minimal') {
|
|
584
|
+
// Simple one-line summary for minimal, similar to docs example
|
|
585
|
+
const statusColor = summary.failed === 0 ? 'green' : 'red';
|
|
586
|
+
const successText =
|
|
587
|
+
summary.failed === 0
|
|
588
|
+
? `${summary.total} request${summary.total === 1 ? '' : 's'} completed successfully`
|
|
589
|
+
: `${summary.successful}/${summary.total} request${summary.total === 1 ? '' : 's'} completed, ${summary.failed} failed`;
|
|
448
590
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
591
|
+
const summaryPrefix = isGlobal ? '◆ Global Summary' : 'Summary';
|
|
592
|
+
console.log(`${summaryPrefix}: ${this.color(successText, statusColor)}`);
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// Compact summary for standard/detailed - much simpler
|
|
597
|
+
const _successRate = ((summary.successful / summary.total) * 100).toFixed(1);
|
|
598
|
+
const statusColor = summary.failed === 0 ? 'green' : 'red';
|
|
599
|
+
const successText =
|
|
600
|
+
summary.failed === 0
|
|
601
|
+
? `${summary.total} request${summary.total === 1 ? '' : 's'} completed successfully`
|
|
602
|
+
: `${summary.successful}/${summary.total} request${summary.total === 1 ? '' : 's'} completed, ${summary.failed} failed`;
|
|
603
|
+
|
|
604
|
+
const summaryPrefix = isGlobal ? '◆ Global Summary' : 'Summary';
|
|
605
|
+
console.log();
|
|
606
|
+
console.log(
|
|
607
|
+
`${summaryPrefix}: ${this.color(successText, statusColor)} (${this.color(this.formatDuration(summary.duration), 'cyan')})`,
|
|
608
|
+
);
|
|
454
609
|
|
|
455
610
|
if (summary.failed > 0 && this.shouldShowRequestDetails()) {
|
|
456
|
-
console.log();
|
|
457
|
-
console.log(this.color(' Failed Requests:', 'red'));
|
|
458
611
|
summary.results
|
|
459
612
|
.filter((r) => !r.success)
|
|
460
613
|
.forEach((r) => {
|
|
461
614
|
const name = r.request.name || r.request.url;
|
|
462
|
-
console.log(`
|
|
615
|
+
console.log(` ${this.color('•', 'red')} ${name}: ${r.error}`);
|
|
463
616
|
});
|
|
464
617
|
}
|
|
465
|
-
|
|
466
|
-
if (this.shouldShowSeparators()) {
|
|
467
|
-
this.printSeparator('═');
|
|
468
|
-
}
|
|
469
618
|
}
|
|
470
619
|
|
|
471
620
|
logError(message: string): void {
|
|
472
|
-
console.error(this.color(
|
|
621
|
+
console.error(this.color(`✗ ${message}`, 'red'));
|
|
473
622
|
}
|
|
474
623
|
|
|
475
624
|
logWarning(message: string): void {
|
|
476
|
-
console.warn(this.color(
|
|
625
|
+
console.warn(this.color(`⚠ ${message}`, 'yellow'));
|
|
477
626
|
}
|
|
478
627
|
|
|
479
628
|
logInfo(message: string): void {
|
|
480
|
-
console.log(this.color(
|
|
629
|
+
console.log(this.color(`ℹ ${message}`, 'blue'));
|
|
481
630
|
}
|
|
482
631
|
|
|
483
632
|
logSuccess(message: string): void {
|
|
484
|
-
console.log(this.color(
|
|
633
|
+
console.log(this.color(`✓ ${message}`, 'green'));
|
|
485
634
|
}
|
|
486
635
|
|
|
487
636
|
logFileHeader(fileName: string, requestCount: number): void {
|
|
@@ -491,11 +640,9 @@ export class Logger {
|
|
|
491
640
|
|
|
492
641
|
const shortName = fileName.replace(/.*\//, '').replace('.yaml', '');
|
|
493
642
|
console.log();
|
|
494
|
-
this.printSeparator('─');
|
|
495
643
|
console.log(
|
|
496
|
-
this.color(
|
|
644
|
+
this.color(`▶ ${shortName}.yaml`, 'bright') +
|
|
497
645
|
this.color(` (${requestCount} request${requestCount === 1 ? '' : 's'})`, 'dim'),
|
|
498
646
|
);
|
|
499
|
-
this.printSeparator('─');
|
|
500
647
|
}
|
|
501
648
|
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import type { ExecutionResult } from '../types/config';
|
|
3
|
+
import {
|
|
4
|
+
createStoreContext,
|
|
5
|
+
extractStoreValues,
|
|
6
|
+
getValueByPath,
|
|
7
|
+
mergeStoreContext,
|
|
8
|
+
valueToString,
|
|
9
|
+
} from './response-store';
|
|
10
|
+
|
|
11
|
+
describe('getValueByPath', () => {
|
|
12
|
+
const testObj = {
|
|
13
|
+
status: 200,
|
|
14
|
+
body: {
|
|
15
|
+
id: 123,
|
|
16
|
+
user: {
|
|
17
|
+
name: 'John',
|
|
18
|
+
email: 'john@example.com',
|
|
19
|
+
},
|
|
20
|
+
items: [
|
|
21
|
+
{ id: 1, name: 'Item 1' },
|
|
22
|
+
{ id: 2, name: 'Item 2' },
|
|
23
|
+
],
|
|
24
|
+
},
|
|
25
|
+
headers: {
|
|
26
|
+
'content-type': 'application/json',
|
|
27
|
+
'x-request-id': 'abc123',
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
test('should get top-level value', () => {
|
|
32
|
+
expect(getValueByPath(testObj, 'status')).toBe(200);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('should get nested value', () => {
|
|
36
|
+
expect(getValueByPath(testObj, 'body.id')).toBe(123);
|
|
37
|
+
expect(getValueByPath(testObj, 'body.user.name')).toBe('John');
|
|
38
|
+
expect(getValueByPath(testObj, 'body.user.email')).toBe('john@example.com');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('should get header value', () => {
|
|
42
|
+
expect(getValueByPath(testObj, 'headers.content-type')).toBe('application/json');
|
|
43
|
+
expect(getValueByPath(testObj, 'headers.x-request-id')).toBe('abc123');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('should get array element by index', () => {
|
|
47
|
+
expect(getValueByPath(testObj, 'body.items.0.id')).toBe(1);
|
|
48
|
+
expect(getValueByPath(testObj, 'body.items.1.name')).toBe('Item 2');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('should get array element using bracket notation', () => {
|
|
52
|
+
expect(getValueByPath(testObj, 'body.items[0].id')).toBe(1);
|
|
53
|
+
expect(getValueByPath(testObj, 'body.items[1].name')).toBe('Item 2');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('should return undefined for non-existent path', () => {
|
|
57
|
+
expect(getValueByPath(testObj, 'body.nonexistent')).toBeUndefined();
|
|
58
|
+
expect(getValueByPath(testObj, 'body.user.age')).toBeUndefined();
|
|
59
|
+
expect(getValueByPath(testObj, 'nonexistent.path')).toBeUndefined();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('should return undefined for null or undefined object', () => {
|
|
63
|
+
expect(getValueByPath(null, 'any.path')).toBeUndefined();
|
|
64
|
+
expect(getValueByPath(undefined, 'any.path')).toBeUndefined();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('should handle primitive values correctly', () => {
|
|
68
|
+
expect(getValueByPath('string', 'length')).toBeUndefined();
|
|
69
|
+
expect(getValueByPath(123, 'toString')).toBeUndefined();
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe('valueToString', () => {
|
|
74
|
+
test('should convert string to string', () => {
|
|
75
|
+
expect(valueToString('hello')).toBe('hello');
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('should convert number to string', () => {
|
|
79
|
+
expect(valueToString(123)).toBe('123');
|
|
80
|
+
expect(valueToString(45.67)).toBe('45.67');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('should convert boolean to string', () => {
|
|
84
|
+
expect(valueToString(true)).toBe('true');
|
|
85
|
+
expect(valueToString(false)).toBe('false');
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test('should convert null and undefined to empty string', () => {
|
|
89
|
+
expect(valueToString(null)).toBe('');
|
|
90
|
+
expect(valueToString(undefined)).toBe('');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('should JSON stringify objects', () => {
|
|
94
|
+
expect(valueToString({ a: 1 })).toBe('{"a":1}');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('should JSON stringify arrays', () => {
|
|
98
|
+
expect(valueToString([1, 2, 3])).toBe('[1,2,3]');
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe('extractStoreValues', () => {
|
|
103
|
+
const mockResult: ExecutionResult = {
|
|
104
|
+
request: {
|
|
105
|
+
url: 'https://api.example.com/users',
|
|
106
|
+
method: 'POST',
|
|
107
|
+
},
|
|
108
|
+
success: true,
|
|
109
|
+
status: 201,
|
|
110
|
+
headers: {
|
|
111
|
+
'content-type': 'application/json',
|
|
112
|
+
'x-request-id': 'req-12345',
|
|
113
|
+
},
|
|
114
|
+
body: {
|
|
115
|
+
id: 456,
|
|
116
|
+
data: {
|
|
117
|
+
token: 'jwt-token-here',
|
|
118
|
+
user: {
|
|
119
|
+
id: 789,
|
|
120
|
+
name: 'Test User',
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
metrics: {
|
|
125
|
+
duration: 150,
|
|
126
|
+
size: 1024,
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
test('should extract status', () => {
|
|
131
|
+
const result = extractStoreValues(mockResult, {
|
|
132
|
+
statusCode: 'status',
|
|
133
|
+
});
|
|
134
|
+
expect(result.statusCode).toBe('201');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test('should extract body fields', () => {
|
|
138
|
+
const result = extractStoreValues(mockResult, {
|
|
139
|
+
userId: 'body.id',
|
|
140
|
+
token: 'body.data.token',
|
|
141
|
+
userName: 'body.data.user.name',
|
|
142
|
+
});
|
|
143
|
+
expect(result.userId).toBe('456');
|
|
144
|
+
expect(result.token).toBe('jwt-token-here');
|
|
145
|
+
expect(result.userName).toBe('Test User');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test('should extract header values', () => {
|
|
149
|
+
const result = extractStoreValues(mockResult, {
|
|
150
|
+
contentType: 'headers.content-type',
|
|
151
|
+
requestId: 'headers.x-request-id',
|
|
152
|
+
});
|
|
153
|
+
expect(result.contentType).toBe('application/json');
|
|
154
|
+
expect(result.requestId).toBe('req-12345');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('should extract metrics', () => {
|
|
158
|
+
const result = extractStoreValues(mockResult, {
|
|
159
|
+
duration: 'metrics.duration',
|
|
160
|
+
});
|
|
161
|
+
expect(result.duration).toBe('150');
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test('should handle non-existent paths', () => {
|
|
165
|
+
const result = extractStoreValues(mockResult, {
|
|
166
|
+
missing: 'body.nonexistent',
|
|
167
|
+
});
|
|
168
|
+
expect(result.missing).toBe('');
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test('should extract multiple values', () => {
|
|
172
|
+
const result = extractStoreValues(mockResult, {
|
|
173
|
+
id: 'body.id',
|
|
174
|
+
status: 'status',
|
|
175
|
+
contentType: 'headers.content-type',
|
|
176
|
+
});
|
|
177
|
+
expect(result.id).toBe('456');
|
|
178
|
+
expect(result.status).toBe('201');
|
|
179
|
+
expect(result.contentType).toBe('application/json');
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
describe('createStoreContext', () => {
|
|
184
|
+
test('should create empty context', () => {
|
|
185
|
+
const context = createStoreContext();
|
|
186
|
+
expect(context).toEqual({});
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
describe('mergeStoreContext', () => {
|
|
191
|
+
test('should merge contexts', () => {
|
|
192
|
+
const existing = { a: '1', b: '2' };
|
|
193
|
+
const newValues = { c: '3', d: '4' };
|
|
194
|
+
const merged = mergeStoreContext(existing, newValues);
|
|
195
|
+
expect(merged).toEqual({ a: '1', b: '2', c: '3', d: '4' });
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test('should override existing values', () => {
|
|
199
|
+
const existing = { a: '1', b: '2' };
|
|
200
|
+
const newValues = { b: 'new', c: '3' };
|
|
201
|
+
const merged = mergeStoreContext(existing, newValues);
|
|
202
|
+
expect(merged).toEqual({ a: '1', b: 'new', c: '3' });
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test('should not mutate original contexts', () => {
|
|
206
|
+
const existing = { a: '1' };
|
|
207
|
+
const newValues = { b: '2' };
|
|
208
|
+
const merged = mergeStoreContext(existing, newValues);
|
|
209
|
+
expect(existing).toEqual({ a: '1' });
|
|
210
|
+
expect(newValues).toEqual({ b: '2' });
|
|
211
|
+
expect(merged).toEqual({ a: '1', b: '2' });
|
|
212
|
+
});
|
|
213
|
+
});
|