@geometra/proxy 1.42.0 → 1.43.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.
- package/dist/dom-actions.d.ts +28 -0
- package/dist/dom-actions.d.ts.map +1 -1
- package/dist/dom-actions.js +310 -0
- package/dist/dom-actions.js.map +1 -1
- package/dist/extractor.d.ts.map +1 -1
- package/dist/extractor.js +74 -0
- package/dist/extractor.js.map +1 -1
- package/dist/geometry-ws.d.ts.map +1 -1
- package/dist/geometry-ws.js +14 -2
- package/dist/geometry-ws.js.map +1 -1
- package/dist/types.d.ts +10 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
package/dist/dom-actions.d.ts
CHANGED
|
@@ -23,6 +23,34 @@ export declare function attachFiles(page: Page, paths: string[], opts?: {
|
|
|
23
23
|
dropY?: number;
|
|
24
24
|
cache?: FillLookupCache;
|
|
25
25
|
}): Promise<void>;
|
|
26
|
+
/**
|
|
27
|
+
* Fill an OTP / verification-code input group with a value, char by char.
|
|
28
|
+
*
|
|
29
|
+
* Detection is generic — see `findOtpBoxGroup`. The actual typing strategy
|
|
30
|
+
* uses `page.keyboard.type()` (not a low-level `keyboard.insertText()`
|
|
31
|
+
* batch) so that every char dispatches a real keydown/keypress/keyup event
|
|
32
|
+
* cycle, which is what React's per-cell onKeyDown handler listens for to
|
|
33
|
+
* auto-advance focus to the next box.
|
|
34
|
+
*
|
|
35
|
+
* Why not `fill_form` or `fill_fields` for this? The 8 boxes share
|
|
36
|
+
* accessible bounds because they're visually adjacent and a11y collapses
|
|
37
|
+
* them into one logical textbox node. Any semantic "find the textbox and
|
|
38
|
+
* write 8 chars" path writes the entire string to box 0, which has
|
|
39
|
+
* maxlength=1 and silently truncates to one char. The pattern can only be
|
|
40
|
+
* driven through physical key events.
|
|
41
|
+
*
|
|
42
|
+
* Verification: after typing, read the `.value` of every box and confirm
|
|
43
|
+
* it matches the expected per-cell char. Throws a descriptive error if
|
|
44
|
+
* verification fails so callers get an honest failure instead of a silent
|
|
45
|
+
* "success" that leaves the form in a bad state.
|
|
46
|
+
*/
|
|
47
|
+
export declare function fillOtp(page: Page, value: string, opts?: {
|
|
48
|
+
fieldLabel?: string;
|
|
49
|
+
perCharDelayMs?: number;
|
|
50
|
+
}): Promise<{
|
|
51
|
+
cellCount: number;
|
|
52
|
+
filledCount: number;
|
|
53
|
+
}>;
|
|
26
54
|
export declare function setFieldText(page: Page, fieldLabel: string, value: string, opts?: {
|
|
27
55
|
exact?: boolean;
|
|
28
56
|
cache?: FillLookupCache;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dom-actions.d.ts","sourceRoot":"","sources":["../src/dom-actions.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAwB,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAA;AACrE,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAMnE,MAAM,MAAM,kBAAkB,GAAG,MAAM,GAAG,SAAS,GAAG,QAAQ,GAAG,MAAM,CAAA;AAqCvE,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,CAAC,CAAA;IACpC,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,CAAC,CAAA;IACrC,SAAS,EAAE,GAAG,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,CAAC,CAAA;CACvC;AAED,wBAAgB,qBAAqB,IAAI,eAAe,CAMvD;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,eAAe,GAAG,IAAI,CAIjE;AA42ED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,MAAM,EAAE,CAclE;AA4OD,wBAAsB,WAAW,CAC/B,IAAI,EAAE,IAAI,EACV,KAAK,EAAE,MAAM,EAAE,EACf,IAAI,CAAC,EAAE;IACL,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,QAAQ,CAAC,EAAE,kBAAkB,CAAA;IAC7B,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,KAAK,CAAC,EAAE,eAAe,CAAA;CACxB,GACA,OAAO,CAAC,IAAI,CAAC,CA+Df;
|
|
1
|
+
{"version":3,"file":"dom-actions.d.ts","sourceRoot":"","sources":["../src/dom-actions.ts"],"names":[],"mappings":"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"}
|
package/dist/dom-actions.js
CHANGED
|
@@ -2841,6 +2841,19 @@ async function attemptNativeBatchFill(page, fields) {
|
|
|
2841
2841
|
continue;
|
|
2842
2842
|
}
|
|
2843
2843
|
if (field.kind === 'text') {
|
|
2844
|
+
// Bug #2 (v1.43): if this text field's label looks like a
|
|
2845
|
+
// verification-code / OTP prompt AND its value is a plausible code
|
|
2846
|
+
// (4+ chars, no whitespace), skip the in-page native batch fill.
|
|
2847
|
+
// The native path writes `el.value = value` on the first matching
|
|
2848
|
+
// textbox, which for an OTP widget is cell 0 (maxlength=1) — the
|
|
2849
|
+
// assignment silently truncates to the first char, the readback
|
|
2850
|
+
// then reports "mismatch", and the fallback path tries again with
|
|
2851
|
+
// the same broken strategy. Leaving this field out of the native
|
|
2852
|
+
// batch means it falls through to `setFieldText`, which auto-
|
|
2853
|
+
// routes to the `fillOtp` primitive.
|
|
2854
|
+
if (labelLooksLikeOtp(field.fieldLabel) && /^\S{4,}$/.test(field.value)) {
|
|
2855
|
+
continue;
|
|
2856
|
+
}
|
|
2844
2857
|
pending.push({
|
|
2845
2858
|
index,
|
|
2846
2859
|
kind: 'text',
|
|
@@ -3166,7 +3179,304 @@ async function attemptNativeBatchFill(page, fields) {
|
|
|
3166
3179
|
}
|
|
3167
3180
|
return results;
|
|
3168
3181
|
}
|
|
3182
|
+
/**
|
|
3183
|
+
* Regex covering every field label the Greenhouse / Ashby / Lever / Workday
|
|
3184
|
+
* verification-code / security-code / 2FA UIs use. Matching this on a fill
|
|
3185
|
+
* target triggers the OTP-box detection path before falling back to the
|
|
3186
|
+
* plain text-fill pipeline. Shared between setFieldText, fillFields, and
|
|
3187
|
+
* the explicit geometra_fill_otp tool so any entry point auto-handles the
|
|
3188
|
+
* 8-cell input pattern. See Bug #2 in the v1.43 release notes for the
|
|
3189
|
+
* Greenhouse 8-box security-code flow that broke without this.
|
|
3190
|
+
*/
|
|
3191
|
+
const OTP_LABEL_HINT_PATTERN = /(security|verification|one[- ]?time|authentication|access)\s*code|\botp\b|\bpasscode\b|\b2fa\b|\bmfa\b/i;
|
|
3192
|
+
function labelLooksLikeOtp(label) {
|
|
3193
|
+
if (!label)
|
|
3194
|
+
return false;
|
|
3195
|
+
return OTP_LABEL_HINT_PATTERN.test(label);
|
|
3196
|
+
}
|
|
3197
|
+
/**
|
|
3198
|
+
* Find an OTP / verification-code input group on the page.
|
|
3199
|
+
*
|
|
3200
|
+
* An OTP group is detected when a frame contains ≥2 sibling `<input>`
|
|
3201
|
+
* elements where each:
|
|
3202
|
+
* - has `maxlength="1"` (or `maxLength: 1` via the DOM property)
|
|
3203
|
+
* - has `type="text"`, `type="tel"`, `type="number"`, `inputmode="numeric"`,
|
|
3204
|
+
* or an empty type (defaults to text)
|
|
3205
|
+
* - shares a common parent with the others
|
|
3206
|
+
* - has a roughly comparable y-coordinate (same row) and strictly
|
|
3207
|
+
* increasing x-coordinates (left-to-right visual order)
|
|
3208
|
+
*
|
|
3209
|
+
* When `fieldLabel` is passed, the search is scoped to the nearest
|
|
3210
|
+
* labelled form section containing that label (so the generic detector
|
|
3211
|
+
* cannot be hijacked by an unrelated per-character autosave field
|
|
3212
|
+
* elsewhere on the page).
|
|
3213
|
+
*
|
|
3214
|
+
* Returns the group as a Playwright `Locator` referring to ALL boxes in
|
|
3215
|
+
* DOM order, plus the cell count. The caller clicks box 0 via the
|
|
3216
|
+
* center-of-bounds coordinate path (semantic click resolves to whichever
|
|
3217
|
+
* box paints topmost, which in Greenhouse's layout is always box 7) and
|
|
3218
|
+
* uses `page.keyboard.type(value, { delay })` to feed characters one by
|
|
3219
|
+
* one, letting React's onKeyDown focus-advance handler commit each cell.
|
|
3220
|
+
*/
|
|
3221
|
+
async function findOtpBoxGroup(page, fieldLabel, expectedLength) {
|
|
3222
|
+
for (const frame of page.frames()) {
|
|
3223
|
+
// If a label is provided, scope the search to that label's nearest
|
|
3224
|
+
// ancestor form section. Without this, the generic detector could
|
|
3225
|
+
// hijack an unrelated row of per-character inputs elsewhere on the
|
|
3226
|
+
// page (e.g. a zip-code splitter elsewhere in the form).
|
|
3227
|
+
let scope = null;
|
|
3228
|
+
if (fieldLabel) {
|
|
3229
|
+
try {
|
|
3230
|
+
const labeledInput = await frame
|
|
3231
|
+
.getByLabel(fieldLabel, { exact: false })
|
|
3232
|
+
.first()
|
|
3233
|
+
.elementHandle({ timeout: 500 });
|
|
3234
|
+
if (labeledInput) {
|
|
3235
|
+
// Walk up to the nearest form/fieldset/group wrapper so the
|
|
3236
|
+
// sibling scan has a stable root even when the label is not a
|
|
3237
|
+
// direct parent.
|
|
3238
|
+
scope = await labeledInput.evaluateHandle((el) => {
|
|
3239
|
+
let cur = el;
|
|
3240
|
+
for (let depth = 0; depth < 6 && cur; depth++) {
|
|
3241
|
+
if (cur.tagName === 'FORM' ||
|
|
3242
|
+
cur.tagName === 'FIELDSET' ||
|
|
3243
|
+
cur.getAttribute('role') === 'group' ||
|
|
3244
|
+
cur.getAttribute('role') === 'form' ||
|
|
3245
|
+
(cur.getAttribute('class') ?? '').toLowerCase().includes('otp') ||
|
|
3246
|
+
(cur.getAttribute('data-otp') !== null)) {
|
|
3247
|
+
return cur;
|
|
3248
|
+
}
|
|
3249
|
+
cur = cur.parentElement;
|
|
3250
|
+
}
|
|
3251
|
+
return el.parentElement ?? el;
|
|
3252
|
+
});
|
|
3253
|
+
}
|
|
3254
|
+
}
|
|
3255
|
+
catch {
|
|
3256
|
+
scope = null;
|
|
3257
|
+
}
|
|
3258
|
+
}
|
|
3259
|
+
const candidate = await frame.evaluate(({ scopeHint, wantLength }) => {
|
|
3260
|
+
const scopeRoot = scopeHint ?? document.body;
|
|
3261
|
+
if (!scopeRoot)
|
|
3262
|
+
return null;
|
|
3263
|
+
// Gather every <input> in the scope whose maxlength is 1.
|
|
3264
|
+
const all = Array.from(scopeRoot.querySelectorAll('input'));
|
|
3265
|
+
const singles = all.filter((input) => {
|
|
3266
|
+
if (!input.isConnected)
|
|
3267
|
+
return false;
|
|
3268
|
+
const style = getComputedStyle(input);
|
|
3269
|
+
if (style.display === 'none' || style.visibility === 'hidden')
|
|
3270
|
+
return false;
|
|
3271
|
+
const rect = input.getBoundingClientRect();
|
|
3272
|
+
if (rect.width <= 0 || rect.height <= 0)
|
|
3273
|
+
return false;
|
|
3274
|
+
const maxAttr = input.getAttribute('maxlength');
|
|
3275
|
+
const max = maxAttr !== null ? Number(maxAttr) : input.maxLength;
|
|
3276
|
+
if (!Number.isFinite(max) || max !== 1)
|
|
3277
|
+
return false;
|
|
3278
|
+
const type = (input.type ?? 'text').toLowerCase();
|
|
3279
|
+
if (type && !['text', 'tel', 'number', 'password', ''].includes(type))
|
|
3280
|
+
return false;
|
|
3281
|
+
return true;
|
|
3282
|
+
});
|
|
3283
|
+
if (singles.length < 2)
|
|
3284
|
+
return null;
|
|
3285
|
+
// Group by direct parent — all cells of an OTP widget are siblings
|
|
3286
|
+
// inside a single wrapper (the `<input>`s might be wrapped in a
|
|
3287
|
+
// span each, but the wrappers share the same grandparent row).
|
|
3288
|
+
const byParent = new Map();
|
|
3289
|
+
for (const input of singles) {
|
|
3290
|
+
// Climb up to the nearest wrapper that contains ALL siblings
|
|
3291
|
+
// on the same row.
|
|
3292
|
+
let wrapper = input.parentElement;
|
|
3293
|
+
for (let depth = 0; depth < 3 && wrapper; depth++) {
|
|
3294
|
+
const group = byParent.get(wrapper) ?? [];
|
|
3295
|
+
group.push(input);
|
|
3296
|
+
byParent.set(wrapper, group);
|
|
3297
|
+
wrapper = wrapper.parentElement;
|
|
3298
|
+
}
|
|
3299
|
+
}
|
|
3300
|
+
// Score candidate groups: a group qualifies if all its inputs are
|
|
3301
|
+
// on the same row (y-delta < 8px) and their x-coordinates are
|
|
3302
|
+
// strictly increasing (left-to-right). Accept the largest
|
|
3303
|
+
// qualifying group.
|
|
3304
|
+
let best = null;
|
|
3305
|
+
for (const [root, inputs] of byParent.entries()) {
|
|
3306
|
+
if (inputs.length < 2)
|
|
3307
|
+
continue;
|
|
3308
|
+
// De-dup since a given input will be counted under multiple
|
|
3309
|
+
// wrapper ancestors during the climb above.
|
|
3310
|
+
const unique = Array.from(new Set(inputs));
|
|
3311
|
+
if (unique.length < 2)
|
|
3312
|
+
continue;
|
|
3313
|
+
const sorted = unique
|
|
3314
|
+
.map((input) => ({ input, rect: input.getBoundingClientRect() }))
|
|
3315
|
+
.sort((a, b) => a.rect.left - b.rect.left);
|
|
3316
|
+
const rows = sorted.map((entry) => entry.rect.top);
|
|
3317
|
+
const yMin = Math.min(...rows);
|
|
3318
|
+
const yMax = Math.max(...rows);
|
|
3319
|
+
if (yMax - yMin > 8)
|
|
3320
|
+
continue;
|
|
3321
|
+
let increasing = true;
|
|
3322
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
3323
|
+
if (sorted[i].rect.left <= sorted[i - 1].rect.left) {
|
|
3324
|
+
increasing = false;
|
|
3325
|
+
break;
|
|
3326
|
+
}
|
|
3327
|
+
}
|
|
3328
|
+
if (!increasing)
|
|
3329
|
+
continue;
|
|
3330
|
+
if (!best || sorted.length > best.inputs.length) {
|
|
3331
|
+
best = { inputs: sorted.map((entry) => entry.input), root };
|
|
3332
|
+
}
|
|
3333
|
+
}
|
|
3334
|
+
if (!best)
|
|
3335
|
+
return null;
|
|
3336
|
+
if (wantLength !== undefined && best.inputs.length !== wantLength) {
|
|
3337
|
+
// Only accept the group if its cell count is compatible with
|
|
3338
|
+
// the typed value. Compatibility: either exact match, or the
|
|
3339
|
+
// group is ≥ the value length (allow typing a 6-char code into
|
|
3340
|
+
// an 8-box group with 2 empty trailing boxes).
|
|
3341
|
+
if (best.inputs.length < wantLength)
|
|
3342
|
+
return null;
|
|
3343
|
+
}
|
|
3344
|
+
// Stamp a unique data attribute on each cell so the caller can
|
|
3345
|
+
// address them unambiguously via a single selector. Using a
|
|
3346
|
+
// content-hash-ish random id avoids collisions with existing
|
|
3347
|
+
// data attributes and re-renders across re-entry.
|
|
3348
|
+
const marker = `data-geometra-otp-${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`;
|
|
3349
|
+
best.inputs.forEach((input, index) => {
|
|
3350
|
+
input.setAttribute(marker, String(index));
|
|
3351
|
+
});
|
|
3352
|
+
return { cssSelector: `[${marker}]`, count: best.inputs.length };
|
|
3353
|
+
}, { scopeHint: scope, wantLength: expectedLength });
|
|
3354
|
+
if (candidate) {
|
|
3355
|
+
return {
|
|
3356
|
+
boxes: frame.locator(candidate.cssSelector),
|
|
3357
|
+
cellCount: candidate.count,
|
|
3358
|
+
};
|
|
3359
|
+
}
|
|
3360
|
+
}
|
|
3361
|
+
return null;
|
|
3362
|
+
}
|
|
3363
|
+
/**
|
|
3364
|
+
* Fill an OTP / verification-code input group with a value, char by char.
|
|
3365
|
+
*
|
|
3366
|
+
* Detection is generic — see `findOtpBoxGroup`. The actual typing strategy
|
|
3367
|
+
* uses `page.keyboard.type()` (not a low-level `keyboard.insertText()`
|
|
3368
|
+
* batch) so that every char dispatches a real keydown/keypress/keyup event
|
|
3369
|
+
* cycle, which is what React's per-cell onKeyDown handler listens for to
|
|
3370
|
+
* auto-advance focus to the next box.
|
|
3371
|
+
*
|
|
3372
|
+
* Why not `fill_form` or `fill_fields` for this? The 8 boxes share
|
|
3373
|
+
* accessible bounds because they're visually adjacent and a11y collapses
|
|
3374
|
+
* them into one logical textbox node. Any semantic "find the textbox and
|
|
3375
|
+
* write 8 chars" path writes the entire string to box 0, which has
|
|
3376
|
+
* maxlength=1 and silently truncates to one char. The pattern can only be
|
|
3377
|
+
* driven through physical key events.
|
|
3378
|
+
*
|
|
3379
|
+
* Verification: after typing, read the `.value` of every box and confirm
|
|
3380
|
+
* it matches the expected per-cell char. Throws a descriptive error if
|
|
3381
|
+
* verification fails so callers get an honest failure instead of a silent
|
|
3382
|
+
* "success" that leaves the form in a bad state.
|
|
3383
|
+
*/
|
|
3384
|
+
export async function fillOtp(page, value, opts) {
|
|
3385
|
+
if (!value) {
|
|
3386
|
+
throw new Error('fillOtp: value is empty — nothing to type');
|
|
3387
|
+
}
|
|
3388
|
+
const group = await findOtpBoxGroup(page, opts?.fieldLabel, value.length);
|
|
3389
|
+
if (!group) {
|
|
3390
|
+
throw new Error(`fillOtp: no OTP box group found${opts?.fieldLabel ? ` near label "${opts.fieldLabel}"` : ''}. Expected ≥2 sibling <input maxlength="1"> elements on the same row.`);
|
|
3391
|
+
}
|
|
3392
|
+
// Click the leftmost box via its center point. We use a low-level
|
|
3393
|
+
// bounding-box click because a semantic "click the first input" path
|
|
3394
|
+
// resolves to whichever input paints topmost in the stacking order,
|
|
3395
|
+
// which in Greenhouse's CSS grid layout is always box N-1 (the last
|
|
3396
|
+
// cell). The bounding-box center click is unambiguous and puts focus on
|
|
3397
|
+
// cell 0 every time.
|
|
3398
|
+
const firstBox = group.boxes.nth(0);
|
|
3399
|
+
await firstBox.scrollIntoViewIfNeeded();
|
|
3400
|
+
const box = await firstBox.boundingBox();
|
|
3401
|
+
if (!box) {
|
|
3402
|
+
throw new Error('fillOtp: first OTP cell has no bounding box (detached or invisible?)');
|
|
3403
|
+
}
|
|
3404
|
+
await page.mouse.click(box.x + box.width / 2, box.y + box.height / 2);
|
|
3405
|
+
// Clear any pre-existing values in all cells so we write a clean slate.
|
|
3406
|
+
// Some widgets pre-populate cells with zero-width spaces or previous
|
|
3407
|
+
// values after a re-render; typing over them would offset the
|
|
3408
|
+
// auto-advance count by one.
|
|
3409
|
+
const perCellCount = group.cellCount;
|
|
3410
|
+
for (let i = 0; i < perCellCount; i++) {
|
|
3411
|
+
try {
|
|
3412
|
+
await group.boxes.nth(i).evaluate((el) => {
|
|
3413
|
+
if (el instanceof HTMLInputElement) {
|
|
3414
|
+
el.value = '';
|
|
3415
|
+
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
3416
|
+
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
3417
|
+
}
|
|
3418
|
+
});
|
|
3419
|
+
}
|
|
3420
|
+
catch {
|
|
3421
|
+
/* individual cell may have been removed/re-rendered; tolerate */
|
|
3422
|
+
}
|
|
3423
|
+
}
|
|
3424
|
+
// Re-focus cell 0 after clearing — dispatching input events may have
|
|
3425
|
+
// blurred the active element on some React builds.
|
|
3426
|
+
await page.mouse.click(box.x + box.width / 2, box.y + box.height / 2);
|
|
3427
|
+
// Type the value char by char. `page.keyboard.type` already dispatches
|
|
3428
|
+
// keydown+keypress+input+keyup for each character and honors the delay.
|
|
3429
|
+
// 30ms is enough for React's onKeyDown handler to run and move focus
|
|
3430
|
+
// to the next cell before the next char arrives.
|
|
3431
|
+
const perCharDelay = opts?.perCharDelayMs ?? 30;
|
|
3432
|
+
await page.keyboard.type(value, { delay: perCharDelay });
|
|
3433
|
+
// Small settle window for the last cell's onChange propagation.
|
|
3434
|
+
await delay(80);
|
|
3435
|
+
// Verify: read every cell's value and confirm per-char match.
|
|
3436
|
+
const readback = [];
|
|
3437
|
+
for (let i = 0; i < perCellCount; i++) {
|
|
3438
|
+
const cellValue = await group.boxes.nth(i).evaluate((el) => {
|
|
3439
|
+
if (el instanceof HTMLInputElement)
|
|
3440
|
+
return el.value;
|
|
3441
|
+
if (el instanceof HTMLElement)
|
|
3442
|
+
return el.textContent ?? '';
|
|
3443
|
+
return '';
|
|
3444
|
+
});
|
|
3445
|
+
readback.push(cellValue ?? '');
|
|
3446
|
+
}
|
|
3447
|
+
// For each typed char, the corresponding cell must contain exactly that
|
|
3448
|
+
// char. Cells beyond the typed length are allowed to be empty.
|
|
3449
|
+
const expected = Array.from(value);
|
|
3450
|
+
const mismatches = [];
|
|
3451
|
+
for (let i = 0; i < expected.length; i++) {
|
|
3452
|
+
if (readback[i] !== expected[i]) {
|
|
3453
|
+
mismatches.push({ index: i, expected: expected[i], got: readback[i] ?? '' });
|
|
3454
|
+
}
|
|
3455
|
+
}
|
|
3456
|
+
if (mismatches.length > 0) {
|
|
3457
|
+
const summary = mismatches
|
|
3458
|
+
.map((m) => `cell ${m.index}: expected "${m.expected}", got "${m.got}"`)
|
|
3459
|
+
.join('; ');
|
|
3460
|
+
throw new Error(`fillOtp: typed ${expected.length} chars into ${perCellCount}-cell group but readback mismatch — ${summary}. Readback: [${readback.map((v) => JSON.stringify(v)).join(', ')}]`);
|
|
3461
|
+
}
|
|
3462
|
+
return { cellCount: perCellCount, filledCount: expected.length };
|
|
3463
|
+
}
|
|
3169
3464
|
export async function setFieldText(page, fieldLabel, value, opts) {
|
|
3465
|
+
// Bug #2 (v1.43) auto-routing: if the caller's label looks like a
|
|
3466
|
+
// verification-code / security-code / OTP prompt, try the dedicated OTP
|
|
3467
|
+
// path first. If no OTP group is detected, fall through to the normal
|
|
3468
|
+
// text-fill path — the label match alone is not enough to guarantee the
|
|
3469
|
+
// field is split into cells. If the group IS detected, any thrown error
|
|
3470
|
+
// from fillOtp propagates out of setFieldText unchanged: silent fallback
|
|
3471
|
+
// to the plain text-fill path would write the whole string into box 0
|
|
3472
|
+
// and pretend success, which is the bug we're fixing.
|
|
3473
|
+
if (labelLooksLikeOtp(fieldLabel) && /^\S{4,}$/.test(value)) {
|
|
3474
|
+
const group = await findOtpBoxGroup(page, fieldLabel, value.length);
|
|
3475
|
+
if (group) {
|
|
3476
|
+
await fillOtp(page, value, { fieldLabel });
|
|
3477
|
+
return;
|
|
3478
|
+
}
|
|
3479
|
+
}
|
|
3170
3480
|
const exact = opts?.exact ?? false;
|
|
3171
3481
|
const locator = await findLabeledEditableField(page, fieldLabel, exact, opts?.cache, opts?.fieldId);
|
|
3172
3482
|
if (!locator) {
|