mark readonly element as interactable (#3845)
This commit is contained in:
@@ -668,6 +668,13 @@ class InputToInvisibleElement(SkyvernException):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class InputToReadonlyElement(SkyvernException):
|
||||||
|
def __init__(self, element_id: str):
|
||||||
|
super().__init__(
|
||||||
|
f"The element(id={element_id}) now is readonly. Try to interact with other elements, or try to interact with it later when it's not readonly."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class FailedToParseActionInstruction(SkyvernException):
|
class FailedToParseActionInstruction(SkyvernException):
|
||||||
def __init__(self, reason: str | None, error_type: str | None):
|
def __init__(self, reason: str | None, error_type: str | None):
|
||||||
super().__init__(
|
super().__init__(
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ from skyvern.exceptions import (
|
|||||||
IllegitComplete,
|
IllegitComplete,
|
||||||
ImaginaryFileUrl,
|
ImaginaryFileUrl,
|
||||||
InputToInvisibleElement,
|
InputToInvisibleElement,
|
||||||
|
InputToReadonlyElement,
|
||||||
InteractWithDisabledElement,
|
InteractWithDisabledElement,
|
||||||
InteractWithDropdownContainer,
|
InteractWithDropdownContainer,
|
||||||
InvalidElementForTextInput,
|
InvalidElementForTextInput,
|
||||||
@@ -1225,6 +1226,17 @@ async def handle_input_text_action(
|
|||||||
# force to move focus back to the element
|
# force to move focus back to the element
|
||||||
await skyvern_element.get_locator().focus(timeout=timeout)
|
await skyvern_element.get_locator().focus(timeout=timeout)
|
||||||
|
|
||||||
|
# check if the element is readonly(some elements will be non-readonly after focused)
|
||||||
|
if await skyvern_element.is_readonly(dynamic=True):
|
||||||
|
LOG.warning(
|
||||||
|
"Try to input text on a readonly element",
|
||||||
|
task_id=task.task_id,
|
||||||
|
step_id=step.step_id,
|
||||||
|
element_id=skyvern_element.get_id(),
|
||||||
|
action=action,
|
||||||
|
)
|
||||||
|
return [ActionFailure(InputToReadonlyElement(element_id=skyvern_element.get_id()))]
|
||||||
|
|
||||||
# check the phone number format when type=tel and the text is not a secret value
|
# check the phone number format when type=tel and the text is not a secret value
|
||||||
if not is_secret_value and await skyvern_element.get_attr("type") == "tel":
|
if not is_secret_value and await skyvern_element.get_attr("type") == "tel":
|
||||||
try:
|
try:
|
||||||
@@ -2939,6 +2951,13 @@ async def select_from_emerging_elements(
|
|||||||
if current_text == actual_value:
|
if current_text == actual_value:
|
||||||
return ActionSuccess()
|
return ActionSuccess()
|
||||||
|
|
||||||
|
if await input_element.is_readonly(dynamic=True):
|
||||||
|
LOG.warning(
|
||||||
|
"Try to input text on a readonly element",
|
||||||
|
element_id=element_id,
|
||||||
|
)
|
||||||
|
return ActionFailure(InputToReadonlyElement(element_id=element_id))
|
||||||
|
|
||||||
await input_element.input_clear()
|
await input_element.input_clear()
|
||||||
await input_element.input_sequentially(actual_value)
|
await input_element.input_sequentially(actual_value)
|
||||||
return ActionSuccess()
|
return ActionSuccess()
|
||||||
@@ -3084,6 +3103,16 @@ async def select_from_dropdown(
|
|||||||
single_select_result.action_result = ActionSuccess()
|
single_select_result.action_result = ActionSuccess()
|
||||||
return single_select_result
|
return single_select_result
|
||||||
|
|
||||||
|
if await input_element.is_readonly(dynamic=True):
|
||||||
|
LOG.warning(
|
||||||
|
"Try to input text on a readonly element",
|
||||||
|
element_id=element_id,
|
||||||
|
task_id=task.task_id,
|
||||||
|
step_id=step.step_id,
|
||||||
|
)
|
||||||
|
single_select_result.action_result = ActionFailure(InputToReadonlyElement(element_id=element_id))
|
||||||
|
return single_select_result
|
||||||
|
|
||||||
await input_element.input_clear()
|
await input_element.input_clear()
|
||||||
await input_element.input_sequentially(actual_value)
|
await input_element.input_sequentially(actual_value)
|
||||||
single_select_result.action_result = ActionSuccess()
|
single_select_result.action_result = ActionSuccess()
|
||||||
|
|||||||
@@ -790,13 +790,7 @@ function isInteractableInput(element, hoverStylesMap) {
|
|||||||
// "city", "state", "zip", "country"
|
// "city", "state", "zip", "country"
|
||||||
// That's the reason I (Kerem) removed the valid input types check
|
// That's the reason I (Kerem) removed the valid input types check
|
||||||
var type = element.getAttribute("type")?.toLowerCase().trim() ?? "text";
|
var type = element.getAttribute("type")?.toLowerCase().trim() ?? "text";
|
||||||
var readOnly = isReadonlyElement(element);
|
return isHoverPointerElement(element, hoverStylesMap) || type !== "hidden";
|
||||||
|
|
||||||
return (
|
|
||||||
isHoverPointerElement(element, hoverStylesMap) ||
|
|
||||||
isReadonlyInputDropdown(element) ||
|
|
||||||
(!readOnly && type !== "hidden")
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function isValidCSSSelector(selector) {
|
function isValidCSSSelector(selector) {
|
||||||
|
|||||||
@@ -37,10 +37,11 @@ ScrapeExcludeFunc = Callable[[Page, Frame], Awaitable[bool]]
|
|||||||
RESERVED_ATTRIBUTES = {
|
RESERVED_ATTRIBUTES = {
|
||||||
"accept", # for input file
|
"accept", # for input file
|
||||||
"alt",
|
"alt",
|
||||||
"shape-description", # for css shape
|
|
||||||
"aria-checked", # for option tag
|
"aria-checked", # for option tag
|
||||||
"aria-current",
|
"aria-current",
|
||||||
|
"aria-disabled",
|
||||||
"aria-label",
|
"aria-label",
|
||||||
|
"aria-readonly",
|
||||||
"aria-required",
|
"aria-required",
|
||||||
"aria-role",
|
"aria-role",
|
||||||
"aria-selected", # for option tag
|
"aria-selected", # for option tag
|
||||||
@@ -48,7 +49,6 @@ 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",
|
||||||
@@ -58,6 +58,7 @@ RESERVED_ATTRIBUTES = {
|
|||||||
"readonly",
|
"readonly",
|
||||||
"required",
|
"required",
|
||||||
"selected", # for option tag
|
"selected", # for option tag
|
||||||
|
"shape-description", # for css shape
|
||||||
"src", # do we need this?
|
"src", # do we need this?
|
||||||
"text-value",
|
"text-value",
|
||||||
"title",
|
"title",
|
||||||
@@ -897,16 +898,23 @@ class IncrementalScrapePage(ElementTreeBuilder):
|
|||||||
|
|
||||||
def _should_keep_unique_id(element: dict) -> bool:
|
def _should_keep_unique_id(element: dict) -> bool:
|
||||||
# case where we shouldn't keep unique_id
|
# case where we shouldn't keep unique_id
|
||||||
# 1. not disable attr and no interactable
|
# 1. no readonly attr and not disable attr and no interactable
|
||||||
# 2. disable=false and intrecatable=false
|
# 2. readonly=false and disable=false and interactable=false
|
||||||
|
|
||||||
attributes = element.get("attributes", {})
|
attributes = element.get("attributes", {})
|
||||||
if "disabled" not in attributes and "aria-disabled" not in attributes:
|
if (
|
||||||
|
"disabled" not in attributes
|
||||||
|
and "aria-disabled" not in attributes
|
||||||
|
and "readonly" not in attributes
|
||||||
|
and "aria-readonly" not in attributes
|
||||||
|
):
|
||||||
return element.get("interactable", False)
|
return element.get("interactable", False)
|
||||||
|
|
||||||
disabled = attributes.get("disabled")
|
disabled = attributes.get("disabled")
|
||||||
aria_disabled = attributes.get("aria-disabled")
|
aria_disabled = attributes.get("aria-disabled")
|
||||||
if disabled or aria_disabled:
|
readonly = attributes.get("readonly")
|
||||||
|
aria_readonly = attributes.get("aria-readonly")
|
||||||
|
if disabled or aria_disabled or readonly or aria_readonly:
|
||||||
return True
|
return True
|
||||||
return element.get("interactable", False)
|
return element.get("interactable", False)
|
||||||
|
|
||||||
|
|||||||
@@ -281,6 +281,43 @@ class SkyvernElement:
|
|||||||
|
|
||||||
return disabled or aria_disabled or style_disabled
|
return disabled or aria_disabled or style_disabled
|
||||||
|
|
||||||
|
async def is_readonly(self, dynamic: bool = False) -> bool:
|
||||||
|
# if attr not exist, return None
|
||||||
|
# if attr is like 'readonly', return empty string or True
|
||||||
|
# if attr is like `readonly=false`, return the value
|
||||||
|
readonly = False
|
||||||
|
aria_readonly = False
|
||||||
|
|
||||||
|
readonly_attr: bool | str | None = None
|
||||||
|
aria_readonly_attr: bool | str | None = None
|
||||||
|
mode: typing.Literal["auto", "dynamic"] = "dynamic" if dynamic else "auto"
|
||||||
|
|
||||||
|
try:
|
||||||
|
readonly_attr = await self.get_attr("readonly", mode=mode)
|
||||||
|
aria_readonly_attr = await self.get_attr("aria-readonly", mode=mode)
|
||||||
|
except Exception:
|
||||||
|
LOG.exception(
|
||||||
|
"Failed to get the readonly attribute",
|
||||||
|
element=self.__static_element,
|
||||||
|
element_id=self.get_id(),
|
||||||
|
)
|
||||||
|
|
||||||
|
if readonly_attr is not None:
|
||||||
|
# readonly_attr should be bool or str
|
||||||
|
if isinstance(readonly_attr, bool):
|
||||||
|
readonly = readonly_attr
|
||||||
|
if isinstance(readonly_attr, str):
|
||||||
|
readonly = readonly_attr.lower() != "false"
|
||||||
|
|
||||||
|
if aria_readonly_attr is not None:
|
||||||
|
# aria_readonly_attr should be bool or str
|
||||||
|
if isinstance(aria_readonly_attr, bool):
|
||||||
|
aria_readonly = aria_readonly_attr
|
||||||
|
if isinstance(aria_readonly_attr, str):
|
||||||
|
aria_readonly = aria_readonly_attr.lower() != "false"
|
||||||
|
|
||||||
|
return readonly or aria_readonly
|
||||||
|
|
||||||
async def is_selectable(self) -> bool:
|
async def is_selectable(self) -> bool:
|
||||||
return await self.get_selectable() or self.get_tag_name() in SELECTABLE_ELEMENT
|
return await self.get_selectable() or self.get_tag_name() in SELECTABLE_ELEMENT
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user