@flash-ai-team/flash-test-framework 0.0.1 → 0.0.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/dist/index.d.ts CHANGED
@@ -2,3 +2,4 @@ export { Web } from './keywords/WebUI';
2
2
  export { AIWeb } from './keywords/AIWebUI';
3
3
  export { Keyword, KeywordContext } from './core/Keyword';
4
4
  export { TestObject, el } from './core/ObjectRepository';
5
+ export { TestCase } from './core/TestDecorators';
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.el = exports.TestObject = exports.KeywordContext = exports.Keyword = exports.AIWeb = exports.Web = void 0;
3
+ exports.TestCase = exports.el = exports.TestObject = exports.KeywordContext = exports.Keyword = exports.AIWeb = exports.Web = void 0;
4
4
  var WebUI_1 = require("./keywords/WebUI");
5
5
  Object.defineProperty(exports, "Web", { enumerable: true, get: function () { return WebUI_1.Web; } });
6
6
  var AIWebUI_1 = require("./keywords/AIWebUI");
@@ -11,3 +11,5 @@ Object.defineProperty(exports, "KeywordContext", { enumerable: true, get: functi
11
11
  var ObjectRepository_1 = require("./core/ObjectRepository");
12
12
  Object.defineProperty(exports, "TestObject", { enumerable: true, get: function () { return ObjectRepository_1.TestObject; } });
13
13
  Object.defineProperty(exports, "el", { enumerable: true, get: function () { return ObjectRepository_1.el; } });
14
+ var TestDecorators_1 = require("./core/TestDecorators");
15
+ Object.defineProperty(exports, "TestCase", { enumerable: true, get: function () { return TestDecorators_1.TestCase; } });
@@ -12,6 +12,11 @@ export declare class AIWeb {
12
12
  static click(description: string): Promise<void>;
13
13
  static setText(description: string, text: string): Promise<void>;
14
14
  static verifyText(description: string, expectedText: string): Promise<void>;
15
+ /**
16
+ * Specialized handler for reCAPTCHA.
17
+ * Searches for the reCAPTCHA iframe and clicks the checkbox.
18
+ */
19
+ static clickReCaptcha(): Promise<void>;
15
20
  /**
16
21
  * Executes manual test steps by converting them to WebUI keyword calls using AI.
17
22
  * @param steps - Natural language test steps (one per line or paragraph).
@@ -152,6 +152,36 @@ class AIWeb {
152
152
  throw new Error(`Expected text "${expectedText}" not found in element "${description}". Found: "${text}"`);
153
153
  }
154
154
  }
155
+ /**
156
+ * Specialized handler for reCAPTCHA.
157
+ * Searches for the reCAPTCHA iframe and clicks the checkbox.
158
+ */
159
+ static async clickReCaptcha() {
160
+ console.log("Attempting to find and click reCAPTCHA...");
161
+ const frames = this.page.frames();
162
+ let captchaFrame = null;
163
+ // Find the reCAPTCHA iframe
164
+ for (const frame of frames) {
165
+ if (frame.url().includes('google.com/recaptcha/api2/anchor')) {
166
+ captchaFrame = frame;
167
+ break;
168
+ }
169
+ }
170
+ if (!captchaFrame) {
171
+ console.warn("reCAPTCHA iframe not found among " + frames.length + " frames.");
172
+ // Try waiting? fallback
173
+ return;
174
+ }
175
+ const anchor = captchaFrame.locator('#recaptcha-anchor, .recaptcha-checkbox-border').first();
176
+ try {
177
+ await anchor.waitFor({ state: 'visible', timeout: 5000 });
178
+ await anchor.click();
179
+ console.log("Clicked reCAPTCHA anchor.");
180
+ }
181
+ catch (e) {
182
+ console.error("Failed to click reCAPTCHA anchor", e);
183
+ }
184
+ }
155
185
  /**
156
186
  * Executes manual test steps by converting them to WebUI keyword calls using AI.
157
187
  * @param steps - Natural language test steps (one per line or paragraph).
@@ -164,14 +194,56 @@ class AIWeb {
164
194
  // Remove numbering (e.g., "1. ")
165
195
  const cleanLine = line.replace(/^\d+\.\s*/, '').trim();
166
196
  console.log(`Processing step: "${cleanLine}"`);
197
+ // 8. Wait for "..." to disappear (Moved to top priority to avoid conflicts)
198
+ const waitDisappearMatch = cleanLine.match(/^Wait for "([^"]+)"(?: .+)? (?:to )?disappear$/i);
199
+ if (waitDisappearMatch) {
200
+ const targetDesc = waitDisappearMatch[1];
201
+ const selector = `text="${targetDesc}" >> nth=0`;
202
+ await WebUI_1.Web.waitForElementNotVisible((0, ObjectRepository_1.el)(selector, targetDesc));
203
+ continue;
204
+ }
205
+ // 10. Verify that element "..." contains text "..." is also "..."
206
+ const tripleCheckMatch = cleanLine.match(/^Verify that element "([^"]+)" contains (?:text )?"([^"]+)" is also "([^"]+)"$/i);
207
+ if (tripleCheckMatch) {
208
+ const containerDesc = tripleCheckMatch[1];
209
+ const text1 = tripleCheckMatch[2];
210
+ const text2 = tripleCheckMatch[3];
211
+ const selector = `*:has-text("${containerDesc}"):has-text("${text1}"):has-text("${text2}") >> nth=-1`;
212
+ await WebUI_1.Web.verifyElementPresent((0, ObjectRepository_1.el)(selector, `Element '${containerDesc}' containing '${text1}' and '${text2}'`));
213
+ continue;
214
+ }
215
+ // 9. Verify that element "..." contains text "..."
216
+ const elementContainsMatch = cleanLine.match(/^Verify that element "([^"]+)" contains (?:text )?"([^"]+)"$/i);
217
+ if (elementContainsMatch) {
218
+ const containerDesc = elementContainsMatch[1];
219
+ const containedText = elementContainsMatch[2];
220
+ // Select the deepest element (*) that contains BOTH strings.
221
+ // nth=-1 selects the last one, which is typically the most specific common ancestor in the DOM tree order.
222
+ const selector = `*:has-text("${containerDesc}"):has-text("${containedText}") >> nth=-1`;
223
+ await WebUI_1.Web.verifyElementPresent((0, ObjectRepository_1.el)(selector, `Element '${containerDesc}' with text '${containedText}'`));
224
+ continue;
225
+ }
226
+ // SPECIAL CASE: reCAPTCHA
227
+ if (cleanLine.toLowerCase().includes("i'm not a robot") || cleanLine.toLowerCase().includes("i am not robot")) {
228
+ await this.clickReCaptcha();
229
+ continue;
230
+ }
231
+ // 8a. Wait for "..." to appear
232
+ const waitAppearMatch = cleanLine.match(/^Wait for "([^"]+)"(?: .+)? (?:to )?appear$/i);
233
+ if (waitAppearMatch) {
234
+ const targetDesc = waitAppearMatch[1];
235
+ const selector = `text="${targetDesc}" >> nth=0`;
236
+ await WebUI_1.Web.waitForElementVisible((0, ObjectRepository_1.el)(selector, targetDesc));
237
+ continue;
238
+ }
167
239
  // 1. Navigate to "url"
