@flash-ai-team/flash-test-framework 0.0.1

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.
@@ -0,0 +1,276 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.AIWeb = void 0;
16
+ const Keyword_1 = require("../core/Keyword");
17
+ const WebUI_1 = require("./WebUI");
18
+ const ObjectRepository_1 = require("../core/ObjectRepository");
19
+ const fs_1 = __importDefault(require("fs"));
20
+ const path_1 = __importDefault(require("path"));
21
+ const openai_1 = __importDefault(require("openai"));
22
+ /**
23
+ * AIWeb: Uses LLM to locate elements based on natural language descriptions.
24
+ */
25
+ class AIWeb {
26
+ static get page() {
27
+ return Keyword_1.KeywordContext.page;
28
+ }
29
+ static get config() {
30
+ try {
31
+ const configPath = path_1.default.resolve(process.cwd(), 'ai.config.json');
32
+ if (fs_1.default.existsSync(configPath)) {
33
+ return JSON.parse(fs_1.default.readFileSync(configPath, 'utf-8'));
34
+ }
35
+ }
36
+ catch (e) {
37
+ console.error('Failed to load ai.config.json', e);
38
+ }
39
+ return {};
40
+ }
41
+ static async getOpenAIClient() {
42
+ const config = this.config;
43
+ if (!config.enabled || !config.apiKey || config.apiKey === 'YOUR_OPENAI_API_KEY') {
44
+ console.warn('AI functionality is disabled or API key is missing in ai.config.json');
45
+ return null;
46
+ }
47
+ return new openai_1.default({ apiKey: config.apiKey });
48
+ }
49
+ /**
50
+ * Finds a locator using AI based on a natural language description.
51
+ */
52
+ static async findElement(description) {
53
+ const client = await this.getOpenAIClient();
54
+ if (!client) {
55
+ throw new Error('AI is not configured. Please check ai.config.json.');
56
+ }
57
+ // 1. Get simplified DOM snapshot
58
+ // We strip non-interactive elements to save tokens and reduce noise
59
+ const snapshot = await this.page.evaluate(() => {
60
+ function getSimplifiedDOM(node) {
61
+ const relevantTags = ['BUTTON', 'INPUT', 'A', 'SELECT', 'TEXTAREA', 'LABEL', 'DIV', 'SPAN', 'IMG', 'H1', 'H2', 'H3', 'P'];
62
+ if (!relevantTags.includes(node.tagName))
63
+ return null;
64
+ const interactable = node.tagName === 'A' || node.tagName === 'BUTTON' || node.tagName === 'INPUT' ||
65
+ node.tagName === 'SELECT' || node.tagName === 'TEXTAREA' ||
66
+ node.hasAttribute('onclick') || node.getAttribute('role') === 'button';
67
+ // Skip invisible elements (basic check)
68
+ const rect = node.getBoundingClientRect();
69
+ if (rect.width === 0 || rect.height === 0)
70
+ return null;
71
+ // Generate a unique selector path
72
+ // This is a naive implementation; for production, use a robust unique selector generator
73
+ let selector = node.tagName.toLowerCase();
74
+ if (node.id)
75
+ selector += `#${node.id}`;
76
+ else if (node.className)
77
+ selector += `.${node.className.split(' ').join('.')}`;
78
+ // If it's deeper, we might need a path, but let's try strict attributes first
79
+ const attributes = {};
80
+ if (node.id)
81
+ attributes.id = node.id;
82
+ if (node.getAttribute('name'))
83
+ attributes.name = node.getAttribute('name');
84
+ if (node.getAttribute('placeholder'))
85
+ attributes.placeholder = node.getAttribute('placeholder');
86
+ if (node.getAttribute('aria-label'))
87
+ attributes['aria-label'] = node.getAttribute('aria-label');
88
+ if (node.textContent)
89
+ attributes.text = node.textContent.trim().substring(0, 50);
90
+ return {
91
+ tag: node.tagName,
92
+ ...attributes,
93
+ // We need a way to target this back.
94
+ // Let's assign a temporary data-ai-id if it doesn't have a good selector?
95
+ // Or just trust the attributes.
96
+ // Let's stick with best-effort selector generation on the client side validation?
97
+ // Actually, let's inject a data-ai-id to be safe.
98
+ };
99
+ }
100
+ // Inject temporary IDs for robust targeting
101
+ let counter = 0;
102
+ const elements = [];
103
+ document.querySelectorAll('*').forEach(el => {
104
+ const simple = getSimplifiedDOM(el);
105
+ if (simple) {
106
+ const aiId = `ai-${counter++}`;
107
+ el.setAttribute('data-ai-id', aiId);
108
+ simple['data-ai-id'] = aiId;
109
+ elements.push(simple);
110
+ }
111
+ });
112
+ return elements;
113
+ });
114
+ // 2. Query LLM
115
+ const prompt = `
116
+ You are a test automation assistant.
117
+ I will provide a simplified JSON list of DOM elements.
118
+ Find the element that best matches this description: "${description}".
119
+
120
+ Return ONLY the 'data-ai-id' of the best matching element.
121
+ If no match found, return "NOT_FOUND".
122
+
123
+ Elements:
124
+ ${JSON.stringify(snapshot).substring(0, 15000)} // Truncate to avoid context limit
125
+ `;
126
+ const completion = await client.chat.completions.create({
127
+ messages: [{ role: 'user', content: prompt }],
128
+ model: this.config.model || 'gpt-4o',
129
+ });
130
+ const aiId = completion.choices[0].message.content?.trim();
131
+ if (!aiId || aiId === 'NOT_FOUND') {
132
+ throw new Error(`AI could not locate element for description: "${description}"`);
133
+ }
134
+ // 3. Return Locator
135
+ // We clean up the attribute afterwards? Or just leave it for debug?
136
+ // Let's leave it for now.
137
+ return this.page.locator(`[data-ai-id="${aiId}"]`);
138
+ }
139
+ static async click(description) {
140
+ const locator = await this.findElement(description);
141
+ await locator.click();
142
+ }
143
+ static async setText(description, text) {
144
+ const locator = await this.findElement(description);
145
+ await locator.fill(text);
146
+ }
147
+ static async verifyText(description, expectedText) {
148
+ const locator = await this.findElement(description);
149
+ await locator.waitFor();
150
+ const text = await locator.innerText();
151
+ if (!text.includes(expectedText)) {
152
+ throw new Error(`Expected text "${expectedText}" not found in element "${description}". Found: "${text}"`);
153
+ }
154
+ }
155
+ /**
156
+ * Executes manual test steps by converting them to WebUI keyword calls using AI.
157
+ * @param steps - Natural language test steps (one per line or paragraph).
158
+ */
159
+ static async executeManualSteps(steps) {
160
+ // Split steps by newlines and filter empty lines
161
+ const stepLines = steps.split('\n').map(s => s.trim()).filter(s => s.length > 0);
162
+ let lastContextText = "";
163
+ for (const line of stepLines) {
164
+ // Remove numbering (e.g., "1. ")
165
+ const cleanLine = line.replace(/^\d+\.\s*/, '').trim();
166
+ console.log(`Processing step: "${cleanLine}"`);
167
+ // 1. Navigate to "url"
168
+ const navigateMatch = cleanLine.match(/^Navigate to "([^"]+)"$/i);
169
+ if (navigateMatch) {
170
+ await WebUI_1.Web.navigateToUrl(navigateMatch[1]);
171
+ continue;
172
+ }
173
+ // 2. Enter "text" into ...
174
+ const enterMatch = cleanLine.match(/^Enter "([^"]+)" into (?:the )?(.+)$/i);
175
+ if (enterMatch) {
176
+ const text = enterMatch[1];
177
+ let targetDesc = enterMatch[2];
178
+ lastContextText = text;
179
+ // Clean up target description
180
+ targetDesc = targetDesc.replace(/ field$/i, '').replace(/ input$/i, '').trim();
181
+ const selector = `internal:label="${targetDesc}"`;
182
+ await WebUI_1.Web.setText((0, ObjectRepository_1.el)(selector, targetDesc), text);
183
+ continue;
184
+ }
185
+ // Search "text"
186
+ const searchMatch = cleanLine.match(/^Search "([^"]+)"$/i);
187
+ if (searchMatch) {
188
+ const text = searchMatch[1];
189
+ lastContextText = text;
190
+ await WebUI_1.Web.search(text);
191
+ continue;
192
+ }
193
+ // 3. Click ...
194
+ const quotedClickRound = cleanLine.match(/^Click (?:on )?(?:the )?"([^"]+)"/i);
195
+ const clickMatch = cleanLine.match(/^Click (?:on )?(?:the )?(.+)$/i);
196
+ if (quotedClickRound || clickMatch) {
197
+ let targetDesc = quotedClickRound ? quotedClickRound[1] : clickMatch[1];
198
+ // Existing cleanup (irrelevant for quoted match usually, but good for safety)
199
+ targetDesc = targetDesc.replace(/ button$/i, '').replace(/ link$/i, '').replace(/ marker$/i, '').replace(/ image$/i, '').trim();
200
+ if ((targetDesc === "" || targetDesc.toLowerCase() === "marker") && lastContextText) {
201
+ targetDesc = lastContextText;
202
+ }
203
+ // Check if image exists for targetDesc
204
+ // Assume assets folder is tests/assets
205
+ const imageExtensions = ['png', 'jpg', 'jpeg'];
206
+ let imagePath = null;
207
+ for (const ext of imageExtensions) {
208
+ const checkPath = path_1.default.resolve(process.cwd(), 'tests/assets', `${targetDesc}.${ext}`);
209
+ if (fs_1.default.existsSync(checkPath)) {
210
+ imagePath = checkPath;
211
+ break;
212
+ }
213
+ }
214
+ if (imagePath) {
215
+ await WebUI_1.Web.clickImage(imagePath);
216
+ continue;
217
+ }
218
+ // For buttons, use button role if possible, or similar text
219
+ // Fallback to simple text selector to avoid invalid selector errors
220
+ // Use nth=0 to avoid strict mode violations if multiple elements match (common in search results)
221
+ const selector = `text="${targetDesc}" >> nth=0`;
222
+ await WebUI_1.Web.click((0, ObjectRepository_1.el)(selector, targetDesc));
223
+ continue;
224
+ }
225
+ // 4. Press ...
226
+ const pressMatch = cleanLine.match(/^Press (?:key )?"?([^"]+)"?$/i);
227
+ if (pressMatch) {
228
+ const key = pressMatch[1];
229
+ await WebUI_1.Web.pressKey(key);
230
+ continue;
231
+ }
232
+ // 5. Scroll to ...
233
+ const scrollMatch = cleanLine.match(/^Scroll to (?:the )?(.+)$/i);
234
+ if (scrollMatch) {
235
+ let targetDesc = scrollMatch[1];
236
+ targetDesc = targetDesc.replace(/ button$/i, '').replace(/ link$/i, '').replace(/ marker$/i, '').trim();
237
+ const selector = `text="${targetDesc}" >> nth=0`;
238
+ await WebUI_1.Web.scrollToElement((0, ObjectRepository_1.el)(selector, targetDesc));
239
+ continue;
240
+ }
241
+ // 6. Verify that the text "text" is present
242
+ const verifyTextMatch = cleanLine.match(/^Verify that the text "([^"]+)" is present$/i);
243
+ if (verifyTextMatch) {
244
+ const expectedText = verifyTextMatch[1];
245
+ await WebUI_1.Web.verifyElementPresent((0, ObjectRepository_1.el)(`text="${expectedText}"`, `Text '${expectedText}'`));
246
+ continue;
247
+ }
248
+ console.warn(`Could not parse step: "${cleanLine}"`);
249
+ }
250
+ }
251
+ }
252
+ exports.AIWeb = AIWeb;
253
+ __decorate([
254
+ (0, Keyword_1.Keyword)("AI Click"),
255
+ __metadata("design:type", Function),
256
+ __metadata("design:paramtypes", [String]),
257
+ __metadata("design:returntype", Promise)
258
+ ], AIWeb, "click", null);
259
+ __decorate([
260
+ (0, Keyword_1.Keyword)("AI Set Text"),
261
+ __metadata("design:type", Function),
262
+ __metadata("design:paramtypes", [String, String]),
263
+ __metadata("design:returntype", Promise)
264
+ ], AIWeb, "setText", null);
265
+ __decorate([
266
+ (0, Keyword_1.Keyword)("AI Verify Text"),
267
+ __metadata("design:type", Function),
268
+ __metadata("design:paramtypes", [String, String]),
269
+ __metadata("design:returntype", Promise)
270
+ ], AIWeb, "verifyText", null);
271
+ __decorate([
272
+ (0, Keyword_1.Keyword)("Execute Manual Steps"),
273
+ __metadata("design:type", Function),
274
+ __metadata("design:paramtypes", [String]),
275
+ __metadata("design:returntype", Promise)
276
+ ], AIWeb, "executeManualSteps", null);
@@ -0,0 +1,166 @@
1
+ import { TestObject } from '../core/ObjectRepository';
2
+ export declare class Web {
3
+ private static get page();
4
+ private static getLocator;
5
+ /**
6
+ * Navigates to the specified URL.
7
+ * @param url - The URL to navigate to.
8
+ */
9
+ static navigateToUrl(url: string): Promise<void>;
10
+ /**
11
+ * Clicks on the specified element.
12
+ * @param to - The TestObject representing the element.
13
+ */
14
+ static click(to: TestObject): Promise<void>;
15
+ /**
16
+ * Sets the text of an input element.
17
+ * @param to - The TestObject representing the input element.
18
+ * @param text - The text to set.
19
+ */
20
+ static setText(to: TestObject, text: string): Promise<void>;
21
+ /**
22
+ * Searches for the specified text using heuristic selectors.
23
+ * @param text - The text to search for.
24
+ */
25
+ static search(text: string): Promise<void>;
26
+ /**
27
+ * Verifies that the specified element is present (visible) on the page.
28
+ * @param to - The TestObject representing the element.
29
+ * @param timeout - The maximum time to wait in milliseconds (default: 5000).
30
+ */
31
+ static verifyElementPresent(to: TestObject, timeout?: number): Promise<void>;
32
+ /**
33
+ * Gets the visible text of the specified element.
34
+ * @param to - The TestObject representing the element.
35
+ * @returns The text content of the element.
36
+ */
37
+ static getText(to: TestObject): Promise<string>;
38
+ /**
39
+ * Closes the current browser context/page.
40
+ */
41
+ static closeBrowser(): Promise<void>;
42
+ /**
43
+ * Double clicks on the specified element.
44
+ * @param to - The TestObject representing the element.
45
+ */
46
+ static doubleClick(to: TestObject): Promise<void>;
47
+ /**
48
+ * Right clicks (context click) on the specified element.
49
+ * @param to - The TestObject representing the element.
50
+ */
51
+ static rightClick(to: TestObject): Promise<void>;
52
+ /**
53
+ * Hovers over the specified element.
54
+ * @param to - The TestObject representing the element.
55
+ */
56
+ static mouseOver(to: TestObject): Promise<void>;
57
+ /**
58
+ * Drags the source element and drops it onto the target element.
59
+ * @param source - The TestObject representing the source element.
60
+ * @param target - The TestObject representing the target element.
61
+ */
62
+ static dragAndDrop(source: TestObject, target: TestObject): Promise<void>;
63
+ /**
64
+ * Checks (selects) the specified checkbox or radio button.
65
+ * @param to - The TestObject representing the element.
66
+ */
67
+ static check(to: TestObject): Promise<void>;
68
+ /**
69
+ * Unchecks (deselects) the specified checkbox.
70
+ * @param to - The TestObject representing the element.
71
+ */
72
+ static uncheck(to: TestObject): Promise<void>;
73
+ /**
74
+ * Selects an option by its value.
75
+ * @param to - The TestObject representing the dropdown.
76
+ * @param value - The value of the option to select.
77
+ */
78
+ static selectOptionByValue(to: TestObject, value: string): Promise<void>;
79
+ /**
80
+ * Selects an option by its label (visible text).
81
+ * @param to - The TestObject representing the dropdown.
82
+ * @param label - The label of the option to select.
83
+ */
84
+ static selectOptionByLabel(to: TestObject, label: string): Promise<void>;
85
+ /**
86
+ * Sends keys to the specified element (sequentially).
87
+ * @param to - The TestObject representing the element.
88
+ * @param key - The key(s) to send.
89
+ */
90
+ static sendKeys(to: TestObject, key: string): Promise<void>;
91
+ /**
92
+ * Presses a specific key on the keyboard.
93
+ * @param key - The name of the key to press (e.g., "Enter", "Tab", "ArrowDown").
94
+ */
95
+ static pressKey(key: string): Promise<void>;
96
+ /**
97
+ * Uploads a file to the specified file input element.
98
+ * @param to - The TestObject representing the file input.
99
+ * @param absolutePath - The absolute path to the file.
100
+ */
101
+ static uploadFile(to: TestObject, absolutePath: string): Promise<void>;
102
+ /**
103
+ * Clicks on an image template found on the screen.
104
+ * @param imagePath - The absolute path to the template image.
105
+ */
106
+ static clickImage(imagePath: string): Promise<void>;
107
+ /**
108
+ * Verifies that the specified element is NOT present (hidden) on the page.
109
+ * @param to - The TestObject representing the element.
110
+ * @param timeout - The maximum time to wait in milliseconds (default: 5000).
111
+ */
112
+ static verifyElementNotPresent(to: TestObject, timeout?: number): Promise<void>;
113
+ /**
114
+ * Verifies that the element's text matches the expected text.
115
+ * @param to - The TestObject representing the element.
116
+ * @param expectedText - The expected text.
117
+ */
118
+ static verifyElementText(to: TestObject, expectedText: string): Promise<void>;
119
+ /**
120
+ * Verifies that the element's attribute matches the expected value.
121
+ * @param to - The TestObject representing the element.
122
+ * @param attribute - The attribute name.
123
+ * @param expectedValue - The expected attribute value.
124
+ */
125
+ static verifyElementAttributeValue(to: TestObject, attribute: string, expectedValue: string): Promise<void>;
126
+ /**
127
+ * Waits for the specified element to be visible.
128
+ * @param to - The TestObject representing the element.
129
+ * @param timeout - The maximum time to wait in milliseconds (default: 5000).
130
+ */
131
+ static waitForElementVisible(to: TestObject, timeout?: number): Promise<void>;
132
+ /**
133
+ * Waits for the specified element to be clickable.
134
+ * @param to - The TestObject representing the element.
135
+ * @param timeout - The maximum time to wait in milliseconds (default: 5000).
136
+ */
137
+ static waitForElementClickable(to: TestObject, timeout?: number): Promise<void>;
138
+ /**
139
+ * Waits for the specified element to be not visible (hidden).
140
+ * @param to - The TestObject representing the element.
141
+ * @param timeout - The maximum time to wait in milliseconds (default: 5000).
142
+ */
143
+ static waitForElementNotVisible(to: TestObject, timeout?: number): Promise<void>;
144
+ /**
145
+ * Delays execution for a specified number of seconds.
146
+ * @param seconds - The number of seconds to wait.
147
+ */
148
+ static delay(seconds: number): Promise<void>;
149
+ /**
150
+ * Takes a screenshot and saves it to the reports folder.
151
+ * @param filename - The name of the screenshot file (optional).
152
+ */
153
+ static takeScreenshot(filename?: string): Promise<void>;
154
+ /**
155
+ * Gets the value of the specified attribute.
156
+ * @param to - The TestObject representing the element.
157
+ * @param attribute - The name of the attribute.
158
+ * @returns The attribute value or null.
159
+ */
160
+ static getAttribute(to: TestObject, attribute: string): Promise<string | null>;
161
+ /**
162
+ * Scrolls the element into view.
163
+ * @param to - The TestObject representing the element.
164
+ */
165
+ static scrollToElement(to: TestObject): Promise<void>;
166
+ }