support shadow dom mutation observer (#2206)
Co-authored-by: lawyzheng <lawyzheng1106@gmail.com>
This commit is contained in:
@@ -711,7 +711,7 @@ async def handle_input_text_action(
|
|||||||
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():
|
||||||
await skyvern_element.scroll_into_view()
|
await skyvern_element.scroll_into_view()
|
||||||
# press arrowdown to watch if there's any options popping up
|
# press arrowdown to watch if there's any options popping up
|
||||||
await incremental_scraped.start_listen_dom_increment()
|
await incremental_scraped.start_listen_dom_increment(await skyvern_element.get_element_handler())
|
||||||
try:
|
try:
|
||||||
await skyvern_element.input_clear()
|
await skyvern_element.input_clear()
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -925,7 +925,7 @@ async def handle_input_text_action(
|
|||||||
auto_complete_hacky_flag = False
|
auto_complete_hacky_flag = False
|
||||||
return [result]
|
return [result]
|
||||||
|
|
||||||
await incremental_scraped.start_listen_dom_increment()
|
await incremental_scraped.start_listen_dom_increment(await skyvern_element.get_element_handler())
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await skyvern_element.input_sequentially(text=text)
|
await skyvern_element.input_sequentially(text=text)
|
||||||
@@ -1263,7 +1263,7 @@ async def handle_select_option_action(
|
|||||||
results: list[ActionResult] = []
|
results: list[ActionResult] = []
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await incremental_scraped.start_listen_dom_increment()
|
await incremental_scraped.start_listen_dom_increment(await skyvern_element.get_element_handler())
|
||||||
await skyvern_element.scroll_into_view()
|
await skyvern_element.scroll_into_view()
|
||||||
|
|
||||||
await skyvern_element.click(page=page, dom=dom, timeout=timeout)
|
await skyvern_element.click(page=page, dom=dom, timeout=timeout)
|
||||||
@@ -1343,7 +1343,7 @@ async def handle_select_option_action(
|
|||||||
step_id=step.step_id,
|
step_id=step.step_id,
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
await incremental_scraped.start_listen_dom_increment()
|
await incremental_scraped.start_listen_dom_increment(await skyvern_element.get_element_handler())
|
||||||
timeout = settings.BROWSER_ACTION_TIMEOUT_MS
|
timeout = settings.BROWSER_ACTION_TIMEOUT_MS
|
||||||
await skyvern_element.scroll_into_view()
|
await skyvern_element.scroll_into_view()
|
||||||
|
|
||||||
@@ -1879,7 +1879,7 @@ async def choose_auto_completion_dropdown(
|
|||||||
current_frame = skyvern_element.get_frame()
|
current_frame = skyvern_element.get_frame()
|
||||||
skyvern_frame = await SkyvernFrame.create_instance(current_frame)
|
skyvern_frame = await SkyvernFrame.create_instance(current_frame)
|
||||||
incremental_scraped = IncrementalScrapePage(skyvern_frame=skyvern_frame)
|
incremental_scraped = IncrementalScrapePage(skyvern_frame=skyvern_frame)
|
||||||
await incremental_scraped.start_listen_dom_increment()
|
await incremental_scraped.start_listen_dom_increment(await skyvern_element.get_element_handler())
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await skyvern_element.press_fill(text)
|
await skyvern_element.press_fill(text)
|
||||||
|
|||||||
@@ -545,6 +545,7 @@ function hasWidgetRole(element) {
|
|||||||
"spinbutton",
|
"spinbutton",
|
||||||
"switch",
|
"switch",
|
||||||
"gridcell",
|
"gridcell",
|
||||||
|
"option",
|
||||||
];
|
];
|
||||||
return widgetRoles.includes(role.toLowerCase().trim());
|
return widgetRoles.includes(role.toLowerCase().trim());
|
||||||
}
|
}
|
||||||
@@ -1423,6 +1424,9 @@ async function buildElementTree(
|
|||||||
const interactable = isInteractable(element, hoverStylesMap);
|
const interactable = isInteractable(element, hoverStylesMap);
|
||||||
let elementObj = null;
|
let elementObj = null;
|
||||||
let isParentSVG = null;
|
let isParentSVG = null;
|
||||||
|
if (element.shadowRoot) {
|
||||||
|
children = getChildElements(element.shadowRoot);
|
||||||
|
}
|
||||||
if (interactable) {
|
if (interactable) {
|
||||||
elementObj = await buildElementObject(frame, element, interactable);
|
elementObj = await buildElementObject(frame, element, interactable);
|
||||||
} else if (
|
} else if (
|
||||||
@@ -1433,7 +1437,6 @@ async function buildElementTree(
|
|||||||
elementObj = await buildElementObject(frame, element, interactable);
|
elementObj = await buildElementObject(frame, element, interactable);
|
||||||
} else if (element.shadowRoot) {
|
} else if (element.shadowRoot) {
|
||||||
elementObj = await buildElementObject(frame, element, interactable);
|
elementObj = await buildElementObject(frame, element, interactable);
|
||||||
children = getChildElements(element.shadowRoot);
|
|
||||||
} else if (isTableRelatedElement(element)) {
|
} else if (isTableRelatedElement(element)) {
|
||||||
// build all table related elements into skyvern element
|
// build all table related elements into skyvern element
|
||||||
// we need these elements to preserve the DOM structure
|
// we need these elements to preserve the DOM structure
|
||||||
@@ -2341,7 +2344,7 @@ if (window.globalObserverForDOMIncrement === undefined) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function startGlobalIncrementalObserver() {
|
function startGlobalIncrementalObserver(element = null) {
|
||||||
window.globalListnerFlag = true;
|
window.globalListnerFlag = true;
|
||||||
window.globalDomDepthMap = new Map();
|
window.globalDomDepthMap = new Map();
|
||||||
window.globalOneTimeIncrementElements = [];
|
window.globalOneTimeIncrementElements = [];
|
||||||
@@ -2354,6 +2357,17 @@ function startGlobalIncrementalObserver() {
|
|||||||
subtree: true,
|
subtree: true,
|
||||||
characterData: true,
|
characterData: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// if the element is in shadow DOM, we need to observe the shadow DOM as well
|
||||||
|
if (element && element.getRootNode() instanceof ShadowRoot) {
|
||||||
|
window.globalObserverForDOMIncrement.observe(element.getRootNode(), {
|
||||||
|
attributes: true,
|
||||||
|
attributeOldValue: true,
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
characterData: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function stopGlobalIncrementalObserver() {
|
async function stopGlobalIncrementalObserver() {
|
||||||
@@ -2397,6 +2411,10 @@ async function getIncrementElements(wait_until_finished = true) {
|
|||||||
let children = element.children;
|
let children = element.children;
|
||||||
for (let i = 0; i < children.length; i++) {
|
for (let i = 0; i < children.length; i++) {
|
||||||
const child = children[i];
|
const child = children[i];
|
||||||
|
// FIXME: skip to update the element if it is in shadow DOM, since document.querySelector will not work
|
||||||
|
if (child.shadowHost) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const domElement = document.querySelector(`[unique_id="${child.id}"]`);
|
const domElement = document.querySelector(`[unique_id="${child.id}"]`);
|
||||||
// if the element is still on the page, we rebuild the element to update the information
|
// if the element is still on the page, we rebuild the element to update the information
|
||||||
if (domElement) {
|
if (domElement) {
|
||||||
@@ -2430,21 +2448,24 @@ async function getIncrementElements(wait_until_finished = true) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
for (let treeHeadElement of treeList) {
|
for (let treeHeadElement of treeList) {
|
||||||
const domElement = document.querySelector(
|
// FIXME: skip to update the element if it is in shadow DOM, since document.querySelector will not work
|
||||||
`[unique_id="${treeHeadElement.id}"]`,
|
if (!treeHeadElement.shadowHost) {
|
||||||
);
|
const domElement = document.querySelector(
|
||||||
// if the element is still on the page, we rebuild the element to update the information
|
`[unique_id="${treeHeadElement.id}"]`,
|
||||||
if (domElement) {
|
|
||||||
let newHead = await buildElementObject(
|
|
||||||
"",
|
|
||||||
domElement,
|
|
||||||
treeHeadElement.interactable,
|
|
||||||
treeHeadElement.purgeable,
|
|
||||||
);
|
);
|
||||||
newHead.children = treeHeadElement.children;
|
// if the element is still on the page, we rebuild the element to update the information
|
||||||
treeHeadElement = newHead;
|
if (domElement) {
|
||||||
} else {
|
let newHead = await buildElementObject(
|
||||||
treeHeadElement.interactable = false;
|
"",
|
||||||
|
domElement,
|
||||||
|
treeHeadElement.interactable,
|
||||||
|
treeHeadElement.purgeable,
|
||||||
|
);
|
||||||
|
newHead.children = treeHeadElement.children;
|
||||||
|
treeHeadElement = newHead;
|
||||||
|
} else {
|
||||||
|
treeHeadElement.interactable = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if the element is existed
|
// check if the element is existed
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from typing import Any, Awaitable, Callable, Self
|
|||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
from playwright._impl._errors import TimeoutError
|
from playwright._impl._errors import TimeoutError
|
||||||
from playwright.async_api import Frame, Locator, Page
|
from playwright.async_api import ElementHandle, Frame, Locator, Page
|
||||||
from pydantic import BaseModel, PrivateAttr
|
from pydantic import BaseModel, PrivateAttr
|
||||||
|
|
||||||
from skyvern.config import settings
|
from skyvern.config import settings
|
||||||
@@ -712,9 +712,9 @@ class IncrementalScrapePage:
|
|||||||
|
|
||||||
return self.element_tree_trimmed
|
return self.element_tree_trimmed
|
||||||
|
|
||||||
async def start_listen_dom_increment(self) -> None:
|
async def start_listen_dom_increment(self, element: ElementHandle | None = None) -> None:
|
||||||
js_script = "() => startGlobalIncrementalObserver()"
|
js_script = "(element) => startGlobalIncrementalObserver(element)"
|
||||||
await SkyvernFrame.evaluate(frame=self.skyvern_frame.get_frame(), expression=js_script)
|
await SkyvernFrame.evaluate(frame=self.skyvern_frame.get_frame(), expression=js_script, arg=element)
|
||||||
|
|
||||||
async def stop_listen_dom_increment(self) -> None:
|
async def stop_listen_dom_increment(self) -> None:
|
||||||
# check if the DOM has navigated away or refreshed
|
# check if the DOM has navigated away or refreshed
|
||||||
|
|||||||
Reference in New Issue
Block a user