@specsage/cli 0.1.16 → 0.1.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/lib/browser.js +59 -10
- package/package.json +1 -1
package/lib/browser.js
CHANGED
|
@@ -860,21 +860,70 @@ async function handleCommand(msg) {
|
|
|
860
860
|
const centerX = x + w / 2;
|
|
861
861
|
centerY = y + h / 2;
|
|
862
862
|
|
|
863
|
-
//
|
|
864
|
-
const
|
|
865
|
-
has: page.locator(`text="${value}"`)
|
|
866
|
-
}).or(page.locator('select')).first();
|
|
867
|
-
|
|
868
|
-
// Try to find the exact select element by position
|
|
869
|
-
const selectAtPoint = await page.evaluateHandle(
|
|
863
|
+
// Try to find a native <select> element by position
|
|
864
|
+
const selectHandle = await page.evaluateHandle(
|
|
870
865
|
({ x, y }) => document.elementFromPoint(x, y)?.closest('select'),
|
|
871
866
|
{ x: centerX, y: centerY }
|
|
872
867
|
);
|
|
873
868
|
|
|
874
|
-
|
|
875
|
-
|
|
869
|
+
// evaluateHandle returns a JSHandle even for null — check the underlying value
|
|
870
|
+
const isNativeSelect = await selectHandle.evaluate(el => el !== null).catch(() => false);
|
|
871
|
+
|
|
872
|
+
if (isNativeSelect) {
|
|
873
|
+
await selectHandle.selectOption({ label: value });
|
|
876
874
|
} else {
|
|
877
|
-
|
|
875
|
+
// Custom combobox/select: click to open, then poll for the option.
|
|
876
|
+
// Options may load asynchronously (e.g. MUI selects that fetch data
|
|
877
|
+
// after a dependent field changes), so we retry instead of a fixed wait.
|
|
878
|
+
|
|
879
|
+
// Grab aria-controls before clicking so we can scope to the correct listbox
|
|
880
|
+
const listboxId = await page.evaluate(
|
|
881
|
+
({ x, y }) => {
|
|
882
|
+
const el = document.elementFromPoint(x, y)?.closest('[role="combobox"]');
|
|
883
|
+
return el?.getAttribute('aria-controls') || null;
|
|
884
|
+
},
|
|
885
|
+
{ x: centerX, y: centerY }
|
|
886
|
+
);
|
|
887
|
+
|
|
888
|
+
await page.mouse.click(centerX, centerY);
|
|
889
|
+
|
|
890
|
+
const SELECT_TIMEOUT = 10000;
|
|
891
|
+
const POLL_INTERVAL = 300;
|
|
892
|
+
const deadline = Date.now() + SELECT_TIMEOUT;
|
|
893
|
+
let clicked = false;
|
|
894
|
+
|
|
895
|
+
// Build a scoped listbox locator: prefer aria-controls ID, fall back to last visible listbox
|
|
896
|
+
const listboxLocator = listboxId
|
|
897
|
+
? page.locator(`#${CSS.escape(listboxId)}`)
|
|
898
|
+
: page.locator('[role="listbox"]').last();
|
|
899
|
+
|
|
900
|
+
while (Date.now() < deadline) {
|
|
901
|
+
await new Promise(r => setTimeout(r, POLL_INTERVAL));
|
|
902
|
+
|
|
903
|
+
// Try scoped role=option first (MUI, Radix, Headless UI, etc.)
|
|
904
|
+
const optionLocator = listboxLocator.getByRole('option', { name: value, exact: true });
|
|
905
|
+
const optionCount = await optionLocator.count();
|
|
906
|
+
if (optionCount > 0) {
|
|
907
|
+
await optionLocator.first().click();
|
|
908
|
+
clicked = true;
|
|
909
|
+
break;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// Fallback: text match inside the scoped listbox or any ul within it
|
|
913
|
+
const textLocator = listboxLocator.locator(`text="${value}"`).or(
|
|
914
|
+
page.locator(`ul >> text="${value}"`)
|
|
915
|
+
).first();
|
|
916
|
+
const textCount = await textLocator.count();
|
|
917
|
+
if (textCount > 0) {
|
|
918
|
+
await textLocator.click();
|
|
919
|
+
clicked = true;
|
|
920
|
+
break;
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
if (!clicked) {
|
|
925
|
+
throw new Error(`Option "${value}" not found in custom select after ${SELECT_TIMEOUT}ms`);
|
|
926
|
+
}
|
|
878
927
|
}
|
|
879
928
|
|
|
880
929
|
// Wait for page to settle after selection (form updates, validation)
|