lexxit-automation-framework 2.0.20 → 3.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.
Files changed (61) hide show
  1. package/README.md +246 -57
  2. package/npmignore +8 -0
  3. package/package.json +32 -1
  4. package/public/dashboard.html +591 -0
  5. package/src/actions/baseHandler.ts +351 -0
  6. package/src/actions/browserManager.ts +432 -0
  7. package/src/actions/checkboxHandler.ts +276 -0
  8. package/src/actions/clickHandler.ts +513 -0
  9. package/src/actions/customcodehandler.ts +251 -0
  10. package/src/actions/dropdownHandler.ts +501 -0
  11. package/src/actions/radiobuttonHandler.ts +286 -0
  12. package/src/actions/textHandler.ts +498 -0
  13. package/src/api/server.ts +210 -0
  14. package/src/config.ts +7 -0
  15. package/src/executor/functionMap.ts +153 -0
  16. package/src/executor/mapping.ts +70 -0
  17. package/src/executor/scriptExecutor.ts +162 -0
  18. package/src/executor/stepExecutor.ts +289 -0
  19. package/src/runner/testRunner.ts +78 -0
  20. package/src/sse/sseManager.ts +130 -0
  21. package/src/store/executionStore.ts +152 -0
  22. package/src/store/testDataStore.ts +46 -0
  23. package/src/types/types.ts +159 -0
  24. package/src/utils/healingService.ts +210 -0
  25. package/src/utils/locatorService.ts +73 -0
  26. package/src/utils/logger.ts +27 -0
  27. package/src/utils/metadataService.ts +137 -0
  28. package/src/utils/waitConditions.ts +141 -0
  29. package/src/validator/validator.ts +140 -0
  30. package/tsconfig.json +16 -0
  31. package/bin/lexxit-automation-framework.js +0 -224
  32. package/dist-obf/app.js +0 -1
  33. package/dist-obf/controllers/controller.js +0 -1
  34. package/dist-obf/core/BrowserManager.js +0 -1
  35. package/dist-obf/core/PlaywrightEngine.js +0 -1
  36. package/dist-obf/core/ScreenshotManager.js +0 -1
  37. package/dist-obf/core/TestData.js +0 -1
  38. package/dist-obf/core/TestExecutor.js +0 -1
  39. package/dist-obf/core/handlers/AllHandlers.js +0 -1
  40. package/dist-obf/core/handlers/BaseHandler.js +0 -1
  41. package/dist-obf/core/handlers/ClickHandler.js +0 -1
  42. package/dist-obf/core/handlers/CustomCodeHandler.js +0 -1
  43. package/dist-obf/core/handlers/DropdownHandler.js +0 -1
  44. package/dist-obf/core/handlers/InputHandler.js +0 -1
  45. package/dist-obf/core/registry/ActionRegistry.js +0 -1
  46. package/dist-obf/installer/frameworkLauncher.js +0 -1
  47. package/dist-obf/public/dashboard.html +0 -591
  48. package/dist-obf/public/execution.html +0 -510
  49. package/dist-obf/public/queue-monitor.html +0 -408
  50. package/dist-obf/queue/ExecutionQueue.js +0 -1
  51. package/dist-obf/routes/api.routes.js +0 -1
  52. package/dist-obf/server.js +0 -1
  53. package/dist-obf/types/types.js +0 -1
  54. package/dist-obf/utils/chromefinder.js +0 -1
  55. package/dist-obf/utils/elementHighlight.js +0 -1
  56. package/dist-obf/utils/locatorHelper.js +0 -1
  57. package/dist-obf/utils/logger.js +0 -1
  58. package/dist-obf/utils/response.js +0 -1
  59. package/dist-obf/utils/responseFormatter.js +0 -1
  60. package/dist-obf/utils/sseManager.js +0 -1
  61. package/scripts/postinstall.js +0 -52
