@geometra/proxy 1.65.3 → 1.65.5

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.
@@ -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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"}
@@ -2429,26 +2429,67 @@ async function readFormLevelInvalidState(handle, opts) {
2429
2429
  const form = el.closest('form');
2430
2430
  if (!form)
2431
2431
  return false;
2432
- // Find the field wrapper — the closest ancestor that "contains a label"
2433
- // and is still inside the form. This is the react-hook-form convention:
2434
- // every field lives under a wrapper that renders both the control and
2435
- // the error message as siblings. If we can't find one, use a bounded
2436
- // walk up to ~6 ancestors and look at each one's siblings.
2437
- let wrapper = el;
2438
- let depth = 0;
2439
- while (wrapper && wrapper !== form && depth < 6) {
2440
- // A "field wrapper" is anything that contains a <label>, a legend,
2441
- // or a [class*="field"] class up to 6 ancestors deep.
2442
- const hasLabel = wrapper.querySelector && (wrapper.querySelector('label') || wrapper.querySelector('legend'));
2443
- const className = (wrapper.getAttribute('class') ?? '').toLowerCase();
2444
- const looksLikeField = className.includes('field') ||
2445
- className.includes('form-group') ||
2446
- className.includes('form-control') ||
2447
- className.includes('input-wrapper');
2448
- if (hasLabel || looksLikeField)
2449
- break;
2450
- wrapper = wrapper.parentElement;
2451
- depth++;
2432
+ function rootScopedElementById(owner, id) {
2433
+ const root = owner.getRootNode();
2434
+ if (root instanceof ShadowRoot)
2435
+ return root.getElementById(id);
2436
+ if (root instanceof Document)
2437
+ return root.getElementById(id);
2438
+ return null;
2439
+ }
2440
+ function linkedLabels(control) {
2441
+ const labels = new Set();
2442
+ for (const id of (control.getAttribute('aria-labelledby') ?? '').split(/\s+/).filter(Boolean)) {
2443
+ const label = rootScopedElementById(control, id);
2444
+ if (label)
2445
+ labels.add(label);
2446
+ }
2447
+ if (control instanceof HTMLInputElement || control instanceof HTMLSelectElement || control instanceof HTMLTextAreaElement) {
2448
+ for (const label of Array.from(control.labels ?? []))
2449
+ labels.add(label);
2450
+ }
2451
+ return [...labels];
2452
+ }
2453
+ function linkedFieldWrapper(control) {
2454
+ let best = null;
2455
+ for (const label of linkedLabels(control)) {
2456
+ let candidate = control;
2457
+ let depth = 0;
2458
+ while (candidate && candidate !== form && depth < 12) {
2459
+ if (candidate.contains(label) && (!best || depth < best.depth)) {
2460
+ best = { wrapper: candidate, depth };
2461
+ break;
2462
+ }
2463
+ candidate = candidate.parentElement;
2464
+ depth++;
2465
+ }
2466
+ }
2467
+ return best?.wrapper ?? null;
2468
+ }
2469
+ // Prefer an ancestor that jointly owns this exact control and its
2470
+ // explicitly linked label. Looking for any descendant <label> can pull
2471
+ // an unrelated required field into the verification scope.
2472
+ let wrapper = linkedFieldWrapper(el);
2473
+ if (!wrapper) {
2474
+ const semanticGroup = el.closest('fieldset, [role="group"]');
2475
+ if (semanticGroup && semanticGroup !== form)
2476
+ wrapper = semanticGroup;
2477
+ }
2478
+ if (!wrapper) {
2479
+ let candidate = el;
2480
+ let depth = 0;
2481
+ while (candidate && candidate !== form && depth < 8) {
2482
+ const className = (candidate.getAttribute('class') ?? '').toLowerCase();
2483
+ if (className.includes('field') ||
2484
+ className.includes('form-group') ||
2485
+ className.includes('form-control') ||
2486
+ className.includes('input-wrapper')) {
2487
+ wrapper = candidate;
2488
+ break;
2489
+ }
2490
+ candidate = candidate.parentElement;
2491
+ depth++;
2492
+ }
2452
2493
  }
2453
2494
  if (!wrapper || wrapper === form)
2454
2495
  wrapper = el.parentElement ?? el;
@@ -3273,22 +3314,94 @@ async function beginFileInputCommitGuard(handle) {
3273
3314
  }
3274
3315
  return null;
3275
3316
  }
3317
+ function explicitlyAssociatesFileInput(scope, exactInput) {
3318
+ const broadContainerSelector = 'main, nav, aside, article, section, header, footer, ' +
3319
+ '[role="main"], [role="application"], [role="document"], [role="navigation"]';
3320
+ if (scope.matches(broadContainerSelector) || scope.querySelector(broadContainerSelector)) {
3321
+ return false;
3322
+ }
3323
+ const labels = Array.from(exactInput.labels ?? []);
3324
+ const interactiveElements = Array.from(scope.querySelectorAll('input, select, textarea, button, [role="button"], a[href], [contenteditable="true"]'));
3325
+ const hasUnrelatedControl = interactiveElements.some((control) => {
3326
+ if (control === exactInput)
3327
+ return false;
3328
+ if (control instanceof HTMLLabelElement && labels.includes(control))
3329
+ return false;
3330
+ const style = getComputedStyle(control);
3331
+ const visible = style.display !== 'none' &&
3332
+ style.visibility !== 'hidden' &&
3333
+ style.visibility !== 'collapse' &&
3334
+ control.getClientRects().length > 0;
3335
+ if (!visible)
3336
+ return false;
3337
+ if (!control.matches('button, [role="button"], a[href]'))
3338
+ return true;
3339
+ const accessibleText = (control.getAttribute('aria-label') ??
3340
+ control.getAttribute('title') ??
3341
+ control.textContent ?? '').replace(/\s+/g, ' ').trim();
3342
+ const action = /^(attach|upload|browse|choose|select|add)\b/i.exec(accessibleText);
3343
+ if (!action)
3344
+ return true;
3345
+ const remainder = accessibleText.slice(action[0].length).trim();
3346
+ if (!remainder)
3347
+ return !/^(attach|upload|browse|choose)$/i.test(action[1] ?? '');
3348
+ return !/^(?:(?:a|an|the|this|new|your)\s+)?(?:files?|attachments?|upload|documents?|resume|cv|cover letter)\b/i.test(remainder);
3349
+ });
3350
+ if (hasUnrelatedControl)
3351
+ return false;
3352
+ if (scope instanceof HTMLLabelElement &&
3353
+ labels.includes(scope))
3354
+ return true;
3355
+ if (labels.some(label => scope.contains(label)))
3356
+ return true;
3357
+ if (scope instanceof HTMLFieldSetElement)
3358
+ return true;
3359
+ if (scope.getAttribute('role')?.trim().toLowerCase() !== 'group')
3360
+ return false;
3361
+ if (scope.getAttribute('aria-label')?.trim())
3362
+ return true;
3363
+ const labelledBy = scope.getAttribute('aria-labelledby')?.trim().split(/\s+/).filter(Boolean) ?? [];
3364
+ return labelledBy.some(id => {
3365
+ const label = exactInput.ownerDocument.getElementById(id);
3366
+ return Boolean(label && (scope.contains(label) || label === scope.previousElementSibling));
3367
+ });
3368
+ }
3276
3369
  const input = element instanceof HTMLInputElement && element.type === 'file' ? element : null;
3277
3370
  const guardedAncestors = new Set();
3371
+ const receiptScopes = [];
3372
+ let capturedReceiptScope = false;
3278
3373
  let current = input;
3279
3374
  while (current) {
3280
3375
  guardedAncestors.add(current);
3376
+ if (!capturedReceiptScope &&
3377
+ current !== input &&
3378
+ !(current instanceof HTMLFormElement) &&
3379
+ !(current instanceof HTMLBodyElement) &&
3380
+ !(current instanceof HTMLHtmlElement) &&
3381
+ input !== null &&
3382
+ explicitlyAssociatesFileInput(current, input) &&
3383
+ current.querySelectorAll('input[type="file"]').length === 1) {
3384
+ const initialExactTexts = [current, ...Array.from(current.querySelectorAll('*'))]
3385
+ .map(candidate => candidate.textContent?.replace(/\s+/g, ' ').trim() ?? '')
3386
+ .filter(Boolean);
3387
+ receiptScopes.push({ element: current, initialExactTexts });
3388
+ capturedReceiptScope = true;
3389
+ }
3390
+ if (current instanceof HTMLFormElement)
3391
+ break;
3281
3392
  current = composedParent(current);
3282
3393
  }
3283
3394
  const initial = {
3284
3395
  validFileInput: Boolean(input?.isConnected),
3285
3396
  blockReason: input ? mutationBlockReason(input) : 'detached or no longer a file input',
3286
3397
  accept: input?.getAttribute('accept') ?? null,
3398
+ multiple: input?.multiple ?? false,
3287
3399
  names: input ? Array.from(input.files ?? []).map(file => file.name) : [],
3288
3400
  };
3289
3401
  const state = {
3290
3402
  input,
3291
3403
  initial,
3404
+ receiptScopes,
3292
3405
  invalidReason: null,
3293
3406
  observer: null,
3294
3407
  record: () => { },
@@ -3371,6 +3484,113 @@ async function stopFileInputCommitGuard(guard) {
3371
3484
  value.observer.disconnect();
3372
3485
  }).catch(() => { });
3373
3486
  }
