add disable element id (#1052)

This commit is contained in:
LawyZheng
2024-10-25 14:52:02 +08:00
committed by GitHub
parent d2f4e06855
commit c9335888ff
5 changed files with 113 additions and 3 deletions

View File

@@ -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."
)

View File

@@ -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>",

View File

@@ -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;

View File

@@ -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:

View File

@@ -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)