remove useless select support legacy (#863)

This commit is contained in:
LawyZheng
2024-09-20 10:55:07 +08:00
committed by GitHub
parent 1b2bdcb949
commit fb56cba6ba
4 changed files with 1 additions and 435 deletions

View File

@@ -313,21 +313,6 @@ class ElementIsNotLabel(SkyvernException):
super().__init__(f"<{tag_name}> element is not <label>") super().__init__(f"<{tag_name}> element is not <label>")
class ElementIsNotSelect2Dropdown(SkyvernException):
def __init__(self, element_id: str, element: dict):
super().__init__(f"element[{element}] is not select2 dropdown. element_id={element_id}")
class ElementIsNotReactSelectDropdown(SkyvernException):
def __init__(self, element_id: str, element: dict):
super().__init__(f"element[{element}] is not react select dropdown. element_id={element_id}")
class ElementIsNotComboboxDropdown(SkyvernException):
def __init__(self, element_id: str, element: dict):
super().__init__(f"element[{element}] is not combobox dropdown. element_id={element_id}")
class NoneFrameError(SkyvernException): class NoneFrameError(SkyvernException):
def __init__(self, frame_id: str): def __init__(self, frame_id: str):
super().__init__(f"frame content is none. frame_id={frame_id}") super().__init__(f"frame content is none. frame_id={frame_id}")
@@ -411,23 +396,6 @@ class NoSelectableElementFound(SkyvernException):
super().__init__(f"No selectable elements found in the children list. element_id={element_id}") super().__init__(f"No selectable elements found in the children list. element_id={element_id}")
class NoDropdownAnchorErr(SkyvernException):
def __init__(self, dropdowm_type: str, element_id: str):
super().__init__(f"No {dropdowm_type} dropdown found. element_id={element_id}")
class MultipleDropdownAnchorErr(SkyvernException):
def __init__(self, dropdowm_type: str, element_id: str):
super().__init__(f"Multiple {dropdowm_type} dropdown found. element_id={element_id}")
class FailedToGetCurrentValueOfDropdown(SkyvernException):
def __init__(self, dropdowm_type: str, element_id: str, fail_reason: str):
super().__init__(
f"Failed to get current value of {dropdowm_type} dropdown. element_id={element_id}, failure_reason={fail_reason}"
)
class HttpException(SkyvernException): class HttpException(SkyvernException):
def __init__(self, status_code: int, url: str, msg: str | None = None) -> None: def __init__(self, status_code: int, url: str, msg: str | None = None) -> None:
super().__init__(f"HTTP Exception, status_code={status_code}, url={url}" + (f", msg={msg}" if msg else "")) super().__init__(f"HTTP Exception, status_code={status_code}, url={url}" + (f", msg={msg}" if msg else ""))

View File

@@ -745,151 +745,6 @@ function getSelectOptions(element) {
return [selectOptions, removeMultipleSpaces(selectedOption.textContent)]; return [selectOptions, removeMultipleSpaces(selectedOption.textContent)];
} }
function getListboxOptions(element) {
// get all the elements with role="option" under the element
var optionElements = element.querySelectorAll('[role="option"]');
let selectOptions = [];
for (var i = 0; i < optionElements.length; i++) {
let ele = optionElements[i];
selectOptions.push({
optionIndex: i,
text: removeMultipleSpaces(getVisibleText(ele)),
});
}
return selectOptions;
}
async function getSelect2OptionElements(element) {
let optionList = [];
const document = element.getRootNode();
while (true) {
oldOptionCount = optionList.length;
let newOptionList = document.querySelectorAll("[id='select2-drop'] ul li");
if (newOptionList.length === oldOptionCount) {
console.log("no more options loaded, wait 5s to query again");
// sometimes need more time to load the options, so sleep 10s and try again
await globalSleep(5000); // wait 5s
newOptionList = document.querySelectorAll("[id='select2-drop'] ul li");
console.log(newOptionList.length, " options found, after 5s");
}
optionList = newOptionList;
if (optionList.length === 0 || optionList.length === oldOptionCount) {
break;
}
lastOption = optionList[optionList.length - 1];
if (!lastOption.className.toString().includes("select2-more-results")) {
break;
}
lastOption.scrollIntoView();
}
return optionList;
}
async function getSelect2Options(element) {
const optionList = await getSelect2OptionElements(element);
let selectOptions = [];
for (let i = 0; i < optionList.length; i++) {
let ele = optionList[i];
if (ele.className.toString().includes("select2-more-results")) {
continue;
}
selectOptions.push({
optionIndex: i,
text: removeMultipleSpaces(ele.textContent),
});
}
return selectOptions;
}
async function getReactSelectOptionElements(element) {
var scrollLeft = window.scrollX;
var scrollTop = window.scrollY;
let optionList = [];
// wait for 2s until the element is updated with `aria-controls`
console.log("wait 2s for the dropdown being updated.");
await globalSleep(2000);
dropdownId = element.getAttribute("aria-controls");
if (!dropdownId) {
return optionList;
}
const document = element.getRootNode();
dropdownDiv = document.querySelector(`div[id="${dropdownId}"]`);
let previousOptionCount = null;
while (true) {
// sometimes need more time to load the options
console.log("wait 5s to load all options");
await globalSleep(5000); // wait 5s
optionList = dropdownDiv.querySelectorAll("div[class*='select__option']");
if (optionList.length === 0) {
break;
}
if (
previousOptionCount !== null &&
previousOptionCount == optionList.length
) {
break;
}
previousOptionCount = optionList.length;
lastOption = optionList[optionList.length - 1];
lastOption.scrollIntoView({ behavior: "instant" });
lastOption.dispatchEvent(
new WheelEvent("wheel", {
bubbles: true,
cancelable: true,
deltaX: 0,
deltaY: -20,
deltaZ: 0,
}),
);
lastOption.dispatchEvent(
new WheelEvent("wheel", {
bubbles: true,
cancelable: true,
deltaX: 0,
deltaY: 20,
deltaZ: 0,
}),
);
}
// scroll back to the original place
window.scroll({
top: scrollTop,
left: scrollLeft,
behavior: "instant",
});
return optionList;
}
async function getReactSelectOptions(element) {
const optionList = await getReactSelectOptionElements(element);
let selectOptions = [];
for (let i = 0; i < optionList.length; i++) {
let ele = optionList[i];
selectOptions.push({
optionIndex: i,
text: removeMultipleSpaces(ele.textContent),
});
}
return selectOptions;
}
function getDOMElementBySkyvenElement(elementObj) { function getDOMElementBySkyvenElement(elementObj) {
// if element has shadowHost set, we need to find the shadowHost element first then find the element // if element has shadowHost set, we need to find the shadowHost element first then find the element
if (elementObj.shadowHost) { if (elementObj.shadowHost) {
@@ -1674,10 +1529,6 @@ function scrollToElementTop(element) {
}); });
} }
async function globalSleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
// Helper method for debugging // Helper method for debugging
function findNodeById(arr, targetId, path = []) { function findNodeById(arr, targetId, path = []) {
for (let i = 0; i < arr.length; i++) { for (let i = 0; i < arr.length; i++) {

View File

@@ -3,7 +3,6 @@ from __future__ import annotations
import asyncio import asyncio
import copy import copy
import typing import typing
from abc import ABC, abstractmethod
from enum import StrEnum from enum import StrEnum
from random import uniform from random import uniform
@@ -12,25 +11,18 @@ from playwright.async_api import ElementHandle, Frame, FrameLocator, Locator, Pa
from skyvern.constants import SKYVERN_ID_ATTR from skyvern.constants import SKYVERN_ID_ATTR
from skyvern.exceptions import ( from skyvern.exceptions import (
ElementIsNotComboboxDropdown,
ElementIsNotLabel, ElementIsNotLabel,
ElementIsNotReactSelectDropdown,
ElementIsNotSelect2Dropdown,
FailedToGetCurrentValueOfDropdown,
MissingElement, MissingElement,
MissingElementDict, MissingElementDict,
MissingElementInCSSMap, MissingElementInCSSMap,
MissingElementInIframe, MissingElementInIframe,
MultipleDropdownAnchorErr,
MultipleElementsFound, MultipleElementsFound,
NoDropdownAnchorErr,
NoElementBoudingBox, NoElementBoudingBox,
NoneFrameError, NoneFrameError,
SkyvernException, SkyvernException,
) )
from skyvern.forge.sdk.settings_manager import SettingsManager from skyvern.forge.sdk.settings_manager import SettingsManager
from skyvern.webeye.scraper.scraper import IncrementalScrapePage, ScrapedPage, json_to_html, trim_element from skyvern.webeye.scraper.scraper import IncrementalScrapePage, ScrapedPage, json_to_html, trim_element
from skyvern.webeye.utils.page import SkyvernFrame
LOG = structlog.get_logger() LOG = structlog.get_logger()
@@ -276,27 +268,6 @@ class SkyvernElement:
assert handler is not None assert handler is not None
return handler return handler
async def get_select2_dropdown(self) -> Select2Dropdown:
if not await self.is_select2_dropdown():
raise ElementIsNotSelect2Dropdown(self.get_id(), self.__static_element)
frame = await SkyvernFrame.create_instance(self.get_frame())
return Select2Dropdown(frame, self)
async def get_react_select_dropdown(self) -> ReactSelectDropdown:
if not await self.is_react_select_dropdown():
raise ElementIsNotReactSelectDropdown(self.get_id(), self.__static_element)
frame = await SkyvernFrame.create_instance(self.get_frame())
return ReactSelectDropdown(frame, self)
async def get_combobox_dropdown(self) -> ComboboxDropdown:
if not await self.is_combobox_dropdown():
raise ElementIsNotComboboxDropdown(self.get_id(), self.__static_element)
frame = await SkyvernFrame.create_instance(self.get_frame())
return ComboboxDropdown(frame, self)
def find_element_id_in_label_children(self, element_type: InteractiveElement) -> str | None: def find_element_id_in_label_children(self, element_type: InteractiveElement) -> str | None:
tag_name = self.get_tag_name() tag_name = self.get_tag_name()
if tag_name != "label": if tag_name != "label":
@@ -540,212 +511,3 @@ class DomUtil:
raise MultipleElementsFound(num=num_elements, selector=css, element_id=element_id) raise MultipleElementsFound(num=num_elements, selector=css, element_id=element_id)
return SkyvernElement(locator, frame_content, element) return SkyvernElement(locator, frame_content, element)
class AbstractSelectDropdown(ABC):
@abstractmethod
def name(self) -> str:
pass
@abstractmethod
async def open(self, timeout: float = SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS) -> None:
pass
@abstractmethod
async def close(self, timeout: float = SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS) -> None:
pass
@abstractmethod
async def get_current_value(self, timeout: float = SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS) -> str:
pass
@abstractmethod
async def get_options(
self, timeout: float = SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS
) -> typing.List[SkyvernOptionType]:
pass
@abstractmethod
async def select_by_index(
self, index: int, timeout: float = SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS
) -> None:
pass
class Select2Dropdown(AbstractSelectDropdown):
def __init__(self, skyvern_frame: SkyvernFrame, skyvern_element: SkyvernElement) -> None:
self.skyvern_element = skyvern_element
self.skyvern_frame = skyvern_frame
async def __find_anchor(self, timeout: float) -> Locator:
locator = self.skyvern_element.get_frame().locator("[id='select2-drop']")
await locator.wait_for(state="visible", timeout=timeout)
cnt = await locator.count()
if cnt == 0:
raise NoDropdownAnchorErr(self.name(), self.skyvern_element.get_id())
if cnt > 1:
raise MultipleDropdownAnchorErr(self.name(), self.skyvern_element.get_id())
return locator
def name(self) -> str:
return "select2"
async def open(self, timeout: float = SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS) -> None:
await self.skyvern_element.get_locator().click(timeout=timeout)
await self.__find_anchor(timeout=timeout)
async def close(self, timeout: float = SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS) -> None:
anchor = await self.__find_anchor(timeout=timeout)
await anchor.press("Escape", timeout=timeout)
async def get_current_value(self, timeout: float = SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS) -> str:
tag_name = self.skyvern_element.get_tag_name()
if tag_name == "input":
# TODO: this is multiple options case, we haven't fully supported it yet.
return ""
# check SkyvernElement.is_select2_dropdown() method, only <a> and <span> element left
# we should make sure the locator is on <a>, so we're able to find the [class="select2-chosen"] child
locator = self.skyvern_element.get_locator()
if tag_name == "span":
locator = locator.locator("..")
elif tag_name == "a":
pass
else:
raise FailedToGetCurrentValueOfDropdown(
self.name(), self.skyvern_element.get_id(), "invalid element of select2"
)
try:
return await locator.locator("span[class='select2-chosen']").text_content(timeout=timeout)
except Exception as e:
raise FailedToGetCurrentValueOfDropdown(self.name(), self.skyvern_element.get_id(), repr(e))
async def get_options(
self, timeout: float = SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS
) -> typing.List[SkyvernOptionType]:
anchor = await self.__find_anchor(timeout=timeout)
element_handler = await anchor.element_handle(timeout=timeout)
options = await self.skyvern_frame.get_select2_options(element_handler)
return typing.cast(typing.List[SkyvernOptionType], options)
async def select_by_index(
self, index: int, timeout: float = SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS
) -> None:
anchor = await self.__find_anchor(timeout=timeout)
options = anchor.locator("ul").locator("li")
await options.nth(index).click(timeout=timeout)
class ReactSelectDropdown(AbstractSelectDropdown):
def __init__(self, skyvern_frame: SkyvernFrame, skyvern_element: SkyvernElement) -> None:
self.skyvern_element = skyvern_element
self.skyvern_frame = skyvern_frame
def __find_input_locator(self) -> Locator:
tag_name = self.skyvern_element.get_tag_name()
locator = self.skyvern_element.get_locator()
if tag_name == InteractiveElement.BUTTON:
return locator.locator("..").locator("..").locator("input[class*='select__input']")
return locator
async def __find_anchor(self, timeout: float) -> Locator:
input_locator = self.__find_input_locator()
anchor_id = await input_locator.get_attribute("aria-controls", timeout=timeout)
locator = self.skyvern_element.get_frame().locator(f"div[id='{anchor_id}']")
await locator.wait_for(state="visible", timeout=timeout)
cnt = await locator.count()
if cnt == 0:
raise NoDropdownAnchorErr(self.name(), self.skyvern_element.get_id())
if cnt > 1:
raise MultipleDropdownAnchorErr(self.name(), self.skyvern_element.get_id())
return locator
def name(self) -> str:
return "react-select"
async def open(self, timeout: float = SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS) -> None:
await self.skyvern_element.get_locator().focus(timeout=timeout)
await self.skyvern_element.get_locator().press(key="ArrowDown", timeout=timeout)
await self.__find_anchor(timeout=timeout)
async def close(self, timeout: float = SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS) -> None:
await self.__find_anchor(timeout=timeout)
await self.skyvern_element.get_locator().press(key="Escape", timeout=timeout)
async def get_current_value(self, timeout: float = SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS) -> str:
input_locator = self.__find_input_locator()
# TODO: only support single value now
value_locator = input_locator.locator("..").locator("..").locator("div[class*='select__single-value']")
if await value_locator.count() == 0:
return ""
try:
return await value_locator.text_content(timeout=timeout)
except Exception as e:
raise FailedToGetCurrentValueOfDropdown(self.name(), self.skyvern_element.get_id(), repr(e))
async def get_options(
self, timeout: float = SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS
) -> typing.List[SkyvernOptionType]:
input_locator = self.__find_input_locator()
element_handler = await input_locator.element_handle(timeout=timeout)
options = await self.skyvern_frame.get_react_select_options(element_handler)
return typing.cast(typing.List[SkyvernOptionType], options)
async def select_by_index(
self, index: int, timeout: float = SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS
) -> None:
anchor = await self.__find_anchor(timeout=timeout)
options = anchor.locator("div[class*='select__option']")
await options.nth(index).click(timeout=timeout)
class ComboboxDropdown(AbstractSelectDropdown):
def __init__(self, skyvern_frame: SkyvernFrame, skyvern_element: SkyvernElement) -> None:
self.skyvern_element = skyvern_element
self.skyvern_frame = skyvern_frame
async def __find_anchor(self, timeout: float) -> Locator:
control_id = await self.skyvern_element.get_attr("aria-controls", timeout=timeout)
locator = self.skyvern_element.get_frame().locator(f"[id='{control_id}']")
await locator.wait_for(state="visible", timeout=timeout)
cnt = await locator.count()
if cnt == 0:
raise NoDropdownAnchorErr(self.name(), self.skyvern_element.get_id())
if cnt > 1:
raise MultipleDropdownAnchorErr(self.name(), self.skyvern_element.get_id())
return locator
def name(self) -> str:
return "combobox"
async def open(self, timeout: float = SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS) -> None:
await self.skyvern_element.get_locator().click(timeout=timeout)
await self.__find_anchor(timeout=timeout)
async def close(self, timeout: float = SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS) -> None:
await self.skyvern_element.get_locator().press("Tab", timeout=timeout)
async def get_current_value(self, timeout: float = SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS) -> str:
try:
return await self.skyvern_element.get_attr("value", dynamic=True, timeout=timeout)
except Exception as e:
raise FailedToGetCurrentValueOfDropdown(self.name(), self.skyvern_element.get_id(), repr(e))
async def get_options(
self, timeout: float = SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS
) -> typing.List[SkyvernOptionType]:
anchor = await self.__find_anchor(timeout=timeout)
element_handler = await anchor.element_handle()
options = await self.skyvern_frame.get_combobox_options(element_handler)
return typing.cast(typing.List[SkyvernOptionType], options)
async def select_by_index(
self, index: int, timeout: float = SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS
) -> None:
anchor = await self.__find_anchor(timeout=timeout)
options = anchor.locator("li[role='option']")
await options.nth(index).click(timeout=timeout)

View File

@@ -2,7 +2,7 @@ from __future__ import annotations
import asyncio import asyncio
import time import time
from typing import Any, Dict, List from typing import Dict, List
import structlog import structlog
from playwright._impl._errors import TimeoutError from playwright._impl._errors import TimeoutError
@@ -168,21 +168,6 @@ class SkyvernFrame:
js_script = "(element) => scrollToElementTop(element)" js_script = "(element) => scrollToElementTop(element)"
return await self.frame.evaluate(js_script, element) return await self.frame.evaluate(js_script, element)
async def get_select2_options(self, element: ElementHandle) -> List[Dict[str, Any]]:
await self.frame.evaluate(JS_FUNCTION_DEFS)
js_script = "async (element) => await getSelect2Options(element)"
return await self.frame.evaluate(js_script, element)
async def get_react_select_options(self, element: ElementHandle) -> List[Dict[str, Any]]:
await self.frame.evaluate(JS_FUNCTION_DEFS)
js_script = "async (element) => await getReactSelectOptions(element)"
return await self.frame.evaluate(js_script, element)
async def get_combobox_options(self, element: ElementHandle) -> List[Dict[str, Any]]:
await self.frame.evaluate(JS_FUNCTION_DEFS)
js_script = "async (element) => await getListboxOptions(element)"
return await self.frame.evaluate(js_script, element)
async def parse_element_from_html(self, frame: str, element: ElementHandle, interactable: bool) -> Dict: async def parse_element_from_html(self, frame: str, element: ElementHandle, interactable: bool) -> Dict:
js_script = "([frame, element, interactable]) => buildElementObject(frame, element, interactable)" js_script = "([frame, element, interactable]) => buildElementObject(frame, element, interactable)"
return await self.frame.evaluate(js_script, [frame, element, interactable]) return await self.frame.evaluate(js_script, [frame, element, interactable])