3487
+ async function readReactiveFileReceipt(guard, expectedNames) {
3488
+ return await guard.evaluate((value, names) => {
3489
+ function visible(element) {
3490
+ const style = getComputedStyle(element);
3491
+ return style.display !== 'none' &&
3492
+ style.visibility !== 'hidden' &&
3493
+ style.visibility !== 'collapse' &&
3494
+ element.getClientRects().length > 0;
3495
+ }
3496
+ const normalize = (text) => (text ?? '').replace(/\s+/g, ' ').trim();
3497
+ // The guard snapshots at most the exact input's nearest local wrapper.
3498
+ // Never combine its evidence with controls or filenames from an ancestor.
3499
+ const candidate = value.receiptScopes.find((entry) => entry.element.isConnected);
3500
+ if (!candidate) {
3501
+ return { hasScope: false, hasReceipt: false, hasProgress: false, hasError: false };
3502
+ }
3503
+ if (names.some(name => candidate.initialExactTexts.includes(name))) {
3504
+ return { hasScope: true, hasReceipt: false, hasProgress: false, hasError: false };
3505
+ }
3506
+ const scope = candidate.element;
3507
+ const descendants = [scope, ...Array.from(scope.querySelectorAll('*'))];
3508
+ const expectedCounts = new Map();
3509
+ for (const name of names)
3510
+ expectedCounts.set(name, (expectedCounts.get(name) ?? 0) + 1);
3511
+ const observedCounts = new Map();
3512
+ for (const element of descendants) {
3513
+ if (!visible(element))
3514
+ continue;
3515
+ const text = normalize(element.textContent);
3516
+ if (!expectedCounts.has(text))
3517
+ continue;
3518
+ // Count only the innermost exact-text element so nested wrappers cannot
3519
+ // manufacture duplicate filename evidence for one rendered receipt.
3520
+ const nestedExactText = Array.from(element.querySelectorAll('*')).some(descendant => visible(descendant) && normalize(descendant.textContent) === text);
3521
+ if (!nestedExactText)
3522
+ observedCounts.set(text, (observedCounts.get(text) ?? 0) + 1);
3523
+ }
3524
+ const hasExactNames = Array.from(expectedCounts).every(([name, count]) => observedCounts.get(name) === count);
3525
+ const hasRemovalControl = descendants.some((element) => {
3526
+ if (!visible(element) || !element.matches('button, [role="button"], a'))
3527
+ return false;
3528
+ const accessibleText = normalize(element.getAttribute('aria-label') ??
3529
+ element.getAttribute('title') ??
3530
+ element.textContent);
3531
+ const action = /^(remove|delete|clear|replace|change)\b/i.exec(accessibleText);
3532
+ if (!action)
3533
+ return false;
3534
+ const remainder = accessibleText.slice(action[0].length).trim();
3535
+ if (!remainder)
3536
+ return true;
3537
+ if (names.some(name => remainder.toLowerCase().includes(name.toLowerCase())))
3538
+ return true;
3539
+ return /^(?:(?:this|the|selected|uploaded|current)\s+)?(?:file|attachment|upload|document|resume|cv|cover letter)\b/i.test(remainder);
3540
+ });
3541
+ const hasProgress = descendants.some(element => visible(element) && element.matches('[role="progressbar"], progress, [aria-busy="true"]'));
3542
+ const hasError = descendants.some((element) => {
3543
+ if (!visible(element))
3544
+ return false;
3545
+ const role = normalize(element.getAttribute('role')).toLowerCase();
3546
+ if (role === 'alert' || role === 'alertdialog')
3547
+ return true;
3548
+ if (element.getAttribute('aria-invalid')?.trim().toLowerCase() === 'true')
3549
+ return true;
3550
+ const dataError = element.getAttribute('data-error');
3551
+ if (dataError !== null &&
3552
+ !/^(?:false|0|off|none)$/i.test(dataError.trim()))
3553
+ return true;
3554
+ for (const attribute of ['data-state', 'data-status', 'data-upload-state', 'data-file-state']) {
3555
+ const state = element.getAttribute(attribute)?.trim().toLowerCase();
3556
+ if (state && /^(?:error|invalid|failed|failure|rejected)$/.test(state))
3557
+ return true;
3558
+ }
3559
+ return Array.from(element.classList).some(className => /(?:^|[-_:])(?:error|errors|invalid|failed|failure)(?:$|[-_:])/i.test(className));
3560
+ });
3561
+ if (hasExactNames && hasRemovalControl && !hasProgress && !hasError) {
3562
+ return { hasScope: true, hasReceipt: true, hasProgress: false, hasError: false };
3563
+ }
3564
+ return { hasScope: true, hasReceipt: false, hasProgress, hasError };
3565
+ }, expectedNames);
3566
+ }
3567
+ /**
3568
+ * Some reactive uploaders detach the native input while streaming the file,
3569
+ * then render a filename receipt in the same field wrapper. Treat that as
3570
+ * success only when the exact new filenames and a removal control remain
3571
+ * stable after progress completes. A filename in an error message is not a
3572
+ * receipt, and an unbounded form-level match is never considered.
3573
+ */
3574
+ async function confirmStableReactiveFileReceipt(guard, paths) {
3575
+ const expectedNames = expectedUploadNames(paths);
3576
+ const signalDeadline = Date.now() + 500;
3577
+ let completionDeadline = null;
3578
+ while (Date.now() < (completionDeadline ?? signalDeadline)) {
3579
+ const state = await readReactiveFileReceipt(guard, expectedNames).catch(() => null);
3580
+ if (!state?.hasScope || state.hasError)
3581
+ return false;
3582
+ if (state.hasReceipt) {
3583
+ await delay(100);
3584
+ const stable = await readReactiveFileReceipt(guard, expectedNames).catch(() => null);
3585
+ return Boolean(stable?.hasReceipt && !stable.hasProgress && !stable.hasError);
3586
+ }
3587
+ if (state.hasProgress && completionDeadline === null) {
3588
+ completionDeadline = Date.now() + 15_000;
3589
+ }
3590
+ await delay(50);
3591
+ }
3592
+ return false;
3593
+ }
3374
3594
  async function clearAmbiguousRetainedFiles(handle, actualNames, initialNames) {
3375
3595
  const selectionChanged = !uploadedNamesMatch(actualNames, initialNames);
3376
3596
  if (!selectionChanged) {
@@ -3391,7 +3611,84 @@ async function clearAmbiguousRetainedFiles(handle, actualNames, initialNames) {
3391
3611
  }
3392
3612
  return 'the changed selection was cleared best-effort without trying another candidate';
3393
3613
  }
3394
- async function commitFilesToExactInput(handle, paths, targetName, setFiles = async (selectedPaths) => handle.setInputFiles(selectedPaths)) {
3614
+ async function confirmSameKeyFileInputSuccessor(page, original, fieldKey, paths, initial) {
3615
+ const expectedNames = expectedUploadNames(paths);
3616
+ const deadline = Date.now() + 1_500;
3617
+ while (Date.now() < deadline) {
3618
+ let successor;
3619
+ try {
3620
+ successor = await findAuthoredFieldByKeyInPage(page, fieldKey, 'file');
3621
+ }
3622
+ catch {
3623
+ // Duplicate authored keys are terminal ambiguity, not a reason to pick
3624
+ // one candidate or try another uploader.
3625
+ return false;
3626
+ }
3627
+ if (!successor) {
3628
+ await delay(50);
3629
+ continue;
3630
+ }
3631
+ const state = await successor.evaluate((input, prior) => {
3632
+ if (!(input instanceof HTMLInputElement) || input.type !== 'file' || !input.isConnected)
3633
+ return null;
3634
+ return {
3635
+ isSuccessor: input !== prior,
3636
+ accept: input.getAttribute('accept'),
3637
+ multiple: input.multiple,
3638
+ names: Array.from(input.files ?? []).map(file => file.name),
3639
+ };
3640
+ }, original).catch(() => null);
3641
+ if (!state?.isSuccessor) {
3642
+ await delay(50);
3643
+ continue;
3644
+ }
3645
+ // Only accept a stable successor whose authored contract still permits
3646
+ // this upload and whose exact FileList proves that this upload, rather
3647
+ // than a sibling input, survived.
3648
+ if (state.multiple !== initial.multiple)
3649
+ return false;
3650
+ try {
3651
+ assertPathsMatchFileAccept(paths, state.accept);
3652
+ }
3653
+ catch {
3654
+ return false;
3655
+ }
3656
+ if (!uploadedNamesMatch(state.names, expectedNames)) {
3657
+ await delay(50);
3658
+ continue;
3659
+ }
3660
+ await delay(40);
3661
+ let confirmed;
3662
+ try {
3663
+ confirmed = await findAuthoredFieldByKeyInPage(page, fieldKey, 'file');
3664
+ }
3665
+ catch {
3666
+ return false;
3667
+ }
3668
+ if (!confirmed)
3669
+ return false;
3670
+ const stable = await confirmed.evaluate((input, payload) => {
3671
+ if (!(input instanceof HTMLInputElement) ||
3672
+ input.type !== 'file' ||
3673
+ !input.isConnected ||
3674
+ input === payload.prior ||
3675
+ input.getAttribute('accept') !== payload.accept ||
3676
+ input.multiple !== payload.multiple)
3677
+ return false;
3678
+ const names = Array.from(input.files ?? []).map(file => file.name);
3679
+ return names.length === payload.names.length &&
3680
+ names.every((name, index) => name === payload.names[index]);
3681
+ }, {
3682
+ prior: original,
3683
+ accept: state.accept,
3684
+ multiple: initial.multiple,
3685
+ names: expectedNames,
3686
+ }).catch(() => false);
3687
+ return stable;
3688
+ }
3689
+ return false;
3690
+ }
3691
+ async function commitFilesToExactInput(handle, paths, targetName, opts) {
3395
3692
  const { guard, initial } = await beginFileInputCommitGuard(handle);
3396
3693
  try {
3397
3694
  if (!initial.validFileInput) {
@@ -3411,7 +3708,7 @@ async function commitFilesToExactInput(handle, paths, targetName, setFiles = asy
3411
3708
  }
3412
3709
  let operationError;
3413
3710
  try {
3414
- await setFiles(paths);
3711
+ await (opts?.setFiles ?? (async (selectedPaths) => handle.setInputFiles(selectedPaths)))(paths);
3415
3712
  }
3416
3713
  catch (error) {
3417
3714
  operationError = error;
@@ -3424,7 +3721,20 @@ async function commitFilesToExactInput(handle, paths, targetName, setFiles = asy
3424
3721
  names: [],
3425
3722
  }));
3426
3723
  const retained = uploadedNamesMatch(final.names, expectedUploadNames(paths));
3427
- if (!operationError && !final.invalidReason && retained)
3724
+ const originalConnected = await handle.evaluate(input => input.isConnected).catch(() => false);
3725
+ if (!operationError && !final.invalidReason && retained && originalConnected)
3726
+ return;
3727
+ const identityChanged = final.invalidReason === 'the exact target identity changed during commit' ||
3728
+ final.invalidReason === 'the exact target detached during commit' ||
3729
+ (!final.invalidReason && !originalConnected);
3730
+ if (!operationError &&
3731
+ identityChanged &&
3732
+ opts?.successor &&
3733
+ await confirmSameKeyFileInputSuccessor(opts.successor.page, handle, opts.successor.fieldKey, paths, initial))
3734
+ return;
3735
+ if (!operationError &&
3736
+ identityChanged &&
3737
+ await confirmStableReactiveFileReceipt(guard, paths))
3428
3738
  return;
3429
3739
  const cleanupOutcome = await clearAmbiguousRetainedFiles(handle, final.names, initial.names);
3430
3740
  const operationReason = operationError instanceof Error
@@ -3446,7 +3756,9 @@ async function attachHiddenInAllFrames(page, paths, opts) {
3446
3756
  const handle = await labeled.elementHandle();
3447
3757
  if (!handle)
3448
3758
  throw new Error('file: matched input detached before upload');
3449
- await commitFilesToExactInput(handle, paths, 'matched input');
3759
+ await commitFilesToExactInput(handle, paths, 'matched input', {
3760
+ successor: opts?.fieldKey ? { page, fieldKey: opts.fieldKey } : undefined,
3761
+ });
3450
3762
  return true;
3451
3763
  }
3452
3764
  return false;
@@ -3486,8 +3798,10 @@ async function attachViaChooser(page, paths, clickX, clickY) {
3486
3798
  ]);
3487
3799
  const input = chooser.element();
3488
3800
  try {
3489
- await commitFilesToExactInput(input, paths, 'chooser input', async (selectedPaths) => {
3490
- await chooser.setFiles(selectedPaths);
3801
+ await commitFilesToExactInput(input, paths, 'chooser input', {
3802
+ setFiles: async (selectedPaths) => {
3803
+ await chooser.setFiles(selectedPaths);
3804
+ },
3491
3805
  });
3492
3806
  }
3493
3807
  finally {