168
240
  const navigateMatch = cleanLine.match(/^Navigate to "([^"]+)"$/i);
169
241
  if (navigateMatch) {
170
242
  await WebUI_1.Web.navigateToUrl(navigateMatch[1]);
171
243
  continue;
172
244
  }
173
- // 2. Enter "text" into ...
174
- const enterMatch = cleanLine.match(/^Enter "([^"]+)" into (?:the )?(.+)$/i);
245
+ // 2. Enter "text" into ... / Type "text" into ...
246
+ const enterMatch = cleanLine.match(/^(?:Enter|Type) "([^"]+)" into (?:the )?(.+)$/i);
175
247
  if (enterMatch) {
176
248
  const text = enterMatch[1];
177
249
  let targetDesc = enterMatch[2];
@@ -230,9 +302,10 @@ class AIWeb {
230
302
  continue;
231
303
  }
232
304
  // 5. Scroll to ...
305
+ const quotedScrollMatch = cleanLine.match(/^Scroll to (?:the )?"([^"]+)"/i);
233
306
  const scrollMatch = cleanLine.match(/^Scroll to (?:the )?(.+)$/i);
234
- if (scrollMatch) {
235
- let targetDesc = scrollMatch[1];
307
+ if (quotedScrollMatch || scrollMatch) {
308
+ let targetDesc = quotedScrollMatch ? quotedScrollMatch[1] : scrollMatch[1];
236
309
  targetDesc = targetDesc.replace(/ button$/i, '').replace(/ link$/i, '').replace(/ marker$/i, '').trim();
237
310
  const selector = `text="${targetDesc}" >> nth=0`;
238
311
  await WebUI_1.Web.scrollToElement((0, ObjectRepository_1.el)(selector, targetDesc));
@@ -242,7 +315,71 @@ class AIWeb {
242
315
  const verifyTextMatch = cleanLine.match(/^Verify that the text "([^"]+)" is present$/i);
243
316
  if (verifyTextMatch) {
244
317
  const expectedText = verifyTextMatch[1];
245
- await WebUI_1.Web.verifyElementPresent((0, ObjectRepository_1.el)(`text="${expectedText}"`, `Text '${expectedText}'`));
318
+ await WebUI_1.Web.verifyElementPresent((0, ObjectRepository_1.el)(`text="${expectedText}" >> nth=0`, `Text '${expectedText}'`));
319
+ continue;
320
+ }
321
+ // 7. Delay for ...
322
+ const delayMatch = cleanLine.match(/^Delay for ([\d.]+) seconds?$/i);
323
+ if (delayMatch) {
324
+ const seconds = parseFloat(delayMatch[1]);
325
+ await WebUI_1.Web.delay(seconds);
326
+ continue;
327
+ }
328
+ // Verify page url is "..."
329
+ const verifyUrlMatch = cleanLine.match(/^Verify page url is "([^"]+)"$/i);
330
+ if (verifyUrlMatch) {
331
+ const url = verifyUrlMatch[1];
332
+ await WebUI_1.Web.verifyUrl(url);
333
+ continue;
334
+ }
335
+ // Maximize Window
336
+ if (cleanLine.match(/^Maximize window$/i)) {
337
+ await WebUI_1.Web.maximizeWindow();
338
+ continue;
339
+ }
340
+ // Minimize Window
341
+ if (cleanLine.match(/^Minimize window$/i)) {
342
+ await WebUI_1.Web.minimizeWindow();
343
+ continue;
344
+ }
345
+ // Wait for Angular to load
346
+ if (cleanLine.match(/^Wait for Angular to load$/i)) {
347
+ await WebUI_1.Web.waitForAngularLoad();
348
+ continue;
349
+ }
350
+ // Check "..."
351
+ const checkMatch = cleanLine.match(/^Check "([^"]+)"$/i);
352
+ if (checkMatch) {
353
+ const targetDesc = checkMatch[1];
354
+ // Try finding by label or just text.
355
+ // For Material checkbox, label usually contains the text.
356
+ const selector = `text="${targetDesc}" >> nth=0`;
357
+ await WebUI_1.Web.check((0, ObjectRepository_1.el)(selector, targetDesc));
358
+ continue;
359
+ }
360
+ // Uncheck "..."
361
+ const uncheckMatch = cleanLine.match(/^Uncheck "([^"]+)"$/i);
362
+ if (uncheckMatch) {
363
+ const targetDesc = uncheckMatch[1];
364
+ const selector = `text="${targetDesc}" >> nth=0`;
365
+ await WebUI_1.Web.uncheck((0, ObjectRepository_1.el)(selector, targetDesc));
366
+ continue;
367
+ }
368
+ // Verify that element "..." is checked/unchecked
369
+ const verifyCheckedMatch = cleanLine.match(/^Verify that element "([^"]+)" is (checked|unchecked)$/i);
370
+ if (verifyCheckedMatch) {
371
+ const targetDesc = verifyCheckedMatch[1];
372
+ const state = verifyCheckedMatch[2].toLowerCase();
373
+ const selector = `text="${targetDesc}" >> nth=0`;
374
+ await WebUI_1.Web.verifyElementChecked((0, ObjectRepository_1.el)(selector, targetDesc), state === 'checked');
375
+ continue;
376
+ }
377
+ // Set window size to WxH
378
+ const setSizeMatch = cleanLine.match(/^Set window size to (\d+)x(\d+)$/i);
379
+ if (setSizeMatch) {
380
+ const width = parseInt(setSizeMatch[1]);
381
+ const height = parseInt(setSizeMatch[2]);
382
+ await WebUI_1.Web.setWindowSize(width, height);
246
383
  continue;
247
384
  }
