add disable element id (#1052)
This commit is contained in:
@@ -521,3 +521,10 @@ class InvalidWorkflowParameter(SkyvernHTTPException):
|
|||||||
message,
|
message,
|
||||||
status_code=status.HTTP_400_BAD_REQUEST,
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class InteractWithDisabledElement(SkyvernException):
|
||||||
|
def __init__(self, element_id: str):
|
||||||
|
super().__init__(
|
||||||
|
f"The element(id={element_id}) now is disabled, try to interact with it later when it's enabled."
|
||||||
|
)
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ from skyvern.exceptions import (
|
|||||||
FailToSelectByValue,
|
FailToSelectByValue,
|
||||||
IllegitComplete,
|
IllegitComplete,
|
||||||
ImaginaryFileUrl,
|
ImaginaryFileUrl,
|
||||||
|
InteractWithDisabledElement,
|
||||||
InvalidElementForTextInput,
|
InvalidElementForTextInput,
|
||||||
MissingElement,
|
MissingElement,
|
||||||
MissingFileUrl,
|
MissingFileUrl,
|
||||||
@@ -347,6 +348,18 @@ async def handle_click_action(
|
|||||||
dom = DomUtil(scraped_page=scraped_page, page=page)
|
dom = DomUtil(scraped_page=scraped_page, page=page)
|
||||||
skyvern_element = await dom.get_skyvern_element_by_id(action.element_id)
|
skyvern_element = await dom.get_skyvern_element_by_id(action.element_id)
|
||||||
await asyncio.sleep(0.3)
|
await asyncio.sleep(0.3)
|
||||||
|
|
||||||
|
# dynamically validate the attr, since it could change into enabled after the previous actions
|
||||||
|
if await skyvern_element.is_disabled(dynamic=True):
|
||||||
|
LOG.warning(
|
||||||
|
"Try to click on a disabled element",
|
||||||
|
action_type=action.action_type,
|
||||||
|
task_id=task.task_id,
|
||||||
|
step_id=step.step_id,
|
||||||
|
element_id=skyvern_element.get_id(),
|
||||||
|
)
|
||||||
|
return [ActionFailure(InteractWithDisabledElement(skyvern_element.get_id()))]
|
||||||
|
|
||||||
if action.download:
|
if action.download:
|
||||||
results = await handle_click_to_download_file_action(action, page, scraped_page, task)
|
results = await handle_click_to_download_file_action(action, page, scraped_page, task)
|
||||||
else:
|
else:
|
||||||
@@ -417,6 +430,17 @@ async def handle_input_text_action(
|
|||||||
if text is None:
|
if text is None:
|
||||||
return [ActionFailure(FailedToFetchSecret())]
|
return [ActionFailure(FailedToFetchSecret())]
|
||||||
|
|
||||||
|
# dynamically validate the attr, since it could change into enabled after the previous actions
|
||||||
|
if await skyvern_element.is_disabled(dynamic=True):
|
||||||
|
LOG.warning(
|
||||||
|
"Try to input text on a disabled element",
|
||||||
|
action_type=action.action_type,
|
||||||
|
task_id=task.task_id,
|
||||||
|
step_id=step.step_id,
|
||||||
|
element_id=skyvern_element.get_id(),
|
||||||
|
)
|
||||||
|
return [ActionFailure(InteractWithDisabledElement(skyvern_element.get_id()))]
|
||||||
|
|
||||||
incremental_element: list[dict] = []
|
incremental_element: list[dict] = []
|
||||||
# check if it's selectable
|
# check if it's selectable
|
||||||
if skyvern_element.get_tag_name() == InteractiveElement.INPUT and not await skyvern_element.is_raw_input():
|
if skyvern_element.get_tag_name() == InteractiveElement.INPUT and not await skyvern_element.is_raw_input():
|
||||||
@@ -572,6 +596,18 @@ async def handle_upload_file_action(
|
|||||||
|
|
||||||
dom = DomUtil(scraped_page=scraped_page, page=page)
|
dom = DomUtil(scraped_page=scraped_page, page=page)
|
||||||
skyvern_element = await dom.get_skyvern_element_by_id(action.element_id)
|
skyvern_element = await dom.get_skyvern_element_by_id(action.element_id)
|
||||||
|
|
||||||
|
# dynamically validate the attr, since it could change into enabled after the previous actions
|
||||||
|
if await skyvern_element.is_disabled(dynamic=True):
|
||||||
|
LOG.warning(
|
||||||
|
"Try to upload file on a disabled element",
|
||||||
|
action_type=action.action_type,
|
||||||
|
task_id=task.task_id,
|
||||||
|
step_id=step.step_id,
|
||||||
|
element_id=skyvern_element.get_id(),
|
||||||
|
)
|
||||||
|
return [ActionFailure(InteractWithDisabledElement(skyvern_element.get_id()))]
|
||||||
|
|
||||||
locator = skyvern_element.locator
|
locator = skyvern_element.locator
|
||||||
|
|
||||||
file_path = await download_file(file_url)
|
file_path = await download_file(file_url)
|
||||||
@@ -717,6 +753,17 @@ async def handle_select_option_action(
|
|||||||
)
|
)
|
||||||
return await handle_select_option_action(select_action, page, scraped_page, task, step)
|
return await handle_select_option_action(select_action, page, scraped_page, task, step)
|
||||||
|
|
||||||
|
# dynamically validate the attr, since it could change into enabled after the previous actions
|
||||||
|
if await skyvern_element.is_disabled(dynamic=True):
|
||||||
|
LOG.warning(
|
||||||
|
"Try to select on a disabled element",
|
||||||
|
action_type=action.action_type,
|
||||||
|
task_id=task.task_id,
|
||||||
|
step_id=step.step_id,
|
||||||
|
element_id=skyvern_element.get_id(),
|
||||||
|
)
|
||||||
|
return [ActionFailure(InteractWithDisabledElement(skyvern_element.get_id()))]
|
||||||
|
|
||||||
if tag_name == InteractiveElement.SELECT:
|
if tag_name == InteractiveElement.SELECT:
|
||||||
LOG.info(
|
LOG.info(
|
||||||
"SelectOptionAction is on <select>",
|
"SelectOptionAction is on <select>",
|
||||||
|
|||||||
@@ -836,7 +836,8 @@ function buildElementObject(frame, element, interactable, purgeable = false) {
|
|||||||
attr.name === "aria-selected" ||
|
attr.name === "aria-selected" ||
|
||||||
attr.name === "readonly" ||
|
attr.name === "readonly" ||
|
||||||
attr.name === "aria-readonly" ||
|
attr.name === "aria-readonly" ||
|
||||||
attr.name === "disabled"
|
attr.name === "disabled" ||
|
||||||
|
attr.name === "aria-disabled"
|
||||||
) {
|
) {
|
||||||
if (attrValue && attrValue.toLowerCase() === "false") {
|
if (attrValue && attrValue.toLowerCase() === "false") {
|
||||||
attrValue = false;
|
attrValue = false;
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ RESERVED_ATTRIBUTES = {
|
|||||||
"data-original-title", # for bootstrap tooltip
|
"data-original-title", # for bootstrap tooltip
|
||||||
"data-ui",
|
"data-ui",
|
||||||
"disabled", # for button
|
"disabled", # for button
|
||||||
|
"aria-disabled",
|
||||||
"for",
|
"for",
|
||||||
"href", # For a tags
|
"href", # For a tags
|
||||||
"maxlength",
|
"maxlength",
|
||||||
@@ -550,6 +551,22 @@ class IncrementalScrapePage:
|
|||||||
return "".join([json_to_html(element) for element in (element_tree or self.element_tree_trimmed)])
|
return "".join([json_to_html(element) for element in (element_tree or self.element_tree_trimmed)])
|
||||||
|
|
||||||
|
|
||||||
|
def _should_keep_unique_id(element: dict) -> bool:
|
||||||
|
# case where we shouldn't keep unique_id
|
||||||
|
# 1. not disable attr and no interactable
|
||||||
|
# 2. disable=false and intrecatable=false
|
||||||
|
|
||||||
|
attributes = element.get("attributes", {})
|
||||||
|
if "disabled" not in attributes and "aria-disabled" not in attributes:
|
||||||
|
return element.get("interactable", False)
|
||||||
|
|
||||||
|
disabled = attributes.get("disabled")
|
||||||
|
aria_disabled = attributes.get("aria-disabled")
|
||||||
|
if disabled or aria_disabled:
|
||||||
|
return True
|
||||||
|
return element.get("interactable", False)
|
||||||
|
|
||||||
|
|
||||||
def trim_element(element: dict) -> dict:
|
def trim_element(element: dict) -> dict:
|
||||||
queue = [element]
|
queue = [element]
|
||||||
while queue:
|
while queue:
|
||||||
@@ -557,7 +574,7 @@ def trim_element(element: dict) -> dict:
|
|||||||
if "frame" in queue_ele:
|
if "frame" in queue_ele:
|
||||||
del queue_ele["frame"]
|
del queue_ele["frame"]
|
||||||
|
|
||||||
if "id" in queue_ele and not queue_ele.get("interactable"):
|
if "id" in queue_ele and not _should_keep_unique_id(queue_ele):
|
||||||
del queue_ele["id"]
|
del queue_ele["id"]
|
||||||
|
|
||||||
if "attributes" in queue_ele:
|
if "attributes" in queue_ele:
|
||||||
|
|||||||
@@ -207,6 +207,43 @@ class SkyvernElement:
|
|||||||
def is_interactable(self) -> bool:
|
def is_interactable(self) -> bool:
|
||||||
return self.__static_element.get("interactable", False)
|
return self.__static_element.get("interactable", False)
|
||||||
|
|
||||||
|
async def is_disabled(self, dynamic: bool = False) -> bool:
|
||||||
|
# if attr not exist, return None
|
||||||
|
# if attr is like 'disabled', return empty string or True
|
||||||
|
# if attr is like `disabled=false`, return the value
|
||||||
|
disabled = False
|
||||||
|
aria_disabled = False
|
||||||
|
|
||||||
|
disabled_attr: bool | str | None = None
|
||||||
|
aria_disabled_attr: bool | str | None = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
disabled_attr = await self.get_attr("disabled", dynamic=dynamic)
|
||||||
|
aria_disabled_attr = await self.get_attr("aria-disabled", dynamic=dynamic)
|
||||||
|
except Exception:
|
||||||
|
# FIXME: maybe it should be considered as "disabled" element if failed to get the attributes?
|
||||||
|
LOG.exception(
|
||||||
|
"Failed to get the disabled attribute",
|
||||||
|
element=self.__static_element,
|
||||||
|
element_id=self.get_id(),
|
||||||
|
)
|
||||||
|
|
||||||
|
if disabled_attr is not None:
|
||||||
|
# disabled_attr should be bool or str
|
||||||
|
if isinstance(disabled_attr, bool):
|
||||||
|
disabled = disabled_attr
|
||||||
|
if isinstance(disabled_attr, str):
|
||||||
|
disabled = disabled_attr.lower() != "false"
|
||||||
|
|
||||||
|
if aria_disabled_attr is not None:
|
||||||
|
# aria_disabled_attr should be bool or str
|
||||||
|
if isinstance(aria_disabled_attr, bool):
|
||||||
|
aria_disabled = aria_disabled_attr
|
||||||
|
if isinstance(aria_disabled_attr, str):
|
||||||
|
aria_disabled = aria_disabled_attr.lower() != "false"
|
||||||
|
|
||||||
|
return disabled or aria_disabled
|
||||||
|
|
||||||
async def is_selectable(self) -> bool:
|
async def is_selectable(self) -> bool:
|
||||||
return self.get_selectable() or self.get_tag_name() in SELECTABLE_ELEMENT
|
return self.get_selectable() or self.get_tag_name() in SELECTABLE_ELEMENT
|
||||||
|
|
||||||
@@ -367,7 +404,8 @@ class SkyvernElement:
|
|||||||
timeout: float = SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS,
|
timeout: float = SettingsManager.get_settings().BROWSER_ACTION_TIMEOUT_MS,
|
||||||
) -> typing.Any:
|
) -> typing.Any:
|
||||||
if not dynamic:
|
if not dynamic:
|
||||||
if attr := self.get_attributes().get(attr_name):
|
attr = self.get_attributes().get(attr_name)
|
||||||
|
if attr is not None:
|
||||||
return attr
|
return attr
|
||||||
|
|
||||||
return await self.locator.get_attribute(attr_name, timeout=timeout)
|
return await self.locator.get_attribute(attr_name, timeout=timeout)
|
||||||
|
|||||||
Reference in New Issue
Block a user