@geometra/proxy 1.19.15 → 1.19.17

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.
@@ -5,9 +5,65 @@ function delay(ms) {
5
5
  }
6
6
  const LABELED_CONTROL_SELECTOR = 'input, select, textarea, button, [role="combobox"], [role="textbox"], [aria-haspopup="listbox"], [contenteditable="true"]';
7
7
  const OPTION_PICKER_SELECTOR = '[role="option"], [role="menuitem"], [role="treeitem"], button, li, [data-value], [aria-selected], [aria-checked]';
8
+ export function createFillLookupCache() {
9
+ return {
10
+ control: new Map(),
11
+ editable: new Map(),
12
+ fileInput: new Map(),
13
+ };
14
+ }
15
+ export function clearFillLookupCache(cache) {
16
+ cache.control.clear();
17
+ cache.editable.clear();
18
+ cache.fileInput.clear();
19
+ }
20
+ function lookupCacheKey(kind, label, exact) {
21
+ return `${kind}:${exact ? 'exact' : 'fuzzy'}:${normalizedOptionLabel(label)}`;
22
+ }
23
+ function lookupCacheKeys(kind, label, exact, fieldId) {
24
+ const keys = [lookupCacheKey(kind, label, exact)];
25
+ if (fieldId)
26
+ keys.unshift(`${kind}:id:${fieldId}`);
27
+ return keys;
28
+ }
29
+ function cacheMapForKind(cache, kind) {
30
+ if (kind === 'control')
31
+ return cache.control;
32
+ if (kind === 'editable')
33
+ return cache.editable;
34
+ return cache.fileInput;
35
+ }
36
+ function readCachedLocator(cache, kind, label, exact, fieldId) {
37
+ if (!cache)
38
+ return undefined;
39
+ const map = cacheMapForKind(cache, kind);
40
+ for (const key of lookupCacheKeys(kind, label, exact, fieldId)) {
41
+ if (map.has(key))
42
+ return map.get(key) ?? null;
43
+ }
44
+ return undefined;
45
+ }
46
+ function writeCachedLocator(cache, kind, label, exact, fieldId, locator) {
47
+ if (!cache)
48
+ return;
49
+ const map = cacheMapForKind(cache, kind);
50
+ for (const key of lookupCacheKeys(kind, label, exact, fieldId)) {
51
+ map.set(key, locator);
52
+ }
53
+ }
8
54
  function normalizedOptionLabel(value) {
9
55
  return value.replace(/\s+/g, ' ').trim().toLowerCase();
10
56
  }
57
+ function prefersGroupedChoiceValue(value) {
58
+ const normalized = normalizedOptionLabel(value);
59
+ return normalized === 'yes' ||
60
+ normalized === 'no' ||
61
+ normalized === 'true' ||
62
+ normalized === 'false' ||
63
+ normalized === 'decline' ||
64
+ normalized === 'prefer not' ||
65
+ normalized === 'opt out';
66
+ }
11
67
  function semanticSelectionAliases(value) {
12
68
  const normalized = normalizedOptionLabel(value);
13
69
  const aliases = new Set([normalized]);
@@ -367,6 +423,24 @@ async function findLabeledControl(frame, fieldLabel, exact, opts) {
367
423
  });
368
424
  return bestIndex >= 0 ? fallbackCandidates.nth(bestIndex) : null;
369
425
  }
426
+ async function findLabeledControlInPage(page, fieldLabel, exact, opts) {
427
+ const cacheable = !opts?.preferredAnchor;
428
+ const cached = cacheable ? readCachedLocator(opts?.cache, 'control', fieldLabel, exact, opts?.fieldId) : undefined;
429
+ if (cached !== undefined) {
430
+ return cached;
431
+ }
432
+ for (const frame of page.frames()) {
433
+ const locator = await findLabeledControl(frame, fieldLabel, exact, { preferredAnchor: opts?.preferredAnchor });
434
+ if (!locator)
435
+ continue;
436
+ if (cacheable)
437
+ writeCachedLocator(opts?.cache, 'control', fieldLabel, exact, opts?.fieldId, locator);
438
+ return locator;
439
+ }
440
+ if (cacheable)
441
+ writeCachedLocator(opts?.cache, 'control', fieldLabel, exact, opts?.fieldId, null);
442
+ return null;
443
+ }
370
444
  function textMatches(candidate, expected, exact) {
371
445
  return selectionMatchScore(candidate, expected, exact) !== null;
372
446
  }
