@curl-runner/cli 1.11.0 → 1.12.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/package.json
CHANGED
|
@@ -11,6 +11,7 @@ import type {
|
|
|
11
11
|
ResponseStoreContext,
|
|
12
12
|
SnapshotConfig,
|
|
13
13
|
} from '../types/config';
|
|
14
|
+
import { evaluateCondition } from '../utils/condition-evaluator';
|
|
14
15
|
import { CurlBuilder } from '../utils/curl-builder';
|
|
15
16
|
import { Logger } from '../utils/logger';
|
|
16
17
|
import { createStoreContext, extractStoreValues } from '../utils/response-store';
|
|
@@ -516,6 +517,23 @@ export class RequestExecutor {
|
|
|
516
517
|
for (let i = 0; i < requests.length; i++) {
|
|
517
518
|
// Interpolate store variables before execution
|
|
518
519
|
const interpolatedRequest = this.interpolateStoreVariables(requests[i], storeContext);
|
|
520
|
+
|
|
521
|
+
// Evaluate `when` condition if present
|
|
522
|
+
if (interpolatedRequest.when) {
|
|
523
|
+
const conditionResult = evaluateCondition(interpolatedRequest.when, storeContext);
|
|
524
|
+
if (!conditionResult.shouldRun) {
|
|
525
|
+
const skippedResult: ExecutionResult = {
|
|
526
|
+
request: interpolatedRequest,
|
|
527
|
+
success: true, // Skipped requests are not failures
|
|
528
|
+
skipped: true,
|
|
529
|
+
skipReason: conditionResult.reason,
|
|
530
|
+
};
|
|
531
|
+
results.push(skippedResult);
|
|
532
|
+
this.logger.logSkipped(interpolatedRequest, i + 1, conditionResult.reason);
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
519
537
|
const result = await this.executeRequest(interpolatedRequest, i + 1);
|
|
520
538
|
results.push(result);
|
|
521
539
|
|
|
@@ -618,13 +636,15 @@ export class RequestExecutor {
|
|
|
618
636
|
}
|
|
619
637
|
|
|
620
638
|
private createSummary(results: ExecutionResult[], duration: number): ExecutionSummary {
|
|
621
|
-
const
|
|
639
|
+
const skipped = results.filter((r) => r.skipped).length;
|
|
640
|
+
const successful = results.filter((r) => r.success && !r.skipped).length;
|
|
622
641
|
const failed = results.filter((r) => !r.success).length;
|
|
623
642
|
|
|
624
643
|
return {
|
|
625
644
|
total: results.length,
|
|
626
645
|
successful,
|
|
627
646
|
failed,
|
|
647
|
+
skipped,
|
|
628
648
|
duration,
|
|
629
649
|
results,
|
|
630
650
|
};
|
package/src/types/config.ts
CHANGED
|
@@ -57,6 +57,87 @@ export type FormDataConfig = Record<string, FormFieldValue>;
|
|
|
57
57
|
*/
|
|
58
58
|
export type StoreConfig = Record<string, string>;
|
|
59
59
|
|
|
60
|
+
/**
|
|
61
|
+
* Operators for conditional expressions.
|
|
62
|
+
*/
|
|
63
|
+
export type ConditionOperator =
|
|
64
|
+
| '=='
|
|
65
|
+
| '!='
|
|
66
|
+
| '>'
|
|
67
|
+
| '<'
|
|
68
|
+
| '>='
|
|
69
|
+
| '<='
|
|
70
|
+
| 'contains'
|
|
71
|
+
| 'matches'
|
|
72
|
+
| 'exists'
|
|
73
|
+
| 'not-exists';
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* A single condition expression comparing a store value against an expected value.
|
|
77
|
+
*
|
|
78
|
+
* Examples:
|
|
79
|
+
* - `{ left: "store.status", operator: "==", right: 200 }`
|
|
80
|
+
* - `{ left: "store.userId", operator: "exists" }`
|
|
81
|
+
* - `{ left: "store.body.type", operator: "contains", right: "user" }`
|
|
82
|
+
*/
|
|
83
|
+
export interface ConditionExpression {
|
|
84
|
+
/** Left operand - typically a store path like "store.status" or "store.body.id" */
|
|
85
|
+
left: string;
|
|
86
|
+
/** Comparison operator */
|
|
87
|
+
operator: ConditionOperator;
|
|
88
|
+
/** Right operand - the value to compare against. Optional for exists/not-exists. */
|
|
89
|
+
right?: string | number | boolean;
|
|
90
|
+
/** Case-sensitive comparison for string operators. Default: false (case-insensitive) */
|
|
91
|
+
caseSensitive?: boolean;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Configuration for conditional request execution.
|
|
96
|
+
* Supports single conditions, AND (all), and OR (any) compound conditions.
|
|
97
|
+
*
|
|
98
|
+
* Examples:
|
|
99
|
+
* ```yaml
|
|
100
|
+
* # Single condition
|
|
101
|
+
* when:
|
|
102
|
+
* left: store.status
|
|
103
|
+
* operator: "=="
|
|
104
|
+
* right: 200
|
|
105
|
+
*
|
|
106
|
+
* # AND condition (all must be true)
|
|
107
|
+
* when:
|
|
108
|
+
* all:
|
|
109
|
+
* - left: store.status
|
|
110
|
+
* operator: "=="
|
|
111
|
+
* right: 200
|
|
112
|
+
* - left: store.userId
|
|
113
|
+
* operator: exists
|
|
114
|
+
*
|
|
115
|
+
* # OR condition (any must be true)
|
|
116
|
+
* when:
|
|
117
|
+
* any:
|
|
118
|
+
* - left: store.type
|
|
119
|
+
* operator: "=="
|
|
120
|
+
* right: "admin"
|
|
121
|
+
* - left: store.type
|
|
122
|
+
* operator: "=="
|
|
123
|
+
* right: "superuser"
|
|
124
|
+
* ```
|
|
125
|
+
*/
|
|
126
|
+
export interface WhenCondition {
|
|
127
|
+
/** All conditions must be true (AND logic) */
|
|
128
|
+
all?: ConditionExpression[];
|
|
129
|
+
/** Any condition must be true (OR logic) */
|
|
130
|
+
any?: ConditionExpression[];
|
|
131
|
+
/** Single condition - left operand */
|
|
132
|
+
left?: string;
|
|
133
|
+
/** Single condition - operator */
|
|
134
|
+
operator?: ConditionOperator;
|
|
135
|
+
/** Single condition - right operand */
|
|
136
|
+
right?: string | number | boolean;
|
|
137
|
+
/** Case-sensitive comparison for string operators. Default: false */
|
|
138
|
+
caseSensitive?: boolean;
|
|
139
|
+
}
|
|
140
|
+
|
|
60
141
|
/**
|
|
61
142
|
* SSL/TLS certificate configuration options.
|
|
62
143
|
*
|
|
@@ -140,6 +221,30 @@ export interface RequestConfig {
|
|
|
140
221
|
* contentType: headers.content-type
|
|
141
222
|
*/
|
|
142
223
|
store?: StoreConfig;
|
|
224
|
+
/**
|
|
225
|
+
* Conditional execution - skip/run request based on previous results.
|
|
226
|
+
* Only works in sequential execution mode.
|
|
227
|
+
*
|
|
228
|
+
* @example
|
|
229
|
+
* # Object syntax
|
|
230
|
+
* when:
|
|
231
|
+
* left: store.status
|
|
232
|
+
* operator: "=="
|
|
233
|
+
* right: 200
|
|
234
|
+
*
|
|
235
|
+
* # String shorthand
|
|
236
|
+
* when: "store.status == 200"
|
|
237
|
+
*
|
|
238
|
+
* # Compound conditions
|
|
239
|
+
* when:
|
|
240
|
+
* all:
|
|
241
|
+
* - left: store.userId
|
|
242
|
+
* operator: exists
|
|
243
|
+
* - left: store.status
|
|
244
|
+
* operator: "<"
|
|
245
|
+
* right: 400
|
|
246
|
+
*/
|
|
247
|
+
when?: WhenCondition | string;
|
|
143
248
|
expect?: {
|
|
144
249
|
failure?: boolean; // If true, expect the request to fail (for negative testing)
|
|
145
250
|
status?: number | number[];
|
|
@@ -264,12 +369,17 @@ export interface ExecutionResult {
|
|
|
264
369
|
};
|
|
265
370
|
/** Snapshot comparison result (if snapshot testing enabled). */
|
|
266
371
|
snapshotResult?: SnapshotCompareResult;
|
|
372
|
+
/** Whether this request was skipped due to a `when` condition. */
|
|
373
|
+
skipped?: boolean;
|
|
374
|
+
/** Reason the request was skipped (condition that failed). */
|
|
375
|
+
skipReason?: string;
|
|
267
376
|
}
|
|
268
377
|
|
|
269
378
|
export interface ExecutionSummary {
|
|
270
379
|
total: number;
|
|
271
380
|
successful: number;
|
|
272
381
|
failed: number;
|
|
382
|
+
skipped: number;
|
|
273
383
|
duration: number;
|
|
274
384
|
results: ExecutionResult[];
|
|
275
385
|
}
|
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import type { ConditionExpression, ResponseStoreContext, WhenCondition } from '../types/config';
|
|
3
|
+
import { evaluateCondition, evaluateExpression, parseStringCondition } from './condition-evaluator';
|
|
4
|
+
|
|
5
|
+
describe('parseStringCondition', () => {
|
|
6
|
+
test('should parse equality condition', () => {
|
|
7
|
+
const result = parseStringCondition('store.status == 200');
|
|
8
|
+
expect(result).toEqual({
|
|
9
|
+
left: 'store.status',
|
|
10
|
+
operator: '==',
|
|
11
|
+
right: 200,
|
|
12
|
+
});
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test('should parse inequality condition', () => {
|
|
16
|
+
const result = parseStringCondition('store.type != admin');
|
|
17
|
+
expect(result).toEqual({
|
|
18
|
+
left: 'store.type',
|
|
19
|
+
operator: '!=',
|
|
20
|
+
right: 'admin',
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('should parse greater than condition', () => {
|
|
25
|
+
const result = parseStringCondition('store.count > 10');
|
|
26
|
+
expect(result).toEqual({
|
|
27
|
+
left: 'store.count',
|
|
28
|
+
operator: '>',
|
|
29
|
+
right: 10,
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('should parse less than condition', () => {
|
|
34
|
+
const result = parseStringCondition('store.price < 100.5');
|
|
35
|
+
expect(result).toEqual({
|
|
36
|
+
left: 'store.price',
|
|
37
|
+
operator: '<',
|
|
38
|
+
right: 100.5,
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('should parse greater than or equal condition', () => {
|
|
43
|
+
const result = parseStringCondition('store.version >= 2');
|
|
44
|
+
expect(result).toEqual({
|
|
45
|
+
left: 'store.version',
|
|
46
|
+
operator: '>=',
|
|
47
|
+
right: 2,
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('should parse less than or equal condition', () => {
|
|
52
|
+
const result = parseStringCondition('store.age <= 65');
|
|
53
|
+
expect(result).toEqual({
|
|
54
|
+
left: 'store.age',
|
|
55
|
+
operator: '<=',
|
|
56
|
+
right: 65,
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('should parse contains condition', () => {
|
|
61
|
+
const result = parseStringCondition('store.message contains error');
|
|
62
|
+
expect(result).toEqual({
|
|
63
|
+
left: 'store.message',
|
|
64
|
+
operator: 'contains',
|
|
65
|
+
right: 'error',
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('should parse matches condition', () => {
|
|
70
|
+
const result = parseStringCondition('store.email matches ^[a-z]+@');
|
|
71
|
+
expect(result).toEqual({
|
|
72
|
+
left: 'store.email',
|
|
73
|
+
operator: 'matches',
|
|
74
|
+
right: '^[a-z]+@',
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('should parse exists condition', () => {
|
|
79
|
+
const result = parseStringCondition('store.userId exists');
|
|
80
|
+
expect(result).toEqual({
|
|
81
|
+
left: 'store.userId',
|
|
82
|
+
operator: 'exists',
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('should parse not-exists condition', () => {
|
|
87
|
+
const result = parseStringCondition('store.error not-exists');
|
|
88
|
+
expect(result).toEqual({
|
|
89
|
+
left: 'store.error',
|
|
90
|
+
operator: 'not-exists',
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('should parse boolean true', () => {
|
|
95
|
+
const result = parseStringCondition('store.enabled == true');
|
|
96
|
+
expect(result).toEqual({
|
|
97
|
+
left: 'store.enabled',
|
|
98
|
+
operator: '==',
|
|
99
|
+
right: true,
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('should parse boolean false', () => {
|
|
104
|
+
const result = parseStringCondition('store.disabled == false');
|
|
105
|
+
expect(result).toEqual({
|
|
106
|
+
left: 'store.disabled',
|
|
107
|
+
operator: '==',
|
|
108
|
+
right: false,
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('should parse quoted strings', () => {
|
|
113
|
+
const result = parseStringCondition('store.name == "John Doe"');
|
|
114
|
+
expect(result).toEqual({
|
|
115
|
+
left: 'store.name',
|
|
116
|
+
operator: '==',
|
|
117
|
+
right: 'John Doe',
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('should return null for invalid syntax', () => {
|
|
122
|
+
expect(parseStringCondition('invalid')).toBeNull();
|
|
123
|
+
expect(parseStringCondition('')).toBeNull();
|
|
124
|
+
expect(parseStringCondition('store.x')).toBeNull();
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe('evaluateExpression', () => {
|
|
129
|
+
const context: ResponseStoreContext = {
|
|
130
|
+
status: '200',
|
|
131
|
+
userId: '123',
|
|
132
|
+
name: 'John',
|
|
133
|
+
empty: '',
|
|
134
|
+
enabled: 'true',
|
|
135
|
+
count: '42',
|
|
136
|
+
body: '{"type":"user","nested":{"id":1}}',
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
describe('exists operator', () => {
|
|
140
|
+
test('should return true for existing value', () => {
|
|
141
|
+
const expr: ConditionExpression = { left: 'store.userId', operator: 'exists' };
|
|
142
|
+
expect(evaluateExpression(expr, context).passed).toBe(true);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test('should return false for empty string', () => {
|
|
146
|
+
const expr: ConditionExpression = { left: 'store.empty', operator: 'exists' };
|
|
147
|
+
expect(evaluateExpression(expr, context).passed).toBe(false);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test('should return false for missing value', () => {
|
|
151
|
+
const expr: ConditionExpression = { left: 'store.missing', operator: 'exists' };
|
|
152
|
+
expect(evaluateExpression(expr, context).passed).toBe(false);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe('not-exists operator', () => {
|
|
157
|
+
test('should return true for missing value', () => {
|
|
158
|
+
const expr: ConditionExpression = { left: 'store.missing', operator: 'not-exists' };
|
|
159
|
+
expect(evaluateExpression(expr, context).passed).toBe(true);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test('should return true for empty string', () => {
|
|
163
|
+
const expr: ConditionExpression = { left: 'store.empty', operator: 'not-exists' };
|
|
164
|
+
expect(evaluateExpression(expr, context).passed).toBe(true);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test('should return false for existing value', () => {
|
|
168
|
+
const expr: ConditionExpression = { left: 'store.userId', operator: 'not-exists' };
|
|
169
|
+
expect(evaluateExpression(expr, context).passed).toBe(false);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe('== operator', () => {
|
|
174
|
+
test('should compare numbers', () => {
|
|
175
|
+
const expr: ConditionExpression = { left: 'store.status', operator: '==', right: 200 };
|
|
176
|
+
expect(evaluateExpression(expr, context).passed).toBe(true);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test('should compare strings case-insensitively by default', () => {
|
|
180
|
+
const expr: ConditionExpression = { left: 'store.name', operator: '==', right: 'john' };
|
|
181
|
+
expect(evaluateExpression(expr, context).passed).toBe(true);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test('should compare strings case-sensitively when specified', () => {
|
|
185
|
+
const expr: ConditionExpression = {
|
|
186
|
+
left: 'store.name',
|
|
187
|
+
operator: '==',
|
|
188
|
+
right: 'john',
|
|
189
|
+
caseSensitive: true,
|
|
190
|
+
};
|
|
191
|
+
expect(evaluateExpression(expr, context).passed).toBe(false);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test('should compare booleans', () => {
|
|
195
|
+
const expr: ConditionExpression = { left: 'store.enabled', operator: '==', right: true };
|
|
196
|
+
expect(evaluateExpression(expr, context).passed).toBe(true);
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
describe('!= operator', () => {
|
|
201
|
+
test('should return true for different values', () => {
|
|
202
|
+
const expr: ConditionExpression = { left: 'store.status', operator: '!=', right: 404 };
|
|
203
|
+
expect(evaluateExpression(expr, context).passed).toBe(true);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test('should return false for equal values', () => {
|
|
207
|
+
const expr: ConditionExpression = { left: 'store.status', operator: '!=', right: 200 };
|
|
208
|
+
expect(evaluateExpression(expr, context).passed).toBe(false);
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
describe('comparison operators', () => {
|
|
213
|
+
test('should compare with >', () => {
|
|
214
|
+
const expr: ConditionExpression = { left: 'store.count', operator: '>', right: 40 };
|
|
215
|
+
expect(evaluateExpression(expr, context).passed).toBe(true);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test('should compare with <', () => {
|
|
219
|
+
const expr: ConditionExpression = { left: 'store.count', operator: '<', right: 50 };
|
|
220
|
+
expect(evaluateExpression(expr, context).passed).toBe(true);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test('should compare with >=', () => {
|
|
224
|
+
const expr: ConditionExpression = { left: 'store.count', operator: '>=', right: 42 };
|
|
225
|
+
expect(evaluateExpression(expr, context).passed).toBe(true);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test('should compare with <=', () => {
|
|
229
|
+
const expr: ConditionExpression = { left: 'store.count', operator: '<=', right: 42 };
|
|
230
|
+
expect(evaluateExpression(expr, context).passed).toBe(true);
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
describe('contains operator', () => {
|
|
235
|
+
test('should find substring case-insensitively', () => {
|
|
236
|
+
const expr: ConditionExpression = { left: 'store.name', operator: 'contains', right: 'OH' };
|
|
237
|
+
expect(evaluateExpression(expr, context).passed).toBe(true);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test('should find substring case-sensitively when specified', () => {
|
|
241
|
+
const expr: ConditionExpression = {
|
|
242
|
+
left: 'store.name',
|
|
243
|
+
operator: 'contains',
|
|
244
|
+
right: 'OH',
|
|
245
|
+
caseSensitive: true,
|
|
246
|
+
};
|
|
247
|
+
expect(evaluateExpression(expr, context).passed).toBe(false);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test('should return false when substring not found', () => {
|
|
251
|
+
const expr: ConditionExpression = { left: 'store.name', operator: 'contains', right: 'xyz' };
|
|
252
|
+
expect(evaluateExpression(expr, context).passed).toBe(false);
|
|
253
|
+
});
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
describe('matches operator', () => {
|
|
257
|
+
test('should match regex pattern', () => {
|
|
258
|
+
const expr: ConditionExpression = {
|
|
259
|
+
left: 'store.name',
|
|
260
|
+
operator: 'matches',
|
|
261
|
+
right: '^[A-Z]',
|
|
262
|
+
};
|
|
263
|
+
expect(evaluateExpression(expr, context).passed).toBe(true);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
test('should be case-insensitive by default', () => {
|
|
267
|
+
const expr: ConditionExpression = {
|
|
268
|
+
left: 'store.name',
|
|
269
|
+
operator: 'matches',
|
|
270
|
+
right: '^john$',
|
|
271
|
+
};
|
|
272
|
+
expect(evaluateExpression(expr, context).passed).toBe(true);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test('should respect caseSensitive flag', () => {
|
|
276
|
+
const expr: ConditionExpression = {
|
|
277
|
+
left: 'store.name',
|
|
278
|
+
operator: 'matches',
|
|
279
|
+
right: '^john$',
|
|
280
|
+
caseSensitive: true,
|
|
281
|
+
};
|
|
282
|
+
expect(evaluateExpression(expr, context).passed).toBe(false);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
test('should handle invalid regex', () => {
|
|
286
|
+
const expr: ConditionExpression = { left: 'store.name', operator: 'matches', right: '[' };
|
|
287
|
+
expect(evaluateExpression(expr, context).passed).toBe(false);
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
describe('evaluateCondition', () => {
|
|
293
|
+
const context: ResponseStoreContext = {
|
|
294
|
+
status: '200',
|
|
295
|
+
userId: '123',
|
|
296
|
+
type: 'admin',
|
|
297
|
+
enabled: 'true',
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
describe('string shorthand', () => {
|
|
301
|
+
test('should evaluate valid string condition', () => {
|
|
302
|
+
const result = evaluateCondition('store.status == 200', context);
|
|
303
|
+
expect(result.shouldRun).toBe(true);
|
|
304
|
+
expect(result.reason).toBeUndefined();
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
test('should return false with reason for failing condition', () => {
|
|
308
|
+
const result = evaluateCondition('store.status == 404', context);
|
|
309
|
+
expect(result.shouldRun).toBe(false);
|
|
310
|
+
expect(result.reason).toContain('condition not met');
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
test('should return false for invalid syntax', () => {
|
|
314
|
+
const result = evaluateCondition('invalid syntax', context);
|
|
315
|
+
expect(result.shouldRun).toBe(false);
|
|
316
|
+
expect(result.reason).toContain('invalid condition syntax');
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
describe('single condition object', () => {
|
|
321
|
+
test('should evaluate single condition', () => {
|
|
322
|
+
const condition: WhenCondition = {
|
|
323
|
+
left: 'store.userId',
|
|
324
|
+
operator: 'exists',
|
|
325
|
+
};
|
|
326
|
+
expect(evaluateCondition(condition, context).shouldRun).toBe(true);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
test('should fail single condition', () => {
|
|
330
|
+
const condition: WhenCondition = {
|
|
331
|
+
left: 'store.missing',
|
|
332
|
+
operator: 'exists',
|
|
333
|
+
};
|
|
334
|
+
const result = evaluateCondition(condition, context);
|
|
335
|
+
expect(result.shouldRun).toBe(false);
|
|
336
|
+
expect(result.reason).toContain('condition not met');
|
|
337
|
+
});
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
describe('all (AND) conditions', () => {
|
|
341
|
+
test('should pass when all conditions true', () => {
|
|
342
|
+
const condition: WhenCondition = {
|
|
343
|
+
all: [
|
|
344
|
+
{ left: 'store.status', operator: '==', right: 200 },
|
|
345
|
+
{ left: 'store.userId', operator: 'exists' },
|
|
346
|
+
{ left: 'store.type', operator: '==', right: 'admin' },
|
|
347
|
+
],
|
|
348
|
+
};
|
|
349
|
+
expect(evaluateCondition(condition, context).shouldRun).toBe(true);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
test('should fail when any condition false (short-circuit)', () => {
|
|
353
|
+
const condition: WhenCondition = {
|
|
354
|
+
all: [
|
|
355
|
+
{ left: 'store.status', operator: '==', right: 200 },
|
|
356
|
+
{ left: 'store.status', operator: '==', right: 404 }, // fails
|
|
357
|
+
{ left: 'store.userId', operator: 'exists' },
|
|
358
|
+
],
|
|
359
|
+
};
|
|
360
|
+
const result = evaluateCondition(condition, context);
|
|
361
|
+
expect(result.shouldRun).toBe(false);
|
|
362
|
+
expect(result.reason).toContain('store.status == 404');
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
describe('any (OR) conditions', () => {
|
|
367
|
+
test('should pass when any condition true (short-circuit)', () => {
|
|
368
|
+
const condition: WhenCondition = {
|
|
369
|
+
any: [
|
|
370
|
+
{ left: 'store.status', operator: '==', right: 404 }, // fails
|
|
371
|
+
{ left: 'store.status', operator: '==', right: 200 }, // passes
|
|
372
|
+
{ left: 'store.status', operator: '==', right: 500 }, // skipped
|
|
373
|
+
],
|
|
374
|
+
};
|
|
375
|
+
expect(evaluateCondition(condition, context).shouldRun).toBe(true);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
test('should fail when all conditions false', () => {
|
|
379
|
+
const condition: WhenCondition = {
|
|
380
|
+
any: [
|
|
381
|
+
{ left: 'store.status', operator: '==', right: 404 },
|
|
382
|
+
{ left: 'store.status', operator: '==', right: 500 },
|
|
383
|
+
],
|
|
384
|
+
};
|
|
385
|
+
const result = evaluateCondition(condition, context);
|
|
386
|
+
expect(result.shouldRun).toBe(false);
|
|
387
|
+
expect(result.reason).toContain('no conditions met');
|
|
388
|
+
});
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
describe('edge cases', () => {
|
|
392
|
+
test('should run when no valid condition specified', () => {
|
|
393
|
+
const condition: WhenCondition = {};
|
|
394
|
+
expect(evaluateCondition(condition, context).shouldRun).toBe(true);
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
test('should handle empty all array', () => {
|
|
398
|
+
const condition: WhenCondition = { all: [] };
|
|
399
|
+
expect(evaluateCondition(condition, context).shouldRun).toBe(true);
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
test('should handle empty any array', () => {
|
|
403
|
+
const condition: WhenCondition = { any: [] };
|
|
404
|
+
expect(evaluateCondition(condition, context).shouldRun).toBe(true);
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
test('should handle nested JSON in store', () => {
|
|
408
|
+
const nestedContext: ResponseStoreContext = {
|
|
409
|
+
body: '{"user":{"id":123,"role":"admin"}}',
|
|
410
|
+
};
|
|
411
|
+
const result = evaluateCondition('store.body.user.role == admin', nestedContext);
|
|
412
|
+
expect(result.shouldRun).toBe(true);
|
|
413
|
+
});
|
|
414
|
+
});
|
|
415
|
+
});
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ConditionExpression,
|
|
3
|
+
ConditionOperator,
|
|
4
|
+
ResponseStoreContext,
|
|
5
|
+
WhenCondition,
|
|
6
|
+
} from '../types/config';
|
|
7
|
+
import { getValueByPath, valueToString } from './response-store';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Result of evaluating a condition.
|
|
11
|
+
*/
|
|
12
|
+
export interface ConditionResult {
|
|
13
|
+
shouldRun: boolean;
|
|
14
|
+
reason?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Parses a string shorthand condition into a ConditionExpression.
|
|
19
|
+
* Supports formats like:
|
|
20
|
+
* - "store.status == 200"
|
|
21
|
+
* - "store.userId exists"
|
|
22
|
+
* - "store.body.type contains user"
|
|
23
|
+
* - "store.version >= 2"
|
|
24
|
+
*
|
|
25
|
+
* @param condition - String condition to parse
|
|
26
|
+
* @returns Parsed ConditionExpression or null if invalid
|
|
27
|
+
*/
|
|
28
|
+
export function parseStringCondition(condition: string): ConditionExpression | null {
|
|
29
|
+
const trimmed = condition.trim();
|
|
30
|
+
|
|
31
|
+
// Check for exists/not-exists operators first (unary)
|
|
32
|
+
const existsMatch = trimmed.match(/^(.+?)\s+(exists|not-exists)$/i);
|
|
33
|
+
if (existsMatch) {
|
|
34
|
+
return {
|
|
35
|
+
left: existsMatch[1].trim(),
|
|
36
|
+
operator: existsMatch[2].toLowerCase() as ConditionOperator,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Binary operators pattern
|
|
41
|
+
const operatorPattern = /^(.+?)\s*(==|!=|>=|<=|>|<|contains|matches)\s*(.+)$/i;
|
|
42
|
+
const match = trimmed.match(operatorPattern);
|
|
43
|
+
|
|
44
|
+
if (!match) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const [, left, operator, right] = match;
|
|
49
|
+
const rightValue = parseRightValue(right.trim());
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
left: left.trim(),
|
|
53
|
+
operator: operator.toLowerCase() as ConditionOperator,
|
|
54
|
+
right: rightValue,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Parses the right-hand value, converting to appropriate type.
|
|
60
|
+
*/
|
|
61
|
+
function parseRightValue(value: string): string | number | boolean {
|
|
62
|
+
// Boolean
|
|
63
|
+
if (value === 'true') {
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
if (value === 'false') {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Number
|
|
71
|
+
const num = Number(value);
|
|
72
|
+
if (!Number.isNaN(num) && value !== '') {
|
|
73
|
+
return num;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// String - remove quotes if present
|
|
77
|
+
if (
|
|
78
|
+
(value.startsWith('"') && value.endsWith('"')) ||
|
|
79
|
+
(value.startsWith("'") && value.endsWith("'"))
|
|
80
|
+
) {
|
|
81
|
+
return value.slice(1, -1);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return value;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Gets a value from the store context using a path.
|
|
89
|
+
* Handles "store.x" prefix by stripping it.
|
|
90
|
+
*/
|
|
91
|
+
function getStoreValue(path: string, context: ResponseStoreContext): unknown {
|
|
92
|
+
// Strip "store." prefix if present
|
|
93
|
+
const normalizedPath = path.startsWith('store.') ? path.slice(6) : path;
|
|
94
|
+
|
|
95
|
+
// First check if it's a direct key in context
|
|
96
|
+
if (normalizedPath in context) {
|
|
97
|
+
const value = context[normalizedPath];
|
|
98
|
+
// Try to parse JSON if it looks like an object/array
|
|
99
|
+
if (value.startsWith('{') || value.startsWith('[')) {
|
|
100
|
+
try {
|
|
101
|
+
return JSON.parse(value);
|
|
102
|
+
} catch {
|
|
103
|
+
return value;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// Try to parse as number
|
|
107
|
+
const num = Number(value);
|
|
108
|
+
if (!Number.isNaN(num) && value !== '') {
|
|
109
|
+
return num;
|
|
110
|
+
}
|
|
111
|
+
// Try to parse as boolean
|
|
112
|
+
if (value === 'true') {
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
if (value === 'false') {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
return value;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Handle nested paths like "body.data.id" where context has "body" as JSON string
|
|
122
|
+
const parts = normalizedPath.split('.');
|
|
123
|
+
const rootKey = parts[0];
|
|
124
|
+
|
|
125
|
+
if (rootKey in context) {
|
|
126
|
+
const rootValue = context[rootKey];
|
|
127
|
+
// Try to parse as JSON and navigate
|
|
128
|
+
try {
|
|
129
|
+
const parsed = JSON.parse(rootValue);
|
|
130
|
+
return getValueByPath(parsed, parts.slice(1).join('.'));
|
|
131
|
+
} catch {
|
|
132
|
+
// Not JSON, can't navigate further
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Compares two values based on case sensitivity setting.
|
|
142
|
+
*/
|
|
143
|
+
function compareStrings(a: string, b: string, caseSensitive: boolean): boolean {
|
|
144
|
+
if (caseSensitive) {
|
|
145
|
+
return a === b;
|
|
146
|
+
}
|
|
147
|
+
return a.toLowerCase() === b.toLowerCase();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Evaluates a single condition expression against the store context.
|
|
152
|
+
*/
|
|
153
|
+
export function evaluateExpression(
|
|
154
|
+
expr: ConditionExpression,
|
|
155
|
+
context: ResponseStoreContext,
|
|
156
|
+
): { passed: boolean; description: string } {
|
|
157
|
+
const leftValue = getStoreValue(expr.left, context);
|
|
158
|
+
const caseSensitive = expr.caseSensitive ?? false;
|
|
159
|
+
|
|
160
|
+
const formatValue = (v: unknown): string => {
|
|
161
|
+
if (v === undefined) {
|
|
162
|
+
return 'undefined';
|
|
163
|
+
}
|
|
164
|
+
if (v === null) {
|
|
165
|
+
return 'null';
|
|
166
|
+
}
|
|
167
|
+
if (typeof v === 'string') {
|
|
168
|
+
return `"${v}"`;
|
|
169
|
+
}
|
|
170
|
+
return String(v);
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const description = `${expr.left} ${expr.operator}${expr.right !== undefined ? ` ${formatValue(expr.right)}` : ''}`;
|
|
174
|
+
|
|
175
|
+
switch (expr.operator) {
|
|
176
|
+
case 'exists': {
|
|
177
|
+
const passed = leftValue !== undefined && leftValue !== null && leftValue !== '';
|
|
178
|
+
return { passed, description };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
case 'not-exists': {
|
|
182
|
+
const passed = leftValue === undefined || leftValue === null || leftValue === '';
|
|
183
|
+
return { passed, description };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
case '==': {
|
|
187
|
+
let passed: boolean;
|
|
188
|
+
if (typeof leftValue === 'string' && typeof expr.right === 'string') {
|
|
189
|
+
passed = compareStrings(leftValue, expr.right, caseSensitive);
|
|
190
|
+
} else {
|
|
191
|
+
// biome-ignore lint/suspicious/noDoubleEquals: intentional loose equality for type coercion
|
|
192
|
+
passed = leftValue == expr.right;
|
|
193
|
+
}
|
|
194
|
+
return { passed, description };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
case '!=': {
|
|
198
|
+
let passed: boolean;
|
|
199
|
+
if (typeof leftValue === 'string' && typeof expr.right === 'string') {
|
|
200
|
+
passed = !compareStrings(leftValue, expr.right, caseSensitive);
|
|
201
|
+
} else {
|
|
202
|
+
// biome-ignore lint/suspicious/noDoubleEquals: intentional loose equality for type coercion
|
|
203
|
+
passed = leftValue != expr.right;
|
|
204
|
+
}
|
|
205
|
+
return { passed, description };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
case '>': {
|
|
209
|
+
const passed = Number(leftValue) > Number(expr.right);
|
|
210
|
+
return { passed, description };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
case '<': {
|
|
214
|
+
const passed = Number(leftValue) < Number(expr.right);
|
|
215
|
+
return { passed, description };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
case '>=': {
|
|
219
|
+
const passed = Number(leftValue) >= Number(expr.right);
|
|
220
|
+
return { passed, description };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
case '<=': {
|
|
224
|
+
const passed = Number(leftValue) <= Number(expr.right);
|
|
225
|
+
return { passed, description };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
case 'contains': {
|
|
229
|
+
const leftStr = valueToString(leftValue);
|
|
230
|
+
const rightStr = String(expr.right ?? '');
|
|
231
|
+
const passed = caseSensitive
|
|
232
|
+
? leftStr.includes(rightStr)
|
|
233
|
+
: leftStr.toLowerCase().includes(rightStr.toLowerCase());
|
|
234
|
+
return { passed, description };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
case 'matches': {
|
|
238
|
+
const leftStr = valueToString(leftValue);
|
|
239
|
+
const pattern = String(expr.right ?? '');
|
|
240
|
+
try {
|
|
241
|
+
const flags = caseSensitive ? '' : 'i';
|
|
242
|
+
const regex = new RegExp(pattern, flags);
|
|
243
|
+
const passed = regex.test(leftStr);
|
|
244
|
+
return { passed, description };
|
|
245
|
+
} catch {
|
|
246
|
+
// Invalid regex pattern
|
|
247
|
+
return { passed: false, description: `${description} (invalid regex)` };
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
default:
|
|
252
|
+
return { passed: false, description: `unknown operator: ${expr.operator}` };
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Evaluates a WhenCondition against the store context.
|
|
258
|
+
*
|
|
259
|
+
* @param condition - The condition to evaluate
|
|
260
|
+
* @param context - The store context with values from previous requests
|
|
261
|
+
* @returns Result indicating whether the request should run
|
|
262
|
+
*/
|
|
263
|
+
export function evaluateCondition(
|
|
264
|
+
condition: WhenCondition | string,
|
|
265
|
+
context: ResponseStoreContext,
|
|
266
|
+
): ConditionResult {
|
|
267
|
+
// Handle string shorthand
|
|
268
|
+
if (typeof condition === 'string') {
|
|
269
|
+
const parsed = parseStringCondition(condition);
|
|
270
|
+
if (!parsed) {
|
|
271
|
+
return { shouldRun: false, reason: `invalid condition syntax: "${condition}"` };
|
|
272
|
+
}
|
|
273
|
+
const result = evaluateExpression(parsed, context);
|
|
274
|
+
return {
|
|
275
|
+
shouldRun: result.passed,
|
|
276
|
+
reason: result.passed ? undefined : `condition not met: ${result.description}`,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Handle compound "all" condition (AND logic with short-circuit)
|
|
281
|
+
if (condition.all && condition.all.length > 0) {
|
|
282
|
+
for (const expr of condition.all) {
|
|
283
|
+
const result = evaluateExpression(expr, context);
|
|
284
|
+
if (!result.passed) {
|
|
285
|
+
return {
|
|
286
|
+
shouldRun: false,
|
|
287
|
+
reason: `condition not met: ${result.description}`,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
return { shouldRun: true };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Handle compound "any" condition (OR logic with short-circuit)
|
|
295
|
+
if (condition.any && condition.any.length > 0) {
|
|
296
|
+
const descriptions: string[] = [];
|
|
297
|
+
for (const expr of condition.any) {
|
|
298
|
+
const result = evaluateExpression(expr, context);
|
|
299
|
+
if (result.passed) {
|
|
300
|
+
return { shouldRun: true };
|
|
301
|
+
}
|
|
302
|
+
descriptions.push(result.description);
|
|
303
|
+
}
|
|
304
|
+
return {
|
|
305
|
+
shouldRun: false,
|
|
306
|
+
reason: `no conditions met: ${descriptions.join(', ')}`,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Handle single condition (inline left/operator/right)
|
|
311
|
+
if (condition.left && condition.operator) {
|
|
312
|
+
const expr: ConditionExpression = {
|
|
313
|
+
left: condition.left,
|
|
314
|
+
operator: condition.operator,
|
|
315
|
+
right: condition.right,
|
|
316
|
+
caseSensitive: condition.caseSensitive,
|
|
317
|
+
};
|
|
318
|
+
const result = evaluateExpression(expr, context);
|
|
319
|
+
return {
|
|
320
|
+
shouldRun: result.passed,
|
|
321
|
+
reason: result.passed ? undefined : `condition not met: ${result.description}`,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// No valid condition found - default to run
|
|
326
|
+
return { shouldRun: true };
|
|
327
|
+
}
|
package/src/utils/logger.ts
CHANGED
|
@@ -573,6 +573,7 @@ export class Logger {
|
|
|
573
573
|
total: summary.total,
|
|
574
574
|
successful: summary.successful,
|
|
575
575
|
failed: summary.failed,
|
|
576
|
+
skipped: summary.skipped,
|
|
576
577
|
duration: summary.duration,
|
|
577
578
|
},
|
|
578
579
|
results: summary.results.map((result) => ({
|
|
@@ -608,10 +609,11 @@ export class Logger {
|
|
|
608
609
|
if (level === 'minimal') {
|
|
609
610
|
// Simple one-line summary for minimal, similar to docs example
|
|
610
611
|
const statusColor = summary.failed === 0 ? 'green' : 'red';
|
|
612
|
+
const skippedText = summary.skipped > 0 ? `, ${summary.skipped} skipped` : '';
|
|
611
613
|
const successText =
|
|
612
614
|
summary.failed === 0
|
|
613
|
-
? `${summary.total} request${summary.total === 1 ? '' : 's'} completed successfully`
|
|
614
|
-
: `${summary.successful}/${summary.total} request${summary.total === 1 ? '' : 's'} completed, ${summary.failed} failed`;
|
|
615
|
+
? `${summary.total} request${summary.total === 1 ? '' : 's'} completed successfully${skippedText}`
|
|
616
|
+
: `${summary.successful}/${summary.total} request${summary.total === 1 ? '' : 's'} completed, ${summary.failed} failed${skippedText}`;
|
|
615
617
|
|
|
616
618
|
const summaryPrefix = isGlobal ? '◆ Global Summary' : 'Summary';
|
|
617
619
|
console.log(`${summaryPrefix}: ${this.color(successText, statusColor)}`);
|
|
@@ -621,10 +623,11 @@ export class Logger {
|
|
|
621
623
|
// Compact summary for standard/detailed - much simpler
|
|
622
624
|
const _successRate = ((summary.successful / summary.total) * 100).toFixed(1);
|
|
623
625
|
const statusColor = summary.failed === 0 ? 'green' : 'red';
|
|
626
|
+
const skippedText = summary.skipped > 0 ? `, ${summary.skipped} skipped` : '';
|
|
624
627
|
const successText =
|
|
625
628
|
summary.failed === 0
|
|
626
|
-
? `${summary.total} request${summary.total === 1 ? '' : 's'} completed successfully`
|
|
627
|
-
: `${summary.successful}/${summary.total} request${summary.total === 1 ? '' : 's'} completed, ${summary.failed} failed`;
|
|
629
|
+
? `${summary.total} request${summary.total === 1 ? '' : 's'} completed successfully${skippedText}`
|
|
630
|
+
: `${summary.successful}/${summary.total} request${summary.total === 1 ? '' : 's'} completed, ${summary.failed} failed${skippedText}`;
|
|
628
631
|
|
|
629
632
|
const summaryPrefix = isGlobal ? '◆ Global Summary' : 'Summary';
|
|
630
633
|
console.log();
|
|
@@ -658,6 +661,41 @@ export class Logger {
|
|
|
658
661
|
console.log(this.color(`✓ ${message}`, 'green'));
|
|
659
662
|
}
|
|
660
663
|
|
|
664
|
+
logSkipped(config: RequestConfig, index: number, reason?: string): void {
|
|
665
|
+
if (!this.shouldShowOutput()) {
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
const name = config.name || `Request ${index}`;
|
|
670
|
+
|
|
671
|
+
if (this.config.format === 'json') {
|
|
672
|
+
const jsonResult = {
|
|
673
|
+
request: {
|
|
674
|
+
name: config.name,
|
|
675
|
+
url: config.url,
|
|
676
|
+
method: config.method || 'GET',
|
|
677
|
+
},
|
|
678
|
+
skipped: true,
|
|
679
|
+
reason: reason || 'condition not met',
|
|
680
|
+
};
|
|
681
|
+
console.log(JSON.stringify(jsonResult, null, 2));
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// Pretty format
|
|
686
|
+
console.log(
|
|
687
|
+
`${this.color('⊘', 'yellow')} ${this.color(name, 'bright')} ${this.color('[SKIP]', 'yellow')}`,
|
|
688
|
+
);
|
|
689
|
+
|
|
690
|
+
if (reason) {
|
|
691
|
+
const treeNodes: TreeNode[] = [{ label: 'Reason', value: reason, color: 'yellow' }];
|
|
692
|
+
const renderer = new TreeRenderer(this.colors);
|
|
693
|
+
renderer.render(treeNodes);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
console.log();
|
|
697
|
+
}
|
|
698
|
+
|
|
661
699
|
logFileHeader(fileName: string, requestCount: number): void {
|
|
662
700
|
if (!this.shouldShowOutput() || this.config.format !== 'pretty') {
|
|
663
701
|
return;
|