248
385
  console.warn(`Could not parse step: "${cleanLine}"`);
@@ -7,6 +7,12 @@ export declare class Web {
7
7
  * @param url - The URL to navigate to.
8
8
  */
9
9
  static navigateToUrl(url: string): Promise<void>;
10
+ /**
11
+ * Verifies that the current page URL matches the expected URL.
12
+ * @param url - The expected URL.
13
+ * @param timeout - The maximum time to wait in milliseconds (default: 5000).
14
+ */
15
+ static verifyUrl(url: string, timeout?: number): Promise<void>;
10
16
  /**
11
17
  * Clicks on the specified element.
12
18
  * @param to - The TestObject representing the element.
@@ -123,6 +129,12 @@ export declare class Web {
123
129
  * @param expectedValue - The expected attribute value.
124
130
  */
125
131
  static verifyElementAttributeValue(to: TestObject, attribute: string, expectedValue: string): Promise<void>;
132
+ /**
133
+ * Verifies that the element is checked or unchecked.
134
+ * @param to - The TestObject representing the checkbox/radio.
135
+ * @param checked - True to verify checked, false to verify unchecked (default: true).
136
+ */
137
+ static verifyElementChecked(to: TestObject, checked?: boolean): Promise<void>;
126
138
  /**
127
139
  * Waits for the specified element to be visible.
128
140
  * @param to - The TestObject representing the element.
@@ -163,4 +175,22 @@ export declare class Web {
163
175
  * @param to - The TestObject representing the element.
164
176
  */
165
177
  static scrollToElement(to: TestObject): Promise<void>;
178
+ /**
179
+ * Maximizes the browser window (sets viewport to 1920x1080).
180
+ */
181
+ static maximizeWindow(): Promise<void>;
182
+ /**
183
+ * Minimizes the browser window (sets viewport to a small size).
184
+ */
185
+ static minimizeWindow(): Promise<void>;
186
+ /**
187
+ * Sets the browser window size.
188
+ * @param width - The width in pixels.
189
+ * @param height - The height in pixels.
190
+ */
191
+ static setWindowSize(width: number, height: number): Promise<void>;
192
+ /**
193
+ * Waits for Angular to finish rendering.
194
+ */
195
+ static waitForAngularLoad(): Promise<void>;
166
196
  }
