@qualitas-id/mcp 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 +262 -0
- package/dist/constants.d.ts +18 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +18 -0
- package/dist/constants.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +82 -0
- package/dist/index.js.map +1 -0
- package/dist/schemas/common.d.ts +20 -0
- package/dist/schemas/common.d.ts.map +1 -0
- package/dist/schemas/common.js +10 -0
- package/dist/schemas/common.js.map +1 -0
- package/dist/schemas/flow.d.ts +358 -0
- package/dist/schemas/flow.d.ts.map +1 -0
- package/dist/schemas/flow.js +72 -0
- package/dist/schemas/flow.js.map +1 -0
- package/dist/schemas/project.d.ts +70 -0
- package/dist/schemas/project.d.ts.map +1 -0
- package/dist/schemas/project.js +26 -0
- package/dist/schemas/project.js.map +1 -0
- package/dist/schemas/run.d.ts +54 -0
- package/dist/schemas/run.d.ts.map +1 -0
- package/dist/schemas/run.js +20 -0
- package/dist/schemas/run.js.map +1 -0
- package/dist/schemas/variable.d.ts +91 -0
- package/dist/schemas/variable.d.ts.map +1 -0
- package/dist/schemas/variable.js +30 -0
- package/dist/schemas/variable.js.map +1 -0
- package/dist/services/api-client.d.ts +101 -0
- package/dist/services/api-client.d.ts.map +1 -0
- package/dist/services/api-client.js +184 -0
- package/dist/services/api-client.js.map +1 -0
- package/dist/services/flow-generator.d.ts +31 -0
- package/dist/services/flow-generator.d.ts.map +1 -0
- package/dist/services/flow-generator.js +638 -0
- package/dist/services/flow-generator.js.map +1 -0
- package/dist/shared-types.d.ts +579 -0
- package/dist/shared-types.d.ts.map +1 -0
- package/dist/shared-types.js +12 -0
- package/dist/shared-types.js.map +1 -0
- package/dist/tools/flows.d.ts +13 -0
- package/dist/tools/flows.d.ts.map +1 -0
- package/dist/tools/flows.js +458 -0
- package/dist/tools/flows.js.map +1 -0
- package/dist/tools/projects.d.ts +13 -0
- package/dist/tools/projects.d.ts.map +1 -0
- package/dist/tools/projects.js +381 -0
- package/dist/tools/projects.js.map +1 -0
- package/dist/tools/runs.d.ts +9 -0
- package/dist/tools/runs.d.ts.map +1 -0
- package/dist/tools/runs.js +342 -0
- package/dist/tools/runs.js.map +1 -0
- package/dist/tools/utils.d.ts +12 -0
- package/dist/tools/utils.d.ts.map +1 -0
- package/dist/tools/utils.js +144 -0
- package/dist/tools/utils.js.map +1 -0
- package/dist/tools/variables.d.ts +9 -0
- package/dist/tools/variables.d.ts.map +1 -0
- package/dist/tools/variables.js +316 -0
- package/dist/tools/variables.js.map +1 -0
- package/dist/types.d.ts +117 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +8 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/flow-layout.d.ts +34 -0
- package/dist/utils/flow-layout.d.ts.map +1 -0
- package/dist/utils/flow-layout.js +109 -0
- package/dist/utils/flow-layout.js.map +1 -0
- package/dist/utils/flow-validation.d.ts +74 -0
- package/dist/utils/flow-validation.d.ts.map +1 -0
- package/dist/utils/flow-validation.js +386 -0
- package/dist/utils/flow-validation.js.map +1 -0
- package/dist/utils/ocr.d.ts +25 -0
- package/dist/utils/ocr.d.ts.map +1 -0
- package/dist/utils/ocr.js +88 -0
- package/dist/utils/ocr.js.map +1 -0
- package/package.json +65 -0
- package/skills/qualitas.md +253 -0
|
@@ -0,0 +1,638 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Flow Generator Service
|
|
3
|
+
*
|
|
4
|
+
* Generates Qualitas flow graphs from natural language scenarios.
|
|
5
|
+
* Maps scenario steps to appropriate node types and builds the
|
|
6
|
+
* full FlowNode/FlowEdge/Variable structure ready for the API.
|
|
7
|
+
*/
|
|
8
|
+
// ===========================================
|
|
9
|
+
// NODE_REFERENCE - Comprehensive documentation of all 40 FlowNodeType values
|
|
10
|
+
// Embedded in the MCP tool description so the AI knows what nodes are available.
|
|
11
|
+
// ===========================================
|
|
12
|
+
export const NODE_REFERENCE = `## Available Flow Node Types (40 types)
|
|
13
|
+
|
|
14
|
+
Use these node types when constructing flows. Each node has a \`type\` and a \`data\` object with type-specific fields.
|
|
15
|
+
Fields marked **required** must be provided. All others are optional.
|
|
16
|
+
|
|
17
|
+
Variable references: Use \`{{project.VAR_NAME}}\` for project variables, \`{{flow.VAR_NAME}}\` for flow variables.
|
|
18
|
+
Selectors: Use CSS selectors (e.g., \`.btn-primary\`, \`#email-input\`, \`[data-testid="submit"]\`).
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
### Triggers
|
|
23
|
+
|
|
24
|
+
| Type | Description | Required Fields | Optional Fields |
|
|
25
|
+
|------|-------------|-----------------|-----------------|
|
|
26
|
+
| \`start\` | Entry point of the flow (always first node) | _(none)_ | \`label\`, \`description\`, \`timeout\` |
|
|
27
|
+
| \`schedule\` | Cron-based scheduled trigger | \`cron\` (string, e.g. "0 9 * * 1-5") | \`description\`, \`timeout\` |
|
|
28
|
+
|
|
29
|
+
### Navigation
|
|
30
|
+
|
|
31
|
+
| Type | Description | Required Fields | Optional Fields |
|
|
32
|
+
|------|-------------|-----------------|-----------------|
|
|
33
|
+
| \`navigate\` | Navigate to a URL | \`url\` (string) | \`waitUntil\` ("load" \\| "domcontentloaded" \\| "networkidle"), \`description\`, \`timeout\` |
|
|
34
|
+
| \`reload\` | Reload the current page | _(none)_ | \`waitUntil\`, \`description\`, \`timeout\` |
|
|
35
|
+
| \`goBack\` | Browser back navigation | _(none)_ | \`description\`, \`timeout\` |
|
|
36
|
+
| \`goForward\` | Browser forward navigation | _(none)_ | \`description\`, \`timeout\` |
|
|
37
|
+
|
|
38
|
+
### Mouse Actions
|
|
39
|
+
|
|
40
|
+
| Type | Description | Required Fields | Optional Fields |
|
|
41
|
+
|------|-------------|-----------------|-----------------|
|
|
42
|
+
| \`click\` | Click an element | \`selector\` (string) | \`button\` ("left" \\| "right" \\| "middle"), \`clickCount\`, \`position\` ({x,y}), \`description\`, \`timeout\` |
|
|
43
|
+
| \`dblclick\` | Double-click an element | \`selector\` | \`position\`, \`description\`, \`timeout\` |
|
|
44
|
+
| \`rightclick\` | Right-click an element | \`selector\` | \`position\`, \`description\`, \`timeout\` |
|
|
45
|
+
| \`hover\` | Hover over an element | \`selector\` | \`position\`, \`description\`, \`timeout\` |
|
|
46
|
+
| \`dragAndDrop\` | Drag from source to target | \`sourceSelector\`, \`targetSelector\` | \`description\`, \`timeout\` |
|
|
47
|
+
|
|
48
|
+
### Input
|
|
49
|
+
|
|
50
|
+
| Type | Description | Required Fields | Optional Fields |
|
|
51
|
+
|------|-------------|-----------------|-----------------|
|
|
52
|
+
| \`fill\` | Fill a text input (replaces content) | \`selector\`, \`text\` | \`description\`, \`timeout\` |
|
|
53
|
+
| \`type\` | Type text character by character | \`selector\`, \`text\` | \`delay\` (ms between keystrokes), \`description\`, \`timeout\` |
|
|
54
|
+
| \`clear\` | Clear an input field | \`selector\` | \`description\`, \`timeout\` |
|
|
55
|
+
| \`press\` | Press a keyboard key | \`key\` (e.g. "Enter", "Tab", "Escape", "ArrowDown") | \`selector\` (focus first), \`modifiers\` (array of "ctrl"/"alt"/"shift"/"meta"), \`description\`, \`timeout\` |
|
|
56
|
+
|
|
57
|
+
### Form
|
|
58
|
+
|
|
59
|
+
| Type | Description | Required Fields | Optional Fields |
|
|
60
|
+
|------|-------------|-----------------|-----------------|
|
|
61
|
+
| \`submit\` | Submit a form | \`selector\` | \`description\`, \`timeout\` |
|
|
62
|
+
| \`check\` | Check a checkbox | \`selector\` | \`description\`, \`timeout\` |
|
|
63
|
+
| \`uncheck\` | Uncheck a checkbox | \`selector\` | \`description\`, \`timeout\` |
|
|
64
|
+
| \`select\` | Select dropdown option | \`selector\`, \`value\` | \`byLabel\` (boolean - select by visible label), \`description\`, \`timeout\` |
|
|
65
|
+
| \`upload\` | Upload a file | \`selector\`, \`filePath\` | \`description\`, \`timeout\`. \`filePath\` can be: local path, URL (http/https), or base64 data URL |
|
|
66
|
+
|
|
67
|
+
### Focus / Scroll
|
|
68
|
+
|
|
69
|
+
| Type | Description | Required Fields | Optional Fields |
|
|
70
|
+
|------|-------------|-----------------|-----------------|
|
|
71
|
+
| \`focus\` | Focus an element | \`selector\` | \`description\`, \`timeout\` |
|
|
72
|
+
| \`blur\` | Remove focus from element | \`selector\` | \`description\`, \`timeout\` |
|
|
73
|
+
| \`scroll\` | Scroll the page or element | _(none)_ | \`selector\`, \`x\`, \`y\`, \`behavior\` ("auto" \\| "smooth"), \`direction\` ("up"/"down"/"left"/"right"), \`amount\`, \`description\`, \`timeout\` |
|
|
74
|
+
| \`scrollIntoView\` | Scroll element into view | \`selector\` | \`behavior\`, \`description\`, \`timeout\` |
|
|
75
|
+
|
|
76
|
+
### Wait / Sync
|
|
77
|
+
|
|
78
|
+
| Type | Description | Required Fields | Optional Fields |
|
|
79
|
+
|------|-------------|-----------------|-----------------|
|
|
80
|
+
| \`wait\` | Wait for a duration | \`duration\` (ms) | \`description\`, \`timeout\` |
|
|
81
|
+
| \`waitForElement\` | Wait for element state | \`selector\` | \`state\` ("visible" \\| "hidden" \\| "attached" \\| "detached"), \`description\`, \`timeout\` |
|
|
82
|
+
| \`waitForNavigation\` | Wait for page navigation | _(none)_ | \`url\` (URL pattern), \`description\`, \`timeout\` |
|
|
83
|
+
| \`waitForResponse\` | Wait for network response | \`urlPattern\` (string) | \`method\`, \`statusCode\`, \`description\`, \`timeout\` |
|
|
84
|
+
|
|
85
|
+
### Assertions
|
|
86
|
+
|
|
87
|
+
| Type | Description | Required Fields | Optional Fields |
|
|
88
|
+
|------|-------------|-----------------|-----------------|
|
|
89
|
+
| \`assertVisible\` | Assert element visibility | \`selector\` | \`state\` ("visible" \\| "hidden"), \`description\`, \`timeout\` |
|
|
90
|
+
| \`assertText\` | Assert element text content | \`selector\`, \`expectedText\` | \`exact\` (boolean), \`description\`, \`timeout\` |
|
|
91
|
+
| \`assertValue\` | Assert input value | \`selector\`, \`expectedValue\` | \`description\`, \`timeout\` |
|
|
92
|
+
| \`assertUrl\` | Assert current URL | \`expectedUrl\` | \`exact\` (boolean), \`description\`, \`timeout\` |
|
|
93
|
+
| \`assertTitle\` | Assert page title | \`expectedTitle\` | \`exact\` (boolean), \`description\`, \`timeout\` |
|
|
94
|
+
| \`assertVariable\` | Assert variable comparison | \`leftValue\`, \`operator\`, \`rightValue\` | \`description\`, \`timeout\`. Operators: "equals", "notEquals", "contains", "greaterThan", "lessThan" |
|
|
95
|
+
| \`assertTableCell\` | Assert table cell value | \`tableSelector\`, \`findColumnName\`, \`findOperator\`, \`findValue\`, \`assertColumnName\`, \`assertOperator\`, \`expectedValue\` | \`description\`, \`timeout\` |
|
|
96
|
+
| \`assertCount\` | Assert element count | \`selector\`, \`operator\`, \`expectedValue\` (number) | \`description\`, \`timeout\`. Operators: "equals", "greaterThan", "lessThan" |
|
|
97
|
+
|
|
98
|
+
### Utility
|
|
99
|
+
|
|
100
|
+
| Type | Description | Required Fields | Optional Fields |
|
|
101
|
+
|------|-------------|-----------------|-----------------|
|
|
102
|
+
| \`screenshot\` | Take a screenshot | _(none)_ | \`name\`, \`fullPage\` (boolean), \`description\`, \`timeout\` |
|
|
103
|
+
| \`extractFromElement\` | Extract data from element | \`selector\`, \`extractType\`, \`saveAs\` | \`attributeName\`, \`trim\`, \`toNumber\`, \`regex\`, \`replace\`, \`description\`, \`timeout\`. extractType: "text" \\| "value" \\| "attribute" \\| "count" |
|
|
104
|
+
| \`ifCondition\` | Conditional branching | \`saveResultAs\` | \`conditionType\` ("variable" \\| "element"), \`leftValue\`, \`operator\`, \`rightValue\`, \`selector\`, \`description\`, \`timeout\` |
|
|
105
|
+
| \`loop\` | Loop over items | \`loopVariableName\` | \`loopType\` ("list" \\| "elements"), \`dataSource\`, \`selector\`, \`description\`, \`timeout\` |
|
|
106
|
+
| \`executeScript\` | Run custom JavaScript | \`script\` (string) | \`description\`, \`timeout\` |
|
|
107
|
+
|
|
108
|
+
### Integration
|
|
109
|
+
|
|
110
|
+
| Type | Description | Required Fields | Optional Fields |
|
|
111
|
+
|------|-------------|-----------------|-----------------|
|
|
112
|
+
| \`apiCall\` | Make an HTTP request | \`method\`, \`url\` | \`headers\` (array of {key, value, enabled}), \`body\`, \`extractions\` (array of {id, variableName, type, expression}), \`description\`, \`timeout\` |
|
|
113
|
+
| \`generateEmail\` | Generate a test email | \`outputKey\` | \`emailName\`, \`retentionMinutes\`, \`provider\`, \`localPartPrefix\`, \`description\`, \`timeout\` |
|
|
114
|
+
| \`readEmail\` | Read from email inbox | \`inboxId\`, \`outputKey\` | \`generatedFrom\`, \`subjectContains\`, \`bodyContains\`, \`fromContains\`, \`timeoutMs\`, \`pollIntervalMs\`, \`description\` |
|
|
115
|
+
| \`extractEmail\` | Extract data from email | \`source\`, \`variableName\`, \`pattern\` | \`readSourceNodeId\`, \`sourceField\`, \`extractionMode\`, \`flags\`, \`matchIndex\`, \`selector\`, \`selectorTarget\`, \`attributeName\`, \`description\`, \`timeout\` |
|
|
116
|
+
|
|
117
|
+
### Reusable
|
|
118
|
+
|
|
119
|
+
| Type | Description | Required Fields | Optional Fields |
|
|
120
|
+
|------|-------------|-----------------|-----------------|
|
|
121
|
+
| \`callFragment\` | Call a reusable flow fragment | \`fragmentId\` | \`fragmentName\`, \`description\`, \`timeout\` |
|
|
122
|
+
`;
|
|
123
|
+
// ===========================================
|
|
124
|
+
// Scenario Parsing Helpers
|
|
125
|
+
// ===========================================
|
|
126
|
+
/** Map of keywords to node type detection */
|
|
127
|
+
const NAVIGATION_KEYWORDS = [
|
|
128
|
+
"navigate", "go to", "open", "visit", "browse to", "load", "url",
|
|
129
|
+
];
|
|
130
|
+
const CLICK_KEYWORDS = [
|
|
131
|
+
"click", "tap", "press button", "select button", "hit", "push",
|
|
132
|
+
];
|
|
133
|
+
const DBLCLICK_KEYWORDS = [
|
|
134
|
+
"double click", "double-click", "dblclick",
|
|
135
|
+
];
|
|
136
|
+
const RIGHTCLICK_KEYWORDS = [
|
|
137
|
+
"right click", "right-click", "rightclick",
|
|
138
|
+
];
|
|
139
|
+
const HOVER_KEYWORDS = [
|
|
140
|
+
"hover", "mouse over", "mouse-over", "mouseover",
|
|
141
|
+
];
|
|
142
|
+
const FILL_KEYWORDS = [
|
|
143
|
+
"fill", "enter", "input", "type into", "type in", "fill in", "fill out",
|
|
144
|
+
"write", "provide", "specify",
|
|
145
|
+
];
|
|
146
|
+
const SELECT_KEYWORDS = [
|
|
147
|
+
"select", "choose", "pick", "dropdown",
|
|
148
|
+
];
|
|
149
|
+
const CHECK_KEYWORDS = [
|
|
150
|
+
"check", "tick", "enable", "turn on",
|
|
151
|
+
];
|
|
152
|
+
const UNCHECK_KEYWORDS = [
|
|
153
|
+
"uncheck", "untick", "disable", "turn off",
|
|
154
|
+
];
|
|
155
|
+
const SUBMIT_KEYWORDS = [
|
|
156
|
+
"submit", "send form", "post form",
|
|
157
|
+
];
|
|
158
|
+
const ASSERT_VISIBLE_KEYWORDS = [
|
|
159
|
+
"verify", "assert", "check that", "should see", "should display",
|
|
160
|
+
"expect", "confirm", "validate", "visible", "appears",
|
|
161
|
+
];
|
|
162
|
+
const ASSERT_TEXT_KEYWORDS = [
|
|
163
|
+
"text should be", "text is", "shows", "displays", "contains text",
|
|
164
|
+
"should say", "should read", "message",
|
|
165
|
+
];
|
|
166
|
+
const WAIT_KEYWORDS = [
|
|
167
|
+
"wait", "pause", "delay", "sleep",
|
|
168
|
+
];
|
|
169
|
+
const SCREENSHOT_KEYWORDS = [
|
|
170
|
+
"screenshot", "capture", "take screenshot", "take a screenshot",
|
|
171
|
+
];
|
|
172
|
+
const SCROLL_KEYWORDS = [
|
|
173
|
+
"scroll", "scroll down", "scroll up", "scroll to",
|
|
174
|
+
];
|
|
175
|
+
const LOGIN_KEYWORDS = [
|
|
176
|
+
"log in", "login", "sign in", "signin", "authenticate",
|
|
177
|
+
];
|
|
178
|
+
const NAVIGATE_URL_PATTERN = /(?:navigate|go|open|visit|browse)\s+(?:to\s+)?(https?:\/\/[^\s,;]+)/i;
|
|
179
|
+
const URL_PATTERN = /(https?:\/\/[^\s,;]+)/i;
|
|
180
|
+
const SELECTOR_PATTERN = /[.#][\w-]+|\[data-testid=["'][^"']+["']\]|(?:button|input|form|a|div|span|p|h[1-6]|select|textarea|label)\[?[^\]]*\]?/i;
|
|
181
|
+
const VARIABLE_PATTERN = /\{\{(?:project|flow)\.[A-Z_]+\}\}/g;
|
|
182
|
+
const DURATION_PATTERN = /(\d+)\s*(?:ms|milliseconds?|seconds?|s)/i;
|
|
183
|
+
/**
|
|
184
|
+
* Extract a CSS selector from a step description.
|
|
185
|
+
* Tries explicit selectors first, then infers from common patterns.
|
|
186
|
+
*/
|
|
187
|
+
function extractSelector(step) {
|
|
188
|
+
// Look for explicit CSS selectors
|
|
189
|
+
const selectorMatch = step.match(SELECTOR_PATTERN);
|
|
190
|
+
if (selectorMatch)
|
|
191
|
+
return selectorMatch[0];
|
|
192
|
+
// Look for data-testid patterns
|
|
193
|
+
const testIdMatch = step.match(/(?:data-testid|testid|test-id)[\s=:]+["']?([\w-]+)["']?/i);
|
|
194
|
+
if (testIdMatch)
|
|
195
|
+
return `[data-testid="${testIdMatch[1]}"]`;
|
|
196
|
+
// Look for quoted element descriptions -> convert to common selectors
|
|
197
|
+
const quotedMatch = step.match(/["']([^"']+)["']/);
|
|
198
|
+
if (quotedMatch) {
|
|
199
|
+
const text = quotedMatch[1];
|
|
200
|
+
// Check for button-like text
|
|
201
|
+
if (/\b(btn|button|submit|cancel|save|delete|add|remove|close)\b/i.test(text)) {
|
|
202
|
+
return `button:has-text("${text}")`;
|
|
203
|
+
}
|
|
204
|
+
// Check for link-like text
|
|
205
|
+
if (/\b(link|href)\b/i.test(step) || step.includes("link")) {
|
|
206
|
+
return `a:has-text("${text}")`;
|
|
207
|
+
}
|
|
208
|
+
// Default to text selector
|
|
209
|
+
return `text="${text}"`;
|
|
210
|
+
}
|
|
211
|
+
// Infer from common UI element mentions
|
|
212
|
+
if (/\bemail\b/i.test(step))
|
|
213
|
+
return 'input[type="email"], input[name="email"], #email';
|
|
214
|
+
if (/\bpassword\b/i.test(step))
|
|
215
|
+
return 'input[type="password"], input[name="password"], #password';
|
|
216
|
+
if (/\busername\b/i.test(step))
|
|
217
|
+
return 'input[name="username"], #username';
|
|
218
|
+
if (/\bsearch\b/i.test(step))
|
|
219
|
+
return 'input[type="search"], input[name="search"], #search';
|
|
220
|
+
if (/\bform\b/i.test(step))
|
|
221
|
+
return "form";
|
|
222
|
+
if (/\bsubmit\b/i.test(step))
|
|
223
|
+
return 'button[type="submit"], button:has-text("Submit")';
|
|
224
|
+
// Default fallback
|
|
225
|
+
return "body";
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Extract a URL from a step description.
|
|
229
|
+
*/
|
|
230
|
+
function extractUrl(step, variables) {
|
|
231
|
+
const navMatch = step.match(NAVIGATE_URL_PATTERN);
|
|
232
|
+
if (navMatch)
|
|
233
|
+
return navMatch[1];
|
|
234
|
+
const urlMatch = step.match(URL_PATTERN);
|
|
235
|
+
if (urlMatch)
|
|
236
|
+
return urlMatch[1];
|
|
237
|
+
// Check for variable references
|
|
238
|
+
const varMatches = step.match(VARIABLE_PATTERN);
|
|
239
|
+
if (varMatches && varMatches.length > 0)
|
|
240
|
+
return varMatches[0];
|
|
241
|
+
// Common URL patterns
|
|
242
|
+
if (/\bhomepage\b|\bhome page\b/i.test(step))
|
|
243
|
+
return "{{project.BASE_URL}}";
|
|
244
|
+
if (/\bdashboard\b/i.test(step))
|
|
245
|
+
return "{{project.BASE_URL}}/dashboard";
|
|
246
|
+
if (/\blogin\b|\bsign in\b/i.test(step))
|
|
247
|
+
return "{{project.BASE_URL}}/login";
|
|
248
|
+
return "{{project.BASE_URL}}";
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Extract text to fill/type from a step description.
|
|
252
|
+
*/
|
|
253
|
+
function extractText(step) {
|
|
254
|
+
// Look for quoted text
|
|
255
|
+
const quotedMatch = step.match(/(?:with|text|value|content|input)\s*[:=]?\s*["']([^"']+)["']/i);
|
|
256
|
+
if (quotedMatch)
|
|
257
|
+
return quotedMatch[1];
|
|
258
|
+
// Look for email patterns
|
|
259
|
+
const emailMatch = step.match(/[\w.+-]+@[\w-]+\.[\w.]+/);
|
|
260
|
+
if (emailMatch)
|
|
261
|
+
return emailMatch[0];
|
|
262
|
+
// Look for variable references
|
|
263
|
+
const varMatch = step.match(/\{\{(?:project|flow)\.[A-Z_]+\}\}/);
|
|
264
|
+
if (varMatch)
|
|
265
|
+
return varMatch[0];
|
|
266
|
+
// Look for "with X" pattern
|
|
267
|
+
const withMatch = step.match(/with\s+(?:the\s+)?(?:text\s+)?["']?([^"']+?)["']?\s*(?:in|on|into|to|$)/i);
|
|
268
|
+
if (withMatch)
|
|
269
|
+
return withMatch[1].trim();
|
|
270
|
+
return "";
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Extract a duration in ms from a step description.
|
|
274
|
+
*/
|
|
275
|
+
function extractDuration(step) {
|
|
276
|
+
const match = step.match(DURATION_PATTERN);
|
|
277
|
+
if (!match)
|
|
278
|
+
return 1000;
|
|
279
|
+
const value = parseInt(match[1], 10);
|
|
280
|
+
const unit = match[0].toLowerCase();
|
|
281
|
+
if (unit.includes("ms") || unit.includes("millisecond"))
|
|
282
|
+
return value;
|
|
283
|
+
return value * 1000; // seconds -> ms
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Extract expected text from assertion steps.
|
|
287
|
+
*/
|
|
288
|
+
function extractExpectedText(step) {
|
|
289
|
+
const quotedMatch = step.match(/(?:should|expect|assert|verify|contains?|shows?|displays?)\s*[:=]?\s*["']([^"']+)["']/i);
|
|
290
|
+
if (quotedMatch)
|
|
291
|
+
return quotedMatch[1];
|
|
292
|
+
const textMatch = step.match(/text\s*[:=]?\s*["']([^"']+)["']/i);
|
|
293
|
+
if (textMatch)
|
|
294
|
+
return textMatch[1];
|
|
295
|
+
return "";
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* Classify a step and return the appropriate node type + data.
|
|
299
|
+
*/
|
|
300
|
+
function classifyStep(step, variables) {
|
|
301
|
+
const lower = step.toLowerCase().trim();
|
|
302
|
+
// --- Navigation ---
|
|
303
|
+
if (NAVIGATION_KEYWORDS.some((kw) => lower.includes(kw)) || URL_PATTERN.test(step)) {
|
|
304
|
+
return {
|
|
305
|
+
type: "navigate",
|
|
306
|
+
data: { url: extractUrl(step, variables), waitUntil: "load" },
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
// --- Login (composite pattern -> fill email, fill password, click submit) ---
|
|
310
|
+
// We handle this as a single navigate + fill + click in parseScenario
|
|
311
|
+
// --- Double Click ---
|
|
312
|
+
if (DBLCLICK_KEYWORDS.some((kw) => lower.includes(kw))) {
|
|
313
|
+
return { type: "dblclick", data: { selector: extractSelector(step) } };
|
|
314
|
+
}
|
|
315
|
+
// --- Right Click ---
|
|
316
|
+
if (RIGHTCLICK_KEYWORDS.some((kw) => lower.includes(kw))) {
|
|
317
|
+
return { type: "rightclick", data: { selector: extractSelector(step) } };
|
|
318
|
+
}
|
|
319
|
+
// --- Hover ---
|
|
320
|
+
if (HOVER_KEYWORDS.some((kw) => lower.includes(kw))) {
|
|
321
|
+
return { type: "hover", data: { selector: extractSelector(step) } };
|
|
322
|
+
}
|
|
323
|
+
// --- Submit ---
|
|
324
|
+
if (SUBMIT_KEYWORDS.some((kw) => lower.includes(kw))) {
|
|
325
|
+
return { type: "submit", data: { selector: extractSelector(step) } };
|
|
326
|
+
}
|
|
327
|
+
// --- Check/Uncheck ---
|
|
328
|
+
if (UNCHECK_KEYWORDS.some((kw) => lower.includes(kw))) {
|
|
329
|
+
return { type: "uncheck", data: { selector: extractSelector(step) } };
|
|
330
|
+
}
|
|
331
|
+
if (CHECK_KEYWORDS.some((kw) => lower.includes(kw))) {
|
|
332
|
+
return { type: "check", data: { selector: extractSelector(step) } };
|
|
333
|
+
}
|
|
334
|
+
// --- Select/Dropdown ---
|
|
335
|
+
if (SELECT_KEYWORDS.some((kw) => lower.includes(kw)) && (lower.includes("option") || lower.includes("dropdown") || lower.includes("from"))) {
|
|
336
|
+
const value = extractText(step);
|
|
337
|
+
return { type: "select", data: { selector: extractSelector(step), value, byLabel: true } };
|
|
338
|
+
}
|
|
339
|
+
// --- Fill/Input ---
|
|
340
|
+
if (FILL_KEYWORDS.some((kw) => lower.includes(kw))) {
|
|
341
|
+
const text = extractText(step);
|
|
342
|
+
return { type: "fill", data: { selector: extractSelector(step), text } };
|
|
343
|
+
}
|
|
344
|
+
// --- Click ---
|
|
345
|
+
if (CLICK_KEYWORDS.some((kw) => lower.includes(kw))) {
|
|
346
|
+
return { type: "click", data: { selector: extractSelector(step) } };
|
|
347
|
+
}
|
|
348
|
+
// --- Wait ---
|
|
349
|
+
if (WAIT_KEYWORDS.some((kw) => lower.includes(kw))) {
|
|
350
|
+
if (lower.includes("element") || lower.includes("appear") || lower.includes("visible")) {
|
|
351
|
+
return { type: "waitForElement", data: { selector: extractSelector(step), state: "visible" } };
|
|
352
|
+
}
|
|
353
|
+
if (lower.includes("navigation") || lower.includes("page load") || lower.includes("redirect")) {
|
|
354
|
+
return { type: "waitForNavigation", data: {} };
|
|
355
|
+
}
|
|
356
|
+
if (lower.includes("response") || lower.includes("api") || lower.includes("request")) {
|
|
357
|
+
return { type: "waitForResponse", data: { urlPattern: extractUrl(step, variables) || "**" } };
|
|
358
|
+
}
|
|
359
|
+
return { type: "wait", data: { duration: extractDuration(step) } };
|
|
360
|
+
}
|
|
361
|
+
// --- Screenshot ---
|
|
362
|
+
if (SCREENSHOT_KEYWORDS.some((kw) => lower.includes(kw))) {
|
|
363
|
+
const nameMatch = step.match(/(?:named?|called?)\s*["']([^"']+)["']/i);
|
|
364
|
+
return { type: "screenshot", data: { name: nameMatch?.[1], fullPage: lower.includes("full") } };
|
|
365
|
+
}
|
|
366
|
+
// --- Scroll ---
|
|
367
|
+
if (SCROLL_KEYWORDS.some((kw) => lower.includes(kw))) {
|
|
368
|
+
if (lower.includes("to") || lower.includes("into view") || lower.includes("element")) {
|
|
369
|
+
return { type: "scrollIntoView", data: { selector: extractSelector(step), behavior: "smooth" } };
|
|
370
|
+
}
|
|
371
|
+
const direction = lower.includes("up") ? "up" : lower.includes("left") ? "left" : lower.includes("right") ? "right" : "down";
|
|
372
|
+
return { type: "scroll", data: { direction, amount: 500 } };
|
|
373
|
+
}
|
|
374
|
+
// --- Assertions (after fill/click, so more specific matches take priority) ---
|
|
375
|
+
if (ASSERT_VISIBLE_KEYWORDS.some((kw) => lower.includes(kw))) {
|
|
376
|
+
// Check if it's a text assertion
|
|
377
|
+
const expectedText = extractExpectedText(step);
|
|
378
|
+
if (expectedText && (lower.includes("text") || lower.includes("shows") || lower.includes("displays") || lower.includes("contains") || lower.includes("says") || lower.includes("reads"))) {
|
|
379
|
+
return {
|
|
380
|
+
type: "assertText",
|
|
381
|
+
data: { selector: extractSelector(step), expectedText, exact: false },
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
// Check if it's a URL assertion
|
|
385
|
+
if (lower.includes("url") || lower.includes("redirected") || lower.includes("page is")) {
|
|
386
|
+
return { type: "assertUrl", data: { expectedUrl: extractUrl(step, variables) } };
|
|
387
|
+
}
|
|
388
|
+
// Check if it's a title assertion
|
|
389
|
+
if (lower.includes("title")) {
|
|
390
|
+
return { type: "assertTitle", data: { expectedTitle: expectedText || "" } };
|
|
391
|
+
}
|
|
392
|
+
// Default to visible assertion
|
|
393
|
+
return { type: "assertVisible", data: { selector: extractSelector(step), state: "visible" } };
|
|
394
|
+
}
|
|
395
|
+
// --- Upload ---
|
|
396
|
+
if (lower.includes("upload")) {
|
|
397
|
+
const fileMatch = step.match(/(?:file|path)\s*[:=]?\s*["']([^"']+)["']/i);
|
|
398
|
+
return { type: "upload", data: { selector: extractSelector(step), filePath: fileMatch?.[1] || "/tmp/upload.txt" } };
|
|
399
|
+
}
|
|
400
|
+
// --- Focus ---
|
|
401
|
+
if (lower.includes("focus")) {
|
|
402
|
+
return { type: "focus", data: { selector: extractSelector(step) } };
|
|
403
|
+
}
|
|
404
|
+
// --- Default: assume it's a click on something ---
|
|
405
|
+
return { type: "click", data: { selector: extractSelector(step) } };
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Split a scenario description into discrete steps.
|
|
409
|
+
*/
|
|
410
|
+
function parseScenarioSteps(scenario) {
|
|
411
|
+
// Normalize line endings and clean up
|
|
412
|
+
const normalized = scenario
|
|
413
|
+
.replace(/\r\n/g, "\n")
|
|
414
|
+
.replace(/\t/g, " ")
|
|
415
|
+
.trim();
|
|
416
|
+
const steps = [];
|
|
417
|
+
// Try splitting by numbered steps: "1. ", "2. ", etc.
|
|
418
|
+
const numberedSteps = normalized.split(/\n\s*\d+[.)]\s*/);
|
|
419
|
+
if (numberedSteps.length > 1) {
|
|
420
|
+
for (const s of numberedSteps) {
|
|
421
|
+
const trimmed = s.trim();
|
|
422
|
+
if (trimmed)
|
|
423
|
+
steps.push(trimmed);
|
|
424
|
+
}
|
|
425
|
+
return steps;
|
|
426
|
+
}
|
|
427
|
+
// Try splitting by bullet points: "- ", "* ", "• "
|
|
428
|
+
const bulletSteps = normalized.split(/\n\s*[-*•]\s+/);
|
|
429
|
+
if (bulletSteps.length > 1) {
|
|
430
|
+
for (const s of bulletSteps) {
|
|
431
|
+
const trimmed = s.trim();
|
|
432
|
+
if (trimmed)
|
|
433
|
+
steps.push(trimmed);
|
|
434
|
+
}
|
|
435
|
+
return steps;
|
|
436
|
+
}
|
|
437
|
+
// Try splitting by newlines
|
|
438
|
+
const lineSteps = normalized.split(/\n+/);
|
|
439
|
+
if (lineSteps.length > 1) {
|
|
440
|
+
for (const s of lineSteps) {
|
|
441
|
+
const trimmed = s.trim();
|
|
442
|
+
if (trimmed)
|
|
443
|
+
steps.push(trimmed);
|
|
444
|
+
}
|
|
445
|
+
return steps;
|
|
446
|
+
}
|
|
447
|
+
// Try splitting by "and then", "then", ". " connectors
|
|
448
|
+
const connectorSteps = normalized.split(/\s*(?:,\s*then|;\s*then|\.\s*then|,\s*and then|\.\s*And|\. Then|,\s*then|\.\s*Next|\. After|\. Finally)\s*/i);
|
|
449
|
+
if (connectorSteps.length > 1) {
|
|
450
|
+
for (const s of connectorSteps) {
|
|
451
|
+
const trimmed = s.trim();
|
|
452
|
+
if (trimmed)
|
|
453
|
+
steps.push(trimmed);
|
|
454
|
+
}
|
|
455
|
+
return steps;
|
|
456
|
+
}
|
|
457
|
+
// Fall back to sentence splitting by period
|
|
458
|
+
const sentences = normalized.split(/\.\s+/);
|
|
459
|
+
for (const s of sentences) {
|
|
460
|
+
const trimmed = s.trim();
|
|
461
|
+
if (trimmed)
|
|
462
|
+
steps.push(trimmed.endsWith(".") ? trimmed : trimmed);
|
|
463
|
+
}
|
|
464
|
+
return steps;
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Detect if a step describes a login pattern (composite action).
|
|
468
|
+
* Returns sub-steps if it's a login, or null otherwise.
|
|
469
|
+
*/
|
|
470
|
+
function detectLoginPattern(step) {
|
|
471
|
+
const lower = step.toLowerCase();
|
|
472
|
+
const isLogin = LOGIN_KEYWORDS.some((kw) => lower.includes(kw));
|
|
473
|
+
if (!isLogin)
|
|
474
|
+
return null;
|
|
475
|
+
const subSteps = [];
|
|
476
|
+
// Extract email/username
|
|
477
|
+
const emailMatch = step.match(/[\w.+-]+@[\w-]+\.[\w.]+/);
|
|
478
|
+
const usernameMatch = step.match(/(?:user\s*name|username|email)\s*[:=]?\s*["']?([^"',\s]+)["']?/i);
|
|
479
|
+
// Extract password
|
|
480
|
+
const passwordMatch = step.match(/(?:password|pwd|pass)\s*[:=]?\s*["']?([^"',\s]+)["']?/i);
|
|
481
|
+
// Determine login page URL
|
|
482
|
+
const urlMatch = step.match(URL_PATTERN);
|
|
483
|
+
if (urlMatch) {
|
|
484
|
+
subSteps.push(`Navigate to ${urlMatch[1]}`);
|
|
485
|
+
}
|
|
486
|
+
else {
|
|
487
|
+
subSteps.push("Navigate to login page");
|
|
488
|
+
}
|
|
489
|
+
const identifier = emailMatch?.[0] || usernameMatch?.[1] || "{{flow.USERNAME}}";
|
|
490
|
+
const password = passwordMatch?.[1] || "{{flow.PASSWORD}}";
|
|
491
|
+
subSteps.push(`Fill email/username field with "${identifier}"`);
|
|
492
|
+
subSteps.push(`Fill password field with "${password}"`);
|
|
493
|
+
subSteps.push("Click the login/submit button");
|
|
494
|
+
return subSteps;
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* Detect if a step describes a form-fill pattern (composite action).
|
|
498
|
+
*/
|
|
499
|
+
function detectFormFillPattern(step) {
|
|
500
|
+
const lower = step.toLowerCase();
|
|
501
|
+
if (!lower.includes("form") && !lower.includes("fill out") && !lower.includes("complete"))
|
|
502
|
+
return null;
|
|
503
|
+
// Extract field:value pairs
|
|
504
|
+
const fieldPattern = /(\w+)\s*[:=]\s*["']([^"']+)["']/gi;
|
|
505
|
+
const fields = [];
|
|
506
|
+
let match;
|
|
507
|
+
while ((match = fieldPattern.exec(step)) !== null) {
|
|
508
|
+
fields.push({ name: match[1], value: match[2] });
|
|
509
|
+
}
|
|
510
|
+
if (fields.length === 0)
|
|
511
|
+
return null;
|
|
512
|
+
return fields.map((f) => `Fill ${f.name} field with "${f.value}"`);
|
|
513
|
+
}
|
|
514
|
+
// ===========================================
|
|
515
|
+
// Main Generator
|
|
516
|
+
// ===========================================
|
|
517
|
+
/**
|
|
518
|
+
* Generate a complete flow graph from a natural language scenario.
|
|
519
|
+
*
|
|
520
|
+
* @param params.scenario - Natural language description of the test steps
|
|
521
|
+
* @param params.name - Optional flow name (auto-generated from scenario if not provided)
|
|
522
|
+
* @param params.environment - Target environment (dev/staging/prod)
|
|
523
|
+
* @returns Generated flow with nodes, edges, and variables
|
|
524
|
+
*/
|
|
525
|
+
export function generateFlowFromScenario(params) {
|
|
526
|
+
const { scenario, name, environment = "dev" } = params;
|
|
527
|
+
// Parse scenario into steps
|
|
528
|
+
let rawSteps = parseScenarioSteps(scenario);
|
|
529
|
+
// Expand composite patterns (login, form fill)
|
|
530
|
+
const expandedSteps = [];
|
|
531
|
+
for (const step of rawSteps) {
|
|
532
|
+
const loginSteps = detectLoginPattern(step);
|
|
533
|
+
if (loginSteps) {
|
|
534
|
+
expandedSteps.push(...loginSteps);
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
const formSteps = detectFormFillPattern(step);
|
|
538
|
+
if (formSteps) {
|
|
539
|
+
expandedSteps.push(...formSteps);
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
542
|
+
expandedSteps.push(step);
|
|
543
|
+
}
|
|
544
|
+
// Extract variables referenced in the scenario
|
|
545
|
+
const variables = [];
|
|
546
|
+
const varSet = new Set();
|
|
547
|
+
const allVarMatches = scenario.match(VARIABLE_PATTERN);
|
|
548
|
+
if (allVarMatches) {
|
|
549
|
+
for (const v of allVarMatches) {
|
|
550
|
+
const varName = v.replace(/\{\{(?:project|flow)\./, "").replace(/\}\}/, "");
|
|
551
|
+
if (!varSet.has(varName)) {
|
|
552
|
+
varSet.add(varName);
|
|
553
|
+
variables.push({
|
|
554
|
+
id: `var_${variables.length + 1}`,
|
|
555
|
+
name: varName,
|
|
556
|
+
scope: v.startsWith("{{project.") ? "project" : "flow",
|
|
557
|
+
values: { dev: "", staging: "", prod: "" },
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
// Auto-detect common variables that might be needed
|
|
563
|
+
const lowerScenario = scenario.toLowerCase();
|
|
564
|
+
if ((lowerScenario.includes("email") || lowerScenario.includes("login") || lowerScenario.includes("sign in")) &&
|
|
565
|
+
!varSet.has("USERNAME") && !varSet.has("EMAIL")) {
|
|
566
|
+
variables.push({
|
|
567
|
+
id: `var_${variables.length + 1}`,
|
|
568
|
+
name: "USERNAME",
|
|
569
|
+
scope: "flow",
|
|
570
|
+
values: { dev: "", staging: "", prod: "" },
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
if ((lowerScenario.includes("password") || lowerScenario.includes("login") || lowerScenario.includes("sign in")) &&
|
|
574
|
+
!varSet.has("PASSWORD")) {
|
|
575
|
+
variables.push({
|
|
576
|
+
id: `var_${variables.length + 1}`,
|
|
577
|
+
name: "PASSWORD",
|
|
578
|
+
scope: "flow",
|
|
579
|
+
values: { dev: "", staging: "", prod: "" },
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
// Build nodes
|
|
583
|
+
const nodes = [];
|
|
584
|
+
const edges = [];
|
|
585
|
+
const X_CENTER = 250;
|
|
586
|
+
const Y_SPACING = 100;
|
|
587
|
+
// Always start with a start node
|
|
588
|
+
nodes.push({
|
|
589
|
+
id: "node_1",
|
|
590
|
+
type: "start",
|
|
591
|
+
position: { x: X_CENTER, y: 0 },
|
|
592
|
+
data: { label: name || "Generated Flow" },
|
|
593
|
+
});
|
|
594
|
+
// Generate nodes for each step
|
|
595
|
+
for (let i = 0; i < expandedSteps.length; i++) {
|
|
596
|
+
const step = expandedSteps[i];
|
|
597
|
+
const nodeId = `node_${i + 2}`; // +2 because start is node_1
|
|
598
|
+
const { type, data } = classifyStep(step, variables);
|
|
599
|
+
nodes.push({
|
|
600
|
+
id: nodeId,
|
|
601
|
+
type,
|
|
602
|
+
position: { x: X_CENTER, y: (i + 1) * Y_SPACING },
|
|
603
|
+
data: { ...data, description: step },
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
// Generate sequential edges
|
|
607
|
+
for (let i = 0; i < nodes.length - 1; i++) {
|
|
608
|
+
edges.push({
|
|
609
|
+
id: `edge_${i + 1}`,
|
|
610
|
+
source: nodes[i].id,
|
|
611
|
+
target: nodes[i + 1].id,
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
// Auto-generate flow name from scenario if not provided
|
|
615
|
+
const flowName = name || generateFlowName(scenario);
|
|
616
|
+
return {
|
|
617
|
+
name: flowName,
|
|
618
|
+
nodes,
|
|
619
|
+
edges,
|
|
620
|
+
variables,
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* Generate a concise flow name from the scenario text.
|
|
625
|
+
*/
|
|
626
|
+
function generateFlowName(scenario) {
|
|
627
|
+
// Take first ~60 chars and clean up
|
|
628
|
+
let name = scenario
|
|
629
|
+
.replace(/\n/g, " ")
|
|
630
|
+
.replace(/\s+/g, " ")
|
|
631
|
+
.trim();
|
|
632
|
+
if (name.length > 60) {
|
|
633
|
+
name = name.substring(0, 57) + "...";
|
|
634
|
+
}
|
|
635
|
+
// Capitalize first letter
|
|
636
|
+
return name.charAt(0).toUpperCase() + name.slice(1);
|
|
637
|
+
}
|
|
638
|
+
//# sourceMappingURL=flow-generator.js.map
|