@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.
@@ -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":"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"}
1
+ {"version":3,"file":"dom-actions.d.ts","sourceRoot":"","sources":["../src/dom-actions.ts"],"names":[],"mappings":"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"}
@@ -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) {