@@ -381,11 +455,9 @@ function displayedValueMatchesSelection(candidate, expected, exact, selectedOpti
381
455
  return false;
382
456
  return (normalizedSelectedOption.includes(normalizedCandidate) || normalizedCandidate.includes(normalizedSelectedOption));
383
457
  }
384
- async function openDropdownControl(page, fieldLabel, exact) {
385
- for (const frame of page.frames()) {
386
- const locator = await findLabeledControl(frame, fieldLabel, exact);
387
- if (!locator)
388
- continue;
458
+ async function openDropdownControl(page, fieldLabel, exact, cache, fieldId) {
459
+ const locator = await findLabeledControlInPage(page, fieldLabel, exact, { cache, fieldId });
460
+ if (locator) {
389
461
  await locator.scrollIntoViewIfNeeded();
390
462
  const handle = await locator.elementHandle();
391
463
  const clickTarget = await resolveMeaningfulClickTarget(locator);
@@ -788,7 +860,33 @@ async function findLabeledFileInput(frame, fieldLabel, exact) {
788
860
  }, { fieldLabel, exact });
789
861
  return bestIndex >= 0 ? loc.nth(bestIndex) : null;
790
862
  }
863
+ async function findLabeledFileInputInPage(page, fieldLabel, exact, cache, fieldId) {
864
+ const cached = readCachedLocator(cache, 'file', fieldLabel, exact, fieldId);
865
+ if (cached !== undefined)
866
+ return cached;
867
+ for (const frame of page.frames()) {
868
+ const locator = await findLabeledFileInput(frame, fieldLabel, exact);
869
+ if (!locator)
870
+ continue;
871
+ writeCachedLocator(cache, 'file', fieldLabel, exact, fieldId, locator);
872
+ return locator;
873
+ }
874
+ writeCachedLocator(cache, 'file', fieldLabel, exact, fieldId, null);
875
+ return null;
876
+ }
791
877
  async function attachHiddenInAllFrames(page, paths, opts) {
878
+ if (opts?.fieldLabel) {
879
+ const labeled = await findLabeledFileInputInPage(page, opts.fieldLabel, opts.exact ?? false, opts.cache, opts.fieldId);
880
+ if (labeled) {
881
+ try {
882
+ await labeled.setInputFiles(paths);
883
+ return true;
884
+ }
885
+ catch {
886
+ /* fall through to uncached scan */
887
+ }
888
+ }
889
+ }
792
890
  for (const frame of page.frames()) {
793
891
  if (opts?.fieldLabel) {
794
892
  const labeled = await findLabeledFileInput(frame, opts.fieldLabel, opts.exact ?? false);
@@ -864,7 +962,7 @@ export async function attachFiles(page, paths, opts) {
864
962
  return;
865
963
  }
866
964
  if (strategy === 'hidden' || strategy === 'auto') {
867
- if (await attachHiddenInAllFrames(page, paths, { fieldLabel, exact }))
965
+ if (await attachHiddenInAllFrames(page, paths, { fieldId: opts?.fieldId, fieldLabel, exact, cache: opts?.cache }))
868
966
  return;
869
967
  if (strategy === 'hidden') {
870
968
  if (fieldLabel) {
@@ -893,7 +991,10 @@ export async function attachFiles(page, paths, opts) {
893
991
  }
894
992
  throw new Error('file: no input[type=file] in any frame; pass x,y (chooser), dropX/dropY (drop), or strategy hidden');
895
993
  }
896
- async function findLabeledEditableField(page, fieldLabel, exact) {
994
+ async function findLabeledEditableField(page, fieldLabel, exact, cache, fieldId) {
995
+ const cached = readCachedLocator(cache, 'editable', fieldLabel, exact, fieldId);
996
+ if (cached !== undefined)
997
+ return cached;
897
998
  for (const frame of page.frames()) {
898
999
  const candidates = [
899
1000
  frame.getByLabel(fieldLabel, { exact }),
@@ -904,13 +1005,18 @@ async function findLabeledEditableField(page, fieldLabel, exact) {
904
1005
  const visible = await firstVisible(candidate, { minWidth: 1, minHeight: 1 });
905
1006
  if (!visible)
906
1007
  continue;
907
- if (await locatorIsEditable(visible))
1008
+ if (await locatorIsEditable(visible)) {
1009
+ writeCachedLocator(cache, 'editable', fieldLabel, exact, fieldId, visible);
908
1010
  return visible;
1011
+ }
909
1012
  }
910
1013
  const fallback = await findLabeledControl(frame, fieldLabel, exact);
911
- if (fallback && await locatorIsEditable(fallback))
1014
+ if (fallback && await locatorIsEditable(fallback)) {
1015
+ writeCachedLocator(cache, 'editable', fieldLabel, exact, fieldId, fallback);
912
1016
  return fallback;
1017
+ }
913
1018
  }
1019
+ writeCachedLocator(cache, 'editable', fieldLabel, exact, fieldId, null);
914
1020
  return null;
915
1021
  }
916
1022
  async function locatorCurrentValue(locator) {
@@ -995,9 +1101,329 @@ async function setLocatorTextValue(locator, value) {
995
1101
  return false;
996
1102
  }
997
1103
  }
1104
+ async function attemptNativeBatchFill(page, fields) {
1105
+ const results = fields.map(() => false);
1106
+ for (const frame of page.frames()) {
1107
+ const pending = [];
1108
+ for (let index = 0; index < fields.length; index++) {
1109
+ if (results[index])
1110
+ continue;
1111
+ const field = fields[index];
1112
+ if (field.kind === 'file')
1113
+ continue;
1114
+ if (field.kind === 'auto') {
1115
+ pending.push({
1116
+ index,
1117
+ kind: 'auto',
1118
+ fieldLabel: field.fieldLabel,
1119
+ value: field.value,
1120
+ exact: field.exact ?? false,
1121
+ });
1122
+ continue;
1123
+ }
1124
+ if (field.kind === 'text') {
1125
+ pending.push({
1126
+ index,
1127
+ kind: 'text',
1128
+ fieldLabel: field.fieldLabel,
1129
+ value: field.value,
1130
+ exact: field.exact ?? false,
1131
+ });
1132
+ continue;
1133
+ }
1134
+ if (field.kind === 'choice') {
1135
+ pending.push({
1136
+ index,
1137
+ kind: 'choice',
1138
+ fieldLabel: field.fieldLabel,
1139
+ value: field.value,
1140
+ exact: field.exact ?? false,
1141
+ choiceType: field.choiceType ?? null,
1142
+ });
1143
+ continue;
1144
+ }
1145
+ pending.push({
1146
+ index,
1147
+ kind: 'toggle',
1148
+ label: field.label,
1149
+ checked: field.checked ?? true,
1150
+ exact: field.exact ?? false,
1151
+ controlType: field.controlType ?? null,
1152
+ });
1153
+ }
1154
+ if (pending.length === 0)
1155
+ break;
1156
+ const frameResults = await frame.evaluate((items) => {
1157
+ function normalize(value) {
1158
+ return (value ?? '').replace(/\s+/g, ' ').trim().toLowerCase();
1159
+ }
1160
+ function matches(candidate, expected, exact) {
1161
+ const normalizedCandidate = normalize(candidate);
1162
+ const normalizedExpected = normalize(expected);
1163
+ if (!normalizedCandidate || !normalizedExpected)
1164
+ return false;
1165
+ return exact ? normalizedCandidate === normalizedExpected : normalizedCandidate.includes(normalizedExpected);
1166
+ }
1167
+ function visible(el) {
1168
+ if (!(el instanceof HTMLElement))
1169
+ return false;
1170
+ const rect = el.getBoundingClientRect();
1171
+ if (rect.width <= 0 || rect.height <= 0)
1172
+ return false;
1173
+ const style = getComputedStyle(el);
1174
+ return style.display !== 'none' && style.visibility !== 'hidden';
1175
+ }
1176
+ function referencedText(ids) {
1177
+ if (!ids)
1178
+ return undefined;
1179
+ const text = ids
1180
+ .split(/\s+/)
1181
+ .map(id => document.getElementById(id)?.textContent?.trim() ?? '')
1182
+ .filter(Boolean)
1183
+ .join(' ')
1184
+ .trim();
1185
+ return text || undefined;
1186
+ }
1187
+ function explicitLabelText(el) {
1188
+ const aria = el.getAttribute('aria-label')?.trim();
1189
+ if (aria)
1190
+ return aria;
1191
+ const labelledBy = referencedText(el.getAttribute('aria-labelledby'));
1192
+ if (labelledBy)
1193
+ return labelledBy;
1194
+ if ((el instanceof HTMLInputElement || el instanceof HTMLSelectElement || el instanceof HTMLTextAreaElement) &&
1195
+ el.labels &&
1196
+ el.labels.length > 0) {
1197
+ return el.labels[0]?.textContent?.trim() || undefined;
1198
+ }
1199
+ if (el instanceof HTMLElement && el.id) {
1200
+ const label = document.querySelector(`label[for="${CSS.escape(el.id)}"]`);
1201
+ const text = label?.textContent?.trim();
1202
+ if (text)
1203
+ return text;
1204
+ }
1205
+ if (el.parentElement?.tagName.toLowerCase() === 'label') {
1206
+ return el.parentElement.textContent?.trim() || undefined;
1207
+ }
1208
+ return undefined;
1209
+ }
1210
+ function dispatch(target) {
1211
+ target.dispatchEvent(new Event('input', { bubbles: true }));
1212
+ target.dispatchEvent(new Event('change', { bubbles: true }));
1213
+ }
1214
+ function setInputLikeValue(target, next) {
1215
+ const proto = target instanceof HTMLInputElement ? HTMLInputElement.prototype : HTMLTextAreaElement.prototype;
1216
+ const descriptor = Object.getOwnPropertyDescriptor(proto, 'value');
1217
+ if (descriptor?.set)
1218
+ descriptor.set.call(target, next);
1219
+ else
1220
+ target.value = next;
1221
+ }
1222
+ function currentValue(el) {
1223
+ if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement)
1224
+ return el.value;
1225
+ if (el instanceof HTMLSelectElement)
1226
+ return el.selectedOptions[0]?.textContent?.trim() || el.value;
1227
+ if (el instanceof HTMLElement && el.isContentEditable)
1228
+ return el.innerText || el.textContent || '';
1229
+ return '';
1230
+ }
1231
+ function prefersGroupedChoice(input) {
1232
+ const normalized = normalize(input);
1233
+ return normalized === 'yes' ||
1234
+ normalized === 'no' ||
1235
+ normalized === 'true' ||
1236
+ normalized === 'false' ||
1237
+ normalized === 'decline' ||
1238
+ normalized === 'prefer not' ||
1239
+ normalized === 'opt out';
1240
+ }
1241
+ function setTextField(fieldLabel, value, exact) {
1242
+ const controls = Array.from(document.querySelectorAll('input, textarea, [contenteditable="true"]'));
1243
+ for (const control of controls) {
1244
+ if (!(control instanceof Element) || !visible(control))
1245
+ continue;
1246
+ if (control instanceof HTMLInputElement && ['checkbox', 'radio', 'file', 'button', 'submit', 'reset', 'hidden'].includes(control.type)) {
1247
+ continue;
1248
+ }
1249
+ if (!matches(explicitLabelText(control), fieldLabel, exact))
1250
+ continue;
1251
+ if (control instanceof HTMLInputElement || control instanceof HTMLTextAreaElement) {
1252
+ control.focus();
1253
+ setInputLikeValue(control, value);
1254
+ dispatch(control);
1255
+ return matches(currentValue(control), value, false);
1256
+ }
1257
+ if (control instanceof HTMLElement && control.isContentEditable) {
1258
+ control.focus();
1259
+ control.textContent = value;
1260
+ dispatch(control);
1261
+ return matches(currentValue(control), value, false);
1262
+ }
1263
+ }
1264
+ return false;
1265
+ }
1266
+ function setSelectField(fieldLabel, value, exact) {
1267
+ const controls = Array.from(document.querySelectorAll('select'));
1268
+ for (const control of controls) {
1269
+ if (!(control instanceof HTMLSelectElement) || !visible(control))
1270
+ continue;
1271
+ if (!matches(explicitLabelText(control), fieldLabel, exact))
1272
+ continue;
1273
+ const expected = normalize(value);
1274
+ const option = Array.from(control.options).find((candidate) => {
1275
+ const label = normalize(candidate.textContent);
1276
+ const rawValue = normalize(candidate.value);
1277
+ return exact ? label === expected || rawValue === expected : label.includes(expected) || rawValue.includes(expected);
1278
+ });
1279
+ if (!option)
1280
+ return false;
1281
+ control.value = option.value;
1282
+ dispatch(control);
1283
+ return matches(currentValue(control), value, false) || normalize(control.value) === expected;
1284
+ }
1285
+ return false;
1286
+ }
1287
+ function groupPrompt(container) {
1288
+ const legend = container.querySelector('legend')?.textContent?.trim();
1289
+ if (legend)
1290
+ return legend;
1291
+ const explicit = explicitLabelText(container);
1292
+ if (explicit)
1293
+ return explicit;
1294
+ const textLike = container.querySelector('h1, h2, h3, h4, h5, h6, p, span, div');
1295
+ const text = textLike?.textContent?.trim();
1296
+ return text || undefined;
1297
+ }
1298
+ function choiceLabel(el) {
1299
+ if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement) {
1300
+ return explicitLabelText(el);
1301
+ }
1302
+ if (el instanceof HTMLLabelElement)
1303
+ return el.textContent?.trim() || undefined;
1304
+ const explicit = explicitLabelText(el);
1305
+ if (explicit)
1306
+ return explicit;
1307
+ return el.textContent?.trim() || undefined;
1308
+ }
1309
+ function setGroupedChoice(fieldLabel, value, exact) {
1310
+ const groups = Array.from(document.querySelectorAll('fieldset, [role="radiogroup"], [role="group"]'))
1311
+ .filter((el) => visible(el) && matches(groupPrompt(el), fieldLabel, exact));
1312
+ for (const group of groups) {
1313
+ const options = Array.from(group.querySelectorAll('input[type="radio"], input[type="checkbox"], [role="radio"], [role="checkbox"], label, button'));
1314
+ for (const option of options) {
1315
+ if (!(option instanceof Element) || !visible(option))
1316
+ continue;
1317
+ if (!matches(choiceLabel(option), value, exact))
1318
+ continue;
1319
+ if (option instanceof HTMLInputElement) {
1320
+ if (!option.checked)
1321
+ option.click();
1322
+ if (!option.checked) {
1323
+ option.checked = true;
1324
+ dispatch(option);
1325
+ }
1326
+ return option.checked;
1327
+ }
1328
+ if (option instanceof HTMLLabelElement) {
1329
+ option.click();
1330
+ const control = option.control;
1331
+ if (control instanceof HTMLInputElement)
1332
+ return control.checked;
1333
+ return true;
1334
+ }
1335
+ option.click();
1336
+ return true;
1337
+ }
1338
+ }
1339
+ return false;
1340
+ }
1341
+ function setToggle(label, checked, exact, controlType) {
1342
+ const inputs = Array.from(document.querySelectorAll('input[type="checkbox"], input[type="radio"]'));
1343
+ for (const input of inputs) {
1344
+ if (!(input instanceof HTMLInputElement) || !visible(input))
1345
+ continue;
1346
+ if (controlType && input.type !== controlType)
1347
+ continue;
1348
+ if (!matches(explicitLabelText(input), label, exact))
1349
+ continue;
1350
+ if (input.checked !== checked) {
1351
+ input.click();
1352
+ if (input.checked !== checked) {
1353
+ input.checked = checked;
1354
+ dispatch(input);
1355
+ }
1356
+ }
1357
+ return input.checked === checked;
1358
+ }
1359
+ const labels = Array.from(document.querySelectorAll('label'));
1360
+ for (const labelEl of labels) {
1361
+ if (!(labelEl instanceof HTMLLabelElement) || !visible(labelEl))
1362
+ continue;
1363
+ if (!matches(labelEl.textContent, label, exact))
1364
+ continue;
1365
+ const control = labelEl.control;
1366
+ if (!(control instanceof HTMLInputElement))
1367
+ continue;
1368
+ if (controlType && control.type !== controlType)
1369
+ continue;
1370
+ if (control.checked !== checked)
1371
+ labelEl.click();
1372
+ if (control.checked !== checked) {
1373
+ control.checked = checked;
1374
+ dispatch(control);
1375
+ }
1376
+ return control.checked === checked;
1377
+ }
1378
+ return false;
1379
+ }
1380
+ function setAutoField(fieldLabel, value, exact) {
1381
+ if (typeof value === 'boolean') {
1382
+ if (setGroupedChoice(fieldLabel, value ? 'Yes' : 'No', exact))
1383
+ return true;
1384
+ if (setToggle(fieldLabel, value, exact, null))
1385
+ return true;
1386
+ return setSelectField(fieldLabel, value ? 'Yes' : 'No', exact);
1387
+ }
1388
+ if (prefersGroupedChoice(value) && setGroupedChoice(fieldLabel, value, exact))
1389
+ return true;
1390
+ if (setSelectField(fieldLabel, value, exact))
1391
+ return true;
1392
+ if (setTextField(fieldLabel, value, exact))
1393
+ return true;
1394
+ return setGroupedChoice(fieldLabel, value, exact);
1395
+ }
1396
+ return items.map((field) => {
1397
+ if (field.kind === 'auto') {
1398
+ return { index: field.index, ok: setAutoField(field.fieldLabel, field.value, field.exact) };
1399
+ }
1400
+ if (field.kind === 'text')
1401
+ return { index: field.index, ok: setTextField(field.fieldLabel, field.value, field.exact) };
1402
+ if (field.kind === 'choice') {
1403
+ if (field.choiceType === 'group') {
1404
+ return { index: field.index, ok: setGroupedChoice(field.fieldLabel, field.value, field.exact) };
1405
+ }
1406
+ if (field.choiceType === 'listbox') {
1407
+ return { index: field.index, ok: false };
1408
+ }
1409
+ const selected = setSelectField(field.fieldLabel, field.value, field.exact);
1410
+ if (selected)
1411
+ return { index: field.index, ok: true };
1412
+ return { index: field.index, ok: setGroupedChoice(field.fieldLabel, field.value, field.exact) };
1413
+ }
1414
+ return { index: field.index, ok: setToggle(field.label, field.checked, field.exact, field.controlType) };
1415
+ });
1416
+ }, pending).catch(() => []);
1417
+ for (const entry of frameResults) {
1418
+ if (entry?.ok === true)
1419
+ results[entry.index] = true;
1420
+ }
1421
+ }
1422
+ return results;
1423
+ }
998
1424
  export async function setFieldText(page, fieldLabel, value, opts) {
999
1425
  const exact = opts?.exact ?? false;
1000
- const locator = await findLabeledEditableField(page, fieldLabel, exact);
1426
+ const locator = await findLabeledEditableField(page, fieldLabel, exact, opts?.cache, opts?.fieldId);
1001
1427
  if (!locator) {
1002
1428
  throw new Error(`setFieldText: no visible editable field matching "${fieldLabel}"`);
1003
1429
  }
@@ -1175,12 +1601,91 @@ async function chooseValueFromLabeledGroup(page, fieldLabel, value, exact) {
1175
1601
  }
1176
1602
  return false;
1177
1603
  }
1604
+ async function autoFieldLocatorKind(locator) {
1605
+ try {
1606
+ return await locator.evaluate((el) => {
1607
+ if (el instanceof HTMLSelectElement)
1608
+ return 'select';
1609
+ if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement)
1610
+ return 'text';
1611
+ if (el instanceof HTMLElement && el.isContentEditable)
1612
+ return 'text';
1613
+ const role = el.getAttribute('role');
1614
+ if (role === 'combobox' || role === 'listbox')
1615
+ return 'choice';
1616
+ if (el instanceof HTMLButtonElement)
1617
+ return 'choice';
1618
+ if (el.getAttribute('aria-haspopup') === 'listbox')
1619
+ return 'choice';
1620
+ return 'unknown';
1621
+ });
1622
+ }
1623
+ catch {
1624
+ return 'unknown';
1625
+ }
1626
+ }
1627
+ export async function setAutoFieldValue(page, fieldLabel, value, opts) {
1628
+ const exact = opts?.exact ?? false;
1629
+ if (typeof value === 'boolean') {
1630
+ if (await chooseValueFromLabeledGroup(page, fieldLabel, value ? 'Yes' : 'No', exact))
1631
+ return;
1632
+ await setCheckedControl(page, fieldLabel, { checked: value, exact });
1633
+ return;
1634
+ }
1635
+ if (prefersGroupedChoiceValue(value) && await chooseValueFromLabeledGroup(page, fieldLabel, value, exact)) {
1636
+ return;
1637
+ }
1638
+ const locator = await findLabeledControlInPage(page, fieldLabel, exact, { cache: opts?.cache, fieldId: opts?.fieldId });
1639
+ if (locator) {
1640
+ const kind = await autoFieldLocatorKind(locator);
1641
+ if (kind === 'select') {
1642
+ await setFieldChoice(page, fieldLabel, value, {
1643
+ fieldId: opts?.fieldId,
1644
+ exact,
1645
+ choiceType: 'select',
1646
+ cache: opts?.cache,
1647
+ });
1648
+ return;
1649
+ }
1650
+ if (kind === 'text') {
1651
+ await setFieldText(page, fieldLabel, value, {
1652
+ fieldId: opts?.fieldId,
1653
+ exact,
1654
+ cache: opts?.cache,
1655
+ });
1656
+ return;
1657
+ }
1658
+ if (kind === 'choice') {
1659
+ await setFieldChoice(page, fieldLabel, value, {
1660
+ fieldId: opts?.fieldId,
1661
+ exact,
1662
+ cache: opts?.cache,
1663
+ });
1664
+ return;
1665
+ }
1666
+ }
1667
+ if (await chooseValueFromLabeledGroup(page, fieldLabel, value, exact))
1668
+ return;
1669
+ try {
1670
+ await setFieldText(page, fieldLabel, value, {
1671
+ fieldId: opts?.fieldId,
1672
+ exact,
1673
+ cache: opts?.cache,
1674
+ });
1675
+ return;
1676
+ }
1677
+ catch {
1678
+ await setFieldChoice(page, fieldLabel, value, {
1679
+ fieldId: opts?.fieldId,
1680
+ exact,
1681
+ cache: opts?.cache,
1682
+ });
1683
+ }
1684
+ }
1178
1685
  export async function setFieldChoice(page, fieldLabel, value, opts) {
1179
1686
  const exact = opts?.exact ?? false;
1180
- for (const frame of page.frames()) {
1181
- const locator = await findLabeledControl(frame, fieldLabel, exact);
1182
- if (!locator)
1183
- continue;
1687
+ const locator = await findLabeledControlInPage(page, fieldLabel, exact, { cache: opts?.cache, fieldId: opts?.fieldId });
1688
+ if (locator) {
1184
1689
  await locator.scrollIntoViewIfNeeded();
1185
1690
  if (await setNativeSelectByLabel(locator, value, exact)) {
1186
1691
  const displayed = await locatorDisplayedValues(locator);
@@ -1188,13 +1693,19 @@ export async function setFieldChoice(page, fieldLabel, value, opts) {
1188
1693
  return;
1189
1694
  throw new Error(`setFieldChoice: selected "${value}" for field "${fieldLabel}" but could not confirm it`);
1190
1695
  }
1191
- break;
1696
+ }
1697
+ if (opts?.choiceType === 'group') {
1698
+ if (await chooseValueFromLabeledGroup(page, fieldLabel, value, exact))
1699
+ return;
1700
+ throw new Error(`setFieldChoice: no grouped choice matching "${value}" for field "${fieldLabel}"`);
1192
1701
  }
1193
1702
  try {
1194
1703
  await pickListboxOption(page, value, {
1704
+ fieldId: opts?.fieldId,
1195
1705
  fieldLabel,
1196
1706
  exact,
1197
1707
  query: opts?.query,
1708
+ cache: opts?.cache,
1198
1709
  });
1199
1710
  return;
1200
1711
  }
@@ -1204,14 +1715,32 @@ export async function setFieldChoice(page, fieldLabel, value, opts) {
1204
1715
  throw listboxError;
1205
1716
  }
1206
1717
  }
1207
- export async function fillFields(page, fields) {
1208
- for (const field of fields) {
1718
+ export async function fillFields(page, fields, cache = createFillLookupCache()) {
1719
+ const nativeResults = await attemptNativeBatchFill(page, fields);
1720
+ for (let index = 0; index < fields.length; index++) {
1721
+ const field = fields[index];
1722
+ if (nativeResults[index])
1723
+ continue;
1724
+ if (field.kind === 'auto') {
1725
+ await setAutoFieldValue(page, field.fieldLabel, field.value, {
1726
+ fieldId: field.fieldId,
1727
+ exact: field.exact,
1728
+ cache,
1729
+ });
1730
+ continue;
1731
+ }
1209
1732
  if (field.kind === 'text') {
1210
- await setFieldText(page, field.fieldLabel, field.value, { exact: field.exact });
1733
+ await setFieldText(page, field.fieldLabel, field.value, { fieldId: field.fieldId, exact: field.exact, cache });
1211
1734
  continue;
1212
1735
  }
1213
1736
  if (field.kind === 'choice') {
1214
- await setFieldChoice(page, field.fieldLabel, field.value, { exact: field.exact, query: field.query });
1737
+ await setFieldChoice(page, field.fieldLabel, field.value, {
1738
+ fieldId: field.fieldId,
1739
+ exact: field.exact,
1740
+ query: field.query,
1741
+ choiceType: field.choiceType,
1742
+ cache,
1743
+ });
1215
1744
  continue;
1216
1745
  }
1217
1746
  if (field.kind === 'toggle') {
@@ -1222,7 +1751,7 @@ export async function fillFields(page, fields) {
1222
1751
  });
1223
1752
  continue;
1224
1753
  }
1225
- await attachFiles(page, field.paths, { fieldLabel: field.fieldLabel, exact: field.exact });
1754
+ await attachFiles(page, field.paths, { fieldId: field.fieldId, fieldLabel: field.fieldLabel, exact: field.exact, cache });
1226
1755
  }
1227
1756
  }
1228
1757
  export async function selectNativeOption(page, x, y, opt) {
@@ -1278,7 +1807,7 @@ export async function pickListboxOption(page, label, opts) {
1278
1807
  let selectedOptionText;
1279
1808
  let openedHandle;
1280
1809
  if (opts?.fieldLabel) {
1281
- const opened = await openDropdownControl(page, opts.fieldLabel, exact);
1810
+ const opened = await openDropdownControl(page, opts.fieldLabel, exact, opts.cache, opts.fieldId);
1282
1811
  anchor = { x: opened.anchorX, y: opened.anchorY };
1283
1812
  openedHandle = opened.handle;
1284
1813
  const query = opts.query ?? label;