app-manager-edge-worker 1.3.0 → 1.3.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 +8 -0
- package/package.json +1 -1
- package/src/banner.js +1 -1
- package/src/client.js +1 -1
- package/src/handlers/interactiveHandler.js +235 -37
package/README.md
CHANGED
|
@@ -201,6 +201,14 @@ Log levels: `[INFO]` · `[SUCCESS]` · `[WARN]` · `[ERROR]` · `[TASK]` (job di
|
|
|
201
201
|
|
|
202
202
|
---
|
|
203
203
|
|
|
204
|
+
### v1.3.1
|
|
205
|
+
- **Robust Portal Input Resolution & Dynamic SPA Lifecycle Engine**:
|
|
206
|
+
- **Dynamic Multi-Selector Fallback Generation**: Form input selectors now automatically generate and evaluate candidate fallbacks (e.g. `input[name="for_gstin"]` expands to include `#for_gstin`, `[name="for_gstin"]`, `input#for_gstin`, `input[data-ng-model*="gstin"]`, and placeholder variants), resolving elements regardless of framework attribute formatting.
|
|
207
|
+
- **IFrame-Aware Element Discovery**: Introduced `locateElementInPageOrFrames` across `HITL_SESSION_INIT`, `HITL_SESSION_REFRESH`, and `HITL_SESSION_SOLVE`, querying the main page as well as all nested frames/iframes for inputs, captchas, and submit buttons.
|
|
208
|
+
- **Realistic Timeout Scaling**: Replaced fragile hardcoded timeouts (4s - 6s) with dynamic portal timeout scaling (`Math.max(timeoutMs, 20000)`), granting heavy client-side frameworks (AngularJS, React) sufficient headroom to download scripts and compile views.
|
|
209
|
+
- **AngularJS Scope & Model Synchronization**: Typed input values now dispatch full keyboard cascades (`input`, `change`, `keydown`, `keyup`, `blur`) and directly synchronize AngularJS `ngModel` (`$setViewValue`, `$render`, `$apply`) to trigger dynamic validation and captcha loaders (e.g. `showCap1`).
|
|
210
|
+
- **Actionable Diagnostic Logging**: Failure to locate an element now logs the browser's active URL and document title in terminal output with detailed context.
|
|
211
|
+
|
|
204
212
|
### v1.3.0
|
|
205
213
|
- **Multi-Error Identifier Rules & Inspection Resilience Engine**:
|
|
206
214
|
- **Custom Error Identifier Rules**: Supported typed error detection rules (`errorDetection.rules`) allowing classification of multiple distinct errors (e.g. `INVALID_GSTIN`, `INVALID_CAPTCHA`, `NOT_FOUND`) with tailored actions (`RETRY_CAPTCHA`, `BUSINESS_ERROR`, `ABORT_SESSION`).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "app-manager-edge-worker",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.1",
|
|
4
4
|
"description": "Distributed Scraper Edge Worker for Apps Manager. Run on any local machine, VPS, or Termux Android device to serve browser and HTTP scraping jobs.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/banner.js
CHANGED
package/src/client.js
CHANGED
|
@@ -20,7 +20,7 @@ import fs from "fs";
|
|
|
20
20
|
import { getWhatsAppHandler } from "./handlers/whatsappHandler.js";
|
|
21
21
|
import { printBanner, logInfo, logSuccess, logWarn, logError, logJob } from "./banner.js";
|
|
22
22
|
|
|
23
|
-
let workerPkgVersion = "1.3.
|
|
23
|
+
let workerPkgVersion = "1.3.1";
|
|
24
24
|
try {
|
|
25
25
|
const pkg = JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
|
|
26
26
|
if (pkg?.version) workerPkgVersion = pkg.version;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import crypto from "crypto";
|
|
2
2
|
import localBrowserManager from "../browser.js";
|
|
3
3
|
import { runTransformPipeline, castValueToType } from "./transformUtils.js";
|
|
4
|
+
import { logInfo, logWarn, logError } from "../banner.js";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* In-memory map of active interactive sessions on this Edge Worker.
|
|
@@ -93,6 +94,82 @@ async function purgeSession(sessionId) {
|
|
|
93
94
|
activeSessions.delete(sessionId);
|
|
94
95
|
}
|
|
95
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Constructs a robust set of CSS selectors for an input field based on its configured selector and paramKey.
|
|
99
|
+
* Handles IDs, name attributes, ng-model attributes, placeholders, and comma-separated selectors.
|
|
100
|
+
*/
|
|
101
|
+
function buildRobustInputSelectors(selector, paramKey) {
|
|
102
|
+
const set = new Set();
|
|
103
|
+
if (selector && typeof selector === "string") {
|
|
104
|
+
selector.split(",").map((s) => s.trim()).filter(Boolean).forEach((s) => set.add(s));
|
|
105
|
+
|
|
106
|
+
// If selector contains [name="xxx"] or [name='xxx'], extract the name
|
|
107
|
+
const nameMatch = selector.match(/\[name=["']?([^"'\]\s]+)["']?\]/i);
|
|
108
|
+
if (nameMatch && nameMatch[1]) {
|
|
109
|
+
const name = nameMatch[1];
|
|
110
|
+
set.add(`#${name}`);
|
|
111
|
+
set.add(`input[name="${name}"]`);
|
|
112
|
+
set.add(`[name="${name}"]`);
|
|
113
|
+
set.add(`input#${name}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// If selector contains #xxx or [id="xxx"], extract the id
|
|
117
|
+
const idMatch = selector.match(/#([a-zA-Z0-9_\-]+)/) || selector.match(/\[id=["']?([^"'\]\s]+)["']?\]/i);
|
|
118
|
+
if (idMatch && idMatch[1]) {
|
|
119
|
+
const id = idMatch[1];
|
|
120
|
+
set.add(`#${id}`);
|
|
121
|
+
set.add(`input[name="${id}"]`);
|
|
122
|
+
set.add(`[name="${id}"]`);
|
|
123
|
+
set.add(`input#${id}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (paramKey && typeof paramKey === "string") {
|
|
128
|
+
const pk = paramKey.trim();
|
|
129
|
+
set.add(`#${pk}`);
|
|
130
|
+
set.add(`input[name="${pk}"]`);
|
|
131
|
+
set.add(`[name="${pk}"]`);
|
|
132
|
+
set.add(`input[data-ng-model*="${pk}"]`);
|
|
133
|
+
set.add(`input[ng-model*="${pk}"]`);
|
|
134
|
+
set.add(`input[placeholder*="${pk}" i]`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return Array.from(set);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Searches for an element in the main page or any child iframe, waiting up to timeoutMs.
|
|
142
|
+
* Returns { target: page | frame, element, matchedSelector } or null.
|
|
143
|
+
*/
|
|
144
|
+
async function locateElementInPageOrFrames(page, selectorList, timeoutMs = 15000) {
|
|
145
|
+
const combinedSelector = Array.isArray(selectorList) ? selectorList.join(", ") : String(selectorList);
|
|
146
|
+
|
|
147
|
+
// 1. Try waiting on the top page first
|
|
148
|
+
try {
|
|
149
|
+
const el = await page.waitForSelector(combinedSelector, { timeout: timeoutMs });
|
|
150
|
+
if (el) {
|
|
151
|
+
return { target: page, element: el, matchedSelector: combinedSelector };
|
|
152
|
+
}
|
|
153
|
+
} catch (_) {
|
|
154
|
+
// Top-level wait timed out or failed; inspect child frames next
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// 2. Scan child frames/iframes if not found on top-level page
|
|
158
|
+
try {
|
|
159
|
+
const frames = page.frames();
|
|
160
|
+
for (const frame of frames) {
|
|
161
|
+
try {
|
|
162
|
+
const fEl = await frame.$(combinedSelector);
|
|
163
|
+
if (fEl) {
|
|
164
|
+
return { target: frame, element: fEl, matchedSelector: combinedSelector };
|
|
165
|
+
}
|
|
166
|
+
} catch (_) {}
|
|
167
|
+
}
|
|
168
|
+
} catch (_) {}
|
|
169
|
+
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
|
|
96
173
|
/**
|
|
97
174
|
* 1. Handles HITL_SESSION_INIT:
|
|
98
175
|
* Launches page, pre-fills initial form inputs, captures captcha screenshot,
|
|
@@ -106,6 +183,7 @@ export async function handleInteractiveSessionInit(payload) {
|
|
|
106
183
|
captchaConfig = {},
|
|
107
184
|
preScrapeActions = [],
|
|
108
185
|
authentication = null,
|
|
186
|
+
navigation = {},
|
|
109
187
|
timeoutMs = 30000,
|
|
110
188
|
fieldMappings = [],
|
|
111
189
|
collections = [],
|
|
@@ -146,10 +224,13 @@ export async function handleInteractiveSessionInit(payload) {
|
|
|
146
224
|
}
|
|
147
225
|
}
|
|
148
226
|
|
|
149
|
-
// Navigate to target portal
|
|
150
|
-
|
|
227
|
+
// Navigate to target portal with flexible waitUntil and realistic timeout
|
|
228
|
+
const navTimeout = navigation.timeoutMs || timeoutMs || 30000;
|
|
229
|
+
const navWaitUntil = navigation.waitUntil || "domcontentloaded";
|
|
230
|
+
|
|
231
|
+
await page.goto(url, { waitUntil: navWaitUntil, timeout: navTimeout });
|
|
151
232
|
try {
|
|
152
|
-
await page.waitForNetworkIdle({ idleTime: 500, timeout:
|
|
233
|
+
await page.waitForNetworkIdle({ idleTime: 500, timeout: 4000 }).catch(() => {});
|
|
153
234
|
} catch (_) {}
|
|
154
235
|
|
|
155
236
|
// Execute pre-scrape actions if present
|
|
@@ -157,14 +238,15 @@ export async function handleInteractiveSessionInit(payload) {
|
|
|
157
238
|
for (const action of preScrapeActions) {
|
|
158
239
|
try {
|
|
159
240
|
const actType = (action.type || action.actionType || "").toUpperCase();
|
|
241
|
+
const actionTimeout = Math.max(action.timeoutMs || 0, 10000);
|
|
160
242
|
if (actType === "CLICK" && action.selector) {
|
|
161
|
-
await page.waitForSelector(action.selector, { timeout:
|
|
243
|
+
await page.waitForSelector(action.selector, { timeout: actionTimeout });
|
|
162
244
|
await page.click(action.selector);
|
|
163
245
|
} else if (actType === "TYPE" && action.selector) {
|
|
164
|
-
await page.waitForSelector(action.selector, { timeout:
|
|
246
|
+
await page.waitForSelector(action.selector, { timeout: actionTimeout });
|
|
165
247
|
await page.type(action.selector, action.value || "", { delay: 30 });
|
|
166
248
|
} else if (actType === "WAIT_FOR" && action.selector) {
|
|
167
|
-
await page.waitForSelector(action.selector, { timeout:
|
|
249
|
+
await page.waitForSelector(action.selector, { timeout: actionTimeout });
|
|
168
250
|
}
|
|
169
251
|
if (action.delayMs > 0) {
|
|
170
252
|
await new Promise((r) => setTimeout(r, Math.min(action.delayMs, 4000)));
|
|
@@ -175,47 +257,98 @@ export async function handleInteractiveSessionInit(payload) {
|
|
|
175
257
|
}
|
|
176
258
|
}
|
|
177
259
|
|
|
178
|
-
// Fill Form Inputs (e.g. GSTIN, PAN, registration number)
|
|
260
|
+
// Fill Form Inputs (e.g. GSTIN, PAN, registration number) with robust selector fallbacks & frame support
|
|
179
261
|
if (Array.isArray(formInputs) && formInputs.length > 0) {
|
|
180
262
|
for (const input of formInputs) {
|
|
181
263
|
if (!input.selector || input.value === undefined || input.value === null) continue;
|
|
182
|
-
|
|
264
|
+
|
|
265
|
+
const inputTimeout = Math.max(input.timeoutMs || 0, timeoutMs || 30000, 20000);
|
|
266
|
+
const robustSelectors = buildRobustInputSelectors(input.selector, input.paramKey);
|
|
267
|
+
|
|
268
|
+
const located = await locateElementInPageOrFrames(page, robustSelectors, inputTimeout);
|
|
269
|
+
if (!located || !located.element) {
|
|
270
|
+
const currentUrl = page.url();
|
|
271
|
+
const pageTitle = (await page.title().catch(() => "")) || "Untitled";
|
|
272
|
+
logWarn(
|
|
273
|
+
`[HITL_SESSION_INIT] Input selector '${input.selector}' (or fallbacks: ${robustSelectors.join(", ")}) not found after ${inputTimeout}ms on page "${pageTitle}" (${currentUrl})`
|
|
274
|
+
);
|
|
275
|
+
throw new Error(
|
|
276
|
+
`Waiting for selector '${input.selector}' failed on page "${pageTitle}" (${currentUrl}) after ${inputTimeout}ms. Ensure portal is reachable and selector is correct.`
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const { target, element, matchedSelector } = located;
|
|
183
281
|
|
|
184
282
|
if (input.clearFirst !== false) {
|
|
185
|
-
await
|
|
283
|
+
await element.click({ clickCount: 3 }).catch(() => {});
|
|
186
284
|
await page.keyboard.press("Backspace").catch(() => {});
|
|
187
285
|
}
|
|
188
286
|
|
|
189
287
|
if (input.actionType === "SELECT") {
|
|
190
|
-
await
|
|
288
|
+
await target.select(matchedSelector, String(input.value));
|
|
191
289
|
} else {
|
|
192
|
-
await
|
|
290
|
+
await element.type(String(input.value), { delay: input.delayMs || 30 });
|
|
193
291
|
}
|
|
194
292
|
|
|
195
|
-
// Explicitly dispatch input, change, and blur events
|
|
196
|
-
await
|
|
293
|
+
// Explicitly dispatch input, change, keydown, keyup, and blur events, plus sync AngularJS ngModel if present
|
|
294
|
+
await target.evaluate((sel) => {
|
|
197
295
|
const el = document.querySelector(sel);
|
|
198
296
|
if (el) {
|
|
199
297
|
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
200
298
|
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
299
|
+
el.dispatchEvent(new KeyboardEvent("keydown", { bubbles: true, key: "Enter" }));
|
|
300
|
+
el.dispatchEvent(new KeyboardEvent("keyup", { bubbles: true, key: "Enter" }));
|
|
201
301
|
el.dispatchEvent(new Event("blur", { bubbles: true }));
|
|
302
|
+
|
|
303
|
+
// AngularJS scope & ngModel synchronization
|
|
304
|
+
if (window.angular) {
|
|
305
|
+
try {
|
|
306
|
+
const ngEl = window.angular.element(el);
|
|
307
|
+
const ngModel = ngEl.controller("ngModel");
|
|
308
|
+
if (ngModel) {
|
|
309
|
+
ngModel.$setViewValue(el.value);
|
|
310
|
+
ngModel.$render();
|
|
311
|
+
ngModel.$commitViewValue();
|
|
312
|
+
}
|
|
313
|
+
const scope = ngEl.scope();
|
|
314
|
+
if (scope && !scope.$$phase) {
|
|
315
|
+
scope.$apply();
|
|
316
|
+
}
|
|
317
|
+
} catch (_) {}
|
|
318
|
+
}
|
|
202
319
|
}
|
|
203
|
-
},
|
|
204
|
-
await page.keyboard.press("Tab").catch(() => {});
|
|
320
|
+
}, matchedSelector).catch(() => {});
|
|
205
321
|
|
|
206
|
-
await
|
|
322
|
+
await page.keyboard.press("Tab").catch(() => {});
|
|
323
|
+
await new Promise((r) => setTimeout(r, 300));
|
|
207
324
|
}
|
|
208
325
|
}
|
|
209
326
|
|
|
210
|
-
// Locate and Screenshot Captcha DOM Element
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
327
|
+
// Locate and Screenshot Captcha DOM Element with robust candidate fallbacks
|
|
328
|
+
const imageTimeout = Math.max(captchaConfig.timeoutMs || 0, timeoutMs || 30000, 15000);
|
|
329
|
+
const candidateImageSelectors = [
|
|
330
|
+
captchaConfig.imageSelector,
|
|
331
|
+
"#imgCaptcha",
|
|
332
|
+
"#captchaImg",
|
|
333
|
+
"img[src*='captcha' i]",
|
|
334
|
+
"img[id*='captcha' i]",
|
|
335
|
+
"img[alt*='captcha' i]",
|
|
336
|
+
"canvas[id*='captcha' i]",
|
|
337
|
+
".captcha-image img",
|
|
338
|
+
"#captcha_image",
|
|
339
|
+
].filter(Boolean);
|
|
340
|
+
|
|
341
|
+
const locatedImage = await locateElementInPageOrFrames(page, candidateImageSelectors, imageTimeout);
|
|
342
|
+
if (!locatedImage || !locatedImage.element) {
|
|
343
|
+
const currentUrl = page.url();
|
|
344
|
+
const pageTitle = (await page.title().catch(() => "")) || "Untitled";
|
|
345
|
+
throw new Error(
|
|
346
|
+
`Captcha element not found with selector '${captchaConfig.imageSelector || candidateImageSelectors[0]}' on page "${pageTitle}" (${currentUrl}) after ${imageTimeout}ms`
|
|
347
|
+
);
|
|
217
348
|
}
|
|
218
349
|
|
|
350
|
+
const captchaElement = locatedImage.element;
|
|
351
|
+
|
|
219
352
|
// Capture screenshot of the exact visual captcha DOM node
|
|
220
353
|
const screenshotBuffer = await captchaElement.screenshot({
|
|
221
354
|
encoding: "binary",
|
|
@@ -291,19 +424,29 @@ export async function handleInteractiveSessionRefresh(payload) {
|
|
|
291
424
|
const imageSelector = cfg.imageSelector || "img[src*='captcha'], #imgCaptcha";
|
|
292
425
|
const refreshSelector = cfg.refreshSelector;
|
|
293
426
|
|
|
427
|
+
const candidateImageSelectors = [
|
|
428
|
+
cfg.imageSelector,
|
|
429
|
+
"#imgCaptcha",
|
|
430
|
+
"#captchaImg",
|
|
431
|
+
"img[src*='captcha' i]",
|
|
432
|
+
"img[id*='captcha' i]",
|
|
433
|
+
].filter(Boolean);
|
|
434
|
+
|
|
294
435
|
// 1. Snapshot current image hash and src attribute
|
|
295
|
-
const
|
|
296
|
-
if (!
|
|
436
|
+
const locatedOldImg = await locateElementInPageOrFrames(page, candidateImageSelectors, 8000);
|
|
437
|
+
if (!locatedOldImg || !locatedOldImg.element) {
|
|
297
438
|
throw new Error(`Captcha element not found with selector: ${imageSelector}`);
|
|
298
439
|
}
|
|
440
|
+
const oldEl = locatedOldImg.element;
|
|
441
|
+
const effectiveImageSelector = locatedOldImg.matchedSelector;
|
|
299
442
|
|
|
300
443
|
const oldBuffer = await oldEl.screenshot({ encoding: "binary" });
|
|
301
444
|
const oldHash = crypto.createHash("md5").update(oldBuffer).digest("hex");
|
|
302
445
|
|
|
303
|
-
const oldSrc = await
|
|
446
|
+
const oldSrc = await locatedOldImg.target.evaluate((sel) => {
|
|
304
447
|
const el = document.querySelector(sel);
|
|
305
448
|
return el ? el.getAttribute("src") : null;
|
|
306
|
-
},
|
|
449
|
+
}, effectiveImageSelector);
|
|
307
450
|
|
|
308
451
|
// 2. Click the reload button with robust candidate fallbacks
|
|
309
452
|
const candidateSelectors = [
|
|
@@ -328,9 +471,9 @@ export async function handleInteractiveSessionRefresh(payload) {
|
|
|
328
471
|
let clicked = false;
|
|
329
472
|
for (const sel of candidateSelectors) {
|
|
330
473
|
try {
|
|
331
|
-
const
|
|
332
|
-
if (
|
|
333
|
-
await
|
|
474
|
+
const locatedBtn = await locateElementInPageOrFrames(page, [sel], 500);
|
|
475
|
+
if (locatedBtn && locatedBtn.element) {
|
|
476
|
+
await locatedBtn.element.click();
|
|
334
477
|
clicked = true;
|
|
335
478
|
break;
|
|
336
479
|
}
|
|
@@ -442,15 +585,70 @@ export async function handleInteractiveSessionSolve(payload) {
|
|
|
442
585
|
|
|
443
586
|
const invalidPattern = cfg.errorDetection?.invalidCaptchaPattern || "captcha.*(incorrect|invalid|wrong|mismatch|enter text)";
|
|
444
587
|
|
|
445
|
-
// 1. Type captcha solution
|
|
446
|
-
|
|
447
|
-
|
|
588
|
+
// 1. Type captcha solution with robust candidate fallbacks & frame search
|
|
589
|
+
const solveTimeout = Math.max(cfg.inputTimeoutMs || 0, 15000);
|
|
590
|
+
const candidateInputSelectors = [
|
|
591
|
+
inputSelector,
|
|
592
|
+
"#fo-captcha",
|
|
593
|
+
"#captcha",
|
|
594
|
+
"input[name='cap']",
|
|
595
|
+
"input[name*='captcha' i]",
|
|
596
|
+
"input[id*='captcha' i]",
|
|
597
|
+
"input[placeholder*='captcha' i]",
|
|
598
|
+
].filter(Boolean);
|
|
599
|
+
|
|
600
|
+
const locatedInput = await locateElementInPageOrFrames(page, candidateInputSelectors, solveTimeout);
|
|
601
|
+
if (!locatedInput || !locatedInput.element) {
|
|
602
|
+
throw new Error(`Captcha input field not found with selector: ${inputSelector}`);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
await locatedInput.element.click({ clickCount: 3 }).catch(() => {});
|
|
448
606
|
await page.keyboard.press("Backspace").catch(() => {});
|
|
449
|
-
await
|
|
607
|
+
await locatedInput.element.type(String(captchaSolution).trim(), { delay: 35 });
|
|
608
|
+
|
|
609
|
+
// Sync input events and AngularJS ngModel for captcha input
|
|
610
|
+
await locatedInput.target.evaluate((sel) => {
|
|
611
|
+
const el = document.querySelector(sel);
|
|
612
|
+
if (el) {
|
|
613
|
+
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
614
|
+
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
615
|
+
el.dispatchEvent(new KeyboardEvent("keydown", { bubbles: true, key: "Enter" }));
|
|
616
|
+
el.dispatchEvent(new KeyboardEvent("keyup", { bubbles: true, key: "Enter" }));
|
|
617
|
+
el.dispatchEvent(new Event("blur", { bubbles: true }));
|
|
618
|
+
|
|
619
|
+
if (window.angular) {
|
|
620
|
+
try {
|
|
621
|
+
const ngEl = window.angular.element(el);
|
|
622
|
+
const ngModel = ngEl.controller("ngModel");
|
|
623
|
+
if (ngModel) {
|
|
624
|
+
ngModel.$setViewValue(el.value);
|
|
625
|
+
ngModel.$render();
|
|
626
|
+
ngModel.$commitViewValue();
|
|
627
|
+
}
|
|
628
|
+
const scope = ngEl.scope();
|
|
629
|
+
if (scope && !scope.$$phase) scope.$apply();
|
|
630
|
+
} catch (_) {}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}, locatedInput.matchedSelector).catch(() => {});
|
|
634
|
+
|
|
635
|
+
// 2. Click submit button with candidate fallbacks & frame search
|
|
636
|
+
const candidateSubmitSelectors = [
|
|
637
|
+
submitSelector,
|
|
638
|
+
"#lotsearch",
|
|
639
|
+
"button[type='submit']",
|
|
640
|
+
"input[type='submit']",
|
|
641
|
+
"button[id*='search' i]",
|
|
642
|
+
"button[id*='submit' i]",
|
|
643
|
+
"button.btn-primary",
|
|
644
|
+
].filter(Boolean);
|
|
645
|
+
|
|
646
|
+
const locatedSubmit = await locateElementInPageOrFrames(page, candidateSubmitSelectors, solveTimeout);
|
|
647
|
+
if (!locatedSubmit || !locatedSubmit.element) {
|
|
648
|
+
throw new Error(`Submit button not found with selector: ${submitSelector}`);
|
|
649
|
+
}
|
|
450
650
|
|
|
451
|
-
|
|
452
|
-
await page.waitForSelector(submitSelector, { timeout: 4000 });
|
|
453
|
-
await page.click(submitSelector);
|
|
651
|
+
await locatedSubmit.element.click();
|
|
454
652
|
|
|
455
653
|
// 3. Post-Submit Race: Wait for Success Selector OR Error Selector
|
|
456
654
|
// In inspection mode, use a shorter safety window (5-7s) so users aren't stalled waiting on empty selectors
|