@@ -31,6 +31,14 @@ class Web {
31
31
  static async navigateToUrl(url) {
32
32
  await this.page.goto(url);
33
33
  }
34
+ /**
35
+ * Verifies that the current page URL matches the expected URL.
36
+ * @param url - The expected URL.
37
+ * @param timeout - The maximum time to wait in milliseconds (default: 5000).
38
+ */
39
+ static async verifyUrl(url, timeout = 5000) {
40
+ await (0, test_1.expect)(this.page).toHaveURL(url, { timeout });
41
+ }
34
42
  /**
35
43
  * Clicks on the specified element.
36
44
  * @param to - The TestObject representing the element.
@@ -262,6 +270,19 @@ class Web {
262
270
  static async verifyElementAttributeValue(to, attribute, expectedValue) {
263
271
  await (0, test_1.expect)(this.getLocator(to)).toHaveAttribute(attribute, expectedValue);
264
272
  }
273
+ /**
274
+ * Verifies that the element is checked or unchecked.
275
+ * @param to - The TestObject representing the checkbox/radio.
276
+ * @param checked - True to verify checked, false to verify unchecked (default: true).
277
+ */
278
+ static async verifyElementChecked(to, checked = true) {
279
+ if (checked) {
280
+ await (0, test_1.expect)(this.getLocator(to)).toBeChecked();
281
+ }
282
+ else {
283
+ await (0, test_1.expect)(this.getLocator(to)).not.toBeChecked();
284
+ }
285
+ }
265
286
  // --- Wait Keywords ---
266
287
  /**
267
288
  * Waits for the specified element to be visible.
@@ -324,6 +345,39 @@ class Web {
324
345
  static async scrollToElement(to) {
325
346
  await this.getLocator(to).scrollIntoViewIfNeeded();
326
347
  }
348
+ // --- Window Keywords ---
349
+ /**
350
+ * Maximizes the browser window (sets viewport to 1920x1080).
351
+ */
352
+ static async maximizeWindow() {
353
+ await this.page.setViewportSize({ width: 1920, height: 1080 });
354
+ }
355
+ /**
356
+ * Minimizes the browser window (sets viewport to a small size).
357
+ */
358
+ static async minimizeWindow() {
359
+ await this.page.setViewportSize({ width: 500, height: 400 });
360
+ }
361
+ /**
362
+ * Sets the browser window size.
363
+ * @param width - The width in pixels.
364
+ * @param height - The height in pixels.
365
+ */
366
+ static async setWindowSize(width, height) {
367
+ await this.page.setViewportSize({ width, height });
368
+ }
369
+ /**
370
+ * Waits for Angular to finish rendering.
371
+ */
372
+ static async waitForAngularLoad() {
373
+ await this.page.evaluate(async () => {
374
+ // @ts-ignore
375
+ if (window.getAllAngularTestabilities) {
376
+ // @ts-ignore
377
+ await Promise.all(window.getAllAngularTestabilities().map(t => new Promise(resolve => t.whenStable(resolve))));
378
+ }
379
+ });
380
+ }
327
381
  }
