@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.
- package/dist/dom-actions.d.ts +24 -3
- package/dist/dom-actions.d.ts.map +1 -1
- package/dist/dom-actions.js +550 -21
- package/dist/dom-actions.js.map +1 -1
- package/dist/geometry-ws.d.ts +2 -0
- package/dist/geometry-ws.d.ts.map +1 -1
- package/dist/geometry-ws.js +40 -7
- package/dist/geometry-ws.js.map +1 -1
- package/dist/index.js +11 -70
- package/dist/index.js.map +1 -1
- package/dist/runtime.d.ts +26 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +110 -0
- package/dist/runtime.js.map +1 -0
- package/dist/types.d.ts +25 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
package/dist/dom-actions.js
CHANGED
|
@@ -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
|
-
|
|
386
|
-
|
|
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
|
-
|
|
1181
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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, {
|
|
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;
|