@@ -0,0 +1,501 @@
1
+ import { Page, Locator } from 'playwright';
2
+ import { StepResult, ScreenshotMode } from '../types/types';
3
+ import { LocatorService } from '../utils/locatorService';
4
+ import { BaseHandler } from './baseHandler';
5
+ import { DEFAULT_SCREENSHOT_MODE } from '../config';
6
+
7
+
8
+ type DropdownType = 'native' | 'combobox' | 'multiselect';
9
+ export class DropdownHandler extends BaseHandler{
10
+ // private page: Page;
11
+ protected page: Page;
12
+ // private lastApiResponse: { status: number; url: string; body: any } | null = null;
13
+
14
+ constructor(page: Page, screenshotMode: ScreenshotMode = DEFAULT_SCREENSHOT_MODE) {
15
+ super();
16
+ this.page = page;
17
+ this.screenshotMode = screenshotMode;
18
+ this.setupNetworkCapture(page);
19
+ }
20
+
21
+
22
+
23
+
24
+ // ─── Helpers ───────────────────────────────────────────────────────────────
25
+
26
+ // private getStartTime(): string {
27
+ // return new Date().toISOString();
28
+ // }
29
+
30
+ // private getDuration(startMs: number): number {
31
+ // return Date.now() - startMs;
32
+ // }
33
+
34
+ // private buildResult(
35
+ // status: 'PASS' | 'FAIL',
36
+ // expected_result: string,
37
+ // comments: string,
38
+ // startMs: number,
39
+ // startTime: string,
40
+ // step_name: string = '',
41
+ // uid: string | null = null,
42
+ // obj_uid: string | null = null,
43
+ // page_uid: string | null = null
44
+ // ): StepResult {
45
+ // return {
46
+ // uid,
47
+ // obj_uid,
48
+ // page_uid,
49
+ // step_name,
50
+ // step_script: '',
51
+ // status,
52
+ // expected_result,
53
+ // comments,
54
+ // duration: this.getDuration(startMs),
55
+ // start_time: startTime
56
+ // };
57
+ // }
58
+
59
+ // private async captureScreenshot(isFailure: boolean): Promise<string | undefined> {
60
+ // if (this.screenshotMode === 'never') return undefined;
61
+ // if (this.screenshotMode === 'on_failure' && !isFailure) return undefined;
62
+ // try {
63
+ // if (!this.page || this.page.isClosed()) return undefined;
64
+ // const buf = await this.page.screenshot({ type: 'png', fullPage: false });
65
+ // return buf.toString('base64');
66
+ // } catch {
67
+ // return undefined;
68
+ // }
69
+ // }
70
+
71
+ // private classifyError(error: any): string {
72
+ // const msg: string = error?.message || String(error);
73
+
74
+ // if (msg.includes('Element not found') || msg.includes('not visible')) {
75
+ // return `Element not found or not visible: ${msg}`;
76
+ // }
77
+ // if (msg.includes('Timeout') || msg.includes('timeout')) {
78
+ // return `Timeout error - element took too long to respond: ${msg}`;
79
+ // }
80
+ // if (msg.includes('not enabled')) {
81
+ // return `Element is disabled: ${msg}`;
82
+ // }
83
+ // if (msg.includes('detached')) {
84
+ // return `Element got detached from DOM: ${msg}`;
85
+ // }
86
+ // if (msg.includes('Target closed') || msg.includes('has been closed')) {
87
+ // return `Browser or page closed unexpectedly: ${msg}`;
88
+ // }
89
+
90
+ // return `Unexpected error: ${msg}`;
91
+ // }
92
+
93
+ // private appendMatchedLocatorInfo(comments: string, matchedIndex: number, matchedXPath: string): string {
94
+ // if (matchedIndex > 0) {
95
+ // return `${comments} (Matched locator at position ${matchedIndex}: ${matchedXPath})`;
96
+ // }
97
+ // return comments;
98
+ // }
99
+
100
+ // ─── detectType ────────────────────────────────────────────────────────────
101
+
102
+ private async detectType(element: Locator): Promise<DropdownType> {
103
+ const tagName = await element.evaluate((el) => el.tagName.toLowerCase()).catch(() => '');
104
+ if (tagName === 'select') {
105
+ const isMultiple = await element.evaluate((el) => (el as HTMLSelectElement).multiple).catch(() => false);
106
+ return isMultiple ? 'multiselect' : 'native';
107
+ }
108
+
109
+ const role = await element.getAttribute('role').catch(() => null);
110
+ if (role === 'combobox') return 'combobox';
111
+
112
+ const ariaMultiselectable = await element.getAttribute('aria-multiselectable').catch(() => null);
113
+ if (ariaMultiselectable === 'true') return 'multiselect';
114
+
115
+ return 'combobox';
116
+ }
117
+
118
+ // ─── openDropdown (for combobox/multiselect) ──────────────────────────────
119
+
120
+ private async openDropdown(element: Locator): Promise<void> {
121
+ const isExpanded = await element.getAttribute('aria-expanded').catch(() => null);
122
+ if (isExpanded !== 'true') {
123
+ await element.click();
124
+ }
125
+ }
126
+
127
+ private getOptionLocator(optionText: string): Locator {
128
+ return this.page.locator(
129
+ `[role="option"]:has-text("${optionText}"), li:has-text("${optionText}"), div[role="listbox"] >> text="${optionText}"`
130
+ ).first();
131
+ }
132
+
133
+ // ─── selectOption ──────────────────────────────────────────────────────────
134
+
135
+ // async selectOption(label: string, locators: string[], optionText: string, step_name: string = 'Select dropdown option'): Promise<StepResult> {
136
+ // const startMs = Date.now();
137
+ // const startTime = this.getStartTime();
138
+ // const expected = `'${optionText}' selected in '${label}' successfully`;
139
+
140
+ // try {
141
+ // const resolved = await LocatorService.resolveElement(this.page, locators);
142
+ // const type = await this.detectType(resolved.element);
143
+
144
+ // if (type === 'native') {
145
+ // await resolved.element.selectOption({ label: optionText });
146
+ // } else {
147
+ // await this.openDropdown(resolved.element);
148
+ // const option = this.getOptionLocator(optionText);
149
+ // await option.waitFor({ state: 'visible', timeout: 10000 });
150
+ // await option.click();
151
+ // }
152
+
153
+ // const comments = this.appendMatchedLocatorInfo(expected, resolved.matchedIndex, resolved.matchedXPath);
154
+ // return this.buildResult('pass', expected, comments, startMs, startTime, step_name);
155
+ // } catch (error: any) {
156
+ // const sc = await this.captureScreenshot(true);
157
+ // return this.buildResult('fail', expected, this.classifyError(error), startMs, startTime, step_name, sc);
158
+ // }
159
+ // }
160
+ async selectOption(label: string, locators: string[], optionText: string, step_name: string = 'Select dropdown option'): Promise<StepResult> {
161
+ const startMs = Date.now();
162
+ const startTime = this.getStartTime();
163
+ const expected = `'${optionText}' selected in '${label}' successfully`;
164
+
165
+ try {
166
+ const resolved = await LocatorService.resolveElement(this.page, locators);
167
+ const tagName = await resolved.element.evaluate((e: Element) => e.tagName.toLowerCase());
168
+
169
+ if (tagName === 'select') {
170
+ // Try by label, value, and partial text match
171
+ const byLabel = resolved.element.selectOption({ label: optionText });
172
+ const byValue = resolved.element.selectOption({ value: optionText });
173
+ const byPartial = (async () => {
174
+ const options = await resolved.element.locator('option').all();
175
+ for (const opt of options) {
176
+ const txt = (await opt.textContent() ?? '').trim();
177
+ if (txt.toLowerCase().includes(optionText.toLowerCase())) {
178
+ const val = await opt.getAttribute('value');
179
+ if (!val) throw new Error(`Option "${optionText}" has no value`);
180
+ return resolved.element.selectOption({ value: val });
181
+ }
182
+ }
183
+ throw new Error(`Option "${optionText}" not found`);
184
+ })();
185
+
186
+ await Promise.any([byLabel, byValue, byPartial]);
187
+
188
+ } else {
189
+ // Custom dropdown — click to open then find option
190
+ await resolved.element.click();
191
+
192
+ const optionSelectors = [
193
+ `[role="option"]:has-text("${optionText}")`,
194
+ `mat-option:has-text("${optionText}")`,
195
+ `.ng-option:has-text("${optionText}")`,
196
+ `li:has-text("${optionText}")`,
197
+ `div:has-text("${optionText}")`
198
+ ];
199
+
200
+ let found = false;
201
+ for (const opt of optionSelectors) {
202
+ const option = this.page.locator(opt).first();
203
+ if (await option.isVisible().catch(() => false)) {
204
+ await option.click();
205
+ found = true;
206
+ break;
207
+ }
208
+ }
209
+
210
+ if (!found) {
211
+ throw new Error(`Option "${optionText}" not found`);
212
+ }
213
+ }
214
+
215
+ const comments = this.appendMatchedLocatorInfo(expected, resolved.matchedIndex, resolved.matchedXPath);
216
+ return this.buildResult('PASS', expected, comments, startMs, startTime, step_name);
217
+
218
+ } catch (error: any) {
219
+ // const sc = await this.captureScreenshot(true);
220
+ const msg = (error?.message || String(error)).toLowerCase();
221
+
222
+ if (
223
+ msg.includes('waiting for locator') ||
224
+ msg.includes('timeout') ||
225
+ msg.includes('no element') ||
226
+ msg.includes('not found') ||
227
+ msg.includes('not visible') ||
228
+ msg.includes('not attached') ||
229
+ msg.includes('strict mode violation')
230
+ ) {
231
+ const captured = this.getLastApiResponse();
232
+ if (captured) {
233
+ const bodyText = typeof captured.body === 'object' ? JSON.stringify(captured.body, null, 2) : String(captured.body);
234
+ const comments = `Element "${label}" not found on page.\nNetwork response [HTTP ${captured.status}] from ${captured.url}:\n${bodyText}`;
235
+ return this.buildResult('FAIL', expected, comments, startMs, startTime, step_name);
236
+ }
237
+ return this.buildResult('FAIL', expected, `"${label}" not found`, startMs, startTime, step_name);
238
+ }
239
+
240
+ return this.buildResult('FAIL', expected, this.classifyError(error), startMs, startTime, step_name);
241
+ }
242
+ }
243
+
244
+ // ─── selectMultipleOptions ─────────────────────────────────────────────────
245
+
246
+ async selectMultipleOptions(label: string, locators: string[], optionTexts: string[], step_name: string = 'Select multiple dropdown options'): Promise<StepResult> {
247
+ const startMs = Date.now();
248
+ const startTime = this.getStartTime();
249
+ const expected = `[${optionTexts.join(', ')}] selected in '${label}' successfully`;
250
+
251
+ try {
252
+ const resolved = await LocatorService.resolveElement(this.page, locators);
253
+ const type = await this.detectType(resolved.element);
254
+
255
+ if (type === 'native') {
256
+ await resolved.element.selectOption(optionTexts.map((text) => ({ label: text })));
257
+ } else {
258
+ await this.openDropdown(resolved.element);
259
+ for (const optionText of optionTexts) {
260
+ const option = this.getOptionLocator(optionText);
261
+ await option.waitFor({ state: 'visible', timeout: 10000 });
262
+ await option.click();
263
+ }
264
+ }
265
+
266
+ const comments = this.appendMatchedLocatorInfo(expected, resolved.matchedIndex, resolved.matchedXPath);
267
+ return this.buildResult('PASS', expected, comments, startMs, startTime, step_name);
268
+ } catch (error: any) {
269
+ // const sc = await this.captureScreenshot(true);
270
+ return this.buildResult('FAIL', expected, this.classifyError(error), startMs, startTime, step_name);
271
+ }
272
+ }
273
+
274
+ // ─── deselectOption ────────────────────────────────────────────────────────
275
+
276
+ async deselectOption(label: string, locators: string[], optionText: string, step_name: string = 'Deselect dropdown option'): Promise<StepResult> {
277
+ const startMs = Date.now();
278
+ const startTime = this.getStartTime();
279
+ const expected = `'${optionText}' deselected in '${label}' successfully`;
280
+
281
+ try {
282
+ const resolved = await LocatorService.resolveElement(this.page, locators);
283
+ const type = await this.detectType(resolved.element);
284
+
285
+ if (type === 'native') {
286
+ const currentValues = await resolved.element.evaluate((el) =>
287
+ Array.from((el as HTMLSelectElement).selectedOptions).map((opt) => opt.label)
288
+ );
289
+ const remaining = currentValues.filter((val: string) => val !== optionText);
290
+ await resolved.element.selectOption(remaining.map((text: string) => ({ label: text })));
291
+ } else {
292
+ const chip = this.page.locator(`text="${optionText}"`).first();
293
+ const isChipVisible = await chip.isVisible().catch(() => false);
294
+
295
+ if (isChipVisible) {
296
+ await chip.click();
297
+ } else {
298
+ await this.openDropdown(resolved.element);
299
+ const option = this.getOptionLocator(optionText);
300
+ await option.waitFor({ state: 'visible', timeout: 10000 });
301
+ await option.click();
302
+ }
303
+ }
304
+
305
+ const comments = this.appendMatchedLocatorInfo(expected, resolved.matchedIndex, resolved.matchedXPath);
306
+ return this.buildResult('PASS', expected, comments, startMs, startTime, step_name);
307
+ } catch (error: any) {
308
+ // const sc = await this.captureScreenshot(true);
309
+ return this.buildResult('FAIL', expected, this.classifyError(error), startMs, startTime, step_name);
310
+ }
311
+ }
312
+
313
+ // ─── getSelectedOption ─────────────────────────────────────────────────────
314
+
315
+ async getSelectedOption(label: string, locators: string[], step_name: string = 'Get selected dropdown option'): Promise<StepResult> {
316
+ const startMs = Date.now();
317
+ const startTime = this.getStartTime();
318
+ const expected = `Selected option retrieved from '${label}' successfully`;
319
+
320
+ try {
321
+ const resolved = await LocatorService.resolveElement(this.page, locators);
322
+ const type = await this.detectType(resolved.element);
323
+
324
+ let selectedText = '';
325
+
326
+ if (type === 'native') {
327
+ selectedText = await resolved.element.evaluate((el) => {
328
+ const select = el as HTMLSelectElement;
329
+ return select.selectedOptions[0]?.label || '';
330
+ });
331
+ } else {
332
+ selectedText = await resolved.element.innerText();
333
+ }
334
+
335
+ const comments = this.appendMatchedLocatorInfo(
336
+ `Selected option in '${label}' is: ${selectedText}`,
337
+ resolved.matchedIndex,
338
+ resolved.matchedXPath
339
+ );
340
+ return this.buildResult('PASS', expected, comments, startMs, startTime, step_name);
341
+ } catch (error: any) {
342
+ // const sc = await this.captureScreenshot(true);
343
+ return this.buildResult('FAIL', expected, this.classifyError(error), startMs, startTime, step_name);
344
+ }
345
+ }
346
+
347
+ // ─── getSelectedOptions ────────────────────────────────────────────────────
348
+
349
+ async getSelectedOptions(label: string, locators: string[], step_name: string = 'Get selected dropdown options'): Promise<StepResult> {
350
+ const startMs = Date.now();
351
+ const startTime = this.getStartTime();
352
+ const expected = `Selected options retrieved from '${label}' successfully`;
353
+
354
+ try {
355
+ const resolved = await LocatorService.resolveElement(this.page, locators);
356
+ const type = await this.detectType(resolved.element);
357
+
358
+ let selectedTexts: string[] = [];
359
+
360
+ if (type === 'native') {
361
+ selectedTexts = await resolved.element.evaluate((el) =>
362
+ Array.from((el as HTMLSelectElement).selectedOptions).map((opt) => opt.label)
363
+ );
364
+ } else {
365
+ const chips = this.page.locator('[class*="chip"], [class*="tag"]');
366
+ const count = await chips.count();
367
+ for (let i = 0; i < count; i++) {
368
+ selectedTexts.push(await chips.nth(i).innerText());
369
+ }
370
+ }
371
+
372
+ const comments = this.appendMatchedLocatorInfo(
373
+ `Selected options in '${label}' are: [${selectedTexts.join(', ')}]`,
374
+ resolved.matchedIndex,
375
+ resolved.matchedXPath
376
+ );
377
+ return this.buildResult('PASS', expected, comments, startMs, startTime, step_name);
378
+ } catch (error: any) {
379
+ // const sc = await this.captureScreenshot(true);
380
+ return this.buildResult('FAIL', expected, this.classifyError(error), startMs, startTime, step_name);
381
+ }
382
+ }
383
+
384
+ // ─── getAllOptions ─────────────────────────────────────────────────────────
385
+
386
+ async getAllOptions(label: string, locators: string[], step_name: string = 'Get all dropdown options'): Promise<StepResult> {
387
+ const startMs = Date.now();
388
+ const startTime = this.getStartTime();
389
+ const expected = `All options retrieved from '${label}' successfully`;
390
+
391
+ try {
392
+ const resolved = await LocatorService.resolveElement(this.page, locators);
393
+ const type = await this.detectType(resolved.element);
394
+
395
+ let allOptions: string[] = [];
396
+
397
+ if (type === 'native') {
398
+ allOptions = await resolved.element.evaluate((el) =>
399
+ Array.from((el as HTMLSelectElement).options).map((opt) => opt.label)
400
+ );
401
+ } else {
402
+ await this.openDropdown(resolved.element);
403
+ const options = this.page.locator('[role="option"], div[role="listbox"] li');
404
+ const count = await options.count();
405
+ for (let i = 0; i < count; i++) {
406
+ allOptions.push(await options.nth(i).innerText());
407
+ }
408
+ }
409
+
410
+ const comments = this.appendMatchedLocatorInfo(
411
+ `Options in '${label}' are: [${allOptions.join(', ')}]`,
412
+ resolved.matchedIndex,
413
+ resolved.matchedXPath
414
+ );
415
+ return this.buildResult('PASS', expected, comments, startMs, startTime, step_name);
416
+ } catch (error: any) {
417
+ // const sc = await this.captureScreenshot(true);
418
+ return this.buildResult('FAIL', expected, this.classifyError(error), startMs, startTime, step_name);
419
+ }
420
+ }
421
+
422
+ // ─── isOptionPresent ───────────────────────────────────────────────────────
423
+
424
+ async isOptionPresent(label: string, locators: string[], optionText: string, step_name: string = 'Check if dropdown option is present'): Promise<StepResult> {
425
+ const startMs = Date.now();
426
+ const startTime = this.getStartTime();
427
+ const expected = `Option '${optionText}' presence checked in '${label}' successfully`;
428
+
429
+ try {
430
+ const resolved = await LocatorService.resolveElement(this.page, locators);
431
+ const type = await this.detectType(resolved.element);
432
+
433
+ let isPresent = false;
434
+
435
+ if (type === 'native') {
436
+ const allOptions = await resolved.element.evaluate((el) =>
437
+ Array.from((el as HTMLSelectElement).options).map((opt) => opt.label)
438
+ );
439
+ isPresent = allOptions.includes(optionText);
440
+ } else {
441
+ await this.openDropdown(resolved.element);
442
+ const option = this.getOptionLocator(optionText);
443
+ isPresent = await option.isVisible().catch(() => false);
444
+ }
445
+
446
+ const comments = this.appendMatchedLocatorInfo(
447
+ `Option '${optionText}' is ${isPresent ? 'present' : 'not present'} in '${label}'`,
448
+ resolved.matchedIndex,
449
+ resolved.matchedXPath
450
+ );
451
+ return this.buildResult('PASS', expected, comments, startMs, startTime, step_name);
452
+ } catch (error: any) {
453
+ // const sc = await this.captureScreenshot(true);
454
+ return this.buildResult('FAIL', expected, this.classifyError(error), startMs, startTime, step_name);
455
+ }
456
+ }
457
+
458
+ // ─── isEnabled ─────────────────────────────────────────────────────────────
459
+
460
+ async isEnabled(label: string, locators: string[], step_name: string = 'Check if dropdown is enabled'): Promise<StepResult> {
461
+ const startMs = Date.now();
462
+ const startTime = this.getStartTime();
463
+ const expected = `'${label}' enabled state retrieved successfully`;
464
+
465
+ try {
466
+ const resolved = await LocatorService.resolveElement(this.page, locators);
467
+ const enabled = await resolved.element.isEnabled();
468
+
469
+ const comments = this.appendMatchedLocatorInfo(
470
+ `'${label}' is ${enabled ? 'enabled' : 'disabled'}`,
471
+ resolved.matchedIndex,
472
+ resolved.matchedXPath
473
+ );
474
+ return this.buildResult('PASS', expected, comments, startMs, startTime, step_name);
475
+ } catch (error: any) {
476
+ return this.buildResult('FAIL', expected, this.classifyError(error), startMs, startTime, step_name);
477
+ }
478
+ }
479
+
480
+ // ─── isVisible ─────────────────────────────────────────────────────────────
481
+
482
+ async isVisible(label: string, locators: string[], step_name: string = 'Check if dropdown is visible'): Promise<StepResult> {
483
+ const startMs = Date.now();
484
+ const startTime = this.getStartTime();
485
+ const expected = `'${label}' visibility state retrieved successfully`;
486
+
487
+ try {
488
+ const resolved = await LocatorService.resolveElement(this.page, locators);
489
+ const visible = await resolved.element.isVisible();
490
+
491
+ const comments = this.appendMatchedLocatorInfo(
492
+ `'${label}' is ${visible ? 'visible' : 'not visible'}`,
493
+ resolved.matchedIndex,
494
+ resolved.matchedXPath
495
+ );
496
+ return this.buildResult('PASS', expected, comments, startMs, startTime, step_name);
497
+ } catch (error: any) {
498
+ return this.buildResult('FAIL', expected, this.classifyError(error), startMs, startTime, step_name);
499
+ }
500
+ }
501
+ }