328
382
  exports.Web = Web;
329
383
  __decorate([
@@ -332,6 +386,12 @@ __decorate([
332
386
  __metadata("design:paramtypes", [String]),
333
387
  __metadata("design:returntype", Promise)
334
388
  ], Web, "navigateToUrl", null);
389
+ __decorate([
390
+ (0, Keyword_1.Keyword)("Verify Url"),
391
+ __metadata("design:type", Function),
392
+ __metadata("design:paramtypes", [String, Number]),
393
+ __metadata("design:returntype", Promise)
394
+ ], Web, "verifyUrl", null);
335
395
  __decorate([
336
396
  (0, Keyword_1.Keyword)("Click"),
337
397
  __metadata("design:type", Function),
@@ -458,6 +518,12 @@ __decorate([
458
518
  __metadata("design:paramtypes", [ObjectRepository_1.TestObject, String, String]),
459
519
  __metadata("design:returntype", Promise)
460
520
  ], Web, "verifyElementAttributeValue", null);
521
+ __decorate([
522
+ (0, Keyword_1.Keyword)("Verify Element Checked"),
523
+ __metadata("design:type", Function),
524
+ __metadata("design:paramtypes", [ObjectRepository_1.TestObject, Boolean]),
525
+ __metadata("design:returntype", Promise)
526
+ ], Web, "verifyElementChecked", null);
461
527
  __decorate([
462
528
  (0, Keyword_1.Keyword)("Wait For Element Visible"),
463
529
  __metadata("design:type", Function),
@@ -500,3 +566,27 @@ __decorate([
500
566
  __metadata("design:paramtypes", [ObjectRepository_1.TestObject]),
501
567
  __metadata("design:returntype", Promise)
502
568
  ], Web, "scrollToElement", null);
569
+ __decorate([
570
+ (0, Keyword_1.Keyword)("Maximize Window"),
571
+ __metadata("design:type", Function),
572
+ __metadata("design:paramtypes", []),
573
+ __metadata("design:returntype", Promise)
574
+ ], Web, "maximizeWindow", null);
575
+ __decorate([
576
+ (0, Keyword_1.Keyword)("Minimize Window"),
577
+ __metadata("design:type", Function),
578
+ __metadata("design:paramtypes", []),
579
+ __metadata("design:returntype", Promise)
580
+ ], Web, "minimizeWindow", null);
581
+ __decorate([
582
+ (0, Keyword_1.Keyword)("Set Window Size"),
583
+ __metadata("design:type", Function),
584
+ __metadata("design:paramtypes", [Number, Number]),
585
+ __metadata("design:returntype", Promise)
586
+ ], Web, "setWindowSize", null);
587
+ __decorate([
588
+ (0, Keyword_1.Keyword)("Wait For Angular Load"),
589
+ __metadata("design:type", Function),
590
+ __metadata("design:paramtypes", []),
591
+ __metadata("design:returntype", Promise)
592
+ ], Web, "waitForAngularLoad", null);
@@ -35,6 +35,7 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.getReportFolder = getReportFolder;
37
37
  exports.resetReportFolderCache = resetReportFolderCache;
38
+ const fs = __importStar(require("fs"));
38
39
  const path = __importStar(require("path"));
39
40
  let cachedReportFolder = null;
40
41
  function getReportFolder(suite) {
@@ -56,6 +57,10 @@ function getReportFolder(suite) {
56
57
  }
57
58
  }
58
59
  cachedReportFolder = path.join(process.cwd(), 'reports', suiteName, `test_${timestamp}`);
60
+ // Ensure the folder exists
61
+ if (!fs.existsSync(cachedReportFolder)) {
62
+ fs.mkdirSync(cachedReportFolder, { recursive: true });
63
+ }
59
64
  console.log(`[ReporterUtils] Calculated report folder: ${cachedReportFolder}`);
60
65
  return cachedReportFolder;
61
66
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flash-ai-team/flash-test-framework",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "description": "A powerful keyword-driven automation framework built on top of Playwright and TypeScript.",
5
5
  "keywords": [
6
6
  "playwright",
@@ -60,4 +60,4 @@
60
60
  "@types/nodemailer": "^7.0.4",
61
61
  "typescript": "^5.9.3"
62
62
  }
63
- }
63
+ }