pw-automation-framework 2.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.
- package/README.md +93 -0
- package/bin/lexxit-automation-framework.js +427 -0
- package/dist/app.d.ts +2 -0
- package/dist/app.js +26 -0
- package/dist/app.js.map +1 -0
- package/dist/controllers/controller.d.ts +57 -0
- package/dist/controllers/controller.js +263 -0
- package/dist/controllers/controller.js.map +1 -0
- package/dist/core/BrowserManager.d.ts +46 -0
- package/dist/core/BrowserManager.js +377 -0
- package/dist/core/BrowserManager.js.map +1 -0
- package/dist/core/PlaywrightEngine.d.ts +16 -0
- package/dist/core/PlaywrightEngine.js +246 -0
- package/dist/core/PlaywrightEngine.js.map +1 -0
- package/dist/core/ScreenshotManager.d.ts +10 -0
- package/dist/core/ScreenshotManager.js +28 -0
- package/dist/core/ScreenshotManager.js.map +1 -0
- package/dist/core/TestData.d.ts +12 -0
- package/dist/core/TestData.js +29 -0
- package/dist/core/TestData.js.map +1 -0
- package/dist/core/TestExecutor.d.ts +16 -0
- package/dist/core/TestExecutor.js +355 -0
- package/dist/core/TestExecutor.js.map +1 -0
- package/dist/core/handlers/AllHandlers.d.ts +116 -0
- package/dist/core/handlers/AllHandlers.js +648 -0
- package/dist/core/handlers/AllHandlers.js.map +1 -0
- package/dist/core/handlers/BaseHandler.d.ts +16 -0
- package/dist/core/handlers/BaseHandler.js +27 -0
- package/dist/core/handlers/BaseHandler.js.map +1 -0
- package/dist/core/handlers/ClickHandler.d.ts +34 -0
- package/dist/core/handlers/ClickHandler.js +359 -0
- package/dist/core/handlers/ClickHandler.js.map +1 -0
- package/dist/core/handlers/CustomCodeHandler.d.ts +35 -0
- package/dist/core/handlers/CustomCodeHandler.js +102 -0
- package/dist/core/handlers/CustomCodeHandler.js.map +1 -0
- package/dist/core/handlers/DropdownHandler.d.ts +43 -0
- package/dist/core/handlers/DropdownHandler.js +304 -0
- package/dist/core/handlers/DropdownHandler.js.map +1 -0
- package/dist/core/handlers/InputHandler.d.ts +24 -0
- package/dist/core/handlers/InputHandler.js +197 -0
- package/dist/core/handlers/InputHandler.js.map +1 -0
- package/dist/core/registry/ActionRegistry.d.ts +8 -0
- package/dist/core/registry/ActionRegistry.js +35 -0
- package/dist/core/registry/ActionRegistry.js.map +1 -0
- package/dist/installer/frameworkLauncher.d.ts +31 -0
- package/dist/installer/frameworkLauncher.js +198 -0
- package/dist/installer/frameworkLauncher.js.map +1 -0
- package/dist/queue/ExecutionQueue.d.ts +52 -0
- package/dist/queue/ExecutionQueue.js +175 -0
- package/dist/queue/ExecutionQueue.js.map +1 -0
- package/dist/routes/api.routes.d.ts +2 -0
- package/dist/routes/api.routes.js +16 -0
- package/dist/routes/api.routes.js.map +1 -0
- package/dist/server.d.ts +1 -0
- package/dist/server.js +30 -0
- package/dist/server.js.map +1 -0
- package/dist/types/types.d.ts +135 -0
- package/dist/types/types.js +4 -0
- package/dist/types/types.js.map +1 -0
- package/dist/utils/elementHighlight.d.ts +35 -0
- package/dist/utils/elementHighlight.js +136 -0
- package/dist/utils/elementHighlight.js.map +1 -0
- package/dist/utils/locatorHelper.d.ts +7 -0
- package/dist/utils/locatorHelper.js +53 -0
- package/dist/utils/locatorHelper.js.map +1 -0
- package/dist/utils/logger.d.ts +12 -0
- package/dist/utils/logger.js +35 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/response.d.ts +4 -0
- package/dist/utils/response.js +25 -0
- package/dist/utils/response.js.map +1 -0
- package/dist/utils/responseFormatter.d.ts +78 -0
- package/dist/utils/responseFormatter.js +123 -0
- package/dist/utils/responseFormatter.js.map +1 -0
- package/dist/utils/sseManager.d.ts +32 -0
- package/dist/utils/sseManager.js +122 -0
- package/dist/utils/sseManager.js.map +1 -0
- package/lexxit-automation-framework-2.0.0.tgz +0 -0
- package/npmignore +5 -0
- package/package.json +36 -0
- package/scripts/postinstall.js +52 -0
- package/src/app.ts +27 -0
- package/src/controllers/controller.ts +282 -0
- package/src/core/BrowserManager.ts +398 -0
- package/src/core/PlaywrightEngine.ts +371 -0
- package/src/core/ScreenshotManager.ts +25 -0
- package/src/core/TestData.ts +25 -0
- package/src/core/TestExecutor.ts +436 -0
- package/src/core/handlers/AllHandlers.ts +626 -0
- package/src/core/handlers/BaseHandler.ts +41 -0
- package/src/core/handlers/ClickHandler.ts +482 -0
- package/src/core/handlers/CustomCodeHandler.ts +123 -0
- package/src/core/handlers/DropdownHandler.ts +438 -0
- package/src/core/handlers/InputHandler.ts +192 -0
- package/src/core/registry/ActionRegistry.ts +31 -0
- package/src/installer/frameworkLauncher.ts +242 -0
- package/src/installer/install.sh +107 -0
- package/src/public/dashboard.html +540 -0
- package/src/public/queue-monitor.html +190 -0
- package/src/queue/ExecutionQueue.ts +200 -0
- package/src/routes/api.routes.ts +16 -0
- package/src/server.ts +29 -0
- package/src/types/types.ts +169 -0
- package/src/utils/elementHighlight.ts +174 -0
- package/src/utils/locatorHelper.ts +49 -0
- package/src/utils/logger.ts +40 -0
- package/src/utils/response.ts +27 -0
- package/src/utils/responseFormatter.ts +167 -0
- package/src/utils/sseManager.ts +127 -0
- package/tsconfig.json +18 -0
- package/videos/fb1b94b6-6639-4c9a-82bb-63572606f403/page@5bd5c6c8b62baa700e9810cdd64f5c49.webm +0 -0
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
import { Page } from "playwright";
|
|
2
|
+
import { ActionResponse, TestCaseConfig } from "../../types/types";
|
|
3
|
+
import { pass, fail } from "../../utils/response";
|
|
4
|
+
import { buildLocatorString } from "../../utils/locatorHelper";
|
|
5
|
+
import { BaseHandler } from "./BaseHandler";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* DropdownHandler
|
|
9
|
+
* ===============
|
|
10
|
+
* ROOT CAUSE OF DROPDOWN DELAY (fixed):
|
|
11
|
+
* - Previous: using networkidle wait after every action
|
|
12
|
+
* - Fix: Playwright's selectOption() is synchronous once visible.
|
|
13
|
+
* We waitFor visible then immediately selectOption. No extra waits.
|
|
14
|
+
*
|
|
15
|
+
* Also supports:
|
|
16
|
+
* - Custom dropdowns (non-<select>) via click-then-click pattern
|
|
17
|
+
* - Multi-select
|
|
18
|
+
* - Autocomplete / combobox
|
|
19
|
+
*/
|
|
20
|
+
export class DropdownHandler extends BaseHandler {
|
|
21
|
+
constructor(tConfig: TestCaseConfig, page: Page) { super(tConfig, page); }
|
|
22
|
+
|
|
23
|
+
getActions() {
|
|
24
|
+
return {
|
|
25
|
+
selectdropdown: this.selectDropdown.bind(this),
|
|
26
|
+
selectMuiDropdown: this.selectMuiDropdown.bind(this),
|
|
27
|
+
selectbyindex: this.selectByIndex.bind(this),
|
|
28
|
+
selectbyvalue: this.selectByValue.bind(this),
|
|
29
|
+
multiselect: this.multiSelect.bind(this),
|
|
30
|
+
getdropdownoptions: this.getDropdownOptions.bind(this),
|
|
31
|
+
getselectedoption: this.getSelectedOption.bind(this),
|
|
32
|
+
selectcustomdropdown: this.selectCustomDropdown.bind(this),
|
|
33
|
+
selectautocomplete: this.selectAutocomplete.bind(this),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
async selectMuiDropdown(args: string[], slabel?: string): Promise<ActionResponse> {
|
|
37
|
+
const [strategy, locator, value] = args;
|
|
38
|
+
const displayLabel = slabel || "unknown";
|
|
39
|
+
|
|
40
|
+
if (!strategy || !locator || !value) {
|
|
41
|
+
return fail("selectMuiDropdown needs: strategy, locator, value", "Missing args");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const ls = buildLocatorString(strategy, locator);
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const dropdown = this.page.locator(ls);
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
await dropdown.waitFor({ state: "visible", timeout: this.timeout });
|
|
51
|
+
await this.highlight(ls, "select", `Opening dropdown`);
|
|
52
|
+
await dropdown.click();
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
const option = this.page.locator(`//li[normalize-space()="${value}"]`);
|
|
56
|
+
|
|
57
|
+
await option.waitFor({ state: "visible", timeout: this.timeout });
|
|
58
|
+
await option.click();
|
|
59
|
+
|
|
60
|
+
await this.unhighlight();
|
|
61
|
+
|
|
62
|
+
const sc = await this.shots.onPass(this.page);
|
|
63
|
+
|
|
64
|
+
return pass(
|
|
65
|
+
`Selected "${value}" from ${displayLabel}`,
|
|
66
|
+
sc,
|
|
67
|
+
{ expected_result: `"${value}" selected successfully in "${displayLabel}"` }
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
} catch (error: any) {
|
|
71
|
+
await this.unhighlight();
|
|
72
|
+
const sc = await this.shots.onFail(this.page);
|
|
73
|
+
|
|
74
|
+
const isNotFound = /timeout|waiting for locator|not visible|not found/i.test(error.message);
|
|
75
|
+
|
|
76
|
+
const errorMsg = isNotFound
|
|
77
|
+
? `"${displayLabel}" not found`
|
|
78
|
+
: `Failed selectMuiDropdown "${value}" in ${displayLabel} — ${error.message}`;
|
|
79
|
+
|
|
80
|
+
return fail(
|
|
81
|
+
errorMsg,
|
|
82
|
+
undefined,
|
|
83
|
+
sc,
|
|
84
|
+
{ expected_result: `"${value}" selected successfully in "${displayLabel}"` }
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// tSetup.selectDropdown('xpath', '//select[...]', 'Option Label')
|
|
89
|
+
// ✅ OPTIMIZED: no networkidle, just waitFor visible then selectOption immediately
|
|
90
|
+
async selectDropdown(args: string[], slabel?: string): Promise<ActionResponse> {
|
|
91
|
+
const [strategy, locator, value] = args;
|
|
92
|
+
const displayLabel = slabel || "unknown";
|
|
93
|
+
|
|
94
|
+
if (!strategy || !locator || !value) {
|
|
95
|
+
return fail(
|
|
96
|
+
"selectDropdown needs: strategy, locator, visibleText",
|
|
97
|
+
"Missing args"
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const ls = buildLocatorString(strategy, locator);
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
const el = this.page.locator(ls);
|
|
105
|
+
await el.waitFor({ state: "visible", timeout: this.timeout });
|
|
106
|
+
|
|
107
|
+
// ✅ Highlight before action
|
|
108
|
+
await this.highlight(ls, "select", `Selecting: "${value}"`);
|
|
109
|
+
|
|
110
|
+
const byLabel = el.selectOption({ label: value }, { timeout: this.timeout });
|
|
111
|
+
|
|
112
|
+
const byValue = el.selectOption({ value }, { timeout: this.timeout });
|
|
113
|
+
|
|
114
|
+
const byPartialText = (async () => {
|
|
115
|
+
const options = await el.locator("option").all();
|
|
116
|
+
for (const opt of options) {
|
|
117
|
+
const txt = (await opt.textContent() ?? "").trim();
|
|
118
|
+
if (txt.toLowerCase().includes(value.toLowerCase())) {
|
|
119
|
+
const val = await opt.getAttribute("value");
|
|
120
|
+
if (!val) throw new Error(`Option "${value}" has no value`);
|
|
121
|
+
return el.selectOption({ value: val }, { timeout: this.timeout });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
throw new Error(`Option "${value}" not found`);
|
|
125
|
+
})();
|
|
126
|
+
|
|
127
|
+
await Promise.any([byLabel, byValue, byPartialText]);
|
|
128
|
+
|
|
129
|
+
await this.unhighlight();
|
|
130
|
+
|
|
131
|
+
const sc = await this.shots.onPass(this.page);
|
|
132
|
+
|
|
133
|
+
return pass(
|
|
134
|
+
`Selected "${value}" from ${displayLabel}`,
|
|
135
|
+
sc,
|
|
136
|
+
{ expected_result: `"${value}" selected successfully in "${displayLabel}"` }
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
} catch (error: any) {
|
|
140
|
+
await this.unhighlight();
|
|
141
|
+
const sc = await this.shots.onFail(this.page);
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
const isNotFound = /timeout|waiting for locator|not visible|not found/i.test(error.message);
|
|
145
|
+
|
|
146
|
+
const errorMsg = isNotFound
|
|
147
|
+
? `"${displayLabel}" not found`
|
|
148
|
+
: `Failed selectDropdown "${value}" in ${displayLabel} — ${error.message}`;
|
|
149
|
+
|
|
150
|
+
return fail(
|
|
151
|
+
errorMsg,
|
|
152
|
+
undefined,
|
|
153
|
+
sc,
|
|
154
|
+
{ expected_result: `"${value}" selected successfully in "${displayLabel}"` }
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
// tSetup.selectByIndex('xpath', '//select[...]', '2')
|
|
159
|
+
async selectByIndex(args: string[], slabel?: string): Promise<ActionResponse> {
|
|
160
|
+
const [strategy, locator, indexStr] = args;
|
|
161
|
+
const displayLabel = slabel || "unknown";
|
|
162
|
+
const index = parseInt(indexStr, 10);
|
|
163
|
+
|
|
164
|
+
if (!strategy || !locator || isNaN(index)) {
|
|
165
|
+
return fail("selectByIndex needs: strategy, locator, index", "Missing args");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const ls = buildLocatorString(strategy, locator);
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
const el = this.page.locator(ls);
|
|
172
|
+
await el.waitFor({ state: "visible", timeout: this.timeout });
|
|
173
|
+
|
|
174
|
+
await this.highlight(ls, "select", `Selecting index: ${index}`);
|
|
175
|
+
await el.selectOption({ index }, { timeout: this.timeout });
|
|
176
|
+
await this.unhighlight();
|
|
177
|
+
|
|
178
|
+
const sc = await this.shots.onPass(this.page);
|
|
179
|
+
|
|
180
|
+
return pass(
|
|
181
|
+
`Selected index ${index} in ${displayLabel}`,
|
|
182
|
+
sc,
|
|
183
|
+
{ expected_result: `Index ${index} selected successfully in "${displayLabel}"` }
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
} catch (error: any) {
|
|
187
|
+
await this.unhighlight();
|
|
188
|
+
const sc = await this.shots.onFail(this.page);
|
|
189
|
+
|
|
190
|
+
const isNotFound = /timeout|waiting for locator|not visible|not found/i.test(error.message);
|
|
191
|
+
|
|
192
|
+
const errorMsg = isNotFound
|
|
193
|
+
? `"${displayLabel}" not found`
|
|
194
|
+
: `Failed selectByIndex ${index} in ${displayLabel} — ${error.message}`;
|
|
195
|
+
|
|
196
|
+
return fail(errorMsg, undefined, sc);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async selectByValue(args: string[], slabel?: string): Promise<ActionResponse> {
|
|
201
|
+
const [strategy, locator, value] = args;
|
|
202
|
+
const displayLabel = slabel || "unknown";
|
|
203
|
+
|
|
204
|
+
if (!strategy || !locator || !value) {
|
|
205
|
+
return fail("selectByValue needs: strategy, locator, value", "Missing args");
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const ls = buildLocatorString(strategy, locator);
|
|
209
|
+
|
|
210
|
+
try {
|
|
211
|
+
const el = this.page.locator(ls);
|
|
212
|
+
await el.waitFor({ state: "visible", timeout: this.timeout });
|
|
213
|
+
|
|
214
|
+
await this.highlight(ls, "select", `Selecting value: "${value}"`);
|
|
215
|
+
await el.selectOption({ value }, { timeout: this.timeout });
|
|
216
|
+
await this.unhighlight();
|
|
217
|
+
|
|
218
|
+
const sc = await this.shots.onPass(this.page);
|
|
219
|
+
|
|
220
|
+
return pass(
|
|
221
|
+
`Selected value "${value}" in ${displayLabel}`,
|
|
222
|
+
sc,
|
|
223
|
+
{ expected_result: `"${value}" selected successfully in "${displayLabel}"` }
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
} catch (error: any) {
|
|
227
|
+
await this.unhighlight();
|
|
228
|
+
const sc = await this.shots.onFail(this.page);
|
|
229
|
+
|
|
230
|
+
const isNotFound = /timeout|waiting for locator|not visible|not found/i.test(error.message);
|
|
231
|
+
|
|
232
|
+
const errorMsg = isNotFound
|
|
233
|
+
? `"${displayLabel}" not found`
|
|
234
|
+
: `Failed selectByValue "${value}" in ${displayLabel} — ${error.message}`;
|
|
235
|
+
|
|
236
|
+
return fail(errorMsg, undefined, sc);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async multiSelect(args: string[], slabel?: string): Promise<ActionResponse> {
|
|
241
|
+
const [strategy, locator, ...values] = args;
|
|
242
|
+
const displayLabel = slabel || "unknown";
|
|
243
|
+
|
|
244
|
+
if (!strategy || !locator || values.length === 0) {
|
|
245
|
+
return fail("multiSelect needs: strategy, locator, ...values", "Missing args");
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const ls = buildLocatorString(strategy, locator);
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
const el = this.page.locator(ls);
|
|
252
|
+
await el.waitFor({ state: "visible", timeout: this.timeout });
|
|
253
|
+
|
|
254
|
+
await this.highlight(ls, "select", `Selecting: ${values.join(", ")}`);
|
|
255
|
+
await el.selectOption(values.map(v => ({ label: v })), { timeout: this.timeout });
|
|
256
|
+
await this.unhighlight();
|
|
257
|
+
|
|
258
|
+
const sc = await this.shots.onPass(this.page);
|
|
259
|
+
|
|
260
|
+
return pass(
|
|
261
|
+
`Multi-selected [${values.join(", ")}] in ${displayLabel}`,
|
|
262
|
+
sc,
|
|
263
|
+
{ expected_result: `Values [${values.join(", ")}] selected in "${displayLabel}"` }
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
} catch (error: any) {
|
|
267
|
+
await this.unhighlight();
|
|
268
|
+
const sc = await this.shots.onFail(this.page);
|
|
269
|
+
|
|
270
|
+
const isNotFound = /timeout|waiting for locator|not visible|not found/i.test(error.message);
|
|
271
|
+
|
|
272
|
+
const errorMsg = isNotFound
|
|
273
|
+
? `"${displayLabel}" not found`
|
|
274
|
+
: `Failed multiSelect in ${displayLabel} — ${error.message}`;
|
|
275
|
+
|
|
276
|
+
return fail(errorMsg, undefined, sc);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
async getDropdownOptions(args: string[], slabel?: string): Promise<ActionResponse> {
|
|
280
|
+
const [strategy, locator, storeKey] = args;
|
|
281
|
+
const displayLabel = slabel || "unknown";
|
|
282
|
+
|
|
283
|
+
if (!strategy || !locator) {
|
|
284
|
+
return fail("getDropdownOptions needs: strategy, locator", "Missing args");
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
try {
|
|
288
|
+
const el = this.page.locator(buildLocatorString(strategy, locator));
|
|
289
|
+
await el.waitFor({ state: "attached", timeout: this.timeout });
|
|
290
|
+
|
|
291
|
+
const texts = await el.locator("option").allTextContents();
|
|
292
|
+
if (storeKey) this.testData.set(storeKey, texts);
|
|
293
|
+
|
|
294
|
+
const sc = await this.shots.onPass(this.page);
|
|
295
|
+
|
|
296
|
+
return pass(
|
|
297
|
+
`Options: [${texts.join(", ")}] in ${displayLabel}`,
|
|
298
|
+
sc,
|
|
299
|
+
{ options: texts }
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
} catch (error: any) {
|
|
303
|
+
const sc = await this.shots.onFail(this.page);
|
|
304
|
+
|
|
305
|
+
const isNotFound = /timeout|waiting for locator|not visible|not found/i.test(error.message);
|
|
306
|
+
|
|
307
|
+
const errorMsg = isNotFound
|
|
308
|
+
? `"${displayLabel}" not found`
|
|
309
|
+
: `Failed getDropdownOptions in ${displayLabel} — ${error.message}`;
|
|
310
|
+
|
|
311
|
+
return fail(errorMsg, undefined, sc);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
async getSelectedOption(args: string[], slabel?: string): Promise<ActionResponse> {
|
|
316
|
+
const [strategy, locator, storeKey] = args;
|
|
317
|
+
const displayLabel = slabel || "unknown";
|
|
318
|
+
|
|
319
|
+
if (!strategy || !locator) {
|
|
320
|
+
return fail("getSelectedOption needs: strategy, locator", "Missing args");
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
try {
|
|
324
|
+
const el = this.page.locator(buildLocatorString(strategy, locator));
|
|
325
|
+
await el.waitFor({ state: "attached", timeout: this.timeout });
|
|
326
|
+
|
|
327
|
+
const text = ((await el.locator("option:checked").textContent()) ?? "").trim();
|
|
328
|
+
if (storeKey) this.testData.set(storeKey, text);
|
|
329
|
+
|
|
330
|
+
const sc = await this.shots.onPass(this.page);
|
|
331
|
+
|
|
332
|
+
return pass(
|
|
333
|
+
`Selected: "${text}" in ${displayLabel}`,
|
|
334
|
+
sc,
|
|
335
|
+
{ selected: text }
|
|
336
|
+
);
|
|
337
|
+
|
|
338
|
+
} catch (error: any) {
|
|
339
|
+
const sc = await this.shots.onFail(this.page);
|
|
340
|
+
|
|
341
|
+
const isNotFound = /timeout|waiting for locator|not visible|not found/i.test(error.message);
|
|
342
|
+
|
|
343
|
+
const errorMsg = isNotFound
|
|
344
|
+
? `"${displayLabel}" not found`
|
|
345
|
+
: `Failed getSelectedOption in ${displayLabel} — ${error.message}`;
|
|
346
|
+
|
|
347
|
+
return fail(errorMsg, undefined, sc);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
async selectCustomDropdown(args: string[], slabel?: string): Promise<ActionResponse> {
|
|
352
|
+
const [ts, tl, os, ol] = args;
|
|
353
|
+
const displayLabel = slabel || "unknown";
|
|
354
|
+
|
|
355
|
+
if (!ts || !tl || !os || !ol) {
|
|
356
|
+
return fail("selectCustomDropdown needs locators", "Missing args");
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
try {
|
|
360
|
+
const trigger = this.page.locator(buildLocatorString(ts, tl));
|
|
361
|
+
await trigger.waitFor({ state: "visible", timeout: this.timeout });
|
|
362
|
+
|
|
363
|
+
await this.highlight(buildLocatorString(ts, tl), "select", "Opening dropdown");
|
|
364
|
+
await trigger.click();
|
|
365
|
+
|
|
366
|
+
const option = this.page.locator(buildLocatorString(os, ol));
|
|
367
|
+
await option.waitFor({ state: "visible", timeout: this.timeout });
|
|
368
|
+
await option.click();
|
|
369
|
+
|
|
370
|
+
await this.unhighlight();
|
|
371
|
+
|
|
372
|
+
const sc = await this.shots.onPass(this.page);
|
|
373
|
+
|
|
374
|
+
return pass(`Custom dropdown selected in ${displayLabel}`, sc);
|
|
375
|
+
|
|
376
|
+
} catch (error: any) {
|
|
377
|
+
await this.unhighlight();
|
|
378
|
+
const sc = await this.shots.onFail(this.page);
|
|
379
|
+
|
|
380
|
+
const isNotFound = /timeout|waiting for locator|not visible|not found/i.test(error.message);
|
|
381
|
+
|
|
382
|
+
const errorMsg = isNotFound
|
|
383
|
+
? `"${displayLabel}" not found`
|
|
384
|
+
: `Failed selectCustomDropdown in ${displayLabel} — ${error.message}`;
|
|
385
|
+
|
|
386
|
+
return fail(errorMsg, undefined, sc);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* tSetup.selectAutocomplete('xpath', '//input[@id="search"]', 'New York', 'xpath', '//li[text()="New York"]')
|
|
392
|
+
* Types into autocomplete input, waits for suggestion, clicks it
|
|
393
|
+
*/
|
|
394
|
+
async selectAutocomplete(args: string[], slabel?: string): Promise<ActionResponse> {
|
|
395
|
+
const [is, il, text, os, ol] = args;
|
|
396
|
+
const displayLabel = slabel || "unknown";
|
|
397
|
+
|
|
398
|
+
if (!is || !il || !text) {
|
|
399
|
+
return fail("selectAutocomplete needs input locator & text", "Missing args");
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
try {
|
|
403
|
+
const input = this.page.locator(buildLocatorString(is, il));
|
|
404
|
+
await input.waitFor({ state: "visible", timeout: this.timeout });
|
|
405
|
+
|
|
406
|
+
await this.highlight(buildLocatorString(is, il), "input", `Typing: "${text}"`);
|
|
407
|
+
|
|
408
|
+
await input.clear();
|
|
409
|
+
await input.pressSequentially(text, { delay: 80 });
|
|
410
|
+
|
|
411
|
+
if (os && ol) {
|
|
412
|
+
const option = this.page.locator(buildLocatorString(os, ol));
|
|
413
|
+
await option.waitFor({ state: "visible", timeout: this.timeout });
|
|
414
|
+
await option.click();
|
|
415
|
+
} else {
|
|
416
|
+
await input.press("Enter");
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
await this.unhighlight();
|
|
420
|
+
|
|
421
|
+
const sc = await this.shots.onPass(this.page);
|
|
422
|
+
|
|
423
|
+
return pass(`Autocomplete selected: "${text}"`, sc);
|
|
424
|
+
|
|
425
|
+
} catch (error: any) {
|
|
426
|
+
await this.unhighlight();
|
|
427
|
+
const sc = await this.shots.onFail(this.page);
|
|
428
|
+
|
|
429
|
+
const isNotFound = /timeout|waiting for locator|not visible|not found/i.test(error.message);
|
|
430
|
+
|
|
431
|
+
const errorMsg = isNotFound
|
|
432
|
+
? `"${displayLabel}" not found`
|
|
433
|
+
: `Failed selectAutocomplete "${text}" — ${error.message}`;
|
|
434
|
+
|
|
435
|
+
return fail(errorMsg, undefined, sc);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { Page } from "playwright";
|
|
2
|
+
import { ActionResponse, TestCaseConfig } from "../../types/types";
|
|
3
|
+
import { pass, fail } from "../../utils/response";
|
|
4
|
+
import { buildLocatorString } from "../../utils/locatorHelper";
|
|
5
|
+
import { BaseHandler } from "./BaseHandler";
|
|
6
|
+
import { failElementNotFound } from "../../utils/response";
|
|
7
|
+
|
|
8
|
+
export class InputHandler extends BaseHandler {
|
|
9
|
+
constructor(tConfig: TestCaseConfig, page: Page) { super(tConfig, page); }
|
|
10
|
+
|
|
11
|
+
getActions() {
|
|
12
|
+
return {
|
|
13
|
+
entertext: this.enterText.bind(this),
|
|
14
|
+
typetext: this.typeText.bind(this),
|
|
15
|
+
cleartext: this.clearText.bind(this),
|
|
16
|
+
appendtext: this.appendText.bind(this),
|
|
17
|
+
getinputvalue: this.getInputValue.bind(this),
|
|
18
|
+
setinputvalue: this.setInputValueByJS.bind(this),
|
|
19
|
+
uploadfile: this.uploadFile.bind(this),
|
|
20
|
+
enteriframe: this.enterTextInFrame.bind(this),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// tSetup.enterText('xpath', '//input[...]', 'hello@mail.com')
|
|
25
|
+
async enterText(args: string[], label?: string): Promise<ActionResponse> {
|
|
26
|
+
const [strategy, locator, rawText] = args;
|
|
27
|
+
const text = String(this.testData.resolve(rawText) ?? rawText ?? "");
|
|
28
|
+
const displayLabel = label || 'unknown';
|
|
29
|
+
|
|
30
|
+
if (!strategy || !locator) return fail("enterText needs: strategy, locator, text", "Missing args");
|
|
31
|
+
const ls = buildLocatorString(strategy, locator);
|
|
32
|
+
try {
|
|
33
|
+
const el = this.page.locator(buildLocatorString(strategy, locator));
|
|
34
|
+
await el.waitFor({ state: "visible", timeout: this.timeout });
|
|
35
|
+
// ✅ Highlight field before entering text
|
|
36
|
+
await this.highlight(ls, "input", `Entering: "${text}"`);
|
|
37
|
+
await el.clear();
|
|
38
|
+
await el.fill(text);
|
|
39
|
+
await this.unhighlight();
|
|
40
|
+
const sc = await this.shots.onPass(this.page);
|
|
41
|
+
return pass(
|
|
42
|
+
`Entered "${text}" into ${displayLabel}`,
|
|
43
|
+
sc,
|
|
44
|
+
{ expected_result: `"${text}" entered successfully in "${displayLabel}"` }
|
|
45
|
+
);
|
|
46
|
+
} catch (error: any) {
|
|
47
|
+
await this.unhighlight();
|
|
48
|
+
const sc = await this.shots.onFail(this.page);
|
|
49
|
+
// const isNotFound = /timeout|waiting for locator|not visible|not found/i.test(error.message);
|
|
50
|
+
// const errorMsg = isNotFound
|
|
51
|
+
// ? `"${displayLabel}" not found`
|
|
52
|
+
// : `Failed enterText into ${displayLabel} — ${error.message}`;
|
|
53
|
+
// return fail(errorMsg, undefined, sc, { expected_result: `"${text}" entered successfully in "${displayLabel}"` });
|
|
54
|
+
// }
|
|
55
|
+
const msg = error.message.toLowerCase();
|
|
56
|
+
if (msg.includes("waiting for locator") || msg.includes("timeout") || msg.includes("no element")) {
|
|
57
|
+
return failElementNotFound(displayLabel, sc);
|
|
58
|
+
}
|
|
59
|
+
return fail(`Failed enterText into ${displayLabel} — ${error.message}`, undefined, sc, { expected_result: `"${text}" entered successfully in "${displayLabel}"` });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// tSetup.typeText('xpath', '//input[...]', 'hello') — real char-by-char typing
|
|
64
|
+
async typeText(args: string[], label?: string): Promise<ActionResponse> {
|
|
65
|
+
const [strategy, locator, rawText, delayStr] = args;
|
|
66
|
+
const text = String(this.testData.resolve(rawText) ?? rawText ?? "");
|
|
67
|
+
const delay = parseInt(delayStr || "50", 10);
|
|
68
|
+
if (!strategy || !locator) return fail("typeText needs: strategy, locator, text, [delay]", "Missing args");
|
|
69
|
+
try {
|
|
70
|
+
const el = this.page.locator(buildLocatorString(strategy, locator));
|
|
71
|
+
await el.waitFor({ state: "visible", timeout: this.timeout });
|
|
72
|
+
await el.pressSequentially(text, { delay });
|
|
73
|
+
const sc = await this.shots.onPass(this.page);
|
|
74
|
+
return pass(`Typed "${text}" into ${label || 'unknown'}`, sc);
|
|
75
|
+
} catch (error: any) {
|
|
76
|
+
const sc = await this.shots.onFail(this.page);
|
|
77
|
+
return fail(`Failed typeText into ${label || 'unknown'}`, error.message, sc);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// tSetup.clearText('xpath', '//input[...]')
|
|
82
|
+
async clearText(args: string[], label?: string): Promise<ActionResponse> {
|
|
83
|
+
const [strategy, locator] = args;
|
|
84
|
+
if (!strategy || !locator) return fail("clearText needs: strategy, locator", "Missing args");
|
|
85
|
+
try {
|
|
86
|
+
const el = this.page.locator(buildLocatorString(strategy, locator));
|
|
87
|
+
await el.waitFor({ state: "visible", timeout: this.timeout });
|
|
88
|
+
await el.clear();
|
|
89
|
+
const sc = await this.shots.onPass(this.page);
|
|
90
|
+
return pass(`Cleared ${label || 'unknown'}`, sc);
|
|
91
|
+
} catch (error: any) {
|
|
92
|
+
const sc = await this.shots.onFail(this.page);
|
|
93
|
+
return fail(`Failed clearText ${label || 'unknown'}`, error.message, sc);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// tSetup.appendText('xpath', '//input[...]', ' extra')
|
|
98
|
+
async appendText(args: string[], label?: string): Promise<ActionResponse> {
|
|
99
|
+
const [strategy, locator, rawText] = args;
|
|
100
|
+
const text = String(this.testData.resolve(rawText) ?? rawText ?? "");
|
|
101
|
+
if (!strategy || !locator) return fail("appendText needs: strategy, locator, text", "Missing args");
|
|
102
|
+
try {
|
|
103
|
+
const el = this.page.locator(buildLocatorString(strategy, locator));
|
|
104
|
+
await el.waitFor({ state: "visible", timeout: this.timeout });
|
|
105
|
+
await el.focus();
|
|
106
|
+
await this.page.keyboard.press("End");
|
|
107
|
+
await el.pressSequentially(text, { delay: 30 });
|
|
108
|
+
const sc = await this.shots.onPass(this.page);
|
|
109
|
+
return pass(`Appended "${text}" to ${label || 'unknown'}`, sc);
|
|
110
|
+
} catch (error: any) {
|
|
111
|
+
const sc = await this.shots.onFail(this.page);
|
|
112
|
+
return fail(`Failed appendText into ${label || 'unknown'}]`, error.message, sc);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// tSetup.getInputValue('xpath', '//input[...]', 'storeKey')
|
|
117
|
+
async getInputValue(args: string[], label?: string): Promise<ActionResponse> {
|
|
118
|
+
const [strategy, locator, storeKey] = args;
|
|
119
|
+
if (!strategy || !locator) return fail("getInputValue needs: strategy, locator, [storeKey]", "Missing args");
|
|
120
|
+
try {
|
|
121
|
+
const el = this.page.locator(buildLocatorString(strategy, locator));
|
|
122
|
+
await el.waitFor({ state: "attached", timeout: this.timeout });
|
|
123
|
+
const value = await el.inputValue();
|
|
124
|
+
if (storeKey) this.testData.set(storeKey, value);
|
|
125
|
+
const sc = await this.shots.onPass(this.page);
|
|
126
|
+
return pass(`Input value: "${value}"`, sc, { value });
|
|
127
|
+
} catch (error: any) {
|
|
128
|
+
const sc = await this.shots.onFail(this.page);
|
|
129
|
+
return fail(`Failed getInputValue ${label || 'unknown'}`, error.message, sc);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// tSetup.setInputValue('xpath', '//input[...]', 'value') — JS set (bypasses React controlled input)
|
|
134
|
+
async setInputValueByJS(args: string[], label?: string): Promise<ActionResponse> {
|
|
135
|
+
const [strategy, locator, rawText] = args;
|
|
136
|
+
const value = String(this.testData.resolve(rawText) ?? rawText ?? "");
|
|
137
|
+
const text = String(this.testData.resolve(rawText) ?? rawText ?? "");
|
|
138
|
+
if (!strategy || !locator) return fail("setInputValue needs: strategy, locator, text", "Missing args");
|
|
139
|
+
const ls = buildLocatorString(strategy, locator);
|
|
140
|
+
try {
|
|
141
|
+
const el = this.page.locator(buildLocatorString(strategy, locator));
|
|
142
|
+
await el.waitFor({ state: "visible", timeout: this.timeout });
|
|
143
|
+
await this.highlight(ls, "input", `Set: "${value}"`);
|
|
144
|
+
await el.evaluate((node: HTMLInputElement, val: string) => {
|
|
145
|
+
const setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value")?.set;
|
|
146
|
+
setter?.call(node, val);
|
|
147
|
+
node.dispatchEvent(new Event("input", { bubbles: true }));
|
|
148
|
+
node.dispatchEvent(new Event("change", { bubbles: true }));
|
|
149
|
+
}, text);
|
|
150
|
+
await this.unhighlight();
|
|
151
|
+
const sc = await this.shots.onPass(this.page);
|
|
152
|
+
return pass(`Set input value "${text}" via JS ${label || 'unknown'}`, sc);
|
|
153
|
+
} catch (error: any) {
|
|
154
|
+
await this.unhighlight();
|
|
155
|
+
const sc = await this.shots.onFail(this.page);
|
|
156
|
+
return fail(`Failed setInputValue ${label || 'unknown'}`, error.message, sc);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// tSetup.uploadFile('xpath', '//input[@type="file"]', '/path/to/file.pdf')
|
|
161
|
+
async uploadFile(args: string[], label?: string): Promise<ActionResponse> {
|
|
162
|
+
const [strategy, locator, filePath] = args;
|
|
163
|
+
if (!strategy || !locator || !filePath) return fail("uploadFile needs: strategy, locator, filePath", "Missing args");
|
|
164
|
+
try {
|
|
165
|
+
const el = this.page.locator(buildLocatorString(strategy, locator));
|
|
166
|
+
await el.setInputFiles(filePath);
|
|
167
|
+
const sc = await this.shots.onPass(this.page);
|
|
168
|
+
return pass(`Uploaded file: "${filePath}"`, sc);
|
|
169
|
+
} catch (error: any) {
|
|
170
|
+
const sc = await this.shots.onFail(this.page);
|
|
171
|
+
return fail(`Failed uploadFile ${label || 'unknown'}`, error.message, sc);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// tSetup.enterIframe('iframe[name="myFrame"]', 'xpath', '//input[...]', 'value')
|
|
176
|
+
async enterTextInFrame(args: string[], label?: string): Promise<ActionResponse> {
|
|
177
|
+
const [frameSelector, strategy, locator, rawText] = args;
|
|
178
|
+
const text = String(this.testData.resolve(rawText) ?? rawText ?? "");
|
|
179
|
+
if (!frameSelector || !strategy || !locator) return fail("enterIframe needs: frameSelector, strategy, locator, text", "Missing args");
|
|
180
|
+
try {
|
|
181
|
+
const frame = this.page.frameLocator(frameSelector);
|
|
182
|
+
const el = frame.locator(buildLocatorString(strategy, locator));
|
|
183
|
+
await el.waitFor({ state: "visible", timeout: this.timeout });
|
|
184
|
+
await el.fill(text);
|
|
185
|
+
const sc = await this.shots.onPass(this.page);
|
|
186
|
+
return pass(`Entered "${text}" in frame [${frameSelector}] → [${label || 'unknown'}]`, sc);
|
|
187
|
+
} catch (error: any) {
|
|
188
|
+
const sc = await this.shots.onFail(this.page);
|
|
189
|
+
return fail(`Failed enterIframe ${label || 'unknown'}`, error.message, sc);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { ActionResponse, ActionHandler, IActionHandler } from "../../types/types";
|
|
2
|
+
import { Logger } from "../../utils/logger";
|
|
3
|
+
|
|
4
|
+
const log = Logger.create("ActionRegistry");
|
|
5
|
+
|
|
6
|
+
export class ActionRegistry {
|
|
7
|
+
private handlers: Map<string, (args: string[], label?: string) => Promise<ActionResponse | undefined>> = new Map();
|
|
8
|
+
|
|
9
|
+
register(handler: IActionHandler): void {
|
|
10
|
+
for (const [name, fn] of Object.entries(handler.getActions())) {
|
|
11
|
+
const key = name.toLowerCase();
|
|
12
|
+
if (this.handlers.has(key)) log.warn(`Action "${key}" overwritten`);
|
|
13
|
+
this.handlers.set(key, fn);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async execute(action: string, args: string[], label?: string): Promise<ActionResponse | undefined> {
|
|
18
|
+
const handler = this.handlers.get(action.toLowerCase());
|
|
19
|
+
if (!handler) {
|
|
20
|
+
log.error(`Unknown action: "${action}"`);
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
if (label =='' || label == undefined){
|
|
24
|
+
return handler(args);}
|
|
25
|
+
else{
|
|
26
|
+
return handler(args, label);}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
has(action: string): boolean { return this.handlers.has(action.toLowerCase()); }
|
|
30
|
+
listActions(): string[] { return Array.from(this.handlers.keys()).sort(); }
|
|
31
|
+
}
|