@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.
- package/dist/dom-actions.d.ts.map +1 -1
- package/dist/dom-actions.js +340 -26
- package/dist/dom-actions.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dom-actions.d.ts","sourceRoot":"","sources":["../src/dom-actions.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAkC,OAAO,EAAE,IAAI,EAAE,MAAM,YAAY,CAAA;AAE/E,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAEnE,wBAAgB,KAAK,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE/C;AAED,MAAM,MAAM,kBAAkB,GAAG,MAAM,GAAG,SAAS,GAAG,QAAQ,GAAG,MAAM,CAAA;AAgDvE,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;
|
|
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
|
@@ -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
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
const
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
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
|
|
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
|
-
|
|
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',
|
|
3490
|
-
|
|
3801
|
+
await commitFilesToExactInput(input, paths, 'chooser input', {
|
|
3802
|
+
setFiles: async (selectedPaths) => {
|
|
3803
|
+
await chooser.setFiles(selectedPaths);
|
|
3804
|
+
},
|
|
3491
3805
|
});
|
|
3492
3806
|
}
|
|
3493
3807
|
finally {
|