norn-cli 1.6.0 → 1.6.2
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/AGENTS.md +9 -1
- package/CHANGELOG.md +23 -0
- package/dist/cli.js +246 -80
- package/package.json +1 -1
- package/out/assertionRunner.js +0 -537
- package/out/chatParticipant.js +0 -722
- package/out/cli/colors.js +0 -129
- package/out/cli/formatters/assertion.js +0 -75
- package/out/cli/formatters/index.js +0 -23
- package/out/cli/formatters/response.js +0 -106
- package/out/cli/formatters/summary.js +0 -187
- package/out/cli/redaction.js +0 -237
- package/out/cli/reporters/html.js +0 -634
- package/out/cli/reporters/index.js +0 -22
- package/out/cli/reporters/junit.js +0 -211
- package/out/cli.js +0 -989
- package/out/codeLensProvider.js +0 -248
- package/out/compareContentProvider.js +0 -85
- package/out/completionProvider.js +0 -2404
- package/out/contractDecorationProvider.js +0 -243
- package/out/coverageCalculator.js +0 -837
- package/out/coveragePanel.js +0 -545
- package/out/diagnosticProvider.js +0 -1113
- package/out/environmentProvider.js +0 -442
- package/out/extension.js +0 -1114
- package/out/httpClient.js +0 -269
- package/out/jsonFileReader.js +0 -320
- package/out/nornPrompt.js +0 -580
- package/out/nornapiParser.js +0 -326
- package/out/parser.js +0 -725
- package/out/responsePanel.js +0 -4674
- package/out/schemaGenerator.js +0 -393
- package/out/scriptRunner.js +0 -419
- package/out/sequenceRunner.js +0 -3046
- package/out/swaggerBodyIntellisenseCache.js +0 -147
- package/out/swaggerParser.js +0 -419
- package/out/test/coverageCalculator.test.js +0 -100
- package/out/test/extension.test.js +0 -48
- package/out/testProvider.js +0 -658
- package/out/validationCache.js +0 -245
package/out/sequenceRunner.js
DELETED
|
@@ -1,3046 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.extractReturnVariables = extractReturnVariables;
|
|
4
|
-
exports.bindSequenceArguments = bindSequenceArguments;
|
|
5
|
-
exports.parseTagFilter = parseTagFilter;
|
|
6
|
-
exports.sequenceMatchesTags = sequenceMatchesTags;
|
|
7
|
-
exports.validateSequenceParameters = validateSequenceParameters;
|
|
8
|
-
exports.extractSequences = extractSequences;
|
|
9
|
-
exports.getSequenceAtLine = getSequenceAtLine;
|
|
10
|
-
exports.countSequenceSteps = countSequenceSteps;
|
|
11
|
-
exports.runSequence = runSequence;
|
|
12
|
-
exports.runSequenceWithJar = runSequenceWithJar;
|
|
13
|
-
const parser_1 = require("./parser");
|
|
14
|
-
const httpClient_1 = require("./httpClient");
|
|
15
|
-
const scriptRunner_1 = require("./scriptRunner");
|
|
16
|
-
const assertionRunner_1 = require("./assertionRunner");
|
|
17
|
-
const jsonFileReader_1 = require("./jsonFileReader");
|
|
18
|
-
const nornapiParser_1 = require("./nornapiParser");
|
|
19
|
-
/**
|
|
20
|
-
* Applies header groups found in request text to a parsed request.
|
|
21
|
-
* Handles lines like "Json" or "Auth" that are header group names.
|
|
22
|
-
* Also strips header group names from the URL if they appear on the method line.
|
|
23
|
-
*/
|
|
24
|
-
function applyHeaderGroupsToRequest(parsed, requestText, headerGroups, variables) {
|
|
25
|
-
const lines = requestText.split('\n');
|
|
26
|
-
const headerGroupNames = headerGroups.map(hg => hg.name);
|
|
27
|
-
const headerGroupHeaders = {};
|
|
28
|
-
const foundGroups = [];
|
|
29
|
-
for (const line of lines) {
|
|
30
|
-
const trimmed = line.trim();
|
|
31
|
-
// Skip empty lines, comments, variable lines
|
|
32
|
-
if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith('var ')) {
|
|
33
|
-
continue;
|
|
34
|
-
}
|
|
35
|
-
// Check if this is the HTTP method line - look for header groups at the end
|
|
36
|
-
const methodMatch = trimmed.match(/^(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+(.+)$/i);
|
|
37
|
-
if (methodMatch) {
|
|
38
|
-
const afterMethod = methodMatch[2];
|
|
39
|
-
// Split by space and check trailing tokens for header group names
|
|
40
|
-
const tokens = afterMethod.split(/\s+/);
|
|
41
|
-
// Scan from the end to find header groups
|
|
42
|
-
for (let i = tokens.length - 1; i >= 0; i--) {
|
|
43
|
-
if (headerGroupNames.includes(tokens[i])) {
|
|
44
|
-
foundGroups.push(tokens[i]);
|
|
45
|
-
}
|
|
46
|
-
else {
|
|
47
|
-
break; // Stop at first non-header-group token from the end
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
continue;
|
|
51
|
-
}
|
|
52
|
-
// Skip lines that look like headers (Name: Value)
|
|
53
|
-
if (/^[A-Za-z0-9\-_]+\s*:\s*.+$/.test(trimmed)) {
|
|
54
|
-
continue;
|
|
55
|
-
}
|
|
56
|
-
// Skip lines that look like JSON body
|
|
57
|
-
if (trimmed.startsWith('{') || trimmed.startsWith('[') || trimmed.startsWith('"')) {
|
|
58
|
-
continue;
|
|
59
|
-
}
|
|
60
|
-
// Check if this line contains header group names (can be multiple space-separated)
|
|
61
|
-
const potentialGroups = trimmed.split(/\s+/);
|
|
62
|
-
for (const groupName of potentialGroups) {
|
|
63
|
-
if (headerGroupNames.includes(groupName)) {
|
|
64
|
-
foundGroups.push(groupName);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
// Collect headers from all found groups
|
|
69
|
-
for (const groupName of foundGroups) {
|
|
70
|
-
const group = headerGroups.find(hg => hg.name === groupName);
|
|
71
|
-
if (group) {
|
|
72
|
-
const resolvedHeaders = (0, nornapiParser_1.resolveHeaderValues)(group, variables);
|
|
73
|
-
Object.assign(headerGroupHeaders, resolvedHeaders);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
// Strip header group names from the already-resolved URL
|
|
77
|
-
let modifiedUrl = parsed.url;
|
|
78
|
-
for (const groupName of foundGroups) {
|
|
79
|
-
// Remove the header group name from URL (at the end with possible leading space)
|
|
80
|
-
const endPattern = new RegExp(`\\s+${groupName}$`);
|
|
81
|
-
modifiedUrl = modifiedUrl.replace(endPattern, '');
|
|
82
|
-
}
|
|
83
|
-
modifiedUrl = modifiedUrl.trim();
|
|
84
|
-
// Build the result - update URL if modified, merge headers
|
|
85
|
-
const result = {
|
|
86
|
-
...parsed,
|
|
87
|
-
url: modifiedUrl
|
|
88
|
-
};
|
|
89
|
-
if (Object.keys(headerGroupHeaders).length > 0) {
|
|
90
|
-
// Header groups go first, inline headers take precedence
|
|
91
|
-
result.headers = { ...headerGroupHeaders, ...parsed.headers };
|
|
92
|
-
}
|
|
93
|
-
return result;
|
|
94
|
-
}
|
|
95
|
-
/**
|
|
96
|
-
* Checks if a line is a "run <RequestName>" or "run <Name>(args)" command (for named requests/sequences)
|
|
97
|
-
* Also supports retry/backoff options: "run Name retry 3 backoff 500 ms"
|
|
98
|
-
*/
|
|
99
|
-
function isRunNamedRequestCommand(line) {
|
|
100
|
-
const trimmed = line.trim();
|
|
101
|
-
// Match "run <name>" or "run <name>(args)" but NOT "run bash", "run js", "run powershell"
|
|
102
|
-
if (!trimmed.toLowerCase().startsWith('run ')) {
|
|
103
|
-
return false;
|
|
104
|
-
}
|
|
105
|
-
const afterRun = trimmed.substring(4).trim();
|
|
106
|
-
// Exclude script commands
|
|
107
|
-
if (/^(bash|js|powershell)\s+/i.test(afterRun)) {
|
|
108
|
-
return false;
|
|
109
|
-
}
|
|
110
|
-
// Must have a valid identifier (optionally followed by args in parentheses)
|
|
111
|
-
// Also allow retry/backoff options at the end - use specific patterns
|
|
112
|
-
return /^[a-zA-Z_][a-zA-Z0-9_-]*(?:\s*\([^)]*\))?(?:\s+retry\s+\d+)?(?:\s+backoff\s+\d+(?:\.\d+)?\s*(?:s|ms|seconds?|milliseconds?)?)?$/i.test(afterRun);
|
|
113
|
-
}
|
|
114
|
-
/**
|
|
115
|
-
* Parses a "run <RequestName>" or "run <Name>(args)" command.
|
|
116
|
-
* Returns the request name, optional arguments, and retry/backoff options.
|
|
117
|
-
*/
|
|
118
|
-
function parseRunNamedRequestCommand(line) {
|
|
119
|
-
const trimmed = line.trim();
|
|
120
|
-
// Extract retry/backoff options first using the shared function
|
|
121
|
-
const { cleanedLine, retryCount, backoffMs } = (0, parser_1.extractRetryOptions)(trimmed);
|
|
122
|
-
// Now parse the cleaned line for name and args
|
|
123
|
-
const match = cleanedLine.match(/^run\s+([a-zA-Z_][a-zA-Z0-9_-]*)(?:\s*\(([^)]*)\))?$/i);
|
|
124
|
-
if (!match) {
|
|
125
|
-
return null;
|
|
126
|
-
}
|
|
127
|
-
return {
|
|
128
|
-
name: match[1],
|
|
129
|
-
args: parseRunArguments(match[2] || ''),
|
|
130
|
-
retryCount,
|
|
131
|
-
backoffMs
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
/**
|
|
135
|
-
* Checks if a line is a return statement (return var1, var2)
|
|
136
|
-
*/
|
|
137
|
-
function isReturnStatement(line) {
|
|
138
|
-
return /^return\s+/i.test(line.trim());
|
|
139
|
-
}
|
|
140
|
-
/**
|
|
141
|
-
* Parses a return statement and extracts the return expressions.
|
|
142
|
-
* Supports:
|
|
143
|
-
* return token, userId - variable names
|
|
144
|
-
* return repaymentId.value - path into variable
|
|
145
|
-
* return {{repaymentId.value}} - template syntax (also supported)
|
|
146
|
-
*/
|
|
147
|
-
function parseReturnStatement(line) {
|
|
148
|
-
const trimmed = line.trim();
|
|
149
|
-
const match = trimmed.match(/^return\s+(.+)$/i);
|
|
150
|
-
if (!match) {
|
|
151
|
-
return null;
|
|
152
|
-
}
|
|
153
|
-
// Split by comma and trim each expression
|
|
154
|
-
// Also strip {{ }} if present for convenience
|
|
155
|
-
return match[1].split(',').map(v => {
|
|
156
|
-
let expr = v.trim();
|
|
157
|
-
// Remove {{ }} wrapper if present
|
|
158
|
-
if (expr.startsWith('{{') && expr.endsWith('}}')) {
|
|
159
|
-
expr = expr.slice(2, -2).trim();
|
|
160
|
-
}
|
|
161
|
-
return expr;
|
|
162
|
-
}).filter(v => v.length > 0);
|
|
163
|
-
}
|
|
164
|
-
/**
|
|
165
|
-
* Resolves a return expression to get the actual value.
|
|
166
|
-
* Handles:
|
|
167
|
-
* - Simple variable: "token" -> runtimeVariables["token"]
|
|
168
|
-
* - Path expression: "repaymentId.value" -> parse JSON and get nested value
|
|
169
|
-
* - Array access: "id[0].prop" -> array and object navigation
|
|
170
|
-
*/
|
|
171
|
-
function resolveReturnExpression(expr, runtimeVariables) {
|
|
172
|
-
// Extract the base variable name (before any . or [)
|
|
173
|
-
const varNameMatch = expr.match(/^([a-zA-Z_][a-zA-Z0-9_]*)/);
|
|
174
|
-
if (!varNameMatch) {
|
|
175
|
-
return null;
|
|
176
|
-
}
|
|
177
|
-
const varName = varNameMatch[1];
|
|
178
|
-
const pathPart = expr.substring(varName.length);
|
|
179
|
-
if (!(varName in runtimeVariables)) {
|
|
180
|
-
return null;
|
|
181
|
-
}
|
|
182
|
-
const varValue = runtimeVariables[varName];
|
|
183
|
-
// Simple variable (no path)
|
|
184
|
-
if (!pathPart) {
|
|
185
|
-
return { key: varName, value: varValue };
|
|
186
|
-
}
|
|
187
|
-
// Navigate the path
|
|
188
|
-
try {
|
|
189
|
-
// Parse the value if it's a string (could be JSON)
|
|
190
|
-
let current = varValue;
|
|
191
|
-
if (typeof current === 'string') {
|
|
192
|
-
try {
|
|
193
|
-
current = JSON.parse(current);
|
|
194
|
-
}
|
|
195
|
-
catch {
|
|
196
|
-
// Not JSON, keep as string
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
// Parse the path: convert [0] to .0 and split by .
|
|
200
|
-
const normalizedPath = pathPart.replace(/^\.|^\[/, m => m === '.' ? '' : '[').replace(/\[(\d+)\]/g, '.$1');
|
|
201
|
-
const parts = normalizedPath.split('.').filter(p => p !== '');
|
|
202
|
-
for (const part of parts) {
|
|
203
|
-
if (current === null || current === undefined) {
|
|
204
|
-
return null;
|
|
205
|
-
}
|
|
206
|
-
current = current[part];
|
|
207
|
-
}
|
|
208
|
-
// Use the last part of the path as the key name
|
|
209
|
-
const keyName = parts[parts.length - 1] || varName;
|
|
210
|
-
return { key: keyName, value: current };
|
|
211
|
-
}
|
|
212
|
-
catch {
|
|
213
|
-
// Navigation failed
|
|
214
|
-
return { key: varName, value: varValue };
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
/**
|
|
218
|
-
* Extracts the return statement from sequence content.
|
|
219
|
-
* Returns the list of return expressions, or null if no return statement.
|
|
220
|
-
*/
|
|
221
|
-
function extractReturnVariables(content) {
|
|
222
|
-
const lines = content.split('\n');
|
|
223
|
-
for (const line of lines) {
|
|
224
|
-
if (isReturnStatement(line)) {
|
|
225
|
-
return parseReturnStatement(line);
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
return null;
|
|
229
|
-
}
|
|
230
|
-
/**
|
|
231
|
-
* Parses arguments from a run command.
|
|
232
|
-
* Supports: run Name(arg1, arg2) or run Name(param: value, param2: value2)
|
|
233
|
-
*/
|
|
234
|
-
function parseRunArguments(argsStr) {
|
|
235
|
-
const args = [];
|
|
236
|
-
if (!argsStr || !argsStr.trim()) {
|
|
237
|
-
return args;
|
|
238
|
-
}
|
|
239
|
-
// Split by comma, respecting quoted strings and {{}}
|
|
240
|
-
const parts = argsStr.split(/,(?=(?:[^"]*"[^"]*")*(?:[^{]*\{\{[^}]*\}\})*[^"]*$)/);
|
|
241
|
-
for (const part of parts) {
|
|
242
|
-
const trimmed = part.trim();
|
|
243
|
-
if (!trimmed)
|
|
244
|
-
continue;
|
|
245
|
-
// Check for named argument: param: value
|
|
246
|
-
const namedMatch = trimmed.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\s*:\s*(.+)$/);
|
|
247
|
-
if (namedMatch) {
|
|
248
|
-
let value = namedMatch[2].trim();
|
|
249
|
-
// Strip quotes if present
|
|
250
|
-
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
251
|
-
(value.startsWith("'") && value.endsWith("'"))) {
|
|
252
|
-
value = value.slice(1, -1);
|
|
253
|
-
}
|
|
254
|
-
args.push({
|
|
255
|
-
name: namedMatch[1],
|
|
256
|
-
value
|
|
257
|
-
});
|
|
258
|
-
}
|
|
259
|
-
else {
|
|
260
|
-
// Positional argument
|
|
261
|
-
let value = trimmed;
|
|
262
|
-
// Strip quotes if present
|
|
263
|
-
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
264
|
-
(value.startsWith("'") && value.endsWith("'"))) {
|
|
265
|
-
value = value.slice(1, -1);
|
|
266
|
-
}
|
|
267
|
-
args.push({ value });
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
return args;
|
|
271
|
-
}
|
|
272
|
-
/**
|
|
273
|
-
* Checks if a line is a "var x = run SequenceName" or "var x = run SequenceName(args)" command.
|
|
274
|
-
* Also supports retry/backoff options: "var x = run Name retry 3 backoff 500 ms"
|
|
275
|
-
* Excludes script types (bash, powershell, js) which are handled by isRunCommand.
|
|
276
|
-
*/
|
|
277
|
-
function isVarRunSequenceCommand(line) {
|
|
278
|
-
const trimmed = line.trim();
|
|
279
|
-
// First check the basic pattern - allow retry/backoff options at the end
|
|
280
|
-
// Use specific patterns for retry (retry N) and backoff (backoff N [unit])
|
|
281
|
-
if (!/^var\s+[a-zA-Z_][a-zA-Z0-9_]*\s*=\s*run\s+[a-zA-Z_][a-zA-Z0-9_-]*(?:\s*\([^)]*\))?(?:\s+retry\s+\d+)?(?:\s+backoff\s+\d+(?:\.\d+)?\s*(?:s|ms|seconds?|milliseconds?)?)?\s*$/i.test(trimmed)) {
|
|
282
|
-
return false;
|
|
283
|
-
}
|
|
284
|
-
// Exclude script types - these should be handled by isRunCommand
|
|
285
|
-
const match = trimmed.match(/^var\s+[a-zA-Z_][a-zA-Z0-9_]*\s*=\s*run\s+([a-zA-Z_][a-zA-Z0-9_-]*)/i);
|
|
286
|
-
if (match) {
|
|
287
|
-
const name = match[1].toLowerCase();
|
|
288
|
-
if (name === 'bash' || name === 'powershell' || name === 'js') {
|
|
289
|
-
return false;
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
return true;
|
|
293
|
-
}
|
|
294
|
-
/**
|
|
295
|
-
* Parses a "var x = run SequenceName" or "var x = run SequenceName(args)" command.
|
|
296
|
-
* Returns the variable name, sequence name, arguments, and retry/backoff options.
|
|
297
|
-
*/
|
|
298
|
-
function parseVarRunSequenceCommand(line) {
|
|
299
|
-
const trimmed = line.trim();
|
|
300
|
-
// Extract retry/backoff options first using the shared function
|
|
301
|
-
const { cleanedLine, retryCount, backoffMs } = (0, parser_1.extractRetryOptions)(trimmed);
|
|
302
|
-
const match = cleanedLine.match(/^var\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*run\s+([a-zA-Z_][a-zA-Z0-9_-]*)(?:\s*\(([^)]*)\))?\s*$/i);
|
|
303
|
-
if (!match) {
|
|
304
|
-
return null;
|
|
305
|
-
}
|
|
306
|
-
return {
|
|
307
|
-
varName: match[1],
|
|
308
|
-
sequenceName: match[2],
|
|
309
|
-
args: parseRunArguments(match[3] || ''),
|
|
310
|
-
retryCount,
|
|
311
|
-
backoffMs
|
|
312
|
-
};
|
|
313
|
-
}
|
|
314
|
-
/**
|
|
315
|
-
* Checks if a line is a plain "run SequenceName" or "run SequenceName(args)" command
|
|
316
|
-
*/
|
|
317
|
-
function isRunSequenceCommand(line) {
|
|
318
|
-
const trimmed = line.trim();
|
|
319
|
-
// Must not start with "var" - that's handled by isVarRunSequenceCommand
|
|
320
|
-
if (/^var\s+/i.test(trimmed))
|
|
321
|
-
return false;
|
|
322
|
-
return /^run\s+[a-zA-Z_][a-zA-Z0-9_-]*(?:\s*\([^)]*\))?\s*$/i.test(trimmed);
|
|
323
|
-
}
|
|
324
|
-
/**
|
|
325
|
-
* Parses a plain "run SequenceName" or "run SequenceName(args)" command.
|
|
326
|
-
*/
|
|
327
|
-
function parseRunSequenceCommand(line) {
|
|
328
|
-
const trimmed = line.trim();
|
|
329
|
-
const match = trimmed.match(/^run\s+([a-zA-Z_][a-zA-Z0-9_-]*)(?:\s*\(([^)]*)\))?\s*$/i);
|
|
330
|
-
if (!match) {
|
|
331
|
-
return null;
|
|
332
|
-
}
|
|
333
|
-
return {
|
|
334
|
-
sequenceName: match[1],
|
|
335
|
-
args: parseRunArguments(match[2] || '')
|
|
336
|
-
};
|
|
337
|
-
}
|
|
338
|
-
/**
|
|
339
|
-
* Binds arguments to parameters, resolving positional and named arguments.
|
|
340
|
-
* Returns the resulting variables to inject into the sequence scope, or an error message.
|
|
341
|
-
*
|
|
342
|
-
* Rules:
|
|
343
|
-
* - Positional args are bound in order
|
|
344
|
-
* - Named args can be in any order
|
|
345
|
-
* - Required params (no default) must be provided
|
|
346
|
-
* - Optional params use default if not provided
|
|
347
|
-
* - Cannot mix positional after named
|
|
348
|
-
*/
|
|
349
|
-
function bindSequenceArguments(params, args, runtimeVariables) {
|
|
350
|
-
const result = {};
|
|
351
|
-
const boundParams = new Set();
|
|
352
|
-
let positionalIndex = 0;
|
|
353
|
-
let sawNamedArg = false;
|
|
354
|
-
for (const arg of args) {
|
|
355
|
-
if (arg.name) {
|
|
356
|
-
// Named argument
|
|
357
|
-
sawNamedArg = true;
|
|
358
|
-
const param = params.find(p => p.name === arg.name);
|
|
359
|
-
if (!param) {
|
|
360
|
-
return { error: `Unknown parameter: "${arg.name}"` };
|
|
361
|
-
}
|
|
362
|
-
if (boundParams.has(arg.name)) {
|
|
363
|
-
return { error: `Parameter "${arg.name}" provided multiple times` };
|
|
364
|
-
}
|
|
365
|
-
// Resolve variable references in the value
|
|
366
|
-
let value = arg.value;
|
|
367
|
-
if (value.startsWith('{{') && value.endsWith('}}')) {
|
|
368
|
-
const varName = value.slice(2, -2);
|
|
369
|
-
value = getVariableValueByPath(varName, runtimeVariables);
|
|
370
|
-
}
|
|
371
|
-
else if (runtimeVariables[value] !== undefined) {
|
|
372
|
-
// Allow passing variable by name without braces
|
|
373
|
-
value = runtimeVariables[value];
|
|
374
|
-
}
|
|
375
|
-
result[arg.name] = value;
|
|
376
|
-
boundParams.add(arg.name);
|
|
377
|
-
}
|
|
378
|
-
else {
|
|
379
|
-
// Positional argument
|
|
380
|
-
if (sawNamedArg) {
|
|
381
|
-
return { error: 'Positional arguments cannot follow named arguments' };
|
|
382
|
-
}
|
|
383
|
-
if (positionalIndex >= params.length) {
|
|
384
|
-
return { error: `Too many arguments: expected ${params.length}, got more` };
|
|
385
|
-
}
|
|
386
|
-
const param = params[positionalIndex];
|
|
387
|
-
// Resolve variable references in the value
|
|
388
|
-
let value = arg.value;
|
|
389
|
-
if (value.startsWith('{{') && value.endsWith('}}')) {
|
|
390
|
-
const varName = value.slice(2, -2);
|
|
391
|
-
value = getVariableValueByPath(varName, runtimeVariables);
|
|
392
|
-
}
|
|
393
|
-
else if (runtimeVariables[value] !== undefined) {
|
|
394
|
-
// Allow passing variable by name without braces
|
|
395
|
-
value = runtimeVariables[value];
|
|
396
|
-
}
|
|
397
|
-
result[param.name] = value;
|
|
398
|
-
boundParams.add(param.name);
|
|
399
|
-
positionalIndex++;
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
// Check for missing required params and apply defaults
|
|
403
|
-
for (const param of params) {
|
|
404
|
-
if (!boundParams.has(param.name)) {
|
|
405
|
-
if (param.defaultValue !== undefined) {
|
|
406
|
-
result[param.name] = param.defaultValue;
|
|
407
|
-
}
|
|
408
|
-
else {
|
|
409
|
-
return { error: `Missing required argument: "${param.name}"` };
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
return { variables: result };
|
|
414
|
-
}
|
|
415
|
-
/**
|
|
416
|
-
* Helper to get a value by path from a variables object, supporting nested access.
|
|
417
|
-
* Used for resolving argument values from runtime variables.
|
|
418
|
-
*/
|
|
419
|
-
function getVariableValueByPath(path, variables) {
|
|
420
|
-
const parts = path.split('.');
|
|
421
|
-
let current = variables;
|
|
422
|
-
for (const part of parts) {
|
|
423
|
-
if (current === null || current === undefined) {
|
|
424
|
-
return '';
|
|
425
|
-
}
|
|
426
|
-
if (typeof current === 'string') {
|
|
427
|
-
// Try to parse as JSON
|
|
428
|
-
try {
|
|
429
|
-
current = JSON.parse(current);
|
|
430
|
-
}
|
|
431
|
-
catch {
|
|
432
|
-
return '';
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
current = current[part];
|
|
436
|
-
}
|
|
437
|
-
if (current === null || current === undefined) {
|
|
438
|
-
return '';
|
|
439
|
-
}
|
|
440
|
-
if (typeof current === 'object') {
|
|
441
|
-
return JSON.stringify(current);
|
|
442
|
-
}
|
|
443
|
-
return String(current);
|
|
444
|
-
}
|
|
445
|
-
/**
|
|
446
|
-
* Parses a tag filter string into a TagFilter object.
|
|
447
|
-
* Supports: "smoke" (simple) and "team(CustomerExp)" (key-value)
|
|
448
|
-
*/
|
|
449
|
-
function parseTagFilter(filterStr) {
|
|
450
|
-
const match = filterStr.match(/^([a-zA-Z_][a-zA-Z0-9_-]*)(?:\(([^)]+)\))?$/);
|
|
451
|
-
if (!match) {
|
|
452
|
-
return { name: filterStr }; // Fallback to treating entire string as name
|
|
453
|
-
}
|
|
454
|
-
return {
|
|
455
|
-
name: match[1],
|
|
456
|
-
value: match[2]?.trim()
|
|
457
|
-
};
|
|
458
|
-
}
|
|
459
|
-
/**
|
|
460
|
-
* Checks if a sequence tag matches a filter.
|
|
461
|
-
* Case-insensitive comparison for both name and value.
|
|
462
|
-
* If filter has no value, matches any tag with that name.
|
|
463
|
-
* If filter has a value, requires exact (case-insensitive) match.
|
|
464
|
-
*/
|
|
465
|
-
function tagMatchesFilter(tag, filter) {
|
|
466
|
-
// Case-insensitive name comparison
|
|
467
|
-
if (tag.name.toLowerCase() !== filter.name.toLowerCase()) {
|
|
468
|
-
return false;
|
|
469
|
-
}
|
|
470
|
-
// If filter has no value requirement, any value matches (including no value)
|
|
471
|
-
if (filter.value === undefined) {
|
|
472
|
-
return true;
|
|
473
|
-
}
|
|
474
|
-
// If filter has value requirement, tag must have matching value
|
|
475
|
-
if (tag.value === undefined) {
|
|
476
|
-
return false;
|
|
477
|
-
}
|
|
478
|
-
return tag.value.toLowerCase() === filter.value.toLowerCase();
|
|
479
|
-
}
|
|
480
|
-
/**
|
|
481
|
-
* Checks if a sequence matches the tag filter options.
|
|
482
|
-
* @param sequence The sequence to check
|
|
483
|
-
* @param options The filter options (filters and mode)
|
|
484
|
-
* @returns true if the sequence matches according to the filter mode
|
|
485
|
-
*/
|
|
486
|
-
function sequenceMatchesTags(sequence, options) {
|
|
487
|
-
if (options.filters.length === 0) {
|
|
488
|
-
return true; // No filters = match all
|
|
489
|
-
}
|
|
490
|
-
if (options.mode === 'and') {
|
|
491
|
-
// Must match ALL filters
|
|
492
|
-
return options.filters.every(filter => sequence.tags.some(tag => tagMatchesFilter(tag, filter)));
|
|
493
|
-
}
|
|
494
|
-
else {
|
|
495
|
-
// Must match ANY filter
|
|
496
|
-
return options.filters.some(filter => sequence.tags.some(tag => tagMatchesFilter(tag, filter)));
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
/**
|
|
500
|
-
* Parses sequence parameters from a sequence declaration line.
|
|
501
|
-
* Supports: sequence Name(param1, param2 = "default")
|
|
502
|
-
* Returns empty array if no parameters.
|
|
503
|
-
*/
|
|
504
|
-
function parseSequenceParameters(line) {
|
|
505
|
-
const params = [];
|
|
506
|
-
// Match the parentheses content
|
|
507
|
-
const parenMatch = line.match(/\(([^)]*)\)/);
|
|
508
|
-
if (!parenMatch) {
|
|
509
|
-
return params;
|
|
510
|
-
}
|
|
511
|
-
const paramsStr = parenMatch[1].trim();
|
|
512
|
-
if (!paramsStr) {
|
|
513
|
-
return params;
|
|
514
|
-
}
|
|
515
|
-
// Split by comma, but handle quoted strings
|
|
516
|
-
const paramParts = paramsStr.split(/,(?=(?:[^"]*"[^"]*")*[^"]*$)/);
|
|
517
|
-
let hasSeenDefault = false;
|
|
518
|
-
for (const part of paramParts) {
|
|
519
|
-
const trimmed = part.trim();
|
|
520
|
-
if (!trimmed)
|
|
521
|
-
continue;
|
|
522
|
-
// Check for param = value pattern
|
|
523
|
-
const defaultMatch = trimmed.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(.+)$/);
|
|
524
|
-
if (defaultMatch) {
|
|
525
|
-
hasSeenDefault = true;
|
|
526
|
-
let defaultValue = defaultMatch[2].trim();
|
|
527
|
-
// Strip quotes if present
|
|
528
|
-
if ((defaultValue.startsWith('"') && defaultValue.endsWith('"')) ||
|
|
529
|
-
(defaultValue.startsWith("'") && defaultValue.endsWith("'"))) {
|
|
530
|
-
defaultValue = defaultValue.slice(1, -1);
|
|
531
|
-
}
|
|
532
|
-
params.push({
|
|
533
|
-
name: defaultMatch[1],
|
|
534
|
-
defaultValue
|
|
535
|
-
});
|
|
536
|
-
}
|
|
537
|
-
else {
|
|
538
|
-
// No default value
|
|
539
|
-
const nameMatch = trimmed.match(/^([a-zA-Z_][a-zA-Z0-9_]*)$/);
|
|
540
|
-
if (nameMatch) {
|
|
541
|
-
// Error: required param after optional param
|
|
542
|
-
if (hasSeenDefault) {
|
|
543
|
-
// We'll still add it, but validation should catch this
|
|
544
|
-
console.warn(`Required parameter "${nameMatch[1]}" follows optional parameter`);
|
|
545
|
-
}
|
|
546
|
-
params.push({
|
|
547
|
-
name: nameMatch[1]
|
|
548
|
-
});
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
return params;
|
|
553
|
-
}
|
|
554
|
-
/**
|
|
555
|
-
* Validates that required parameters come before optional ones.
|
|
556
|
-
*/
|
|
557
|
-
function validateSequenceParameters(params) {
|
|
558
|
-
let hasSeenDefault = false;
|
|
559
|
-
for (const param of params) {
|
|
560
|
-
if (param.defaultValue !== undefined) {
|
|
561
|
-
hasSeenDefault = true;
|
|
562
|
-
}
|
|
563
|
-
else if (hasSeenDefault) {
|
|
564
|
-
return `Required parameter "${param.name}" cannot come after optional parameter`;
|
|
565
|
-
}
|
|
566
|
-
}
|
|
567
|
-
return null;
|
|
568
|
-
}
|
|
569
|
-
/**
|
|
570
|
-
* Parses tags from lines immediately preceding a sequence declaration.
|
|
571
|
-
* Supports: @tagname (simple) and @key(value) (key-value)
|
|
572
|
-
* Multiple tags can be on the same line or separate lines.
|
|
573
|
-
* Tag names follow the pattern: [a-zA-Z_][a-zA-Z0-9_-]*
|
|
574
|
-
*
|
|
575
|
-
* Also extracts @data(...) and @theory("file.json") annotations for parameterized tests.
|
|
576
|
-
* @data values are parsed as typed: numbers, booleans, or quoted strings.
|
|
577
|
-
*/
|
|
578
|
-
function parseSequenceTags(lines, sequenceLineIndex) {
|
|
579
|
-
const tags = [];
|
|
580
|
-
const dataCases = [];
|
|
581
|
-
let theorySource;
|
|
582
|
-
let tagStartLine = sequenceLineIndex;
|
|
583
|
-
// Look backwards from the sequence line to collect tag lines
|
|
584
|
-
for (let i = sequenceLineIndex - 1; i >= 0; i--) {
|
|
585
|
-
const line = lines[i].trim();
|
|
586
|
-
// Skip empty lines between tags and sequence
|
|
587
|
-
if (!line) {
|
|
588
|
-
continue;
|
|
589
|
-
}
|
|
590
|
-
// Check if this line contains tags (starts with @)
|
|
591
|
-
if (!line.startsWith('@')) {
|
|
592
|
-
// Not a tag line, stop looking
|
|
593
|
-
break;
|
|
594
|
-
}
|
|
595
|
-
// Check for @data(...) - parameterized test data
|
|
596
|
-
const dataMatch = line.match(/^@data\s*\((.+)\)\s*$/);
|
|
597
|
-
if (dataMatch) {
|
|
598
|
-
const values = parseDataValues(dataMatch[1]);
|
|
599
|
-
dataCases.push(values);
|
|
600
|
-
tagStartLine = i;
|
|
601
|
-
continue;
|
|
602
|
-
}
|
|
603
|
-
// Check for @theory("file.json") - external data file
|
|
604
|
-
const theoryMatch = line.match(/^@theory\s*\(\s*["']([^"']+)["']\s*\)\s*$/);
|
|
605
|
-
if (theoryMatch) {
|
|
606
|
-
theorySource = theoryMatch[1];
|
|
607
|
-
tagStartLine = i;
|
|
608
|
-
continue;
|
|
609
|
-
}
|
|
610
|
-
// Parse regular tags from this line
|
|
611
|
-
// Pattern: @tagname or @key(value)
|
|
612
|
-
const tagPattern = /@([a-zA-Z_][a-zA-Z0-9_-]*)(?:\(([^)]+)\))?/g;
|
|
613
|
-
let match;
|
|
614
|
-
while ((match = tagPattern.exec(line)) !== null) {
|
|
615
|
-
const tagName = match[1];
|
|
616
|
-
// Skip data and theory since we handle them separately
|
|
617
|
-
if (tagName === 'data' || tagName === 'theory') {
|
|
618
|
-
continue;
|
|
619
|
-
}
|
|
620
|
-
const tagValue = match[2]?.trim();
|
|
621
|
-
tags.push({
|
|
622
|
-
name: tagName,
|
|
623
|
-
value: tagValue
|
|
624
|
-
});
|
|
625
|
-
}
|
|
626
|
-
tagStartLine = i;
|
|
627
|
-
}
|
|
628
|
-
// Build theoryData if we have @data or @theory
|
|
629
|
-
let theoryData;
|
|
630
|
-
if (dataCases.length > 0 || theorySource) {
|
|
631
|
-
theoryData = {
|
|
632
|
-
cases: [], // Will be populated with param names after we know them
|
|
633
|
-
source: theorySource
|
|
634
|
-
};
|
|
635
|
-
// Store raw data cases temporarily - will be bound to param names in extractSequences
|
|
636
|
-
// Reverse because we collected them in backwards order (iterating up from sequence line)
|
|
637
|
-
theoryData._rawCases = dataCases.reverse();
|
|
638
|
-
}
|
|
639
|
-
return { tags, tagStartLine, theoryData };
|
|
640
|
-
}
|
|
641
|
-
/**
|
|
642
|
-
* Parses comma-separated values from @data(...) annotation.
|
|
643
|
-
* Supports: numbers (1, 3.14), booleans (true, false), quoted strings ("hello", 'world')
|
|
644
|
-
*/
|
|
645
|
-
function parseDataValues(valuesStr) {
|
|
646
|
-
const values = [];
|
|
647
|
-
let current = '';
|
|
648
|
-
let inQuote = null;
|
|
649
|
-
let i = 0;
|
|
650
|
-
while (i < valuesStr.length) {
|
|
651
|
-
const char = valuesStr[i];
|
|
652
|
-
if (inQuote) {
|
|
653
|
-
if (char === inQuote) {
|
|
654
|
-
// End of quoted string - add without quotes
|
|
655
|
-
values.push(current);
|
|
656
|
-
current = '';
|
|
657
|
-
inQuote = null;
|
|
658
|
-
// Skip to next comma or end
|
|
659
|
-
i++;
|
|
660
|
-
while (i < valuesStr.length && valuesStr[i] !== ',') {
|
|
661
|
-
i++;
|
|
662
|
-
}
|
|
663
|
-
i++; // Skip comma
|
|
664
|
-
continue;
|
|
665
|
-
}
|
|
666
|
-
else {
|
|
667
|
-
current += char;
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
else {
|
|
671
|
-
if (char === '"' || char === "'") {
|
|
672
|
-
inQuote = char;
|
|
673
|
-
current = '';
|
|
674
|
-
}
|
|
675
|
-
else if (char === ',') {
|
|
676
|
-
// End of value
|
|
677
|
-
const trimmed = current.trim();
|
|
678
|
-
if (trimmed) {
|
|
679
|
-
values.push(parseTypedValue(trimmed));
|
|
680
|
-
}
|
|
681
|
-
current = '';
|
|
682
|
-
}
|
|
683
|
-
else {
|
|
684
|
-
current += char;
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
i++;
|
|
688
|
-
}
|
|
689
|
-
// Handle last value
|
|
690
|
-
if (inQuote) {
|
|
691
|
-
// Unclosed quote - treat as string
|
|
692
|
-
values.push(current);
|
|
693
|
-
}
|
|
694
|
-
else {
|
|
695
|
-
const trimmed = current.trim();
|
|
696
|
-
if (trimmed) {
|
|
697
|
-
values.push(parseTypedValue(trimmed));
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
return values;
|
|
701
|
-
}
|
|
702
|
-
/**
|
|
703
|
-
* Parses a value string into its typed representation.
|
|
704
|
-
* Numbers become numbers, booleans become booleans, everything else is a string.
|
|
705
|
-
*/
|
|
706
|
-
function parseTypedValue(value) {
|
|
707
|
-
// Boolean
|
|
708
|
-
if (value === 'true')
|
|
709
|
-
return true;
|
|
710
|
-
if (value === 'false')
|
|
711
|
-
return false;
|
|
712
|
-
// Null
|
|
713
|
-
if (value === 'null')
|
|
714
|
-
return null;
|
|
715
|
-
// Number (integer or float)
|
|
716
|
-
if (/^-?\d+(\.\d+)?$/.test(value)) {
|
|
717
|
-
return parseFloat(value);
|
|
718
|
-
}
|
|
719
|
-
// String (possibly quoted)
|
|
720
|
-
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
721
|
-
(value.startsWith("'") && value.endsWith("'"))) {
|
|
722
|
-
return value.slice(1, -1);
|
|
723
|
-
}
|
|
724
|
-
return value;
|
|
725
|
-
}
|
|
726
|
-
/**
|
|
727
|
-
* Extracts sequence blocks from the document.
|
|
728
|
-
*/
|
|
729
|
-
function extractSequences(text) {
|
|
730
|
-
const lines = text.split('\n');
|
|
731
|
-
const sequences = [];
|
|
732
|
-
let currentSequence = null;
|
|
733
|
-
for (let i = 0; i < lines.length; i++) {
|
|
734
|
-
const line = lines[i].trim();
|
|
735
|
-
// Strip inline comment for matching
|
|
736
|
-
const lineWithoutComment = stripInlineComment(line);
|
|
737
|
-
// Check for sequence start - supports 'test sequence' and 'sequence', with optional parameters
|
|
738
|
-
const testSequenceMatch = lineWithoutComment.match(/^test\s+sequence\s+([a-zA-Z_][a-zA-Z0-9_-]*)(?:\s*\([^)]*\))?\s*$/);
|
|
739
|
-
const sequenceMatch = lineWithoutComment.match(/^sequence\s+([a-zA-Z_][a-zA-Z0-9_-]*)(?:\s*\([^)]*\))?\s*$/);
|
|
740
|
-
if (testSequenceMatch || sequenceMatch) {
|
|
741
|
-
const isTest = !!testSequenceMatch;
|
|
742
|
-
const name = isTest ? testSequenceMatch[1] : sequenceMatch[1];
|
|
743
|
-
const parameters = parseSequenceParameters(lines[i]);
|
|
744
|
-
const { tags, tagStartLine, theoryData } = parseSequenceTags(lines, i);
|
|
745
|
-
// If we have raw @data cases, bind them to parameter names
|
|
746
|
-
let finalTheoryData = theoryData;
|
|
747
|
-
if (theoryData && theoryData._rawCases) {
|
|
748
|
-
const rawCases = theoryData._rawCases;
|
|
749
|
-
// Handle single-param shorthand: @data(1, 2, 3) with single param (id)
|
|
750
|
-
// should create 3 cases [{id:1}, {id:2}, {id:3}] not 1 case with 3 values
|
|
751
|
-
if (parameters.length === 1 && rawCases.length === 1 && rawCases[0].length > 1) {
|
|
752
|
-
const paramName = parameters[0].name;
|
|
753
|
-
finalTheoryData = {
|
|
754
|
-
cases: rawCases[0].map(value => ({ [paramName]: value })),
|
|
755
|
-
source: theoryData.source
|
|
756
|
-
};
|
|
757
|
-
}
|
|
758
|
-
else {
|
|
759
|
-
// Normal multi-param case: each @data line is a test case
|
|
760
|
-
finalTheoryData = {
|
|
761
|
-
cases: rawCases.map(values => {
|
|
762
|
-
const caseObj = {};
|
|
763
|
-
parameters.forEach((param, idx) => {
|
|
764
|
-
caseObj[param.name] = values[idx];
|
|
765
|
-
});
|
|
766
|
-
return caseObj;
|
|
767
|
-
}),
|
|
768
|
-
source: theoryData.source
|
|
769
|
-
};
|
|
770
|
-
}
|
|
771
|
-
}
|
|
772
|
-
currentSequence = {
|
|
773
|
-
name,
|
|
774
|
-
startLine: tagStartLine, // Include tag lines in the sequence range
|
|
775
|
-
lines: [],
|
|
776
|
-
parameters,
|
|
777
|
-
tags,
|
|
778
|
-
isTest,
|
|
779
|
-
theoryData: finalTheoryData
|
|
780
|
-
};
|
|
781
|
-
continue;
|
|
782
|
-
}
|
|
783
|
-
// Check for sequence end
|
|
784
|
-
if (line === 'end sequence' && currentSequence) {
|
|
785
|
-
sequences.push({
|
|
786
|
-
name: currentSequence.name,
|
|
787
|
-
startLine: currentSequence.startLine,
|
|
788
|
-
endLine: i,
|
|
789
|
-
content: currentSequence.lines.join('\n'),
|
|
790
|
-
parameters: currentSequence.parameters,
|
|
791
|
-
tags: currentSequence.tags,
|
|
792
|
-
isTest: currentSequence.isTest,
|
|
793
|
-
theoryData: currentSequence.theoryData
|
|
794
|
-
});
|
|
795
|
-
currentSequence = null;
|
|
796
|
-
continue;
|
|
797
|
-
}
|
|
798
|
-
// Add line to current sequence
|
|
799
|
-
if (currentSequence) {
|
|
800
|
-
currentSequence.lines.push(lines[i]);
|
|
801
|
-
}
|
|
802
|
-
}
|
|
803
|
-
return sequences;
|
|
804
|
-
}
|
|
805
|
-
/**
|
|
806
|
-
* Finds which sequence a line belongs to.
|
|
807
|
-
*/
|
|
808
|
-
function getSequenceAtLine(text, lineNumber) {
|
|
809
|
-
const sequences = extractSequences(text);
|
|
810
|
-
return sequences.find(seq => lineNumber >= seq.startLine && lineNumber <= seq.endLine) || null;
|
|
811
|
-
}
|
|
812
|
-
/**
|
|
813
|
-
* HTTP methods for regex matching
|
|
814
|
-
*/
|
|
815
|
-
const HTTP_METHODS = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'HEAD', 'OPTIONS'];
|
|
816
|
-
/**
|
|
817
|
-
* Checks if a line is a variable request assignment: var name = GET/POST/etc url
|
|
818
|
-
* This captures the response directly into a variable for stable referencing.
|
|
819
|
-
*/
|
|
820
|
-
function isVarRequestCommand(line) {
|
|
821
|
-
const trimmed = line.trim();
|
|
822
|
-
const methodPattern = HTTP_METHODS.join('|');
|
|
823
|
-
const regex = new RegExp(`^var\\s+[a-zA-Z_][a-zA-Z0-9_]*\\s*=\\s*(${methodPattern})\\s+.+$`, 'i');
|
|
824
|
-
return regex.test(trimmed);
|
|
825
|
-
}
|
|
826
|
-
/**
|
|
827
|
-
* Parses a variable request command: var name = METHOD url [retry N] [backoff N ms]
|
|
828
|
-
* Returns the variable name, method, URL, and optional retry options.
|
|
829
|
-
*/
|
|
830
|
-
function parseVarRequestCommand(line) {
|
|
831
|
-
const trimmed = line.trim();
|
|
832
|
-
const methodPattern = HTTP_METHODS.join('|');
|
|
833
|
-
const regex = new RegExp(`^var\\s+([a-zA-Z_][a-zA-Z0-9_]*)\\s*=\\s*(${methodPattern})\\s+(.+)$`, 'i');
|
|
834
|
-
const match = trimmed.match(regex);
|
|
835
|
-
if (!match) {
|
|
836
|
-
return null;
|
|
837
|
-
}
|
|
838
|
-
// Extract retry options from the URL part
|
|
839
|
-
const { cleanedLine: url, retryCount, backoffMs } = (0, parser_1.extractRetryOptions)(match[3].trim());
|
|
840
|
-
return {
|
|
841
|
-
varName: match[1],
|
|
842
|
-
method: match[2].toUpperCase(),
|
|
843
|
-
url,
|
|
844
|
-
retryCount,
|
|
845
|
-
backoffMs
|
|
846
|
-
};
|
|
847
|
-
}
|
|
848
|
-
/**
|
|
849
|
-
* Checks if a line is a variable assignment that should be evaluated at runtime.
|
|
850
|
-
* This handles: var x = someVar.path or var x = "literal string"
|
|
851
|
-
* Does NOT handle: var x = $1.path (response capture), var x = run ... (script/json), var x = METHOD url (request)
|
|
852
|
-
*/
|
|
853
|
-
function isVarAssignCommand(line) {
|
|
854
|
-
const trimmed = line.trim();
|
|
855
|
-
// Must start with 'var'
|
|
856
|
-
if (!/^var\s+/i.test(trimmed)) {
|
|
857
|
-
return false;
|
|
858
|
-
}
|
|
859
|
-
// Exclude response captures (var x = $1...)
|
|
860
|
-
if (/^var\s+[a-zA-Z_][a-zA-Z0-9_]*\s*=\s*\$\d+/.test(trimmed)) {
|
|
861
|
-
return false;
|
|
862
|
-
}
|
|
863
|
-
// Exclude run commands (var x = run ...)
|
|
864
|
-
if (/^var\s+[a-zA-Z_][a-zA-Z0-9_]*\s*=\s*run\s+/i.test(trimmed)) {
|
|
865
|
-
return false;
|
|
866
|
-
}
|
|
867
|
-
// Exclude request assignments (var x = GET/POST/etc ...)
|
|
868
|
-
if (isVarRequestCommand(trimmed)) {
|
|
869
|
-
return false;
|
|
870
|
-
}
|
|
871
|
-
// Must have the form: var name = something
|
|
872
|
-
return /^var\s+[a-zA-Z_][a-zA-Z0-9_]*\s*=\s*.+$/.test(trimmed);
|
|
873
|
-
}
|
|
874
|
-
/**
|
|
875
|
-
* Parses a variable assignment command.
|
|
876
|
-
* Returns the variable name and the value expression.
|
|
877
|
-
*/
|
|
878
|
-
function parseVarAssignCommand(line) {
|
|
879
|
-
const trimmed = line.trim();
|
|
880
|
-
const match = trimmed.match(/^var\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*(.+)$/i);
|
|
881
|
-
if (!match) {
|
|
882
|
-
return null;
|
|
883
|
-
}
|
|
884
|
-
return {
|
|
885
|
-
varName: match[1],
|
|
886
|
-
valueExpr: match[2].trim()
|
|
887
|
-
};
|
|
888
|
-
}
|
|
889
|
-
/**
|
|
890
|
-
* Evaluates a value expression in the context of runtime variables.
|
|
891
|
-
*
|
|
892
|
-
* Supports:
|
|
893
|
-
* - Quoted strings: "hello" or 'hello' (with {{}} interpolation)
|
|
894
|
-
* - Variable paths: someVar.property[0].value
|
|
895
|
-
* - Simple literals: 123, true, false, null
|
|
896
|
-
*/
|
|
897
|
-
function evaluateValueExpression(expr, runtimeVariables) {
|
|
898
|
-
const trimmed = expr.trim();
|
|
899
|
-
// Check for quoted string (literal with optional interpolation)
|
|
900
|
-
if ((trimmed.startsWith('"') && trimmed.endsWith('"')) ||
|
|
901
|
-
(trimmed.startsWith("'") && trimmed.endsWith("'"))) {
|
|
902
|
-
// Remove quotes and substitute variables inside
|
|
903
|
-
const inner = trimmed.slice(1, -1);
|
|
904
|
-
const substituted = (0, parser_1.substituteVariables)(inner, runtimeVariables);
|
|
905
|
-
return { value: substituted };
|
|
906
|
-
}
|
|
907
|
-
// Check for simple literals
|
|
908
|
-
if (trimmed === 'true' || trimmed === 'false' || trimmed === 'null') {
|
|
909
|
-
return { value: trimmed };
|
|
910
|
-
}
|
|
911
|
-
if (/^-?\d+(\.\d+)?$/.test(trimmed)) {
|
|
912
|
-
return { value: trimmed };
|
|
913
|
-
}
|
|
914
|
-
// It's a variable path expression: varName or varName.path or varName[0].path
|
|
915
|
-
const pathMatch = trimmed.match(/^([a-zA-Z_][a-zA-Z0-9_]*)((?:\.[a-zA-Z_][a-zA-Z0-9_]*|\[\d+\])*)$/);
|
|
916
|
-
if (!pathMatch) {
|
|
917
|
-
return { value: trimmed, error: `Invalid expression: ${trimmed}` };
|
|
918
|
-
}
|
|
919
|
-
const varName = pathMatch[1];
|
|
920
|
-
const pathPart = pathMatch[2] || '';
|
|
921
|
-
if (!(varName in runtimeVariables)) {
|
|
922
|
-
return { value: '', error: `Variable '${varName}' is not defined` };
|
|
923
|
-
}
|
|
924
|
-
const varValue = runtimeVariables[varName];
|
|
925
|
-
// If there's a path, navigate into the value
|
|
926
|
-
if (pathPart) {
|
|
927
|
-
// Handle object values directly (from var x = GET responses or parsed JSON)
|
|
928
|
-
let dataToNavigate;
|
|
929
|
-
if (typeof varValue === 'object' && varValue !== null) {
|
|
930
|
-
dataToNavigate = varValue;
|
|
931
|
-
}
|
|
932
|
-
else if (typeof varValue === 'string') {
|
|
933
|
-
try {
|
|
934
|
-
dataToNavigate = JSON.parse(varValue);
|
|
935
|
-
}
|
|
936
|
-
catch {
|
|
937
|
-
return { value: varValue, error: `Cannot parse '${varName}' as JSON for path access` };
|
|
938
|
-
}
|
|
939
|
-
}
|
|
940
|
-
else {
|
|
941
|
-
return { value: String(varValue), error: `Cannot access path on non-object value` };
|
|
942
|
-
}
|
|
943
|
-
// Navigate the path
|
|
944
|
-
const path = pathPart.replace(/^\./, '').replace(/\[(\d+)\]/g, '.$1');
|
|
945
|
-
const parts = path.split('.').filter(p => p !== '');
|
|
946
|
-
let current = dataToNavigate;
|
|
947
|
-
for (const part of parts) {
|
|
948
|
-
if (current === null || current === undefined) {
|
|
949
|
-
return { value: '', error: `Cannot access '${part}' on null/undefined` };
|
|
950
|
-
}
|
|
951
|
-
current = current[part];
|
|
952
|
-
}
|
|
953
|
-
// Return the result - preserve type for objects/arrays
|
|
954
|
-
if (current === null) {
|
|
955
|
-
return { value: null };
|
|
956
|
-
}
|
|
957
|
-
if (current === undefined) {
|
|
958
|
-
return { value: '', error: `Property path '${pathPart}' not found` };
|
|
959
|
-
}
|
|
960
|
-
// Return objects/arrays as-is so they can be used in further expressions
|
|
961
|
-
return { value: current };
|
|
962
|
-
}
|
|
963
|
-
// No path, return the variable value as-is
|
|
964
|
-
return { value: varValue };
|
|
965
|
-
}
|
|
966
|
-
/**
|
|
967
|
-
* Strip inline comment from a line.
|
|
968
|
-
* Finds # that is not inside quotes and removes everything after it.
|
|
969
|
-
*/
|
|
970
|
-
function stripInlineComment(line) {
|
|
971
|
-
let inSingleQuote = false;
|
|
972
|
-
let inDoubleQuote = false;
|
|
973
|
-
for (let i = 0; i < line.length; i++) {
|
|
974
|
-
const char = line[i];
|
|
975
|
-
const prevChar = i > 0 ? line[i - 1] : '';
|
|
976
|
-
// Skip escaped quotes
|
|
977
|
-
if (prevChar === '\\') {
|
|
978
|
-
continue;
|
|
979
|
-
}
|
|
980
|
-
if (char === '"' && !inSingleQuote) {
|
|
981
|
-
inDoubleQuote = !inDoubleQuote;
|
|
982
|
-
}
|
|
983
|
-
else if (char === "'" && !inDoubleQuote) {
|
|
984
|
-
inSingleQuote = !inSingleQuote;
|
|
985
|
-
}
|
|
986
|
-
else if (char === '#' && !inSingleQuote && !inDoubleQuote) {
|
|
987
|
-
// Found an unquoted #, strip from here
|
|
988
|
-
return line.substring(0, i).trimEnd();
|
|
989
|
-
}
|
|
990
|
-
}
|
|
991
|
-
return line;
|
|
992
|
-
}
|
|
993
|
-
/**
|
|
994
|
-
* Checks if a line is a print command.
|
|
995
|
-
*/
|
|
996
|
-
function isPrintCommand(line) {
|
|
997
|
-
return /^print\s+/i.test(line.trim());
|
|
998
|
-
}
|
|
999
|
-
/**
|
|
1000
|
-
* Parses a print command and returns title and optional body.
|
|
1001
|
-
* Format: print Title | Optional body content
|
|
1002
|
-
*/
|
|
1003
|
-
function parsePrintCommand(line) {
|
|
1004
|
-
// Strip inline comments before parsing
|
|
1005
|
-
const lineWithoutComment = stripInlineComment(line);
|
|
1006
|
-
const match = lineWithoutComment.trim().match(/^print\s+(.+)$/i);
|
|
1007
|
-
if (!match) {
|
|
1008
|
-
return { title: '' };
|
|
1009
|
-
}
|
|
1010
|
-
const content = match[1];
|
|
1011
|
-
const pipeIndex = content.indexOf('|');
|
|
1012
|
-
if (pipeIndex === -1) {
|
|
1013
|
-
// No body, just title
|
|
1014
|
-
return { title: content.trim() };
|
|
1015
|
-
}
|
|
1016
|
-
// Split into title and body
|
|
1017
|
-
const title = content.substring(0, pipeIndex).trim();
|
|
1018
|
-
const body = content.substring(pipeIndex + 1).trim();
|
|
1019
|
-
return { title, body: body || undefined };
|
|
1020
|
-
}
|
|
1021
|
-
/**
|
|
1022
|
-
* Checks if a line is a wait command.
|
|
1023
|
-
* Formats: wait 2s, wait 500ms, wait 1.5s
|
|
1024
|
-
*/
|
|
1025
|
-
function isWaitCommand(line) {
|
|
1026
|
-
return /^wait\s+\d+(\.\d+)?\s*(s|ms|seconds?|milliseconds?)?$/i.test(line.trim());
|
|
1027
|
-
}
|
|
1028
|
-
/**
|
|
1029
|
-
* Parses a wait command and returns the duration in milliseconds.
|
|
1030
|
-
* Formats: wait 2s, wait 500ms, wait 1.5s, wait 2 seconds
|
|
1031
|
-
*/
|
|
1032
|
-
function parseWaitCommand(line) {
|
|
1033
|
-
const match = line.trim().match(/^wait\s+(\d+(?:\.\d+)?)\s*(s|ms|seconds?|milliseconds?)?$/i);
|
|
1034
|
-
if (!match) {
|
|
1035
|
-
return 0;
|
|
1036
|
-
}
|
|
1037
|
-
const value = parseFloat(match[1]);
|
|
1038
|
-
const unit = (match[2] || 's').toLowerCase();
|
|
1039
|
-
// Convert to milliseconds
|
|
1040
|
-
if (unit === 'ms' || unit.startsWith('millisecond')) {
|
|
1041
|
-
return value;
|
|
1042
|
-
}
|
|
1043
|
-
// Default to seconds
|
|
1044
|
-
return value * 1000;
|
|
1045
|
-
}
|
|
1046
|
-
/**
|
|
1047
|
-
* Resolves bare variable references in text.
|
|
1048
|
-
* A bare variable is a single identifier that matches a defined variable.
|
|
1049
|
-
* Examples: "id" -> value of id, "user.body.name" -> nested access
|
|
1050
|
-
* Does NOT resolve variables inside strings or expressions with operators.
|
|
1051
|
-
* This is used for print statements where bare variable names should be resolved.
|
|
1052
|
-
*/
|
|
1053
|
-
function resolveBareVariables(text, variables) {
|
|
1054
|
-
const trimmed = text.trim();
|
|
1055
|
-
// Check if the entire text is a bare variable reference (with optional path)
|
|
1056
|
-
// Pattern: varName or varName.path.to.property
|
|
1057
|
-
const bareVarMatch = trimmed.match(/^([a-zA-Z_][a-zA-Z0-9_]*)((?:\.[a-zA-Z_][a-zA-Z0-9_]*|\[\d+\])*)$/);
|
|
1058
|
-
if (bareVarMatch) {
|
|
1059
|
-
const varName = bareVarMatch[1];
|
|
1060
|
-
const pathPart = bareVarMatch[2] || '';
|
|
1061
|
-
if (varName in variables) {
|
|
1062
|
-
const value = variables[varName];
|
|
1063
|
-
if (pathPart) {
|
|
1064
|
-
// Navigate the path
|
|
1065
|
-
const path = pathPart.replace(/^\./, '');
|
|
1066
|
-
const nestedValue = getNestedValueFromObject(value, path);
|
|
1067
|
-
return valueToString(nestedValue);
|
|
1068
|
-
}
|
|
1069
|
-
return valueToString(value);
|
|
1070
|
-
}
|
|
1071
|
-
}
|
|
1072
|
-
// For expressions with + or other operators, resolve each bare variable in parts
|
|
1073
|
-
// Split by + and resolve each part, preserving string literals
|
|
1074
|
-
if (text.includes('+')) {
|
|
1075
|
-
const parts = splitExpressionParts(text);
|
|
1076
|
-
const resolvedParts = parts.map(part => {
|
|
1077
|
-
const partTrimmed = part.trim();
|
|
1078
|
-
// If it's a quoted string, keep as-is but remove quotes
|
|
1079
|
-
if ((partTrimmed.startsWith('"') && partTrimmed.endsWith('"')) ||
|
|
1080
|
-
(partTrimmed.startsWith("'") && partTrimmed.endsWith("'"))) {
|
|
1081
|
-
return partTrimmed.slice(1, -1);
|
|
1082
|
-
}
|
|
1083
|
-
// If it's a bare variable, resolve it
|
|
1084
|
-
const varMatch = partTrimmed.match(/^([a-zA-Z_][a-zA-Z0-9_]*)((?:\.[a-zA-Z_][a-zA-Z0-9_]*|\[\d+\])*)$/);
|
|
1085
|
-
if (varMatch) {
|
|
1086
|
-
const varName = varMatch[1];
|
|
1087
|
-
const pathPart = varMatch[2] || '';
|
|
1088
|
-
if (varName in variables) {
|
|
1089
|
-
const value = variables[varName];
|
|
1090
|
-
if (pathPart) {
|
|
1091
|
-
const path = pathPart.replace(/^\./, '');
|
|
1092
|
-
return valueToString(getNestedValueFromObject(value, path));
|
|
1093
|
-
}
|
|
1094
|
-
return valueToString(value);
|
|
1095
|
-
}
|
|
1096
|
-
}
|
|
1097
|
-
// Otherwise, return as-is
|
|
1098
|
-
return partTrimmed;
|
|
1099
|
-
});
|
|
1100
|
-
return resolvedParts.join('');
|
|
1101
|
-
}
|
|
1102
|
-
// No resolution needed
|
|
1103
|
-
return text;
|
|
1104
|
-
}
|
|
1105
|
-
/**
|
|
1106
|
-
* Split an expression by + operator, respecting string literals
|
|
1107
|
-
*/
|
|
1108
|
-
function splitExpressionParts(expr) {
|
|
1109
|
-
const parts = [];
|
|
1110
|
-
let current = '';
|
|
1111
|
-
let inString = false;
|
|
1112
|
-
let stringChar = '';
|
|
1113
|
-
for (let i = 0; i < expr.length; i++) {
|
|
1114
|
-
const char = expr[i];
|
|
1115
|
-
if (!inString && (char === '"' || char === "'")) {
|
|
1116
|
-
inString = true;
|
|
1117
|
-
stringChar = char;
|
|
1118
|
-
current += char;
|
|
1119
|
-
}
|
|
1120
|
-
else if (inString && char === stringChar) {
|
|
1121
|
-
inString = false;
|
|
1122
|
-
current += char;
|
|
1123
|
-
}
|
|
1124
|
-
else if (!inString && char === '+') {
|
|
1125
|
-
parts.push(current);
|
|
1126
|
-
current = '';
|
|
1127
|
-
}
|
|
1128
|
-
else {
|
|
1129
|
-
current += char;
|
|
1130
|
-
}
|
|
1131
|
-
}
|
|
1132
|
-
if (current) {
|
|
1133
|
-
parts.push(current);
|
|
1134
|
-
}
|
|
1135
|
-
return parts;
|
|
1136
|
-
}
|
|
1137
|
-
/**
|
|
1138
|
-
* Get nested value from an object using a dot-notation path
|
|
1139
|
-
*/
|
|
1140
|
-
function getNestedValueFromObject(obj, path) {
|
|
1141
|
-
if (!path) {
|
|
1142
|
-
return obj;
|
|
1143
|
-
}
|
|
1144
|
-
const parts = path.split(/\.|\[(\d+)\]/).filter(p => p !== '' && p !== undefined);
|
|
1145
|
-
let current = obj;
|
|
1146
|
-
for (const part of parts) {
|
|
1147
|
-
if (current === null || current === undefined) {
|
|
1148
|
-
return undefined;
|
|
1149
|
-
}
|
|
1150
|
-
current = current[part];
|
|
1151
|
-
}
|
|
1152
|
-
return current;
|
|
1153
|
-
}
|
|
1154
|
-
/**
|
|
1155
|
-
* Convert a value to string for display
|
|
1156
|
-
*/
|
|
1157
|
-
function valueToString(value) {
|
|
1158
|
-
if (value === null) {
|
|
1159
|
-
return 'null';
|
|
1160
|
-
}
|
|
1161
|
-
if (value === undefined) {
|
|
1162
|
-
return 'undefined';
|
|
1163
|
-
}
|
|
1164
|
-
if (typeof value === 'object') {
|
|
1165
|
-
return JSON.stringify(value);
|
|
1166
|
-
}
|
|
1167
|
-
return String(value);
|
|
1168
|
-
}
|
|
1169
|
-
/**
|
|
1170
|
-
* Sleep helper function
|
|
1171
|
-
*/
|
|
1172
|
-
function sleep(ms) {
|
|
1173
|
-
return new Promise(resolve => setTimeout(resolve, ms));
|
|
1174
|
-
}
|
|
1175
|
-
/**
|
|
1176
|
-
* Checks if a line is the start of an if block.
|
|
1177
|
-
* Format: if <condition>
|
|
1178
|
-
*/
|
|
1179
|
-
function isIfCommand(line) {
|
|
1180
|
-
return /^if\s+.+$/i.test(line.trim());
|
|
1181
|
-
}
|
|
1182
|
-
/**
|
|
1183
|
-
* Checks if a line is the end of an if block.
|
|
1184
|
-
*/
|
|
1185
|
-
function isEndIfCommand(line) {
|
|
1186
|
-
const trimmed = line.trim().toLowerCase();
|
|
1187
|
-
return trimmed === 'end if' || trimmed === 'endif';
|
|
1188
|
-
}
|
|
1189
|
-
/**
|
|
1190
|
-
* Parses an if condition.
|
|
1191
|
-
* Reuses assertion parsing logic for the condition.
|
|
1192
|
-
*/
|
|
1193
|
-
function parseIfCondition(line) {
|
|
1194
|
-
const match = line.trim().match(/^if\s+(.+)$/i);
|
|
1195
|
-
return match ? match[1].trim() : null;
|
|
1196
|
-
}
|
|
1197
|
-
/**
|
|
1198
|
-
* Evaluates an if condition using the same logic as assertions.
|
|
1199
|
-
* Supports: ==, !=, >, >=, <, <=, contains, exists, !exists, isType
|
|
1200
|
-
*/
|
|
1201
|
-
function evaluateIfCondition(condition, responses, variables, getValueByPathFn) {
|
|
1202
|
-
// Import assertion parsing and evaluation
|
|
1203
|
-
const { parseAssertCommand, evaluateAssertion } = require('./assertionRunner');
|
|
1204
|
-
// Wrap condition as "assert <condition>" to reuse parsing
|
|
1205
|
-
const parsed = parseAssertCommand(`assert ${condition}`);
|
|
1206
|
-
if (!parsed) {
|
|
1207
|
-
// Can't parse - treat as false
|
|
1208
|
-
return false;
|
|
1209
|
-
}
|
|
1210
|
-
const result = evaluateAssertion(parsed, responses, variables, getValueByPathFn);
|
|
1211
|
-
return result.passed;
|
|
1212
|
-
}
|
|
1213
|
-
/**
|
|
1214
|
-
* Counts the number of steps in a sequence (for progress reporting).
|
|
1215
|
-
*/
|
|
1216
|
-
function countSequenceSteps(sequenceContent) {
|
|
1217
|
-
return extractStepsFromSequence(sequenceContent).length;
|
|
1218
|
-
}
|
|
1219
|
-
/**
|
|
1220
|
-
* Extracts steps (requests, run commands, and print statements) from a sequence block.
|
|
1221
|
-
*/
|
|
1222
|
-
function extractStepsFromSequence(content) {
|
|
1223
|
-
const lines = content.split('\n');
|
|
1224
|
-
const steps = [];
|
|
1225
|
-
const methodRegex = /^(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+/i;
|
|
1226
|
-
// Boundaries that end a pending request-like block and start a new step.
|
|
1227
|
-
const stepBoundaryRegex = /^(###|(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)\s+|(test\s+)?sequence\s+|end\s+sequence|end\s+if|endif\b|var\s+|run\s+|print\s+|assert\s+|if\s+|wait\s+|import\s+|return\s+|\[)/i;
|
|
1228
|
-
let currentRequest = [];
|
|
1229
|
-
let currentRequestStartLine = -1;
|
|
1230
|
-
let inRequest = false;
|
|
1231
|
-
// Track varRequest with body support (var x = POST Endpoint Headers + body lines)
|
|
1232
|
-
let currentVarRequest = [];
|
|
1233
|
-
let currentVarRequestStartLine = -1;
|
|
1234
|
-
let inVarRequest = false;
|
|
1235
|
-
// Helper to save pending varRequest
|
|
1236
|
-
const savePendingVarRequest = () => {
|
|
1237
|
-
if (currentVarRequest.length > 0) {
|
|
1238
|
-
steps.push({
|
|
1239
|
-
type: 'varRequest',
|
|
1240
|
-
content: currentVarRequest.join('\n'),
|
|
1241
|
-
lineNumber: currentVarRequestStartLine,
|
|
1242
|
-
});
|
|
1243
|
-
currentVarRequest = [];
|
|
1244
|
-
inVarRequest = false;
|
|
1245
|
-
}
|
|
1246
|
-
};
|
|
1247
|
-
for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
|
|
1248
|
-
const line = lines[lineIdx];
|
|
1249
|
-
const trimmed = line.trim();
|
|
1250
|
-
// Check if this is an assert command
|
|
1251
|
-
if ((0, assertionRunner_1.isAssertCommand)(trimmed)) {
|
|
1252
|
-
// Save any pending request/varRequest first
|
|
1253
|
-
if (currentRequest.length > 0) {
|
|
1254
|
-
steps.push({
|
|
1255
|
-
type: 'request',
|
|
1256
|
-
content: currentRequest.join('\n'),
|
|
1257
|
-
lineNumber: currentRequestStartLine,
|
|
1258
|
-
});
|
|
1259
|
-
currentRequest = [];
|
|
1260
|
-
inRequest = false;
|
|
1261
|
-
}
|
|
1262
|
-
savePendingVarRequest();
|
|
1263
|
-
// Add the assertion step
|
|
1264
|
-
steps.push({
|
|
1265
|
-
type: 'assertion',
|
|
1266
|
-
content: trimmed,
|
|
1267
|
-
lineNumber: lineIdx,
|
|
1268
|
-
});
|
|
1269
|
-
continue;
|
|
1270
|
-
}
|
|
1271
|
-
// Check if this is an if command
|
|
1272
|
-
if (isIfCommand(trimmed)) {
|
|
1273
|
-
// Save any pending request/varRequest first
|
|
1274
|
-
if (currentRequest.length > 0) {
|
|
1275
|
-
steps.push({
|
|
1276
|
-
type: 'request',
|
|
1277
|
-
content: currentRequest.join('\n'),
|
|
1278
|
-
lineNumber: currentRequestStartLine,
|
|
1279
|
-
});
|
|
1280
|
-
currentRequest = [];
|
|
1281
|
-
inRequest = false;
|
|
1282
|
-
}
|
|
1283
|
-
savePendingVarRequest();
|
|
1284
|
-
// Add the if step with the condition
|
|
1285
|
-
const condition = parseIfCondition(trimmed);
|
|
1286
|
-
steps.push({
|
|
1287
|
-
type: 'if',
|
|
1288
|
-
content: condition || '',
|
|
1289
|
-
lineNumber: lineIdx,
|
|
1290
|
-
});
|
|
1291
|
-
continue;
|
|
1292
|
-
}
|
|
1293
|
-
// Check if this is an end if command
|
|
1294
|
-
if (isEndIfCommand(trimmed)) {
|
|
1295
|
-
// Save any pending request/varRequest first
|
|
1296
|
-
if (currentRequest.length > 0) {
|
|
1297
|
-
steps.push({
|
|
1298
|
-
type: 'request',
|
|
1299
|
-
content: currentRequest.join('\n'),
|
|
1300
|
-
lineNumber: currentRequestStartLine,
|
|
1301
|
-
});
|
|
1302
|
-
currentRequest = [];
|
|
1303
|
-
inRequest = false;
|
|
1304
|
-
}
|
|
1305
|
-
savePendingVarRequest();
|
|
1306
|
-
// Add the endif step
|
|
1307
|
-
steps.push({
|
|
1308
|
-
type: 'endif',
|
|
1309
|
-
content: '',
|
|
1310
|
-
lineNumber: lineIdx,
|
|
1311
|
-
});
|
|
1312
|
-
continue;
|
|
1313
|
-
}
|
|
1314
|
-
// Check if this is a print command
|
|
1315
|
-
if (isPrintCommand(trimmed)) {
|
|
1316
|
-
// Save any pending request/varRequest first
|
|
1317
|
-
if (currentRequest.length > 0) {
|
|
1318
|
-
steps.push({
|
|
1319
|
-
type: 'request',
|
|
1320
|
-
content: currentRequest.join('\n'),
|
|
1321
|
-
lineNumber: currentRequestStartLine,
|
|
1322
|
-
});
|
|
1323
|
-
currentRequest = [];
|
|
1324
|
-
inRequest = false;
|
|
1325
|
-
}
|
|
1326
|
-
savePendingVarRequest();
|
|
1327
|
-
// Add the print step - store raw line for later parsing with variable substitution
|
|
1328
|
-
steps.push({
|
|
1329
|
-
type: 'print',
|
|
1330
|
-
content: trimmed, // Store the full print command
|
|
1331
|
-
lineNumber: lineIdx,
|
|
1332
|
-
});
|
|
1333
|
-
continue;
|
|
1334
|
-
}
|
|
1335
|
-
// Check if this is a wait command
|
|
1336
|
-
if (isWaitCommand(trimmed)) {
|
|
1337
|
-
// Save any pending request/varRequest first
|
|
1338
|
-
if (currentRequest.length > 0) {
|
|
1339
|
-
steps.push({
|
|
1340
|
-
type: 'request',
|
|
1341
|
-
content: currentRequest.join('\n'),
|
|
1342
|
-
lineNumber: currentRequestStartLine,
|
|
1343
|
-
});
|
|
1344
|
-
currentRequest = [];
|
|
1345
|
-
inRequest = false;
|
|
1346
|
-
}
|
|
1347
|
-
savePendingVarRequest();
|
|
1348
|
-
// Add the wait step
|
|
1349
|
-
steps.push({
|
|
1350
|
-
type: 'wait',
|
|
1351
|
-
content: trimmed,
|
|
1352
|
-
lineNumber: lineIdx,
|
|
1353
|
-
});
|
|
1354
|
-
continue;
|
|
1355
|
-
}
|
|
1356
|
-
// Check if this is a JSON file load command (var x = run readJson ./path.json)
|
|
1357
|
-
if ((0, jsonFileReader_1.isJsonCommand)(trimmed)) {
|
|
1358
|
-
// Save any pending request/varRequest first
|
|
1359
|
-
if (currentRequest.length > 0) {
|
|
1360
|
-
steps.push({
|
|
1361
|
-
type: 'request',
|
|
1362
|
-
content: currentRequest.join('\n'),
|
|
1363
|
-
lineNumber: currentRequestStartLine,
|
|
1364
|
-
});
|
|
1365
|
-
currentRequest = [];
|
|
1366
|
-
inRequest = false;
|
|
1367
|
-
}
|
|
1368
|
-
savePendingVarRequest();
|
|
1369
|
-
// Add the JSON load step
|
|
1370
|
-
steps.push({
|
|
1371
|
-
type: 'json',
|
|
1372
|
-
content: trimmed,
|
|
1373
|
-
lineNumber: lineIdx,
|
|
1374
|
-
});
|
|
1375
|
-
continue;
|
|
1376
|
-
}
|
|
1377
|
-
// Check if this is a property assignment (config.property = value)
|
|
1378
|
-
if ((0, jsonFileReader_1.isPropertyAssignment)(trimmed)) {
|
|
1379
|
-
// Save any pending request/varRequest first
|
|
1380
|
-
if (currentRequest.length > 0) {
|
|
1381
|
-
steps.push({
|
|
1382
|
-
type: 'request',
|
|
1383
|
-
content: currentRequest.join('\n'),
|
|
1384
|
-
lineNumber: currentRequestStartLine,
|
|
1385
|
-
});
|
|
1386
|
-
currentRequest = [];
|
|
1387
|
-
inRequest = false;
|
|
1388
|
-
}
|
|
1389
|
-
savePendingVarRequest();
|
|
1390
|
-
// Add the property assignment step
|
|
1391
|
-
steps.push({
|
|
1392
|
-
type: 'propAssign',
|
|
1393
|
-
content: trimmed,
|
|
1394
|
-
lineNumber: lineIdx,
|
|
1395
|
-
});
|
|
1396
|
-
continue;
|
|
1397
|
-
}
|
|
1398
|
-
// Check for script run commands (run bash, run js, run powershell) including var x = run ...
|
|
1399
|
-
// Must check BEFORE isVarRunSequenceCommand to avoid treating "powershell" as a sequence name
|
|
1400
|
-
if ((0, scriptRunner_1.isRunCommand)(trimmed)) {
|
|
1401
|
-
// Save any pending request/varRequest first
|
|
1402
|
-
if (currentRequest.length > 0) {
|
|
1403
|
-
steps.push({
|
|
1404
|
-
type: 'request',
|
|
1405
|
-
content: currentRequest.join('\n'),
|
|
1406
|
-
lineNumber: currentRequestStartLine,
|
|
1407
|
-
});
|
|
1408
|
-
currentRequest = [];
|
|
1409
|
-
inRequest = false;
|
|
1410
|
-
}
|
|
1411
|
-
savePendingVarRequest();
|
|
1412
|
-
// Add the script step
|
|
1413
|
-
steps.push({
|
|
1414
|
-
type: 'script',
|
|
1415
|
-
content: trimmed,
|
|
1416
|
-
lineNumber: lineIdx,
|
|
1417
|
-
});
|
|
1418
|
-
continue;
|
|
1419
|
-
}
|
|
1420
|
-
// Check if this is a "var x = run SequenceName" command (capture sequence result)
|
|
1421
|
-
// Must check BEFORE isRunNamedRequestCommand since it also involves "run"
|
|
1422
|
-
if (isVarRunSequenceCommand(trimmed)) {
|
|
1423
|
-
// Save any pending request/varRequest first
|
|
1424
|
-
if (currentRequest.length > 0) {
|
|
1425
|
-
steps.push({
|
|
1426
|
-
type: 'request',
|
|
1427
|
-
content: currentRequest.join('\n'),
|
|
1428
|
-
lineNumber: currentRequestStartLine,
|
|
1429
|
-
});
|
|
1430
|
-
currentRequest = [];
|
|
1431
|
-
inRequest = false;
|
|
1432
|
-
}
|
|
1433
|
-
savePendingVarRequest();
|
|
1434
|
-
// Add the varRunSequence step
|
|
1435
|
-
steps.push({
|
|
1436
|
-
type: 'varRunSequence',
|
|
1437
|
-
content: trimmed,
|
|
1438
|
-
lineNumber: lineIdx,
|
|
1439
|
-
});
|
|
1440
|
-
continue;
|
|
1441
|
-
}
|
|
1442
|
-
// Check if this is a run command (including var x = run ...)
|
|
1443
|
-
// First check for "run <RequestName>" (named request invocation)
|
|
1444
|
-
if (isRunNamedRequestCommand(trimmed)) {
|
|
1445
|
-
// Save any pending request/varRequest first
|
|
1446
|
-
if (currentRequest.length > 0) {
|
|
1447
|
-
steps.push({
|
|
1448
|
-
type: 'request',
|
|
1449
|
-
content: currentRequest.join('\n'),
|
|
1450
|
-
lineNumber: currentRequestStartLine,
|
|
1451
|
-
});
|
|
1452
|
-
currentRequest = [];
|
|
1453
|
-
inRequest = false;
|
|
1454
|
-
}
|
|
1455
|
-
savePendingVarRequest();
|
|
1456
|
-
// Add the named request step - store the whole line so we can parse args later
|
|
1457
|
-
steps.push({
|
|
1458
|
-
type: 'namedRequest',
|
|
1459
|
-
content: trimmed,
|
|
1460
|
-
lineNumber: lineIdx,
|
|
1461
|
-
});
|
|
1462
|
-
continue;
|
|
1463
|
-
}
|
|
1464
|
-
// Check if this is a variable request assignment (var x = GET/POST url)
|
|
1465
|
-
// This must come before varAssign check since it also starts with 'var'
|
|
1466
|
-
if (isVarRequestCommand(trimmed)) {
|
|
1467
|
-
// Save any pending request/varRequest first
|
|
1468
|
-
if (currentRequest.length > 0) {
|
|
1469
|
-
steps.push({
|
|
1470
|
-
type: 'request',
|
|
1471
|
-
content: currentRequest.join('\n'),
|
|
1472
|
-
lineNumber: currentRequestStartLine,
|
|
1473
|
-
});
|
|
1474
|
-
currentRequest = [];
|
|
1475
|
-
inRequest = false;
|
|
1476
|
-
}
|
|
1477
|
-
savePendingVarRequest();
|
|
1478
|
-
// Start collecting the variable request (may have body on subsequent lines)
|
|
1479
|
-
currentVarRequest = [trimmed];
|
|
1480
|
-
currentVarRequestStartLine = lineIdx;
|
|
1481
|
-
inVarRequest = true;
|
|
1482
|
-
continue;
|
|
1483
|
-
}
|
|
1484
|
-
// Check if this is a variable assignment (var x = expr or var x = "string")
|
|
1485
|
-
// This must come after run command check to not catch var x = run ...
|
|
1486
|
-
if (isVarAssignCommand(trimmed)) {
|
|
1487
|
-
// Save any pending request/varRequest first
|
|
1488
|
-
if (currentRequest.length > 0) {
|
|
1489
|
-
steps.push({
|
|
1490
|
-
type: 'request',
|
|
1491
|
-
content: currentRequest.join('\n'),
|
|
1492
|
-
lineNumber: currentRequestStartLine,
|
|
1493
|
-
});
|
|
1494
|
-
currentRequest = [];
|
|
1495
|
-
inRequest = false;
|
|
1496
|
-
}
|
|
1497
|
-
savePendingVarRequest();
|
|
1498
|
-
// Add the variable assignment step
|
|
1499
|
-
steps.push({
|
|
1500
|
-
type: 'varAssign',
|
|
1501
|
-
content: trimmed,
|
|
1502
|
-
lineNumber: lineIdx,
|
|
1503
|
-
});
|
|
1504
|
-
continue;
|
|
1505
|
-
}
|
|
1506
|
-
// Check if this is a new HTTP request (use trimmed for detection)
|
|
1507
|
-
if (methodRegex.test(trimmed)) {
|
|
1508
|
-
// Save previous request if exists
|
|
1509
|
-
if (currentRequest.length > 0) {
|
|
1510
|
-
steps.push({
|
|
1511
|
-
type: 'request',
|
|
1512
|
-
content: currentRequest.join('\n'),
|
|
1513
|
-
lineNumber: currentRequestStartLine,
|
|
1514
|
-
});
|
|
1515
|
-
}
|
|
1516
|
-
savePendingVarRequest();
|
|
1517
|
-
// Use trimmed line for the request content
|
|
1518
|
-
currentRequest = [trimmed];
|
|
1519
|
-
currentRequestStartLine = lineIdx;
|
|
1520
|
-
inRequest = true;
|
|
1521
|
-
}
|
|
1522
|
-
else if (inVarRequest) {
|
|
1523
|
-
// Keep collecting all request lines (headers/body/comments/blank lines)
|
|
1524
|
-
// until we hit a new command boundary.
|
|
1525
|
-
if (trimmed !== '' && stepBoundaryRegex.test(trimmed)) {
|
|
1526
|
-
savePendingVarRequest();
|
|
1527
|
-
// Re-process this line as the next command.
|
|
1528
|
-
lineIdx--;
|
|
1529
|
-
}
|
|
1530
|
-
else {
|
|
1531
|
-
currentVarRequest.push(trimmed);
|
|
1532
|
-
}
|
|
1533
|
-
}
|
|
1534
|
-
else if (inRequest) {
|
|
1535
|
-
// Check if this is a var line with $N reference (capture) - don't include in request
|
|
1536
|
-
if (trimmed.match(/^var\s+[a-zA-Z_][a-zA-Z0-9_]*\s*=\s*\$\d+/)) {
|
|
1537
|
-
// This is a capture line, end the current request
|
|
1538
|
-
if (currentRequest.length > 0) {
|
|
1539
|
-
steps.push({
|
|
1540
|
-
type: 'request',
|
|
1541
|
-
content: currentRequest.join('\n'),
|
|
1542
|
-
lineNumber: currentRequestStartLine,
|
|
1543
|
-
});
|
|
1544
|
-
currentRequest = [];
|
|
1545
|
-
inRequest = false;
|
|
1546
|
-
}
|
|
1547
|
-
}
|
|
1548
|
-
else if (trimmed === '' || trimmed.startsWith('#') || trimmed.startsWith('var ')) {
|
|
1549
|
-
// Empty lines, comments, or static vars might be part of request context
|
|
1550
|
-
if (trimmed.startsWith('var ') && !trimmed.match(/\$\d+/)) {
|
|
1551
|
-
// Static var, keep going - use trimmed
|
|
1552
|
-
currentRequest.push(trimmed);
|
|
1553
|
-
}
|
|
1554
|
-
else if (trimmed.startsWith('#')) {
|
|
1555
|
-
// Comment, keep in request - use trimmed
|
|
1556
|
-
currentRequest.push(trimmed);
|
|
1557
|
-
}
|
|
1558
|
-
else {
|
|
1559
|
-
// Empty line - could be body separator or end
|
|
1560
|
-
currentRequest.push(trimmed);
|
|
1561
|
-
}
|
|
1562
|
-
}
|
|
1563
|
-
else {
|
|
1564
|
-
// Use trimmed for request body lines too
|
|
1565
|
-
currentRequest.push(trimmed);
|
|
1566
|
-
}
|
|
1567
|
-
}
|
|
1568
|
-
}
|
|
1569
|
-
// Don't forget the last request or varRequest
|
|
1570
|
-
if (currentRequest.length > 0) {
|
|
1571
|
-
steps.push({
|
|
1572
|
-
type: 'request',
|
|
1573
|
-
content: currentRequest.join('\n'),
|
|
1574
|
-
lineNumber: currentRequestStartLine,
|
|
1575
|
-
});
|
|
1576
|
-
}
|
|
1577
|
-
savePendingVarRequest();
|
|
1578
|
-
return steps;
|
|
1579
|
-
}
|
|
1580
|
-
/**
|
|
1581
|
-
* Extracts individual requests from a sequence block.
|
|
1582
|
-
* @deprecated Use extractStepsFromSequence instead
|
|
1583
|
-
*/
|
|
1584
|
-
function extractRequestsFromSequence(content) {
|
|
1585
|
-
return extractStepsFromSequence(content)
|
|
1586
|
-
.filter(s => s.type === 'request')
|
|
1587
|
-
.map(s => s.content);
|
|
1588
|
-
}
|
|
1589
|
-
function extractCaptureDirectives(content) {
|
|
1590
|
-
const captures = [];
|
|
1591
|
-
// Match: var x = $1 OR var x = $1.path OR var x = $1[0] OR var x = $1[0].path
|
|
1592
|
-
const captureRegex = /^var\s+([a-zA-Z_][a-zA-Z0-9_]*)\s*=\s*\$(\d+)([\.\[].*)?$/;
|
|
1593
|
-
for (const line of content.split('\n')) {
|
|
1594
|
-
const match = line.trim().match(captureRegex);
|
|
1595
|
-
if (match) {
|
|
1596
|
-
let path = match[3] || '';
|
|
1597
|
-
// Remove leading dot if present (e.g., ".user.id" -> "user.id")
|
|
1598
|
-
if (path.startsWith('.')) {
|
|
1599
|
-
path = path.substring(1);
|
|
1600
|
-
}
|
|
1601
|
-
captures.push({
|
|
1602
|
-
varName: match[1],
|
|
1603
|
-
afterRequest: parseInt(match[2], 10),
|
|
1604
|
-
path: path
|
|
1605
|
-
});
|
|
1606
|
-
}
|
|
1607
|
-
}
|
|
1608
|
-
return captures;
|
|
1609
|
-
}
|
|
1610
|
-
/**
|
|
1611
|
-
* Gets a value from an HTTP response using a path.
|
|
1612
|
-
*
|
|
1613
|
-
* Supported top-level properties:
|
|
1614
|
-
* - status: HTTP status code (e.g., 200)
|
|
1615
|
-
* - statusText: HTTP status text (e.g., "OK")
|
|
1616
|
-
* - headers: Response headers object
|
|
1617
|
-
* - headers.X: Specific header value (e.g., headers.Content-Type)
|
|
1618
|
-
* - duration: Request duration in milliseconds
|
|
1619
|
-
* - body: Response body (required prefix for body access)
|
|
1620
|
-
* - body.path.to.value: Access nested body properties
|
|
1621
|
-
*
|
|
1622
|
-
* Examples:
|
|
1623
|
-
* - "status" → 200
|
|
1624
|
-
* - "headers.Content-Type" → "application/json"
|
|
1625
|
-
* - "body" → entire response body (stringified if object)
|
|
1626
|
-
* - "body.user.id" → nested body value
|
|
1627
|
-
* - "body[0].name" → array access in body
|
|
1628
|
-
*/
|
|
1629
|
-
function getValueByPath(response, path) {
|
|
1630
|
-
// Empty path is invalid - must specify what to access
|
|
1631
|
-
if (!path) {
|
|
1632
|
-
return undefined;
|
|
1633
|
-
}
|
|
1634
|
-
// Convert [0] to .0 and split by dots, filter out empty strings
|
|
1635
|
-
const parts = path.replace(/\[(\d+)\]/g, '.$1').split('.').filter(p => p !== '');
|
|
1636
|
-
if (parts.length === 0) {
|
|
1637
|
-
return undefined;
|
|
1638
|
-
}
|
|
1639
|
-
const topLevel = parts[0];
|
|
1640
|
-
const remainingParts = parts.slice(1);
|
|
1641
|
-
// Handle top-level response properties
|
|
1642
|
-
switch (topLevel) {
|
|
1643
|
-
case 'status':
|
|
1644
|
-
return response.status;
|
|
1645
|
-
case 'statusText':
|
|
1646
|
-
return response.statusText;
|
|
1647
|
-
case 'duration':
|
|
1648
|
-
return response.duration;
|
|
1649
|
-
case 'headers':
|
|
1650
|
-
if (remainingParts.length === 0) {
|
|
1651
|
-
return JSON.stringify(response.headers);
|
|
1652
|
-
}
|
|
1653
|
-
// Access specific header (case-insensitive lookup)
|
|
1654
|
-
const headerName = remainingParts[0];
|
|
1655
|
-
const headerKey = Object.keys(response.headers).find(k => k.toLowerCase() === headerName.toLowerCase());
|
|
1656
|
-
if (headerKey) {
|
|
1657
|
-
return response.headers[headerKey];
|
|
1658
|
-
}
|
|
1659
|
-
return undefined;
|
|
1660
|
-
case 'body':
|
|
1661
|
-
// Access body or nested body property
|
|
1662
|
-
if (remainingParts.length === 0) {
|
|
1663
|
-
// Return entire body as-is (keep objects/arrays for type checking)
|
|
1664
|
-
return response.body;
|
|
1665
|
-
}
|
|
1666
|
-
// Navigate into body
|
|
1667
|
-
let current = response.body;
|
|
1668
|
-
for (const part of remainingParts) {
|
|
1669
|
-
if (current === null || current === undefined) {
|
|
1670
|
-
return undefined;
|
|
1671
|
-
}
|
|
1672
|
-
current = current[part];
|
|
1673
|
-
}
|
|
1674
|
-
// Return the value as-is (keep objects/arrays as objects for type checking)
|
|
1675
|
-
return current;
|
|
1676
|
-
default:
|
|
1677
|
-
// Unknown top-level property
|
|
1678
|
-
return undefined;
|
|
1679
|
-
}
|
|
1680
|
-
}
|
|
1681
|
-
/**
|
|
1682
|
-
* Runs all requests in a sequence, capturing variables between requests.
|
|
1683
|
-
* Uses the shared cookie jar by default (for VS Code extension).
|
|
1684
|
-
* @param fullDocumentText - Optional full document text for resolving named requests (run <RequestName>)
|
|
1685
|
-
* @param onProgress - Optional callback invoked after each step completes
|
|
1686
|
-
* @param callStack - Internal: tracks sequence call stack for circular reference detection
|
|
1687
|
-
* @param sequenceArgs - Optional variables to inject as sequence parameters
|
|
1688
|
-
* @param apiDefinitions - Optional API definitions (header groups and endpoints) from imported .nornapi files
|
|
1689
|
-
* @param tagFilterOptions - Optional tag filter options for filtering nested sequence calls
|
|
1690
|
-
* @param sequenceSources - Optional map of sequence names to source file paths for script path resolution
|
|
1691
|
-
*/
|
|
1692
|
-
async function runSequence(sequenceContent, fileVariables, workingDir, fullDocumentText, onProgress, callStack, sequenceArgs, apiDefinitions, tagFilterOptions, sequenceSources) {
|
|
1693
|
-
return runSequenceWithJar(sequenceContent, fileVariables, undefined, workingDir, fullDocumentText, onProgress, callStack, sequenceArgs, apiDefinitions, tagFilterOptions, sequenceSources);
|
|
1694
|
-
}
|
|
1695
|
-
/**
|
|
1696
|
-
* Runs all requests in a sequence with a specific cookie jar.
|
|
1697
|
-
* If no jar is provided, uses shared jar (VS Code) or creates new one (CLI).
|
|
1698
|
-
* @param fullDocumentText - Optional full document text for resolving named requests (run <RequestName>)
|
|
1699
|
-
* @param onProgress - Optional callback invoked after each step completes
|
|
1700
|
-
* @param callStack - Internal: tracks sequence call stack for circular reference detection
|
|
1701
|
-
* @param sequenceArgs - Optional variables to inject as sequence parameters
|
|
1702
|
-
* @param apiDefinitions - Optional API definitions (header groups and endpoints) from imported .nornapi files
|
|
1703
|
-
* @param tagFilterOptions - Optional tag filter options for filtering nested sequence calls
|
|
1704
|
-
* @param sequenceSources - Optional map of sequence names to source file paths for script path resolution
|
|
1705
|
-
*/
|
|
1706
|
-
async function runSequenceWithJar(sequenceContent, fileVariables, cookieJar, workingDir, fullDocumentText, onProgress, callStack, sequenceArgs, apiDefinitions, tagFilterOptions, sequenceSources) {
|
|
1707
|
-
const startTime = Date.now();
|
|
1708
|
-
const responses = [];
|
|
1709
|
-
const scriptResults = [];
|
|
1710
|
-
const assertionResults = [];
|
|
1711
|
-
const orderedSteps = [];
|
|
1712
|
-
const errors = [];
|
|
1713
|
-
// Helper to add response and update $N references in runtimeVariables
|
|
1714
|
-
const addResponse = (response) => {
|
|
1715
|
-
responses.push(response);
|
|
1716
|
-
// Store as $N for easy reference in print, etc.
|
|
1717
|
-
runtimeVariables[`$${responses.length}`] = response;
|
|
1718
|
-
};
|
|
1719
|
-
// Track mapping from response index (1-based) to variable name for enhanced failure context
|
|
1720
|
-
const responseIndexToVariable = new Map();
|
|
1721
|
-
// Extract steps (requests and scripts) and capture directives
|
|
1722
|
-
const steps = extractStepsFromSequence(sequenceContent);
|
|
1723
|
-
const captures = extractCaptureDirectives(sequenceContent);
|
|
1724
|
-
const totalSteps = steps.length;
|
|
1725
|
-
// Calculate nesting depth from call stack
|
|
1726
|
-
const nestingDepth = callStack ? callStack.length : 0;
|
|
1727
|
-
// Helper to report progress
|
|
1728
|
-
const reportProgress = (stepIdx, stepType, description, result, sequenceName) => {
|
|
1729
|
-
if (onProgress) {
|
|
1730
|
-
onProgress({
|
|
1731
|
-
currentStep: stepIdx + 1,
|
|
1732
|
-
totalSteps,
|
|
1733
|
-
stepType,
|
|
1734
|
-
stepDescription: description,
|
|
1735
|
-
stepResult: result,
|
|
1736
|
-
sequenceName,
|
|
1737
|
-
nestingDepth
|
|
1738
|
-
});
|
|
1739
|
-
}
|
|
1740
|
-
};
|
|
1741
|
-
// Runtime variables (starts with file variables, grows with captures and script outputs)
|
|
1742
|
-
// Inject sequence arguments (parameters) which shadow file variables
|
|
1743
|
-
// Type is any to allow storing response objects (from var x = GET url)
|
|
1744
|
-
const runtimeVariables = { ...fileVariables, ...sequenceArgs };
|
|
1745
|
-
// Track request index for $N capture references
|
|
1746
|
-
let requestIndex = 0;
|
|
1747
|
-
// Track if-block state: stack of booleans (true = executing, false = skipping)
|
|
1748
|
-
const ifStack = [];
|
|
1749
|
-
// Helper to check if we should skip the current step (inside a false if-block)
|
|
1750
|
-
const shouldSkip = () => ifStack.length > 0 && ifStack.some(v => !v);
|
|
1751
|
-
for (let stepIdx = 0; stepIdx < steps.length; stepIdx++) {
|
|
1752
|
-
const step = steps[stepIdx];
|
|
1753
|
-
// Handle if/endif first (they affect control flow)
|
|
1754
|
-
if (step.type === 'if') {
|
|
1755
|
-
// Always evaluate if statements, even when skipping (for nesting)
|
|
1756
|
-
if (shouldSkip()) {
|
|
1757
|
-
// Already skipping, push false to maintain nesting
|
|
1758
|
-
ifStack.push(false);
|
|
1759
|
-
}
|
|
1760
|
-
else {
|
|
1761
|
-
// Evaluate the condition
|
|
1762
|
-
const conditionMet = evaluateIfCondition(step.content, responses, runtimeVariables, getValueByPath);
|
|
1763
|
-
ifStack.push(conditionMet);
|
|
1764
|
-
const stepResult = {
|
|
1765
|
-
type: 'print',
|
|
1766
|
-
stepIndex: stepIdx,
|
|
1767
|
-
print: {
|
|
1768
|
-
title: `if ${step.content}: ${conditionMet ? 'true' : 'false'}`,
|
|
1769
|
-
timestamp: Date.now() - startTime
|
|
1770
|
-
}
|
|
1771
|
-
};
|
|
1772
|
-
orderedSteps.push(stepResult);
|
|
1773
|
-
reportProgress(stepIdx, 'if', `if ${step.content} → ${conditionMet}`, stepResult);
|
|
1774
|
-
}
|
|
1775
|
-
continue;
|
|
1776
|
-
}
|
|
1777
|
-
if (step.type === 'endif') {
|
|
1778
|
-
if (ifStack.length > 0) {
|
|
1779
|
-
ifStack.pop();
|
|
1780
|
-
}
|
|
1781
|
-
continue;
|
|
1782
|
-
}
|
|
1783
|
-
// Skip steps if inside a false if-block
|
|
1784
|
-
if (shouldSkip()) {
|
|
1785
|
-
continue;
|
|
1786
|
-
}
|
|
1787
|
-
if (step.type === 'assertion') {
|
|
1788
|
-
// Parse and evaluate the assertion
|
|
1789
|
-
const parsed = (0, assertionRunner_1.parseAssertCommand)((0, parser_1.substituteVariables)(step.content, runtimeVariables));
|
|
1790
|
-
if (!parsed) {
|
|
1791
|
-
errors.push(`Step ${stepIdx + 1}: Invalid assert command: ${step.content}`);
|
|
1792
|
-
return {
|
|
1793
|
-
name: '',
|
|
1794
|
-
success: false,
|
|
1795
|
-
responses,
|
|
1796
|
-
scriptResults,
|
|
1797
|
-
assertionResults,
|
|
1798
|
-
steps: orderedSteps,
|
|
1799
|
-
errors,
|
|
1800
|
-
duration: Date.now() - startTime
|
|
1801
|
-
};
|
|
1802
|
-
}
|
|
1803
|
-
const result = (0, assertionRunner_1.evaluateAssertion)(parsed, responses, runtimeVariables, getValueByPath, responseIndexToVariable, workingDir);
|
|
1804
|
-
assertionResults.push(result);
|
|
1805
|
-
const stepResult = {
|
|
1806
|
-
type: 'assertion',
|
|
1807
|
-
stepIndex: stepIdx,
|
|
1808
|
-
assertion: result,
|
|
1809
|
-
lineNumber: step.lineNumber
|
|
1810
|
-
};
|
|
1811
|
-
orderedSteps.push(stepResult);
|
|
1812
|
-
const statusIcon = result.passed ? '✓' : '✗';
|
|
1813
|
-
reportProgress(stepIdx, 'assertion', `${statusIcon} assert ${result.expression}`, stepResult);
|
|
1814
|
-
// If assertion failed, stop the sequence
|
|
1815
|
-
if (!result.passed) {
|
|
1816
|
-
const failMessage = result.message
|
|
1817
|
-
? `Assertion failed: ${result.message}`
|
|
1818
|
-
: `Assertion failed: ${result.expression}`;
|
|
1819
|
-
errors.push(failMessage);
|
|
1820
|
-
if (result.error) {
|
|
1821
|
-
errors.push(` Error: ${result.error}`);
|
|
1822
|
-
}
|
|
1823
|
-
else {
|
|
1824
|
-
errors.push(` Expected: ${result.rightExpression ?? result.operator}`);
|
|
1825
|
-
errors.push(` Actual: ${JSON.stringify(result.leftValue)}`);
|
|
1826
|
-
}
|
|
1827
|
-
return {
|
|
1828
|
-
name: '',
|
|
1829
|
-
success: false,
|
|
1830
|
-
responses,
|
|
1831
|
-
scriptResults,
|
|
1832
|
-
assertionResults,
|
|
1833
|
-
steps: orderedSteps,
|
|
1834
|
-
errors,
|
|
1835
|
-
duration: Date.now() - startTime
|
|
1836
|
-
};
|
|
1837
|
-
}
|
|
1838
|
-
continue;
|
|
1839
|
-
}
|
|
1840
|
-
if (step.type === 'print') {
|
|
1841
|
-
// Parse and substitute variables in the print content
|
|
1842
|
-
const parsed = parsePrintCommand(step.content);
|
|
1843
|
-
// First resolve bare variables, then substitute {{var}} patterns
|
|
1844
|
-
const titleResolved = resolveBareVariables(parsed.title, runtimeVariables);
|
|
1845
|
-
const title = (0, parser_1.substituteVariables)(titleResolved, runtimeVariables);
|
|
1846
|
-
const bodyResolved = parsed.body ? resolveBareVariables(parsed.body, runtimeVariables) : undefined;
|
|
1847
|
-
const body = bodyResolved ? (0, parser_1.substituteVariables)(bodyResolved, runtimeVariables) : undefined;
|
|
1848
|
-
const stepResult = {
|
|
1849
|
-
type: 'print',
|
|
1850
|
-
stepIndex: stepIdx,
|
|
1851
|
-
print: {
|
|
1852
|
-
title,
|
|
1853
|
-
body,
|
|
1854
|
-
timestamp: Date.now() - startTime
|
|
1855
|
-
}
|
|
1856
|
-
};
|
|
1857
|
-
orderedSteps.push(stepResult);
|
|
1858
|
-
reportProgress(stepIdx, 'print', `print: ${title}`, stepResult);
|
|
1859
|
-
continue;
|
|
1860
|
-
}
|
|
1861
|
-
if (step.type === 'wait') {
|
|
1862
|
-
// Parse the wait duration
|
|
1863
|
-
const durationMs = parseWaitCommand(step.content);
|
|
1864
|
-
// Report that we're starting to wait
|
|
1865
|
-
reportProgress(stepIdx, 'wait', `wait: ${durationMs}ms`);
|
|
1866
|
-
// Actually wait
|
|
1867
|
-
await sleep(durationMs);
|
|
1868
|
-
// Create a print-like result for the wait step
|
|
1869
|
-
const stepResult = {
|
|
1870
|
-
type: 'print',
|
|
1871
|
-
stepIndex: stepIdx,
|
|
1872
|
-
print: {
|
|
1873
|
-
title: `Waited ${durationMs >= 1000 ? (durationMs / 1000) + 's' : durationMs + 'ms'}`,
|
|
1874
|
-
timestamp: Date.now() - startTime
|
|
1875
|
-
}
|
|
1876
|
-
};
|
|
1877
|
-
orderedSteps.push(stepResult);
|
|
1878
|
-
continue;
|
|
1879
|
-
}
|
|
1880
|
-
if (step.type === 'json') {
|
|
1881
|
-
// Parse the json command with variable substitution in the path
|
|
1882
|
-
// First resolve bare variables, then {{var}} patterns
|
|
1883
|
-
const bareResolved = resolveBareVariables(step.content, runtimeVariables);
|
|
1884
|
-
const parsed = (0, jsonFileReader_1.parseJsonCommand)((0, parser_1.substituteVariables)(bareResolved, runtimeVariables));
|
|
1885
|
-
if (!parsed) {
|
|
1886
|
-
errors.push(`Step ${stepIdx + 1}: Invalid json command: ${step.content}`);
|
|
1887
|
-
return {
|
|
1888
|
-
name: '',
|
|
1889
|
-
success: false,
|
|
1890
|
-
responses,
|
|
1891
|
-
scriptResults,
|
|
1892
|
-
assertionResults,
|
|
1893
|
-
steps: orderedSteps,
|
|
1894
|
-
errors,
|
|
1895
|
-
duration: Date.now() - startTime
|
|
1896
|
-
};
|
|
1897
|
-
}
|
|
1898
|
-
// Read the JSON file
|
|
1899
|
-
const result = (0, jsonFileReader_1.readJsonFile)(parsed.filePath, workingDir);
|
|
1900
|
-
const stepResult = {
|
|
1901
|
-
type: 'json',
|
|
1902
|
-
stepIndex: stepIdx,
|
|
1903
|
-
json: {
|
|
1904
|
-
varName: parsed.varName,
|
|
1905
|
-
filePath: result.filePath,
|
|
1906
|
-
success: result.success,
|
|
1907
|
-
error: result.error,
|
|
1908
|
-
timestamp: Date.now() - startTime
|
|
1909
|
-
}
|
|
1910
|
-
};
|
|
1911
|
-
orderedSteps.push(stepResult);
|
|
1912
|
-
if (!result.success) {
|
|
1913
|
-
errors.push(`Failed to load JSON file: ${result.error}`);
|
|
1914
|
-
reportProgress(stepIdx, 'json', `json: ${parsed.varName} ✗ ${result.error}`, stepResult);
|
|
1915
|
-
return {
|
|
1916
|
-
name: '',
|
|
1917
|
-
success: false,
|
|
1918
|
-
responses,
|
|
1919
|
-
scriptResults,
|
|
1920
|
-
assertionResults,
|
|
1921
|
-
steps: orderedSteps,
|
|
1922
|
-
errors,
|
|
1923
|
-
duration: Date.now() - startTime
|
|
1924
|
-
};
|
|
1925
|
-
}
|
|
1926
|
-
// Store the JSON data as a stringified value in runtimeVariables
|
|
1927
|
-
// This allows {{varName.property}} access via the substituteVariables function
|
|
1928
|
-
runtimeVariables[parsed.varName] = JSON.stringify(result.data);
|
|
1929
|
-
reportProgress(stepIdx, 'json', `json: ${parsed.varName} = ${parsed.filePath}`, stepResult);
|
|
1930
|
-
continue;
|
|
1931
|
-
}
|
|
1932
|
-
if (step.type === 'propAssign') {
|
|
1933
|
-
// Parse the property assignment first to extract the value part
|
|
1934
|
-
// Then resolve variables in the value specifically
|
|
1935
|
-
const parsed = (0, jsonFileReader_1.parsePropertyAssignment)(step.content);
|
|
1936
|
-
if (!parsed) {
|
|
1937
|
-
errors.push(`Step ${stepIdx + 1}: Invalid property assignment: ${step.content}`);
|
|
1938
|
-
return {
|
|
1939
|
-
name: '',
|
|
1940
|
-
success: false,
|
|
1941
|
-
responses,
|
|
1942
|
-
scriptResults,
|
|
1943
|
-
assertionResults,
|
|
1944
|
-
steps: orderedSteps,
|
|
1945
|
-
errors,
|
|
1946
|
-
duration: Date.now() - startTime
|
|
1947
|
-
};
|
|
1948
|
-
}
|
|
1949
|
-
// Check if the variable exists and is a JSON object
|
|
1950
|
-
if (!(parsed.varName in runtimeVariables)) {
|
|
1951
|
-
errors.push(`Step ${stepIdx + 1}: Variable '${parsed.varName}' is not defined`);
|
|
1952
|
-
return {
|
|
1953
|
-
name: '',
|
|
1954
|
-
success: false,
|
|
1955
|
-
responses,
|
|
1956
|
-
scriptResults,
|
|
1957
|
-
assertionResults,
|
|
1958
|
-
steps: orderedSteps,
|
|
1959
|
-
errors,
|
|
1960
|
-
duration: Date.now() - startTime
|
|
1961
|
-
};
|
|
1962
|
-
}
|
|
1963
|
-
// Parse the current JSON value
|
|
1964
|
-
let jsonObj;
|
|
1965
|
-
try {
|
|
1966
|
-
jsonObj = JSON.parse(runtimeVariables[parsed.varName]);
|
|
1967
|
-
}
|
|
1968
|
-
catch {
|
|
1969
|
-
errors.push(`Step ${stepIdx + 1}: Variable '${parsed.varName}' is not a valid JSON object`);
|
|
1970
|
-
return {
|
|
1971
|
-
name: '',
|
|
1972
|
-
success: false,
|
|
1973
|
-
responses,
|
|
1974
|
-
scriptResults,
|
|
1975
|
-
assertionResults,
|
|
1976
|
-
steps: orderedSteps,
|
|
1977
|
-
errors,
|
|
1978
|
-
duration: Date.now() - startTime
|
|
1979
|
-
};
|
|
1980
|
-
}
|
|
1981
|
-
// Check if the value is a bare variable reference - if so, preserve its original type
|
|
1982
|
-
const valueTrimmed = parsed.value.trim();
|
|
1983
|
-
const bareVarMatch = valueTrimmed.match(/^([a-zA-Z_][a-zA-Z0-9_]*)((?:\.[a-zA-Z_][a-zA-Z0-9_]*|\[\d+\])*)$/);
|
|
1984
|
-
let newValue;
|
|
1985
|
-
if (bareVarMatch && bareVarMatch[1] in runtimeVariables) {
|
|
1986
|
-
// It's a bare variable - preserve the original type
|
|
1987
|
-
const varName = bareVarMatch[1];
|
|
1988
|
-
const pathPart = bareVarMatch[2] || '';
|
|
1989
|
-
let value = runtimeVariables[varName];
|
|
1990
|
-
// If it's a JSON string, parse it first
|
|
1991
|
-
if (typeof value === 'string') {
|
|
1992
|
-
try {
|
|
1993
|
-
const parsed = JSON.parse(value);
|
|
1994
|
-
if (typeof parsed === 'object' && parsed !== null) {
|
|
1995
|
-
value = parsed;
|
|
1996
|
-
}
|
|
1997
|
-
}
|
|
1998
|
-
catch {
|
|
1999
|
-
// Not a JSON string, use as-is
|
|
2000
|
-
}
|
|
2001
|
-
}
|
|
2002
|
-
if (pathPart) {
|
|
2003
|
-
// Navigate the path
|
|
2004
|
-
const path = pathPart.replace(/^\./, '');
|
|
2005
|
-
newValue = getNestedValueFromObject(value, path);
|
|
2006
|
-
}
|
|
2007
|
-
else {
|
|
2008
|
-
newValue = value;
|
|
2009
|
-
}
|
|
2010
|
-
}
|
|
2011
|
-
else {
|
|
2012
|
-
// Not a bare variable - resolve variables and parse as JSON
|
|
2013
|
-
const bareResolvedValue = resolveBareVariables(parsed.value, runtimeVariables);
|
|
2014
|
-
const resolvedValue = (0, parser_1.substituteVariables)(bareResolvedValue, runtimeVariables);
|
|
2015
|
-
// Parse the value - try to interpret it as JSON first, then as string
|
|
2016
|
-
newValue = resolvedValue;
|
|
2017
|
-
try {
|
|
2018
|
-
newValue = JSON.parse(resolvedValue);
|
|
2019
|
-
}
|
|
2020
|
-
catch {
|
|
2021
|
-
// Keep as string if not valid JSON
|
|
2022
|
-
}
|
|
2023
|
-
}
|
|
2024
|
-
// Set the nested value
|
|
2025
|
-
const success = (0, jsonFileReader_1.setNestedValue)(jsonObj, parsed.propertyPath, newValue);
|
|
2026
|
-
if (!success) {
|
|
2027
|
-
errors.push(`Step ${stepIdx + 1}: Failed to set property '${parsed.propertyPath}' on '${parsed.varName}'`);
|
|
2028
|
-
return {
|
|
2029
|
-
name: '',
|
|
2030
|
-
success: false,
|
|
2031
|
-
responses,
|
|
2032
|
-
scriptResults,
|
|
2033
|
-
assertionResults,
|
|
2034
|
-
steps: orderedSteps,
|
|
2035
|
-
errors,
|
|
2036
|
-
duration: Date.now() - startTime
|
|
2037
|
-
};
|
|
2038
|
-
}
|
|
2039
|
-
// Update the runtime variable with the modified JSON
|
|
2040
|
-
runtimeVariables[parsed.varName] = JSON.stringify(jsonObj);
|
|
2041
|
-
// No step result needed for property assignment - it's a simple operation
|
|
2042
|
-
// But we report progress for visibility
|
|
2043
|
-
reportProgress(stepIdx, 'propAssign', `${parsed.varName}.${parsed.propertyPath} = ${parsed.value}`, undefined);
|
|
2044
|
-
continue;
|
|
2045
|
-
}
|
|
2046
|
-
if (step.type === 'varAssign') {
|
|
2047
|
-
// Parse the variable assignment
|
|
2048
|
-
const parsed = parseVarAssignCommand(step.content);
|
|
2049
|
-
if (!parsed) {
|
|
2050
|
-
errors.push(`Step ${stepIdx + 1}: Invalid variable assignment: ${step.content}`);
|
|
2051
|
-
return {
|
|
2052
|
-
name: '',
|
|
2053
|
-
success: false,
|
|
2054
|
-
responses,
|
|
2055
|
-
scriptResults,
|
|
2056
|
-
assertionResults,
|
|
2057
|
-
steps: orderedSteps,
|
|
2058
|
-
errors,
|
|
2059
|
-
duration: Date.now() - startTime
|
|
2060
|
-
};
|
|
2061
|
-
}
|
|
2062
|
-
// Evaluate the expression
|
|
2063
|
-
const result = evaluateValueExpression(parsed.valueExpr, runtimeVariables);
|
|
2064
|
-
if (result.error) {
|
|
2065
|
-
errors.push(`Step ${stepIdx + 1}: ${result.error}`);
|
|
2066
|
-
return {
|
|
2067
|
-
name: '',
|
|
2068
|
-
success: false,
|
|
2069
|
-
responses,
|
|
2070
|
-
scriptResults,
|
|
2071
|
-
assertionResults,
|
|
2072
|
-
steps: orderedSteps,
|
|
2073
|
-
errors,
|
|
2074
|
-
duration: Date.now() - startTime
|
|
2075
|
-
};
|
|
2076
|
-
}
|
|
2077
|
-
// Store the result
|
|
2078
|
-
runtimeVariables[parsed.varName] = result.value;
|
|
2079
|
-
// Report progress
|
|
2080
|
-
reportProgress(stepIdx, 'propAssign', `var ${parsed.varName} = ${result.value}`, undefined);
|
|
2081
|
-
continue;
|
|
2082
|
-
}
|
|
2083
|
-
if (step.type === 'varRequest') {
|
|
2084
|
-
// Parse the variable request command: var name = GET/POST url/endpoint
|
|
2085
|
-
// Only parse the first line (command line), body is on subsequent lines
|
|
2086
|
-
const commandLine = step.content.split('\n')[0];
|
|
2087
|
-
const parsed = parseVarRequestCommand(commandLine);
|
|
2088
|
-
if (!parsed) {
|
|
2089
|
-
errors.push(`Step ${stepIdx + 1}: Invalid variable request: ${commandLine}`);
|
|
2090
|
-
return {
|
|
2091
|
-
name: '',
|
|
2092
|
-
success: false,
|
|
2093
|
-
responses,
|
|
2094
|
-
scriptResults,
|
|
2095
|
-
assertionResults,
|
|
2096
|
-
steps: orderedSteps,
|
|
2097
|
-
errors,
|
|
2098
|
-
duration: Date.now() - startTime
|
|
2099
|
-
};
|
|
2100
|
-
}
|
|
2101
|
-
requestIndex++; // Count as a request for $N references
|
|
2102
|
-
let resolvedUrl = '';
|
|
2103
|
-
let resolvedHeaders = {};
|
|
2104
|
-
let requestDescription = `${parsed.method} ${parsed.url}`;
|
|
2105
|
-
try {
|
|
2106
|
-
// Check if the URL is actually a named endpoint (e.g., GetUser(1))
|
|
2107
|
-
const endpointMatch = parsed.url.match(/^([a-zA-Z_][a-zA-Z0-9_]*)(?:\(([^)]*)\))?(\s+.*)?$/);
|
|
2108
|
-
const potentialEndpointName = endpointMatch ? endpointMatch[1] : null;
|
|
2109
|
-
const endpoint = apiDefinitions && potentialEndpointName
|
|
2110
|
-
? (0, nornapiParser_1.getEndpoint)({ headerGroups: apiDefinitions.headerGroups, endpoints: apiDefinitions.endpoints }, potentialEndpointName)
|
|
2111
|
-
: undefined;
|
|
2112
|
-
if (endpoint && apiDefinitions) {
|
|
2113
|
-
// This is a named endpoint - resolve it
|
|
2114
|
-
const paramsStr = endpointMatch[2] || '';
|
|
2115
|
-
const headerGroupsStr = endpointMatch[3]?.trim() || '';
|
|
2116
|
-
// Parse parameters - supports both positional and named syntax
|
|
2117
|
-
// Positional: GetUser("123", "456")
|
|
2118
|
-
// Named: GetUser(id: "123", orderId: "456")
|
|
2119
|
-
const paramTokens = paramsStr ? paramsStr.split(',').map(p => p.trim()) : [];
|
|
2120
|
-
const params = {};
|
|
2121
|
-
// Check if using named parameter syntax (first token contains ':')
|
|
2122
|
-
const isNamedSyntax = paramTokens.length > 0 && paramTokens[0].includes(':');
|
|
2123
|
-
if (isNamedSyntax) {
|
|
2124
|
-
// Named parameter syntax: param: value
|
|
2125
|
-
for (const token of paramTokens) {
|
|
2126
|
-
const kvMatch = token.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\s*:\s*(.+)$/);
|
|
2127
|
-
if (kvMatch) {
|
|
2128
|
-
const paramName = kvMatch[1];
|
|
2129
|
-
let paramValue = kvMatch[2].trim().replace(/^["']|["']$/g, '');
|
|
2130
|
-
// Resolve variables
|
|
2131
|
-
const bareResolved = resolveBareVariables(paramValue, runtimeVariables);
|
|
2132
|
-
params[paramName] = (0, parser_1.substituteVariables)(bareResolved, runtimeVariables);
|
|
2133
|
-
}
|
|
2134
|
-
}
|
|
2135
|
-
}
|
|
2136
|
-
else {
|
|
2137
|
-
// Positional parameter syntax
|
|
2138
|
-
endpoint.parameters.forEach((paramName, idx) => {
|
|
2139
|
-
if (paramTokens[idx] !== undefined) {
|
|
2140
|
-
let paramValue = paramTokens[idx].replace(/^["']|["']$/g, '');
|
|
2141
|
-
// Resolve variables
|
|
2142
|
-
const bareResolved = resolveBareVariables(paramValue, runtimeVariables);
|
|
2143
|
-
params[paramName] = (0, parser_1.substituteVariables)(bareResolved, runtimeVariables);
|
|
2144
|
-
}
|
|
2145
|
-
});
|
|
2146
|
-
}
|
|
2147
|
-
// Resolve endpoint path with variables and parameters
|
|
2148
|
-
let pathWithVars = (0, parser_1.substituteVariables)(endpoint.path, runtimeVariables);
|
|
2149
|
-
for (const paramName of endpoint.parameters) {
|
|
2150
|
-
const value = params[paramName];
|
|
2151
|
-
if (value !== undefined) {
|
|
2152
|
-
pathWithVars = pathWithVars.replace(`{${paramName}}`, value);
|
|
2153
|
-
}
|
|
2154
|
-
}
|
|
2155
|
-
resolvedUrl = pathWithVars;
|
|
2156
|
-
// Collect headers from header groups if specified
|
|
2157
|
-
if (headerGroupsStr) {
|
|
2158
|
-
const headerGroupNames = headerGroupsStr.split(/\s+/).filter(n => n);
|
|
2159
|
-
for (const groupName of headerGroupNames) {
|
|
2160
|
-
const group = (0, nornapiParser_1.getHeaderGroup)({ headerGroups: apiDefinitions.headerGroups, endpoints: apiDefinitions.endpoints }, groupName);
|
|
2161
|
-
if (group) {
|
|
2162
|
-
const groupHeaders = (0, nornapiParser_1.resolveHeaderValues)(group, runtimeVariables);
|
|
2163
|
-
Object.assign(resolvedHeaders, groupHeaders);
|
|
2164
|
-
}
|
|
2165
|
-
}
|
|
2166
|
-
}
|
|
2167
|
-
requestDescription = `${potentialEndpointName}(${paramTokens.join(', ')}) → ${parsed.method} ${resolvedUrl}`;
|
|
2168
|
-
}
|
|
2169
|
-
else {
|
|
2170
|
-
// Regular URL - substitute variables
|
|
2171
|
-
resolvedUrl = (0, parser_1.substituteVariables)(parsed.url, runtimeVariables);
|
|
2172
|
-
requestDescription = `${parsed.method} ${resolvedUrl}`;
|
|
2173
|
-
}
|
|
2174
|
-
// Build request with resolved URL and any headers
|
|
2175
|
-
let requestText = `${parsed.method} ${resolvedUrl}`;
|
|
2176
|
-
if (Object.keys(resolvedHeaders).length > 0) {
|
|
2177
|
-
const headerLines = Object.entries(resolvedHeaders).map(([k, v]) => `${k}: ${v}`);
|
|
2178
|
-
requestText += '\n' + headerLines.join('\n');
|
|
2179
|
-
}
|
|
2180
|
-
// Extract and add body lines from step.content (lines after the command line)
|
|
2181
|
-
const contentLines = step.content.split('\n');
|
|
2182
|
-
if (contentLines.length > 1) {
|
|
2183
|
-
// Additional lines may contain headers + optional blank separator + body,
|
|
2184
|
-
// or body-only content. Preserve explicit structure when present.
|
|
2185
|
-
const extraLines = contentLines.slice(1).map(line => (0, parser_1.substituteVariables)(line, runtimeVariables));
|
|
2186
|
-
const hasExplicitBlank = extraLines.some(line => line.trim() === '');
|
|
2187
|
-
const firstNonEmpty = extraLines.find(line => line.trim() !== '');
|
|
2188
|
-
const startsWithHeader = firstNonEmpty ? /^[A-Za-z0-9\-_]+\s*:/.test(firstNonEmpty.trim()) : false;
|
|
2189
|
-
if (hasExplicitBlank || startsWithHeader) {
|
|
2190
|
-
requestText += '\n' + extraLines.join('\n');
|
|
2191
|
-
}
|
|
2192
|
-
else {
|
|
2193
|
-
// Body-only shorthand (no headers/blank separator) should still parse as body.
|
|
2194
|
-
requestText += '\n\n' + extraLines.join('\n');
|
|
2195
|
-
}
|
|
2196
|
-
}
|
|
2197
|
-
const requestParsed = (0, parser_1.parserHttpRequest)(requestText, runtimeVariables);
|
|
2198
|
-
// Build retry options if specified
|
|
2199
|
-
const retryOpts = parsed.retryCount
|
|
2200
|
-
? {
|
|
2201
|
-
retryCount: parsed.retryCount,
|
|
2202
|
-
backoffMs: parsed.backoffMs || 1000,
|
|
2203
|
-
onRetry: (attempt, total, error, waitMs) => {
|
|
2204
|
-
reportProgress(stepIdx, 'request', `[Retry ${attempt}/${total}] ${requestDescription} - waiting ${waitMs}ms`, undefined);
|
|
2205
|
-
}
|
|
2206
|
-
}
|
|
2207
|
-
: undefined;
|
|
2208
|
-
const response = cookieJar
|
|
2209
|
-
? await (0, httpClient_1.sendRequestWithJar)(requestParsed, cookieJar, retryOpts)
|
|
2210
|
-
: await (0, httpClient_1.sendRequest)(requestParsed, retryOpts);
|
|
2211
|
-
addResponse(response);
|
|
2212
|
-
// Track variable name → response index mapping for enhanced failure context
|
|
2213
|
-
responseIndexToVariable.set(responses.length, parsed.varName);
|
|
2214
|
-
// Store the FULL response object in the variable (not stringified)
|
|
2215
|
-
// This allows {{varName.body.path}}, {{varName.status}}, etc.
|
|
2216
|
-
runtimeVariables[parsed.varName] = response;
|
|
2217
|
-
const stepResult = {
|
|
2218
|
-
type: 'request',
|
|
2219
|
-
stepIndex: stepIdx,
|
|
2220
|
-
response: response,
|
|
2221
|
-
requestMethod: parsed.method,
|
|
2222
|
-
requestUrl: resolvedUrl,
|
|
2223
|
-
variableName: parsed.varName
|
|
2224
|
-
};
|
|
2225
|
-
orderedSteps.push(stepResult);
|
|
2226
|
-
reportProgress(stepIdx, 'request', `var ${parsed.varName} = ${requestDescription}`, stepResult);
|
|
2227
|
-
// Also process captures that reference this response by $N
|
|
2228
|
-
const relevantCaptures = captures.filter(c => c.afterRequest === requestIndex);
|
|
2229
|
-
for (const capture of relevantCaptures) {
|
|
2230
|
-
const value = getValueByPath(response, capture.path);
|
|
2231
|
-
if (value !== undefined) {
|
|
2232
|
-
// Preserve the value as-is (objects, arrays, primitives) for type checking
|
|
2233
|
-
runtimeVariables[capture.varName] = value;
|
|
2234
|
-
}
|
|
2235
|
-
else {
|
|
2236
|
-
errors.push(`Warning: Could not capture ${capture.varName} from $${requestIndex}.${capture.path}`);
|
|
2237
|
-
}
|
|
2238
|
-
}
|
|
2239
|
-
}
|
|
2240
|
-
catch (error) {
|
|
2241
|
-
// Build a more informative error message
|
|
2242
|
-
let errorDetails = `Request failed for var ${parsed.varName}: ${error.message}`;
|
|
2243
|
-
if (requestDescription) {
|
|
2244
|
-
errorDetails += `\n Request: ${requestDescription}`;
|
|
2245
|
-
}
|
|
2246
|
-
errors.push(errorDetails);
|
|
2247
|
-
return {
|
|
2248
|
-
name: '',
|
|
2249
|
-
success: false,
|
|
2250
|
-
responses,
|
|
2251
|
-
scriptResults,
|
|
2252
|
-
assertionResults,
|
|
2253
|
-
steps: orderedSteps,
|
|
2254
|
-
errors,
|
|
2255
|
-
duration: Date.now() - startTime
|
|
2256
|
-
};
|
|
2257
|
-
}
|
|
2258
|
-
continue;
|
|
2259
|
-
}
|
|
2260
|
-
if (step.type === 'script') {
|
|
2261
|
-
// Parse and execute the script
|
|
2262
|
-
const parsed = (0, scriptRunner_1.parseRunCommand)((0, parser_1.substituteVariables)(step.content, runtimeVariables));
|
|
2263
|
-
if (!parsed) {
|
|
2264
|
-
errors.push(`Step ${stepIdx + 1}: Invalid run command: ${step.content}`);
|
|
2265
|
-
return {
|
|
2266
|
-
name: '',
|
|
2267
|
-
success: false,
|
|
2268
|
-
responses,
|
|
2269
|
-
scriptResults,
|
|
2270
|
-
assertionResults,
|
|
2271
|
-
steps: orderedSteps,
|
|
2272
|
-
errors,
|
|
2273
|
-
duration: Date.now() - startTime
|
|
2274
|
-
};
|
|
2275
|
-
}
|
|
2276
|
-
// Substitute variables in args - first resolve bare variables, then {{var}} patterns
|
|
2277
|
-
const substitutedArgs = parsed.args.map(arg => {
|
|
2278
|
-
const bareResolved = resolveBareVariables(arg, runtimeVariables);
|
|
2279
|
-
return (0, parser_1.substituteVariables)(bareResolved, runtimeVariables);
|
|
2280
|
-
});
|
|
2281
|
-
const substitutedPath = (0, parser_1.substituteVariables)(parsed.scriptPath, runtimeVariables);
|
|
2282
|
-
try {
|
|
2283
|
-
const result = await (0, scriptRunner_1.runScript)(parsed.type, substitutedPath, substitutedArgs, workingDir || process.cwd(), runtimeVariables, parsed.captureAs);
|
|
2284
|
-
scriptResults.push(result);
|
|
2285
|
-
const stepResult = {
|
|
2286
|
-
type: 'script',
|
|
2287
|
-
stepIndex: stepIdx,
|
|
2288
|
-
script: result
|
|
2289
|
-
};
|
|
2290
|
-
orderedSteps.push(stepResult);
|
|
2291
|
-
reportProgress(stepIdx, 'script', `run ${parsed.type} ${substitutedPath}`, stepResult);
|
|
2292
|
-
if (!result.success) {
|
|
2293
|
-
errors.push(`Script failed (exit ${result.exitCode}): ${result.error || result.output}`);
|
|
2294
|
-
return {
|
|
2295
|
-
name: '',
|
|
2296
|
-
success: false,
|
|
2297
|
-
responses,
|
|
2298
|
-
scriptResults,
|
|
2299
|
-
assertionResults,
|
|
2300
|
-
steps: orderedSteps,
|
|
2301
|
-
errors,
|
|
2302
|
-
duration: Date.now() - startTime
|
|
2303
|
-
};
|
|
2304
|
-
}
|
|
2305
|
-
// If this was a capture command (var x = run ...), store the output
|
|
2306
|
-
if (parsed.captureAs) {
|
|
2307
|
-
// Try to parse as JSON for object/array/number/boolean
|
|
2308
|
-
let outputValue = result.output;
|
|
2309
|
-
try {
|
|
2310
|
-
const jsonParsed = JSON.parse(result.output);
|
|
2311
|
-
// Accept objects, arrays, numbers, and booleans from JSON parse
|
|
2312
|
-
// This means "1234" becomes 1234 (number), "true" becomes true (boolean)
|
|
2313
|
-
// Strings that don't parse as JSON stay as strings
|
|
2314
|
-
outputValue = jsonParsed;
|
|
2315
|
-
}
|
|
2316
|
-
catch {
|
|
2317
|
-
// Not valid JSON, keep as string
|
|
2318
|
-
}
|
|
2319
|
-
runtimeVariables[parsed.captureAs] = outputValue;
|
|
2320
|
-
}
|
|
2321
|
-
}
|
|
2322
|
-
catch (error) {
|
|
2323
|
-
errors.push(`Script execution error: ${error.message}`);
|
|
2324
|
-
return {
|
|
2325
|
-
name: '',
|
|
2326
|
-
success: false,
|
|
2327
|
-
responses,
|
|
2328
|
-
scriptResults,
|
|
2329
|
-
assertionResults,
|
|
2330
|
-
steps: orderedSteps,
|
|
2331
|
-
errors,
|
|
2332
|
-
duration: Date.now() - startTime
|
|
2333
|
-
};
|
|
2334
|
-
}
|
|
2335
|
-
}
|
|
2336
|
-
else if (step.type === 'namedRequest') {
|
|
2337
|
-
// Named request OR sequence invocation: run <Name> or run <Name>(args)
|
|
2338
|
-
const parsed = parseRunNamedRequestCommand(step.content);
|
|
2339
|
-
if (!parsed) {
|
|
2340
|
-
errors.push(`Invalid run command: ${step.content}`);
|
|
2341
|
-
return {
|
|
2342
|
-
name: '',
|
|
2343
|
-
success: false,
|
|
2344
|
-
responses,
|
|
2345
|
-
scriptResults,
|
|
2346
|
-
assertionResults,
|
|
2347
|
-
steps: orderedSteps,
|
|
2348
|
-
errors,
|
|
2349
|
-
duration: Date.now() - startTime
|
|
2350
|
-
};
|
|
2351
|
-
}
|
|
2352
|
-
const { name: targetName, args: runArgs } = parsed;
|
|
2353
|
-
if (!fullDocumentText) {
|
|
2354
|
-
errors.push(`Cannot run "${targetName}": full document text not provided`);
|
|
2355
|
-
return {
|
|
2356
|
-
name: '',
|
|
2357
|
-
success: false,
|
|
2358
|
-
responses,
|
|
2359
|
-
scriptResults,
|
|
2360
|
-
assertionResults,
|
|
2361
|
-
steps: orderedSteps,
|
|
2362
|
-
errors,
|
|
2363
|
-
duration: Date.now() - startTime
|
|
2364
|
-
};
|
|
2365
|
-
}
|
|
2366
|
-
// First, check if this is a sequence name
|
|
2367
|
-
const allSequences = extractSequences(fullDocumentText);
|
|
2368
|
-
const targetSequence = allSequences.find(s => s.name === targetName);
|
|
2369
|
-
if (targetSequence) {
|
|
2370
|
-
// This is a sequence call - run the sequence recursively
|
|
2371
|
-
// Initialize call stack if not provided
|
|
2372
|
-
const currentCallStack = callStack || [];
|
|
2373
|
-
// Check for circular reference
|
|
2374
|
-
if (currentCallStack.includes(targetName)) {
|
|
2375
|
-
errors.push(`Circular reference detected: ${[...currentCallStack, targetName].join(' → ')}`);
|
|
2376
|
-
return {
|
|
2377
|
-
name: '',
|
|
2378
|
-
success: false,
|
|
2379
|
-
responses,
|
|
2380
|
-
scriptResults,
|
|
2381
|
-
assertionResults,
|
|
2382
|
-
steps: orderedSteps,
|
|
2383
|
-
errors,
|
|
2384
|
-
duration: Date.now() - startTime
|
|
2385
|
-
};
|
|
2386
|
-
}
|
|
2387
|
-
// Bind arguments to parameters
|
|
2388
|
-
let sequenceArgs = {};
|
|
2389
|
-
if (targetSequence.parameters.length > 0 || runArgs.length > 0) {
|
|
2390
|
-
const bindResult = bindSequenceArguments(targetSequence.parameters, runArgs, runtimeVariables);
|
|
2391
|
-
if ('error' in bindResult) {
|
|
2392
|
-
errors.push(`Error calling sequence "${targetName}": ${bindResult.error}`);
|
|
2393
|
-
return {
|
|
2394
|
-
name: '',
|
|
2395
|
-
success: false,
|
|
2396
|
-
responses,
|
|
2397
|
-
scriptResults,
|
|
2398
|
-
assertionResults,
|
|
2399
|
-
steps: orderedSteps,
|
|
2400
|
-
errors,
|
|
2401
|
-
duration: Date.now() - startTime
|
|
2402
|
-
};
|
|
2403
|
-
}
|
|
2404
|
-
sequenceArgs = bindResult.variables;
|
|
2405
|
-
}
|
|
2406
|
-
// Check if tag filtering is active and if the target sequence matches
|
|
2407
|
-
if (tagFilterOptions && tagFilterOptions.filters.length > 0) {
|
|
2408
|
-
if (!sequenceMatchesTags(targetSequence, tagFilterOptions)) {
|
|
2409
|
-
// Silently skip this sequence - it doesn't match the tag filter
|
|
2410
|
-
continue;
|
|
2411
|
-
}
|
|
2412
|
-
}
|
|
2413
|
-
// Report that we're starting a sub-sequence
|
|
2414
|
-
reportProgress(stepIdx, 'sequenceStart', `Starting sequence ${targetName}`, undefined, targetName);
|
|
2415
|
-
// Determine the working directory for this sub-sequence
|
|
2416
|
-
// If we have source info for this sequence (from imports), use that file's directory
|
|
2417
|
-
// This ensures script paths resolve relative to where the sequence was defined
|
|
2418
|
-
let subWorkingDir = workingDir;
|
|
2419
|
-
if (sequenceSources) {
|
|
2420
|
-
const sourceFile = sequenceSources.get(targetName.toLowerCase());
|
|
2421
|
-
if (sourceFile) {
|
|
2422
|
-
const path = await import('path');
|
|
2423
|
-
subWorkingDir = path.dirname(sourceFile);
|
|
2424
|
-
}
|
|
2425
|
-
}
|
|
2426
|
-
// Run the sub-sequence with current runtime variables
|
|
2427
|
-
// Variables set in the sub-sequence will be available after it completes
|
|
2428
|
-
const subResult = await runSequenceWithJar(targetSequence.content, runtimeVariables, // Pass current runtime variables (includes file vars + any captured)
|
|
2429
|
-
cookieJar, subWorkingDir, fullDocumentText, onProgress, // Pass through progress callback
|
|
2430
|
-
[...currentCallStack, targetName], // Add to call stack for circular detection
|
|
2431
|
-
sequenceArgs, // Pass bound arguments as initial scope variables
|
|
2432
|
-
apiDefinitions, // Pass API definitions for endpoint resolution
|
|
2433
|
-
tagFilterOptions, // Pass tag filter options for nested sequence filtering
|
|
2434
|
-
sequenceSources // Pass sequence sources for nested sequences
|
|
2435
|
-
);
|
|
2436
|
-
// Report that sub-sequence ended
|
|
2437
|
-
reportProgress(stepIdx, 'sequenceEnd', `Finished sequence ${targetName}`, undefined, targetName);
|
|
2438
|
-
// Merge results from sub-sequence and update $N references
|
|
2439
|
-
for (const subResponse of subResult.responses) {
|
|
2440
|
-
addResponse(subResponse);
|
|
2441
|
-
}
|
|
2442
|
-
scriptResults.push(...subResult.scriptResults);
|
|
2443
|
-
assertionResults.push(...subResult.assertionResults);
|
|
2444
|
-
// Merge step results (offset step indices)
|
|
2445
|
-
for (const subStep of subResult.steps) {
|
|
2446
|
-
orderedSteps.push({
|
|
2447
|
-
...subStep,
|
|
2448
|
-
stepIndex: orderedSteps.length
|
|
2449
|
-
});
|
|
2450
|
-
}
|
|
2451
|
-
// No variable merging for plain "run Sequence"
|
|
2452
|
-
// Variables are only accessible via "var x = run Sequence" capture syntax
|
|
2453
|
-
// Update request index to account for sub-sequence requests
|
|
2454
|
-
requestIndex += subResult.responses.length;
|
|
2455
|
-
// If sub-sequence failed, propagate the failure
|
|
2456
|
-
if (!subResult.success) {
|
|
2457
|
-
errors.push(`Sequence "${targetName}" failed`);
|
|
2458
|
-
errors.push(...subResult.errors);
|
|
2459
|
-
return {
|
|
2460
|
-
name: '',
|
|
2461
|
-
success: false,
|
|
2462
|
-
responses,
|
|
2463
|
-
scriptResults,
|
|
2464
|
-
assertionResults,
|
|
2465
|
-
steps: orderedSteps,
|
|
2466
|
-
errors,
|
|
2467
|
-
duration: Date.now() - startTime,
|
|
2468
|
-
variables: runtimeVariables
|
|
2469
|
-
};
|
|
2470
|
-
}
|
|
2471
|
-
// Sub-sequence succeeded - continue
|
|
2472
|
-
continue;
|
|
2473
|
-
}
|
|
2474
|
-
// Not a sequence - check for named request
|
|
2475
|
-
const namedRequest = (0, parser_1.getNamedRequest)(fullDocumentText, targetName);
|
|
2476
|
-
if (!namedRequest) {
|
|
2477
|
-
errors.push(`"${targetName}" is not a named request or sequence`);
|
|
2478
|
-
return {
|
|
2479
|
-
name: '',
|
|
2480
|
-
success: false,
|
|
2481
|
-
responses,
|
|
2482
|
-
scriptResults,
|
|
2483
|
-
assertionResults,
|
|
2484
|
-
steps: orderedSteps,
|
|
2485
|
-
errors,
|
|
2486
|
-
duration: Date.now() - startTime
|
|
2487
|
-
};
|
|
2488
|
-
}
|
|
2489
|
-
requestIndex++; // 1-based for user reference
|
|
2490
|
-
let requestUrl = '';
|
|
2491
|
-
let requestMethod = '';
|
|
2492
|
-
try {
|
|
2493
|
-
let requestParsed;
|
|
2494
|
-
// Check if the named request uses API endpoint syntax (e.g., "POST HttpBinPost Form")
|
|
2495
|
-
if (apiDefinitions && apiDefinitions.endpoints.length > 0 &&
|
|
2496
|
-
(0, nornapiParser_1.isApiRequestLine)(namedRequest.content, apiDefinitions.endpoints)) {
|
|
2497
|
-
const apiRequest = (0, nornapiParser_1.parseApiRequest)(namedRequest.content, apiDefinitions.endpoints, apiDefinitions.headerGroups);
|
|
2498
|
-
if (apiRequest) {
|
|
2499
|
-
const endpoint = (0, nornapiParser_1.getEndpoint)({ headerGroups: apiDefinitions.headerGroups, endpoints: apiDefinitions.endpoints }, apiRequest.endpointName);
|
|
2500
|
-
if (endpoint) {
|
|
2501
|
-
// Resolve the endpoint path with variables and parameters
|
|
2502
|
-
let resolvedPath = endpoint.path;
|
|
2503
|
-
// Substitute {{variable}} references
|
|
2504
|
-
resolvedPath = resolvedPath.replace(/\{\{([a-zA-Z_][a-zA-Z0-9_]*)\}\}/g, (_, varName) => {
|
|
2505
|
-
return runtimeVariables[varName] !== undefined ? String(runtimeVariables[varName]) : `{{${varName}}}`;
|
|
2506
|
-
});
|
|
2507
|
-
for (const [paramName, paramValue] of Object.entries(apiRequest.params)) {
|
|
2508
|
-
let resolvedValue = paramValue;
|
|
2509
|
-
if (runtimeVariables[paramValue] !== undefined) {
|
|
2510
|
-
resolvedValue = String(runtimeVariables[paramValue]);
|
|
2511
|
-
}
|
|
2512
|
-
else if (paramValue.startsWith('{{') && paramValue.endsWith('}}')) {
|
|
2513
|
-
const varName = paramValue.slice(2, -2);
|
|
2514
|
-
if (runtimeVariables[varName] !== undefined) {
|
|
2515
|
-
resolvedValue = String(runtimeVariables[varName]);
|
|
2516
|
-
}
|
|
2517
|
-
}
|
|
2518
|
-
resolvedPath = resolvedPath.replace(`{${paramName}}`, resolvedValue);
|
|
2519
|
-
}
|
|
2520
|
-
// Collect headers from header groups
|
|
2521
|
-
const combinedHeaders = {};
|
|
2522
|
-
for (const groupName of apiRequest.headerGroupNames) {
|
|
2523
|
-
const group = (0, nornapiParser_1.getHeaderGroup)(apiDefinitions, groupName);
|
|
2524
|
-
if (group) {
|
|
2525
|
-
const resolvedHeaders = (0, nornapiParser_1.resolveHeaderValues)(group, runtimeVariables);
|
|
2526
|
-
Object.assign(combinedHeaders, resolvedHeaders);
|
|
2527
|
-
}
|
|
2528
|
-
}
|
|
2529
|
-
// Add inline headers
|
|
2530
|
-
for (const [headerName, headerValue] of Object.entries(apiRequest.inlineHeaders)) {
|
|
2531
|
-
let resolved = headerValue;
|
|
2532
|
-
resolved = resolved.replace(/\{\{([a-zA-Z_][a-zA-Z0-9_]*)\}\}/g, (_, varName) => {
|
|
2533
|
-
return runtimeVariables[varName] !== undefined ? String(runtimeVariables[varName]) : `{{${varName}}}`;
|
|
2534
|
-
});
|
|
2535
|
-
combinedHeaders[headerName] = resolved;
|
|
2536
|
-
}
|
|
2537
|
-
// Extract body from the named request content (lines after the request line)
|
|
2538
|
-
const bodyParsed = (0, parser_1.parserHttpRequest)(namedRequest.content, runtimeVariables);
|
|
2539
|
-
requestParsed = {
|
|
2540
|
-
method: apiRequest.method,
|
|
2541
|
-
url: resolvedPath,
|
|
2542
|
-
headers: combinedHeaders,
|
|
2543
|
-
body: bodyParsed.body
|
|
2544
|
-
};
|
|
2545
|
-
}
|
|
2546
|
-
else {
|
|
2547
|
-
throw new Error(`Unknown endpoint: ${apiRequest.endpointName}`);
|
|
2548
|
-
}
|
|
2549
|
-
}
|
|
2550
|
-
else {
|
|
2551
|
-
// Fallback to regular parsing
|
|
2552
|
-
requestParsed = (0, parser_1.parserHttpRequest)(namedRequest.content, runtimeVariables);
|
|
2553
|
-
}
|
|
2554
|
-
}
|
|
2555
|
-
else {
|
|
2556
|
-
// Parse as regular HTTP request
|
|
2557
|
-
requestParsed = (0, parser_1.parserHttpRequest)(namedRequest.content, runtimeVariables);
|
|
2558
|
-
// Apply header groups if available (handles "GET url Json" syntax)
|
|
2559
|
-
if (apiDefinitions && apiDefinitions.headerGroups.length > 0) {
|
|
2560
|
-
requestParsed = applyHeaderGroupsToRequest(requestParsed, namedRequest.content, apiDefinitions.headerGroups, runtimeVariables);
|
|
2561
|
-
}
|
|
2562
|
-
}
|
|
2563
|
-
requestUrl = requestParsed.url;
|
|
2564
|
-
requestMethod = requestParsed.method;
|
|
2565
|
-
// Build retry options - prefer options from the run command, fallback to options in request content
|
|
2566
|
-
const effectiveRetryCount = parsed.retryCount ?? requestParsed.retryCount;
|
|
2567
|
-
const effectiveBackoffMs = parsed.backoffMs ?? requestParsed.backoffMs ?? 1000;
|
|
2568
|
-
const retryOpts = effectiveRetryCount
|
|
2569
|
-
? {
|
|
2570
|
-
retryCount: effectiveRetryCount,
|
|
2571
|
-
backoffMs: effectiveBackoffMs,
|
|
2572
|
-
onRetry: (attempt, total, error, waitMs) => {
|
|
2573
|
-
reportProgress(stepIdx, 'request', `[Retry ${attempt}/${total}] run ${targetName} - waiting ${waitMs}ms`, undefined);
|
|
2574
|
-
}
|
|
2575
|
-
}
|
|
2576
|
-
: undefined;
|
|
2577
|
-
const response = cookieJar
|
|
2578
|
-
? await (0, httpClient_1.sendRequestWithJar)(requestParsed, cookieJar, retryOpts)
|
|
2579
|
-
: await (0, httpClient_1.sendRequest)(requestParsed, retryOpts);
|
|
2580
|
-
addResponse(response);
|
|
2581
|
-
const stepResult = {
|
|
2582
|
-
type: 'request',
|
|
2583
|
-
stepIndex: stepIdx,
|
|
2584
|
-
response: response,
|
|
2585
|
-
requestMethod: requestParsed.method,
|
|
2586
|
-
requestUrl: requestParsed.url
|
|
2587
|
-
};
|
|
2588
|
-
orderedSteps.push(stepResult);
|
|
2589
|
-
reportProgress(stepIdx, 'namedRequest', `run ${targetName} → ${requestParsed.method} ${requestParsed.url}`, stepResult);
|
|
2590
|
-
// Process captures that reference this response
|
|
2591
|
-
const relevantCaptures = captures.filter(c => c.afterRequest === requestIndex);
|
|
2592
|
-
for (const capture of relevantCaptures) {
|
|
2593
|
-
const value = getValueByPath(response, capture.path);
|
|
2594
|
-
if (value !== undefined) {
|
|
2595
|
-
// Preserve the value as-is (objects, arrays, primitives) for type checking
|
|
2596
|
-
runtimeVariables[capture.varName] = value;
|
|
2597
|
-
}
|
|
2598
|
-
else {
|
|
2599
|
-
errors.push(`Warning: Could not capture ${capture.varName} from $${requestIndex}.${capture.path}`);
|
|
2600
|
-
}
|
|
2601
|
-
}
|
|
2602
|
-
}
|
|
2603
|
-
catch (error) {
|
|
2604
|
-
// Build a more informative error message
|
|
2605
|
-
let errorDetails = `Named request "${targetName}" failed: ${error.message}`;
|
|
2606
|
-
if (requestUrl) {
|
|
2607
|
-
errorDetails += `\n Request: ${requestMethod} ${requestUrl}`;
|
|
2608
|
-
}
|
|
2609
|
-
errors.push(errorDetails);
|
|
2610
|
-
return {
|
|
2611
|
-
name: '',
|
|
2612
|
-
success: false,
|
|
2613
|
-
responses,
|
|
2614
|
-
scriptResults,
|
|
2615
|
-
assertionResults,
|
|
2616
|
-
steps: orderedSteps,
|
|
2617
|
-
errors,
|
|
2618
|
-
duration: Date.now() - startTime
|
|
2619
|
-
};
|
|
2620
|
-
}
|
|
2621
|
-
}
|
|
2622
|
-
else if (step.type === 'varRunSequence') {
|
|
2623
|
-
// var x = run SequenceName or var x = run SequenceName(args) - capture sequence result into a variable
|
|
2624
|
-
const parsed = parseVarRunSequenceCommand(step.content);
|
|
2625
|
-
if (!parsed) {
|
|
2626
|
-
errors.push(`Invalid var run sequence command: ${step.content}`);
|
|
2627
|
-
return {
|
|
2628
|
-
name: '',
|
|
2629
|
-
success: false,
|
|
2630
|
-
responses,
|
|
2631
|
-
scriptResults,
|
|
2632
|
-
assertionResults,
|
|
2633
|
-
steps: orderedSteps,
|
|
2634
|
-
errors,
|
|
2635
|
-
duration: Date.now() - startTime
|
|
2636
|
-
};
|
|
2637
|
-
}
|
|
2638
|
-
const { varName, sequenceName, args: runArgs } = parsed;
|
|
2639
|
-
if (!fullDocumentText) {
|
|
2640
|
-
errors.push(`Cannot run sequence "${sequenceName}": full document text not provided`);
|
|
2641
|
-
return {
|
|
2642
|
-
name: '',
|
|
2643
|
-
success: false,
|
|
2644
|
-
responses,
|
|
2645
|
-
scriptResults,
|
|
2646
|
-
assertionResults,
|
|
2647
|
-
steps: orderedSteps,
|
|
2648
|
-
errors,
|
|
2649
|
-
duration: Date.now() - startTime
|
|
2650
|
-
};
|
|
2651
|
-
}
|
|
2652
|
-
// Find the target sequence
|
|
2653
|
-
const allSequences = extractSequences(fullDocumentText);
|
|
2654
|
-
const targetSequence = allSequences.find(s => s.name === sequenceName);
|
|
2655
|
-
if (!targetSequence) {
|
|
2656
|
-
// Not a sequence - check for named request
|
|
2657
|
-
const namedRequest = (0, parser_1.getNamedRequest)(fullDocumentText, sequenceName);
|
|
2658
|
-
if (!namedRequest) {
|
|
2659
|
-
errors.push(`"${sequenceName}" is not a named request or sequence`);
|
|
2660
|
-
return {
|
|
2661
|
-
name: '',
|
|
2662
|
-
success: false,
|
|
2663
|
-
responses,
|
|
2664
|
-
scriptResults,
|
|
2665
|
-
assertionResults,
|
|
2666
|
-
steps: orderedSteps,
|
|
2667
|
-
errors,
|
|
2668
|
-
duration: Date.now() - startTime
|
|
2669
|
-
};
|
|
2670
|
-
}
|
|
2671
|
-
// Execute the named request and capture response to variable
|
|
2672
|
-
requestIndex++; // 1-based for user reference
|
|
2673
|
-
let requestUrl = '';
|
|
2674
|
-
let requestMethod = '';
|
|
2675
|
-
try {
|
|
2676
|
-
// Parse and send the named request with current variables
|
|
2677
|
-
const requestParsed = (0, parser_1.parserHttpRequest)(namedRequest.content, runtimeVariables);
|
|
2678
|
-
requestUrl = requestParsed.url;
|
|
2679
|
-
requestMethod = requestParsed.method;
|
|
2680
|
-
// Build retry options - prefer options from the command, fallback to options in request content
|
|
2681
|
-
const effectiveRetryCount = parsed.retryCount ?? requestParsed.retryCount;
|
|
2682
|
-
const effectiveBackoffMs = parsed.backoffMs ?? requestParsed.backoffMs ?? 1000;
|
|
2683
|
-
const retryOpts = effectiveRetryCount
|
|
2684
|
-
? {
|
|
2685
|
-
retryCount: effectiveRetryCount,
|
|
2686
|
-
backoffMs: effectiveBackoffMs,
|
|
2687
|
-
onRetry: (attempt, total, error, waitMs) => {
|
|
2688
|
-
reportProgress(stepIdx, 'request', `[Retry ${attempt}/${total}] var ${varName} = run ${sequenceName} - waiting ${waitMs}ms`, undefined);
|
|
2689
|
-
}
|
|
2690
|
-
}
|
|
2691
|
-
: undefined;
|
|
2692
|
-
const response = cookieJar
|
|
2693
|
-
? await (0, httpClient_1.sendRequestWithJar)(requestParsed, cookieJar, retryOpts)
|
|
2694
|
-
: await (0, httpClient_1.sendRequest)(requestParsed, retryOpts);
|
|
2695
|
-
addResponse(response);
|
|
2696
|
-
const stepResult = {
|
|
2697
|
-
type: 'request',
|
|
2698
|
-
stepIndex: stepIdx,
|
|
2699
|
-
response: response,
|
|
2700
|
-
requestMethod: requestParsed.method,
|
|
2701
|
-
requestUrl: requestParsed.url
|
|
2702
|
-
};
|
|
2703
|
-
orderedSteps.push(stepResult);
|
|
2704
|
-
reportProgress(stepIdx, 'namedRequest', `var ${varName} = run ${sequenceName} → ${requestParsed.method} ${requestParsed.url}`, stepResult);
|
|
2705
|
-
// Store the response in the variable as a JSON object
|
|
2706
|
-
// Include status, body, and headers for full access
|
|
2707
|
-
const capturedResponse = {
|
|
2708
|
-
status: response.status,
|
|
2709
|
-
statusText: response.statusText,
|
|
2710
|
-
body: response.body,
|
|
2711
|
-
headers: response.headers
|
|
2712
|
-
};
|
|
2713
|
-
runtimeVariables[varName] = JSON.stringify(capturedResponse);
|
|
2714
|
-
// Process captures that reference this response
|
|
2715
|
-
const relevantCaptures = captures.filter(c => c.afterRequest === requestIndex);
|
|
2716
|
-
for (const capture of relevantCaptures) {
|
|
2717
|
-
const value = getValueByPath(response, capture.path);
|
|
2718
|
-
if (value !== undefined) {
|
|
2719
|
-
runtimeVariables[capture.varName] = value;
|
|
2720
|
-
}
|
|
2721
|
-
else {
|
|
2722
|
-
errors.push(`Warning: Could not capture ${capture.varName} from $${requestIndex}.${capture.path}`);
|
|
2723
|
-
}
|
|
2724
|
-
}
|
|
2725
|
-
}
|
|
2726
|
-
catch (error) {
|
|
2727
|
-
let errorDetails = `Named request "${sequenceName}" failed: ${error.message}`;
|
|
2728
|
-
if (requestUrl) {
|
|
2729
|
-
errorDetails += `\n Request: ${requestMethod} ${requestUrl}`;
|
|
2730
|
-
}
|
|
2731
|
-
errors.push(errorDetails);
|
|
2732
|
-
return {
|
|
2733
|
-
name: '',
|
|
2734
|
-
success: false,
|
|
2735
|
-
responses,
|
|
2736
|
-
scriptResults,
|
|
2737
|
-
assertionResults,
|
|
2738
|
-
steps: orderedSteps,
|
|
2739
|
-
errors,
|
|
2740
|
-
duration: Date.now() - startTime
|
|
2741
|
-
};
|
|
2742
|
-
}
|
|
2743
|
-
continue;
|
|
2744
|
-
}
|
|
2745
|
-
// Initialize call stack if not provided
|
|
2746
|
-
const currentCallStack = callStack || [];
|
|
2747
|
-
// Check for circular reference
|
|
2748
|
-
if (currentCallStack.includes(sequenceName)) {
|
|
2749
|
-
errors.push(`Circular reference detected: ${[...currentCallStack, sequenceName].join(' → ')}`);
|
|
2750
|
-
return {
|
|
2751
|
-
name: '',
|
|
2752
|
-
success: false,
|
|
2753
|
-
responses,
|
|
2754
|
-
scriptResults,
|
|
2755
|
-
assertionResults,
|
|
2756
|
-
steps: orderedSteps,
|
|
2757
|
-
errors,
|
|
2758
|
-
duration: Date.now() - startTime
|
|
2759
|
-
};
|
|
2760
|
-
}
|
|
2761
|
-
// Bind arguments to parameters
|
|
2762
|
-
let sequenceArgs = {};
|
|
2763
|
-
if (targetSequence.parameters.length > 0 || runArgs.length > 0) {
|
|
2764
|
-
const bindResult = bindSequenceArguments(targetSequence.parameters, runArgs, runtimeVariables);
|
|
2765
|
-
if ('error' in bindResult) {
|
|
2766
|
-
errors.push(`Error calling sequence "${sequenceName}": ${bindResult.error}`);
|
|
2767
|
-
return {
|
|
2768
|
-
name: '',
|
|
2769
|
-
success: false,
|
|
2770
|
-
responses,
|
|
2771
|
-
scriptResults,
|
|
2772
|
-
assertionResults,
|
|
2773
|
-
steps: orderedSteps,
|
|
2774
|
-
errors,
|
|
2775
|
-
duration: Date.now() - startTime
|
|
2776
|
-
};
|
|
2777
|
-
}
|
|
2778
|
-
sequenceArgs = bindResult.variables;
|
|
2779
|
-
}
|
|
2780
|
-
// Check if tag filtering is active and if the target sequence matches
|
|
2781
|
-
if (tagFilterOptions && tagFilterOptions.filters.length > 0) {
|
|
2782
|
-
if (!sequenceMatchesTags(targetSequence, tagFilterOptions)) {
|
|
2783
|
-
// Silently skip this sequence - it doesn't match the tag filter
|
|
2784
|
-
// For var x = run Sequence, the variable will not be set
|
|
2785
|
-
continue;
|
|
2786
|
-
}
|
|
2787
|
-
}
|
|
2788
|
-
// Report that we're starting a sub-sequence
|
|
2789
|
-
reportProgress(stepIdx, 'sequenceStart', `Starting sequence ${sequenceName}`, undefined, sequenceName);
|
|
2790
|
-
// Determine the working directory for this sub-sequence
|
|
2791
|
-
// If we have source info for this sequence (from imports), use that file's directory
|
|
2792
|
-
let subWorkingDir = workingDir;
|
|
2793
|
-
if (sequenceSources) {
|
|
2794
|
-
const sourceFile = sequenceSources.get(sequenceName.toLowerCase());
|
|
2795
|
-
if (sourceFile) {
|
|
2796
|
-
const path = await import('path');
|
|
2797
|
-
subWorkingDir = path.dirname(sourceFile);
|
|
2798
|
-
}
|
|
2799
|
-
}
|
|
2800
|
-
// Run the sub-sequence with current runtime variables
|
|
2801
|
-
const subResult = await runSequenceWithJar(targetSequence.content, runtimeVariables, cookieJar, subWorkingDir, fullDocumentText, onProgress, [...currentCallStack, sequenceName], sequenceArgs, // Pass bound arguments
|
|
2802
|
-
apiDefinitions, // Pass API definitions for endpoint resolution
|
|
2803
|
-
tagFilterOptions, // Pass tag filter options for nested sequence filtering
|
|
2804
|
-
sequenceSources // Pass sequence sources for nested sequences
|
|
2805
|
-
);
|
|
2806
|
-
// Report that sub-sequence ended
|
|
2807
|
-
reportProgress(stepIdx, 'sequenceEnd', `Finished sequence ${sequenceName}`, undefined, sequenceName);
|
|
2808
|
-
// Merge results from sub-sequence and update $N references
|
|
2809
|
-
for (const subResponse of subResult.responses) {
|
|
2810
|
-
addResponse(subResponse);
|
|
2811
|
-
}
|
|
2812
|
-
scriptResults.push(...subResult.scriptResults);
|
|
2813
|
-
assertionResults.push(...subResult.assertionResults);
|
|
2814
|
-
// Merge step results
|
|
2815
|
-
for (const subStep of subResult.steps) {
|
|
2816
|
-
orderedSteps.push({
|
|
2817
|
-
...subStep,
|
|
2818
|
-
stepIndex: orderedSteps.length
|
|
2819
|
-
});
|
|
2820
|
-
}
|
|
2821
|
-
// Extract return expressions from the sequence
|
|
2822
|
-
const returnExpressions = extractReturnVariables(targetSequence.content);
|
|
2823
|
-
if (returnExpressions && returnExpressions.length > 0 && subResult.variables) {
|
|
2824
|
-
if (returnExpressions.length === 1) {
|
|
2825
|
-
// Single return: store the raw value directly
|
|
2826
|
-
// return foo.value → returns the value, not {value: ...}
|
|
2827
|
-
const resolved = resolveReturnExpression(returnExpressions[0], subResult.variables);
|
|
2828
|
-
if (resolved) {
|
|
2829
|
-
runtimeVariables[varName] = resolved.value;
|
|
2830
|
-
}
|
|
2831
|
-
else {
|
|
2832
|
-
runtimeVariables[varName] = '';
|
|
2833
|
-
}
|
|
2834
|
-
}
|
|
2835
|
-
else {
|
|
2836
|
-
// Multiple returns: create object with named values
|
|
2837
|
-
// return a, b → returns {a: valueA, b: valueB}
|
|
2838
|
-
const returnedObj = {};
|
|
2839
|
-
for (const expr of returnExpressions) {
|
|
2840
|
-
const resolved = resolveReturnExpression(expr, subResult.variables);
|
|
2841
|
-
if (resolved) {
|
|
2842
|
-
returnedObj[resolved.key] = resolved.value;
|
|
2843
|
-
}
|
|
2844
|
-
}
|
|
2845
|
-
runtimeVariables[varName] = JSON.stringify(returnedObj);
|
|
2846
|
-
}
|
|
2847
|
-
}
|
|
2848
|
-
else {
|
|
2849
|
-
// No return statement - store empty string
|
|
2850
|
-
runtimeVariables[varName] = '';
|
|
2851
|
-
}
|
|
2852
|
-
// Update request index
|
|
2853
|
-
requestIndex += subResult.responses.length;
|
|
2854
|
-
// If sub-sequence failed, propagate the failure
|
|
2855
|
-
if (!subResult.success) {
|
|
2856
|
-
errors.push(`Sequence "${sequenceName}" failed`);
|
|
2857
|
-
errors.push(...subResult.errors);
|
|
2858
|
-
return {
|
|
2859
|
-
name: '',
|
|
2860
|
-
success: false,
|
|
2861
|
-
responses,
|
|
2862
|
-
scriptResults,
|
|
2863
|
-
assertionResults,
|
|
2864
|
-
steps: orderedSteps,
|
|
2865
|
-
errors,
|
|
2866
|
-
duration: Date.now() - startTime,
|
|
2867
|
-
variables: runtimeVariables
|
|
2868
|
-
};
|
|
2869
|
-
}
|
|
2870
|
-
// Sub-sequence succeeded - continue
|
|
2871
|
-
continue;
|
|
2872
|
-
}
|
|
2873
|
-
else {
|
|
2874
|
-
// HTTP request (regular or API endpoint-based)
|
|
2875
|
-
requestIndex++; // 1-based for user reference
|
|
2876
|
-
let parsed;
|
|
2877
|
-
let requestDescription = '';
|
|
2878
|
-
try {
|
|
2879
|
-
// Check if this is an API request (uses endpoint syntax)
|
|
2880
|
-
if (apiDefinitions &&
|
|
2881
|
-
apiDefinitions.endpoints.length > 0 &&
|
|
2882
|
-
(0, nornapiParser_1.isApiRequestLine)(step.content, apiDefinitions.endpoints)) {
|
|
2883
|
-
// Parse API request syntax
|
|
2884
|
-
const apiRequest = (0, nornapiParser_1.parseApiRequest)(step.content, apiDefinitions.endpoints, apiDefinitions.headerGroups);
|
|
2885
|
-
if (apiRequest) {
|
|
2886
|
-
// Get the endpoint
|
|
2887
|
-
const endpoint = (0, nornapiParser_1.getEndpoint)({ headerGroups: apiDefinitions.headerGroups, endpoints: apiDefinitions.endpoints }, apiRequest.endpointName);
|
|
2888
|
-
if (!endpoint) {
|
|
2889
|
-
throw new Error(`Unknown endpoint: ${apiRequest.endpointName}`);
|
|
2890
|
-
}
|
|
2891
|
-
// Resolve parameters in the path (substitute values, including from variables)
|
|
2892
|
-
// First substitute {{variable}} references in the endpoint path (e.g., {{baseUrl}})
|
|
2893
|
-
// This must happen BEFORE {param} substitution to avoid conflicts
|
|
2894
|
-
const pathWithVars = (0, parser_1.substituteVariables)(endpoint.path, runtimeVariables);
|
|
2895
|
-
// Then substitute {param} placeholders with the values passed to the endpoint
|
|
2896
|
-
const resolvedParams = {};
|
|
2897
|
-
for (const [key, value] of Object.entries(apiRequest.params)) {
|
|
2898
|
-
// First resolve bare variable names, then substitute {{var}} patterns
|
|
2899
|
-
const bareResolved = resolveBareVariables(value, runtimeVariables);
|
|
2900
|
-
resolvedParams[key] = (0, parser_1.substituteVariables)(bareResolved, runtimeVariables);
|
|
2901
|
-
}
|
|
2902
|
-
// Apply parameter substitutions to the path
|
|
2903
|
-
let resolvedPath = pathWithVars;
|
|
2904
|
-
for (const paramName of endpoint.parameters) {
|
|
2905
|
-
const value = resolvedParams[paramName];
|
|
2906
|
-
if (value !== undefined) {
|
|
2907
|
-
resolvedPath = resolvedPath.replace(`{${paramName}}`, value);
|
|
2908
|
-
}
|
|
2909
|
-
}
|
|
2910
|
-
// Collect headers from header groups
|
|
2911
|
-
const combinedHeaders = {};
|
|
2912
|
-
for (const groupName of apiRequest.headerGroupNames) {
|
|
2913
|
-
const group = (0, nornapiParser_1.getHeaderGroup)({ headerGroups: apiDefinitions.headerGroups, endpoints: apiDefinitions.endpoints }, groupName);
|
|
2914
|
-
if (group) {
|
|
2915
|
-
// Resolve variable references in header values
|
|
2916
|
-
const resolvedHeaders = (0, nornapiParser_1.resolveHeaderValues)(group, runtimeVariables);
|
|
2917
|
-
Object.assign(combinedHeaders, resolvedHeaders);
|
|
2918
|
-
}
|
|
2919
|
-
}
|
|
2920
|
-
// Add inline headers (these take precedence over header groups)
|
|
2921
|
-
for (const [headerName, headerValue] of Object.entries(apiRequest.inlineHeaders)) {
|
|
2922
|
-
// Resolve variable references in header values
|
|
2923
|
-
combinedHeaders[headerName] = (0, parser_1.substituteVariables)(headerValue, runtimeVariables);
|
|
2924
|
-
}
|
|
2925
|
-
// Resolve variables in the body if present
|
|
2926
|
-
let resolvedBody;
|
|
2927
|
-
if (apiRequest.body) {
|
|
2928
|
-
resolvedBody = (0, parser_1.substituteVariables)(apiRequest.body, runtimeVariables);
|
|
2929
|
-
}
|
|
2930
|
-
parsed = {
|
|
2931
|
-
method: apiRequest.method,
|
|
2932
|
-
url: resolvedPath,
|
|
2933
|
-
headers: combinedHeaders,
|
|
2934
|
-
body: resolvedBody
|
|
2935
|
-
};
|
|
2936
|
-
requestDescription = `${apiRequest.endpointName}(${Object.values(resolvedParams).join(', ')}) → ${parsed.method} ${parsed.url}`;
|
|
2937
|
-
}
|
|
2938
|
-
else {
|
|
2939
|
-
// Fallback to regular parsing if API parsing fails
|
|
2940
|
-
parsed = (0, parser_1.parserHttpRequest)(step.content, runtimeVariables);
|
|
2941
|
-
requestDescription = `${parsed.method} ${parsed.url}`;
|
|
2942
|
-
// Check for header group names in the step content
|
|
2943
|
-
if (apiDefinitions && apiDefinitions.headerGroups.length > 0) {
|
|
2944
|
-
parsed = applyHeaderGroupsToRequest(parsed, step.content, apiDefinitions.headerGroups, runtimeVariables);
|
|
2945
|
-
}
|
|
2946
|
-
}
|
|
2947
|
-
}
|
|
2948
|
-
else {
|
|
2949
|
-
// Regular HTTP request (URL-based)
|
|
2950
|
-
parsed = (0, parser_1.parserHttpRequest)(step.content, runtimeVariables);
|
|
2951
|
-
requestDescription = `${parsed.method} ${parsed.url}`;
|
|
2952
|
-
// Check for header group names in the step content
|
|
2953
|
-
if (apiDefinitions && apiDefinitions.headerGroups.length > 0) {
|
|
2954
|
-
parsed = applyHeaderGroupsToRequest(parsed, step.content, apiDefinitions.headerGroups, runtimeVariables);
|
|
2955
|
-
}
|
|
2956
|
-
// Check if body is a bare variable name (no {{}} braces)
|
|
2957
|
-
// This allows: POST url Json\npayload (where payload is a variable containing JSON)
|
|
2958
|
-
if (parsed.body) {
|
|
2959
|
-
const bodyTrimmed = parsed.body.trim();
|
|
2960
|
-
const bareVarMatch = bodyTrimmed.match(/^([a-zA-Z_][a-zA-Z0-9_]*)$/);
|
|
2961
|
-
if (bareVarMatch && bareVarMatch[1] in runtimeVariables) {
|
|
2962
|
-
const varValue = runtimeVariables[bareVarMatch[1]];
|
|
2963
|
-
if (typeof varValue === 'object' && varValue !== null) {
|
|
2964
|
-
parsed.body = JSON.stringify(varValue);
|
|
2965
|
-
}
|
|
2966
|
-
else if (typeof varValue === 'string') {
|
|
2967
|
-
parsed.body = varValue;
|
|
2968
|
-
}
|
|
2969
|
-
else {
|
|
2970
|
-
parsed.body = String(varValue);
|
|
2971
|
-
}
|
|
2972
|
-
}
|
|
2973
|
-
}
|
|
2974
|
-
}
|
|
2975
|
-
// Extract retry options from step content (works for both API and regular requests)
|
|
2976
|
-
const { retryCount, backoffMs } = (0, parser_1.extractRetryOptions)(step.content);
|
|
2977
|
-
const retryOpts = (retryCount ?? parsed.retryCount)
|
|
2978
|
-
? {
|
|
2979
|
-
retryCount: retryCount ?? parsed.retryCount ?? 0,
|
|
2980
|
-
backoffMs: backoffMs ?? parsed.backoffMs ?? 1000,
|
|
2981
|
-
onRetry: (attempt, total, error, waitMs) => {
|
|
2982
|
-
reportProgress(stepIdx, 'request', `[Retry ${attempt}/${total}] ${requestDescription} - waiting ${waitMs}ms`, undefined);
|
|
2983
|
-
}
|
|
2984
|
-
}
|
|
2985
|
-
: undefined;
|
|
2986
|
-
// Send the request (with jar if provided, otherwise uses shared jar)
|
|
2987
|
-
const response = cookieJar
|
|
2988
|
-
? await (0, httpClient_1.sendRequestWithJar)(parsed, cookieJar, retryOpts)
|
|
2989
|
-
: await (0, httpClient_1.sendRequest)(parsed, retryOpts);
|
|
2990
|
-
addResponse(response);
|
|
2991
|
-
const stepResult = {
|
|
2992
|
-
type: 'request',
|
|
2993
|
-
stepIndex: stepIdx,
|
|
2994
|
-
response: response,
|
|
2995
|
-
requestMethod: parsed.method,
|
|
2996
|
-
requestUrl: parsed.url
|
|
2997
|
-
};
|
|
2998
|
-
orderedSteps.push(stepResult);
|
|
2999
|
-
reportProgress(stepIdx, 'request', requestDescription, stepResult);
|
|
3000
|
-
// Process captures that reference this response ($N where N = requestIndex)
|
|
3001
|
-
const relevantCaptures = captures.filter(c => c.afterRequest === requestIndex);
|
|
3002
|
-
for (const capture of relevantCaptures) {
|
|
3003
|
-
const value = getValueByPath(response, capture.path);
|
|
3004
|
-
if (value !== undefined) {
|
|
3005
|
-
// Preserve the value as-is (objects, arrays, primitives) for type checking
|
|
3006
|
-
runtimeVariables[capture.varName] = value;
|
|
3007
|
-
}
|
|
3008
|
-
else {
|
|
3009
|
-
errors.push(`Warning: Could not capture ${capture.varName} from $${requestIndex}.${capture.path}`);
|
|
3010
|
-
}
|
|
3011
|
-
}
|
|
3012
|
-
}
|
|
3013
|
-
catch (error) {
|
|
3014
|
-
// Build a more informative error message
|
|
3015
|
-
let errorDetails = `Request ${requestIndex} failed: ${error.message}`;
|
|
3016
|
-
if (requestDescription) {
|
|
3017
|
-
errorDetails += `\n Request: ${requestDescription}`;
|
|
3018
|
-
}
|
|
3019
|
-
errors.push(errorDetails);
|
|
3020
|
-
// Stop sequence on error
|
|
3021
|
-
return {
|
|
3022
|
-
name: '',
|
|
3023
|
-
success: false,
|
|
3024
|
-
responses,
|
|
3025
|
-
scriptResults,
|
|
3026
|
-
assertionResults,
|
|
3027
|
-
steps: orderedSteps,
|
|
3028
|
-
errors,
|
|
3029
|
-
duration: Date.now() - startTime
|
|
3030
|
-
};
|
|
3031
|
-
}
|
|
3032
|
-
}
|
|
3033
|
-
}
|
|
3034
|
-
return {
|
|
3035
|
-
name: '',
|
|
3036
|
-
success: errors.filter(e => !e.startsWith('Warning')).length === 0,
|
|
3037
|
-
responses,
|
|
3038
|
-
scriptResults,
|
|
3039
|
-
assertionResults,
|
|
3040
|
-
steps: orderedSteps,
|
|
3041
|
-
errors,
|
|
3042
|
-
duration: Date.now() - startTime,
|
|
3043
|
-
variables: runtimeVariables
|
|
3044
|
-
};
|
|
3045
|
-
}
|
|
3046
|
-
//# sourceMappingURL=sequenceRunner.js.map
|