flowstride 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +183 -0
- package/dist/adapters/api/undici.d.ts +46 -0
- package/dist/adapters/api/undici.js +118 -0
- package/dist/adapters/web/playwright.d.ts +51 -0
- package/dist/adapters/web/playwright.js +1108 -0
- package/dist/cli/commands/init.d.ts +1 -0
- package/dist/cli/commands/init.js +77 -0
- package/dist/cli/commands/report.d.ts +1 -0
- package/dist/cli/commands/report.js +78 -0
- package/dist/cli/commands/run.d.ts +1 -0
- package/dist/cli/commands/run.js +158 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +39 -0
- package/dist/parser/lexer.d.ts +29 -0
- package/dist/parser/lexer.js +425 -0
- package/dist/parser/parser.d.ts +41 -0
- package/dist/parser/parser.js +573 -0
- package/dist/parser/resolver.d.ts +5 -0
- package/dist/parser/resolver.js +19 -0
- package/dist/parser/validator.d.ts +4 -0
- package/dist/parser/validator.js +37 -0
- package/dist/parser/variable_resolver.d.ts +7 -0
- package/dist/parser/variable_resolver.js +33 -0
- package/dist/runner/data_generators.d.ts +55 -0
- package/dist/runner/data_generators.js +233 -0
- package/dist/runner/dom_stripper.d.ts +4 -0
- package/dist/runner/dom_stripper.js +61 -0
- package/dist/runner/events.d.ts +15 -0
- package/dist/runner/events.js +20 -0
- package/dist/runner/flow_orchestrator.d.ts +15 -0
- package/dist/runner/flow_orchestrator.js +216 -0
- package/dist/runner/flow_worker.d.ts +53 -0
- package/dist/runner/flow_worker.js +1112 -0
- package/dist/runner/persistent_vault.d.ts +12 -0
- package/dist/runner/persistent_vault.js +84 -0
- package/dist/runner/plugin_manager.d.ts +13 -0
- package/dist/runner/plugin_manager.js +164 -0
- package/dist/runner/temporary_store.d.ts +8 -0
- package/dist/runner/temporary_store.js +22 -0
- package/dist/runner/worker_entry.d.ts +1 -0
- package/dist/runner/worker_entry.js +63 -0
- package/dist/sdk/api_proxy.d.ts +8 -0
- package/dist/sdk/api_proxy.js +28 -0
- package/dist/sdk/db_proxy.d.ts +3 -0
- package/dist/sdk/db_proxy.js +15 -0
- package/dist/sessions/manager.d.ts +17 -0
- package/dist/sessions/manager.js +74 -0
- package/dist/sessions/types.d.ts +39 -0
- package/dist/sessions/types.js +2 -0
- package/dist/sessions/vault.d.ts +8 -0
- package/dist/sessions/vault.js +69 -0
- package/dist/test_data/commerce.json +341 -0
- package/dist/test_data/geography.json +1298 -0
- package/dist/test_data/identities.json +1420 -0
- package/dist/test_data/phone_data.json +27 -0
- package/dist/test_data/utility.json +246 -0
- package/dist/types/index.d.ts +104 -0
- package/dist/types/index.js +2 -0
- package/dist/utils/redactor.d.ts +5 -0
- package/dist/utils/redactor.js +53 -0
- package/package.json +54 -0
|
@@ -0,0 +1,573 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Parser = void 0;
|
|
4
|
+
const lexer_1 = require("./lexer");
|
|
5
|
+
class Parser {
|
|
6
|
+
tokens;
|
|
7
|
+
position = 0;
|
|
8
|
+
currentBlockDescription = "";
|
|
9
|
+
stepCounter = 1;
|
|
10
|
+
constructor(tokens) {
|
|
11
|
+
this.tokens = tokens;
|
|
12
|
+
}
|
|
13
|
+
parse() {
|
|
14
|
+
const features = [];
|
|
15
|
+
let currentFeatureName = null;
|
|
16
|
+
let currentScenarios = [];
|
|
17
|
+
let featureDeclarations = [];
|
|
18
|
+
while (!this.isAtEnd()) {
|
|
19
|
+
const token = this.peek();
|
|
20
|
+
if (token.type === lexer_1.TokenType.LET) {
|
|
21
|
+
if (!currentFeatureName) {
|
|
22
|
+
throw new Error(`Line ${token.line}: Scope Violation. Global aliases must be declared inside a Feature block, before Scenarios.`);
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
featureDeclarations.push(this.parseDeclaration());
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
else if (token.type === lexer_1.TokenType.FEATURE) {
|
|
29
|
+
if (currentFeatureName && currentScenarios.length > 0) {
|
|
30
|
+
features.push({
|
|
31
|
+
name: currentFeatureName,
|
|
32
|
+
declarations: featureDeclarations,
|
|
33
|
+
scenarios: currentScenarios,
|
|
34
|
+
});
|
|
35
|
+
currentScenarios = [];
|
|
36
|
+
featureDeclarations = [];
|
|
37
|
+
}
|
|
38
|
+
this.advance();
|
|
39
|
+
currentFeatureName = this.consume(lexer_1.TokenType.STRING, "Feature must have a description").value;
|
|
40
|
+
}
|
|
41
|
+
else if (token.type === lexer_1.TokenType.SCENARIO) {
|
|
42
|
+
if (!currentFeatureName)
|
|
43
|
+
throw new Error(`Line ${token.line}: Scenario found before Feature.`);
|
|
44
|
+
currentScenarios.push(this.parseScenario());
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
this.advance();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (currentFeatureName && currentScenarios.length > 0) {
|
|
51
|
+
features.push({
|
|
52
|
+
name: currentFeatureName,
|
|
53
|
+
declarations: featureDeclarations,
|
|
54
|
+
scenarios: currentScenarios,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
return features;
|
|
58
|
+
}
|
|
59
|
+
parseDeclaration() {
|
|
60
|
+
const letToken = this.advance();
|
|
61
|
+
const aliasToken = this.consume(lexer_1.TokenType.IDENTIFIER, "Expected alias name after 'let'");
|
|
62
|
+
this.consume(lexer_1.TokenType.ASSIGN, "Expected '=' after alias name");
|
|
63
|
+
const pathToken = this.consume(lexer_1.TokenType.STRING, "Expected plugin file path as string");
|
|
64
|
+
return {
|
|
65
|
+
alias: aliasToken.value,
|
|
66
|
+
path: pathToken.value,
|
|
67
|
+
line: letToken.line,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
parseScenario() {
|
|
71
|
+
this.advance();
|
|
72
|
+
const nameToken = this.consume(lexer_1.TokenType.STRING, "Scenario must have a description");
|
|
73
|
+
const steps = [];
|
|
74
|
+
const declarations = [];
|
|
75
|
+
this.stepCounter = 1;
|
|
76
|
+
let state = "START";
|
|
77
|
+
let hasGiven = false, hasWhen = false, hasAnd = false, hasThen = false;
|
|
78
|
+
let isLocked = false;
|
|
79
|
+
let currentBlockType = "";
|
|
80
|
+
while (!this.isAtEnd() &&
|
|
81
|
+
this.peek().type !== lexer_1.TokenType.SCENARIO &&
|
|
82
|
+
this.peek().type !== lexer_1.TokenType.FEATURE) {
|
|
83
|
+
const token = this.peek();
|
|
84
|
+
if (token.type === lexer_1.TokenType.LET) {
|
|
85
|
+
declarations.push(this.parseDeclaration());
|
|
86
|
+
}
|
|
87
|
+
else if (token.type === lexer_1.TokenType.BLOCK) {
|
|
88
|
+
if (isLocked)
|
|
89
|
+
throw new Error(`Line ${token.line}: Terminal Lock Violation. No steps allowed after Then.`);
|
|
90
|
+
const blockToken = this.advance();
|
|
91
|
+
const blockType = blockToken.value.toLowerCase();
|
|
92
|
+
let description = this.peek().type === lexer_1.TokenType.STRING ? this.advance().value : "";
|
|
93
|
+
if (blockType === "given") {
|
|
94
|
+
if (state !== "START")
|
|
95
|
+
throw new Error(`Line ${token.line}: Given must be first.`);
|
|
96
|
+
state = "GIVEN";
|
|
97
|
+
hasGiven = true;
|
|
98
|
+
}
|
|
99
|
+
else if (blockType === "when") {
|
|
100
|
+
if (state === "START")
|
|
101
|
+
throw new Error(`Line ${token.line}: When cannot come before Given.`);
|
|
102
|
+
state = "WHEN";
|
|
103
|
+
hasWhen = true;
|
|
104
|
+
isLocked = false;
|
|
105
|
+
}
|
|
106
|
+
else if (blockType === "then") {
|
|
107
|
+
if (state !== "WHEN")
|
|
108
|
+
throw new Error(`Line ${token.line}: Then must follow When.`);
|
|
109
|
+
state = "THEN";
|
|
110
|
+
hasThen = true;
|
|
111
|
+
isLocked = true;
|
|
112
|
+
}
|
|
113
|
+
else if (blockType === "and") {
|
|
114
|
+
if (state === "START")
|
|
115
|
+
throw new Error(`Line ${token.line}: And cannot start a scenario.`);
|
|
116
|
+
hasAnd = true;
|
|
117
|
+
}
|
|
118
|
+
currentBlockType = blockType;
|
|
119
|
+
this.currentBlockDescription = description
|
|
120
|
+
? `${blockToken.value} "${description}"`
|
|
121
|
+
: blockToken.value;
|
|
122
|
+
}
|
|
123
|
+
else if (token.type === lexer_1.TokenType.COMMAND ||
|
|
124
|
+
token.type === lexer_1.TokenType.ALIAS_COMMAND) {
|
|
125
|
+
if (isLocked && currentBlockType !== "then") {
|
|
126
|
+
throw new Error(`Line ${token.line}: Terminal Lock Violation.`);
|
|
127
|
+
}
|
|
128
|
+
const step = this.parseCommand();
|
|
129
|
+
if (currentBlockType === "and" &&
|
|
130
|
+
step.action !== "expect" &&
|
|
131
|
+
step.action !== "plugin") {
|
|
132
|
+
throw new Error(`Line ${token.line}: And blocks are strictly for assertions or custom assertion plugins.`);
|
|
133
|
+
}
|
|
134
|
+
steps.push(step);
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
throw new Error(`Line ${token.line}: Unexpected trailing token "${token.value}". Make sure your command arguments are properly formatted without stray words.`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (!hasGiven || !hasWhen || !hasThen) {
|
|
141
|
+
throw new Error(`Scenario "${nameToken.value}" is missing mandatory blocks.`);
|
|
142
|
+
}
|
|
143
|
+
return { name: nameToken.value, declarations, steps };
|
|
144
|
+
}
|
|
145
|
+
parseSmartSelectorPayload() {
|
|
146
|
+
let selector = "";
|
|
147
|
+
if (!this.isAtEnd() &&
|
|
148
|
+
this.peek().type === lexer_1.TokenType.MODIFIER &&
|
|
149
|
+
this.peek().value.toLowerCase() === "placeholder") {
|
|
150
|
+
const mod = this.advance().value.toLowerCase();
|
|
151
|
+
const value = this.consume(lexer_1.TokenType.STRING, "Selector string expected").value;
|
|
152
|
+
selector = `[${mod}="${value}" i]`;
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
selector = this.consume(lexer_1.TokenType.STRING, "Selector expected").value;
|
|
156
|
+
}
|
|
157
|
+
while (!this.isAtEnd()) {
|
|
158
|
+
const nextToken = this.peek();
|
|
159
|
+
if (nextToken.type === lexer_1.TokenType.SPATIAL) {
|
|
160
|
+
selector += ` ${this.advance().value}`;
|
|
161
|
+
if (!this.isAtEnd() && this.peek().type === lexer_1.TokenType.STRING) {
|
|
162
|
+
selector += ` "${this.advance().value}"`;
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
throw new Error(`Line ${nextToken.line}: Expected a string anchor after spatial modifier "${nextToken.value}"`);
|
|
166
|
+
}
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (nextToken.type === lexer_1.TokenType.STRING) {
|
|
170
|
+
const nextStr = nextToken.value;
|
|
171
|
+
if (/^(near|above|under|below|leftOf|rightOf|inside|in):/i.test(nextStr)) {
|
|
172
|
+
selector += ` "${this.advance().value}"`;
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
return selector;
|
|
179
|
+
}
|
|
180
|
+
consumeOptionalElement(payload) {
|
|
181
|
+
if (!this.isAtEnd() && this.peek().type === lexer_1.TokenType.ELEMENT) {
|
|
182
|
+
payload.elementType = this.advance().value.toLowerCase();
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
parseCommand() {
|
|
186
|
+
const commandToken = this.advance();
|
|
187
|
+
if (commandToken.type === lexer_1.TokenType.ALIAS_COMMAND) {
|
|
188
|
+
const parts = commandToken.value.split(".");
|
|
189
|
+
const alias = parts[0];
|
|
190
|
+
const method = parts.slice(1).join(".");
|
|
191
|
+
let dataPayload = {};
|
|
192
|
+
if (!this.isAtEnd() && this.peek().type === lexer_1.TokenType.JSON_PAYLOAD) {
|
|
193
|
+
const jsonToken = this.advance();
|
|
194
|
+
try {
|
|
195
|
+
dataPayload = JSON.parse(jsonToken.value);
|
|
196
|
+
}
|
|
197
|
+
catch (e) {
|
|
198
|
+
throw new Error(`Line ${jsonToken.line}: Invalid JSON payload for plugin -> ${e.message}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
else if (!this.isAtEnd() && this.peek().type === lexer_1.TokenType.STRING) {
|
|
202
|
+
dataPayload = this.advance().value;
|
|
203
|
+
}
|
|
204
|
+
return {
|
|
205
|
+
id: `S${this.stepCounter++}`,
|
|
206
|
+
blockDescription: this.currentBlockDescription,
|
|
207
|
+
action: "plugin",
|
|
208
|
+
payload: {
|
|
209
|
+
pluginAlias: alias,
|
|
210
|
+
pluginMethod: method,
|
|
211
|
+
data: dataPayload,
|
|
212
|
+
},
|
|
213
|
+
line: commandToken.line,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
let rawAction = commandToken.value.toLowerCase();
|
|
217
|
+
let isOptional = false;
|
|
218
|
+
if (rawAction === "flow.try") {
|
|
219
|
+
isOptional = true;
|
|
220
|
+
const nextToken = this.advance();
|
|
221
|
+
if (nextToken.type !== lexer_1.TokenType.COMMAND &&
|
|
222
|
+
nextToken.type !== lexer_1.TokenType.STRING &&
|
|
223
|
+
nextToken.type !== lexer_1.TokenType.ALIAS_COMMAND) {
|
|
224
|
+
throw new Error(`Expected an action command after flow.try at line ${nextToken.line}`);
|
|
225
|
+
}
|
|
226
|
+
rawAction = nextToken.value.toLowerCase();
|
|
227
|
+
}
|
|
228
|
+
const actionBase = rawAction.startsWith("flow.")
|
|
229
|
+
? rawAction.replace("flow.", "")
|
|
230
|
+
: rawAction;
|
|
231
|
+
const actionMap = {
|
|
232
|
+
open: "open",
|
|
233
|
+
click: "click",
|
|
234
|
+
forceclick: "forceClick",
|
|
235
|
+
type: "type",
|
|
236
|
+
forcetype: "forceType",
|
|
237
|
+
passcode: "passcode",
|
|
238
|
+
upload: "upload",
|
|
239
|
+
set: "set",
|
|
240
|
+
select: "select",
|
|
241
|
+
drag: "drag",
|
|
242
|
+
switchto: "switchTo",
|
|
243
|
+
close: "close",
|
|
244
|
+
acceptdialog: "acceptDialog",
|
|
245
|
+
rejectdialog: "rejectDialog",
|
|
246
|
+
waitforpipeline: "waitForPipeline",
|
|
247
|
+
injectaudio: "injectAudio",
|
|
248
|
+
use: "use",
|
|
249
|
+
save: "save",
|
|
250
|
+
expect: "expect",
|
|
251
|
+
check: "check",
|
|
252
|
+
uncheck: "uncheck",
|
|
253
|
+
post: "post",
|
|
254
|
+
get: "get",
|
|
255
|
+
put: "put",
|
|
256
|
+
patch: "patch",
|
|
257
|
+
delete: "delete",
|
|
258
|
+
graphql: "graphql",
|
|
259
|
+
autoheal: "autoHeal",
|
|
260
|
+
"mail.getotp": "getOtp",
|
|
261
|
+
extract: "extract",
|
|
262
|
+
};
|
|
263
|
+
const action = actionMap[actionBase] || actionBase;
|
|
264
|
+
let payload = { optional: isOptional };
|
|
265
|
+
switch (action) {
|
|
266
|
+
case "extract":
|
|
267
|
+
const extTargetToken = this.consume(lexer_1.TokenType.MODIFIER, "Expected target after extract");
|
|
268
|
+
const extTarget = extTargetToken.value.toLowerCase();
|
|
269
|
+
let normalizedTarget = extTarget;
|
|
270
|
+
if (extTarget === "resbody")
|
|
271
|
+
normalizedTarget = "resBody";
|
|
272
|
+
else if (extTarget === "reqbody")
|
|
273
|
+
normalizedTarget = "reqBody";
|
|
274
|
+
else if (extTarget === "resheader")
|
|
275
|
+
normalizedTarget = "resHeader";
|
|
276
|
+
else if (extTarget === "reqheader")
|
|
277
|
+
normalizedTarget = "reqHeader";
|
|
278
|
+
if (!["resBody", "resHeader", "cookie"].includes(normalizedTarget)) {
|
|
279
|
+
throw new Error(`Line ${extTargetToken.line}: Expected 'resBody', 'resHeader', or 'cookie' after extract`);
|
|
280
|
+
}
|
|
281
|
+
payload.target = normalizedTarget;
|
|
282
|
+
payload.jsonPath = this.consume(lexer_1.TokenType.STRING, "JSONPath or Key expected").value;
|
|
283
|
+
const asToken = this.advance();
|
|
284
|
+
if (asToken.value.toLowerCase() !== "as") {
|
|
285
|
+
throw new Error(`Line ${asToken.line}: Expected 'as' after extraction target`);
|
|
286
|
+
}
|
|
287
|
+
let isGlobalExtract = false;
|
|
288
|
+
if (!this.isAtEnd() && this.peek().value.toLowerCase() === "global") {
|
|
289
|
+
this.advance();
|
|
290
|
+
isGlobalExtract = true;
|
|
291
|
+
}
|
|
292
|
+
payload.isGlobal = isGlobalExtract;
|
|
293
|
+
payload.variableName = this.consume(lexer_1.TokenType.STRING, "Variable name expected").value;
|
|
294
|
+
break;
|
|
295
|
+
case "getOtp":
|
|
296
|
+
payload.email = this.consume(lexer_1.TokenType.STRING, "Email address expected").value;
|
|
297
|
+
const prepToken = this.consume(lexer_1.TokenType.PREPOSITION, 'Expected "into" or "as"');
|
|
298
|
+
if (!["into", "as"].includes(prepToken.value.toLowerCase())) {
|
|
299
|
+
throw new Error(`Line ${prepToken.line}: Expected 'into' or 'as', found '${prepToken.value}'`);
|
|
300
|
+
}
|
|
301
|
+
let isGlobalOtp = false;
|
|
302
|
+
if (!this.isAtEnd() && this.peek().value.toLowerCase() === "global") {
|
|
303
|
+
this.advance();
|
|
304
|
+
isGlobalOtp = true;
|
|
305
|
+
}
|
|
306
|
+
payload.isGlobal = isGlobalOtp;
|
|
307
|
+
payload.variable = this.consume(lexer_1.TokenType.STRING, "Variable name expected").value;
|
|
308
|
+
break;
|
|
309
|
+
case "autoHeal":
|
|
310
|
+
payload.targetStatus = this.consume(lexer_1.TokenType.STRING, 'Expected status code string (e.g. "401")').value;
|
|
311
|
+
const viaToken = this.consume(lexer_1.TokenType.PREPOSITION, 'Expected "via"');
|
|
312
|
+
if (viaToken.value.toLowerCase() !== "via")
|
|
313
|
+
throw new Error(`Line ${viaToken.line}: Expected 'via'`);
|
|
314
|
+
payload.verb = this.consume(lexer_1.TokenType.COMMAND, "Expected API verb (e.g. POST)").value.toLowerCase();
|
|
315
|
+
payload.url = this.consume(lexer_1.TokenType.STRING, "Endpoint URL expected").value;
|
|
316
|
+
payload.headers = [];
|
|
317
|
+
this.parseApiModifiers(payload);
|
|
318
|
+
break;
|
|
319
|
+
case "post":
|
|
320
|
+
case "get":
|
|
321
|
+
case "put":
|
|
322
|
+
case "patch":
|
|
323
|
+
case "delete":
|
|
324
|
+
case "graphql":
|
|
325
|
+
payload.url = this.consume(lexer_1.TokenType.STRING, "Endpoint URL expected").value;
|
|
326
|
+
payload.headers = [];
|
|
327
|
+
this.parseApiModifiers(payload);
|
|
328
|
+
break;
|
|
329
|
+
case "open":
|
|
330
|
+
payload.url = this.consume(lexer_1.TokenType.STRING, "URL expected").value;
|
|
331
|
+
break;
|
|
332
|
+
case "click":
|
|
333
|
+
case "forceClick":
|
|
334
|
+
case "check":
|
|
335
|
+
case "uncheck":
|
|
336
|
+
this.consumeOptionalElement(payload);
|
|
337
|
+
payload.selector = this.parseSmartSelectorPayload();
|
|
338
|
+
break;
|
|
339
|
+
case "type":
|
|
340
|
+
case "forceType":
|
|
341
|
+
case "passcode":
|
|
342
|
+
this.consumeOptionalElement(payload);
|
|
343
|
+
payload.selector = this.parseSmartSelectorPayload();
|
|
344
|
+
payload.text = this.consume(lexer_1.TokenType.STRING, "Text expected").value;
|
|
345
|
+
break;
|
|
346
|
+
case "upload":
|
|
347
|
+
payload.file = this.consume(lexer_1.TokenType.STRING, "File path expected").value;
|
|
348
|
+
if (!this.isAtEnd() &&
|
|
349
|
+
this.peek().type === lexer_1.TokenType.PREPOSITION &&
|
|
350
|
+
this.peek().value.toLowerCase() === "into") {
|
|
351
|
+
this.advance();
|
|
352
|
+
}
|
|
353
|
+
this.consumeOptionalElement(payload);
|
|
354
|
+
payload.selector = this.parseSmartSelectorPayload();
|
|
355
|
+
break;
|
|
356
|
+
case "set":
|
|
357
|
+
this.consumeOptionalElement(payload);
|
|
358
|
+
payload.selector = this.parseSmartSelectorPayload();
|
|
359
|
+
if (!this.isAtEnd() &&
|
|
360
|
+
this.peek().type === lexer_1.TokenType.PREPOSITION &&
|
|
361
|
+
this.peek().value.toLowerCase() === "to") {
|
|
362
|
+
this.advance();
|
|
363
|
+
}
|
|
364
|
+
payload.value = this.consume(lexer_1.TokenType.STRING, "Value expected").value;
|
|
365
|
+
break;
|
|
366
|
+
case "select":
|
|
367
|
+
payload.option = this.consume(lexer_1.TokenType.STRING, "Option expected").value;
|
|
368
|
+
if (!this.isAtEnd() &&
|
|
369
|
+
this.peek().type === lexer_1.TokenType.PREPOSITION &&
|
|
370
|
+
this.peek().value.toLowerCase() === "from") {
|
|
371
|
+
this.advance();
|
|
372
|
+
}
|
|
373
|
+
this.consumeOptionalElement(payload);
|
|
374
|
+
payload.selector = this.parseSmartSelectorPayload();
|
|
375
|
+
break;
|
|
376
|
+
case "drag":
|
|
377
|
+
this.consumeOptionalElement(payload);
|
|
378
|
+
payload.source = this.parseSmartSelectorPayload();
|
|
379
|
+
if (!this.isAtEnd() &&
|
|
380
|
+
this.peek().type === lexer_1.TokenType.PREPOSITION &&
|
|
381
|
+
this.peek().value.toLowerCase() === "into") {
|
|
382
|
+
this.advance();
|
|
383
|
+
}
|
|
384
|
+
this.consumeOptionalElement(payload);
|
|
385
|
+
payload.destination = this.parseSmartSelectorPayload();
|
|
386
|
+
break;
|
|
387
|
+
case "switchTo":
|
|
388
|
+
if (!this.isAtEnd() &&
|
|
389
|
+
this.peek().type === lexer_1.TokenType.SPATIAL &&
|
|
390
|
+
(this.peek().value.toLowerCase() === "next" ||
|
|
391
|
+
this.peek().value.toLowerCase() === "previous")) {
|
|
392
|
+
payload.target = this.advance().value;
|
|
393
|
+
}
|
|
394
|
+
else {
|
|
395
|
+
payload.target = this.consume(lexer_1.TokenType.STRING, "Target tab name expected").value;
|
|
396
|
+
}
|
|
397
|
+
break;
|
|
398
|
+
case "close":
|
|
399
|
+
this.consumeOptionalElement(payload);
|
|
400
|
+
payload.selector = this.parseSmartSelectorPayload();
|
|
401
|
+
break;
|
|
402
|
+
case "acceptDialog":
|
|
403
|
+
case "rejectDialog":
|
|
404
|
+
case "waitForPipeline":
|
|
405
|
+
break;
|
|
406
|
+
case "injectAudio":
|
|
407
|
+
payload.file = this.consume(lexer_1.TokenType.STRING, "Audio file path expected").value;
|
|
408
|
+
break;
|
|
409
|
+
case "use":
|
|
410
|
+
case "save":
|
|
411
|
+
let isGlobalOp = false;
|
|
412
|
+
if (!this.isAtEnd() && this.peek().value.toLowerCase() === "global") {
|
|
413
|
+
this.advance();
|
|
414
|
+
isGlobalOp = true;
|
|
415
|
+
}
|
|
416
|
+
this.consume(lexer_1.TokenType.MODIFIER, `${action} session expected`);
|
|
417
|
+
payload.type = "session";
|
|
418
|
+
payload.isGlobal = isGlobalOp;
|
|
419
|
+
payload.name = this.consume(lexer_1.TokenType.STRING, "Session name expected").value;
|
|
420
|
+
if (action === "save" &&
|
|
421
|
+
!this.isAtEnd() &&
|
|
422
|
+
this.peek().value.toLowerCase() === "persist") {
|
|
423
|
+
this.advance();
|
|
424
|
+
payload.persist = true;
|
|
425
|
+
}
|
|
426
|
+
break;
|
|
427
|
+
case "expect":
|
|
428
|
+
const modToken = this.consume(lexer_1.TokenType.MODIFIER, "Modifier expected");
|
|
429
|
+
let mod = modToken.value.toLowerCase();
|
|
430
|
+
if (mod === "resbody")
|
|
431
|
+
mod = "resBody";
|
|
432
|
+
if (mod === "resheader")
|
|
433
|
+
mod = "resHeader";
|
|
434
|
+
payload.type = mod;
|
|
435
|
+
if (mod === "status") {
|
|
436
|
+
payload.statusCode = this.consume(lexer_1.TokenType.STRING, 'Status code expected (e.g., "200")').value;
|
|
437
|
+
}
|
|
438
|
+
else if (mod === "responsetime") {
|
|
439
|
+
const conditionToken = this.advance();
|
|
440
|
+
if (conditionToken.value.toLowerCase() !== "lessthan") {
|
|
441
|
+
throw new Error(`Line ${modToken.line}: Expected 'lessThan' after responseTime`);
|
|
442
|
+
}
|
|
443
|
+
payload.threshold = this.consume(lexer_1.TokenType.STRING, "Time threshold expected").value;
|
|
444
|
+
}
|
|
445
|
+
else if (mod === "resBody" ||
|
|
446
|
+
mod === "resHeader" ||
|
|
447
|
+
mod === "cookie") {
|
|
448
|
+
payload.jsonPath = this.consume(lexer_1.TokenType.STRING, "JSONPath or Key expected").value;
|
|
449
|
+
payload.assertions = [];
|
|
450
|
+
const unaryOperators = ["is.empty", "not.empty"];
|
|
451
|
+
const validConditions = [
|
|
452
|
+
"equals",
|
|
453
|
+
"contains",
|
|
454
|
+
"to.be",
|
|
455
|
+
"to.not.be",
|
|
456
|
+
"is",
|
|
457
|
+
"is.not",
|
|
458
|
+
"type.of",
|
|
459
|
+
"format",
|
|
460
|
+
"matches",
|
|
461
|
+
"lessthan",
|
|
462
|
+
"greaterthan",
|
|
463
|
+
"length",
|
|
464
|
+
"length.greaterthan",
|
|
465
|
+
"length.lessthan",
|
|
466
|
+
"includes",
|
|
467
|
+
"does.not.include",
|
|
468
|
+
"has.key",
|
|
469
|
+
"does.not.have.key",
|
|
470
|
+
...unaryOperators,
|
|
471
|
+
];
|
|
472
|
+
while (!this.isAtEnd()) {
|
|
473
|
+
const nextToken = this.peek();
|
|
474
|
+
if (nextToken.type !== lexer_1.TokenType.PREPOSITION ||
|
|
475
|
+
!validConditions.includes(nextToken.value.toLowerCase())) {
|
|
476
|
+
break;
|
|
477
|
+
}
|
|
478
|
+
const conditionToken = this.advance();
|
|
479
|
+
const condition = conditionToken.value.toLowerCase();
|
|
480
|
+
let value = "";
|
|
481
|
+
if (!unaryOperators.includes(condition)) {
|
|
482
|
+
value = this.consume(lexer_1.TokenType.STRING, `Expected value string after "${condition}"`).value;
|
|
483
|
+
}
|
|
484
|
+
payload.assertions.push({ condition, value });
|
|
485
|
+
}
|
|
486
|
+
if (payload.assertions.length === 0) {
|
|
487
|
+
throw new Error(`Line ${modToken.line}: Expected at least one assertion condition (e.g., 'equals', 'type.of') after JSONPath.`);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
else if (mod === "transcript") {
|
|
491
|
+
const containsMod = this.consume(lexer_1.TokenType.MODIFIER, 'Expected "contains"');
|
|
492
|
+
if (containsMod.value.toLowerCase() !== "contains") {
|
|
493
|
+
throw new Error(`Line ${modToken.line}: Expected 'contains' after transcript`);
|
|
494
|
+
}
|
|
495
|
+
payload.text = this.consume(lexer_1.TokenType.STRING, "Expected text value").value;
|
|
496
|
+
}
|
|
497
|
+
else if (mod === "visible" || mod === "text") {
|
|
498
|
+
if (!this.isAtEnd() &&
|
|
499
|
+
this.peek().type === lexer_1.TokenType.MODIFIER &&
|
|
500
|
+
this.peek().value.toLowerCase() === "contains") {
|
|
501
|
+
this.advance();
|
|
502
|
+
payload.fuzzy = true;
|
|
503
|
+
}
|
|
504
|
+
this.consumeOptionalElement(payload);
|
|
505
|
+
payload.selector = this.parseSmartSelectorPayload();
|
|
506
|
+
}
|
|
507
|
+
else if (mod === "value" || mod === "attribute") {
|
|
508
|
+
this.consumeOptionalElement(payload);
|
|
509
|
+
payload.selector = this.parseSmartSelectorPayload();
|
|
510
|
+
if (mod === "attribute") {
|
|
511
|
+
payload.attribute = this.consume(lexer_1.TokenType.STRING, "Attribute name expected").value;
|
|
512
|
+
}
|
|
513
|
+
if (!this.isAtEnd() &&
|
|
514
|
+
this.peek().type === lexer_1.TokenType.MODIFIER &&
|
|
515
|
+
this.peek().value.toLowerCase() === "contains") {
|
|
516
|
+
this.advance();
|
|
517
|
+
payload.fuzzy = true;
|
|
518
|
+
}
|
|
519
|
+
payload.text = this.consume(lexer_1.TokenType.STRING, "Expected value expected").value;
|
|
520
|
+
}
|
|
521
|
+
break;
|
|
522
|
+
default:
|
|
523
|
+
throw new Error(`Unknown command: ${action}`);
|
|
524
|
+
}
|
|
525
|
+
return {
|
|
526
|
+
id: `S${this.stepCounter++}`,
|
|
527
|
+
blockDescription: this.currentBlockDescription,
|
|
528
|
+
action,
|
|
529
|
+
payload,
|
|
530
|
+
line: commandToken.line,
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
parseApiModifiers(payload) {
|
|
534
|
+
while (!this.isAtEnd()) {
|
|
535
|
+
const nextToken = this.peek();
|
|
536
|
+
const nextVal = nextToken.value.toLowerCase();
|
|
537
|
+
if (nextToken.type === lexer_1.TokenType.PREPOSITION && nextVal === "with") {
|
|
538
|
+
this.advance();
|
|
539
|
+
const targetToken = this.advance();
|
|
540
|
+
const target = targetToken.value.toLowerCase();
|
|
541
|
+
if (target === "reqheader") {
|
|
542
|
+
const hKey = this.consume(lexer_1.TokenType.STRING, "Header key expected").value;
|
|
543
|
+
const hVal = this.consume(lexer_1.TokenType.STRING, "Header value expected").value;
|
|
544
|
+
payload.headers.push({ key: hKey, value: hVal });
|
|
545
|
+
}
|
|
546
|
+
else if (target === "reqbody") {
|
|
547
|
+
payload.body = this.consume(lexer_1.TokenType.STRING, "Body content expected").value;
|
|
548
|
+
}
|
|
549
|
+
else {
|
|
550
|
+
throw new Error(`Line ${targetToken.line}: Expected 'reqHeader' or 'reqBody' after 'with'`);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
else {
|
|
554
|
+
break;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
peek() {
|
|
559
|
+
return this.tokens[this.position];
|
|
560
|
+
}
|
|
561
|
+
advance() {
|
|
562
|
+
return this.tokens[this.position++];
|
|
563
|
+
}
|
|
564
|
+
isAtEnd() {
|
|
565
|
+
return this.peek().type === lexer_1.TokenType.EOF;
|
|
566
|
+
}
|
|
567
|
+
consume(type, msg) {
|
|
568
|
+
if (this.peek().type === type)
|
|
569
|
+
return this.advance();
|
|
570
|
+
throw new Error(`${msg} at line ${this.peek().line}`);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
exports.Parser = Parser;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TemplateResolver = void 0;
|
|
4
|
+
class TemplateResolver {
|
|
5
|
+
pattern = /\{\{(.+?)\}\}/g;
|
|
6
|
+
resolve(input, context) {
|
|
7
|
+
return input.replace(this.pattern, (match, path) => {
|
|
8
|
+
const value = this.getValueFromPath(path.trim(), context);
|
|
9
|
+
if (value === undefined) {
|
|
10
|
+
return match;
|
|
11
|
+
}
|
|
12
|
+
return String(value);
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
getValueFromPath(path, context) {
|
|
16
|
+
return path.split(".").reduce((acc, part) => acc && acc[part], context);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
exports.TemplateResolver = TemplateResolver;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Validator = void 0;
|
|
4
|
+
class Validator {
|
|
5
|
+
static validateStep(step) {
|
|
6
|
+
const { action, payload, line } = step;
|
|
7
|
+
switch (action) {
|
|
8
|
+
case "open":
|
|
9
|
+
case "navigate":
|
|
10
|
+
if (!payload.url) {
|
|
11
|
+
throw new Error(`[Line ${line}] Validation Error: 'OPEN' requires a URL string (e.g., OPEN "https://google.com").`);
|
|
12
|
+
}
|
|
13
|
+
break;
|
|
14
|
+
case "type":
|
|
15
|
+
if (!payload.selector || !payload.text) {
|
|
16
|
+
throw new Error(`[Line ${line}] Validation Error: 'TYPE' requires both a selector and text (e.g., TYPE "#email" "user@test.com").`);
|
|
17
|
+
}
|
|
18
|
+
break;
|
|
19
|
+
case "click":
|
|
20
|
+
if (!payload.selector) {
|
|
21
|
+
throw new Error(`[Line ${line}] Validation Error: 'CLICK' requires a selector.`);
|
|
22
|
+
}
|
|
23
|
+
break;
|
|
24
|
+
case "expect":
|
|
25
|
+
if (payload.type === "visible" && !payload.selector) {
|
|
26
|
+
throw new Error(`[Line ${line}] Validation Error: 'EXPECT VISIBLE' requires a selector.`);
|
|
27
|
+
}
|
|
28
|
+
break;
|
|
29
|
+
case "call":
|
|
30
|
+
if (!payload.name) {
|
|
31
|
+
throw new Error(`[Line ${line}] Validation Error: 'CALL' requires a plugin name.`);
|
|
32
|
+
}
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
exports.Validator = Validator;
|