From 654cdb14e46df237fa4730c39c9168646fa85422 Mon Sep 17 00:00:00 2001 From: LawyZheng Date: Fri, 15 Aug 2025 15:24:54 +0800 Subject: [PATCH] fix wait for animation end (#3201) --- skyvern/webeye/scraper/domUtils.js | 22 ++++++++++++++++++ skyvern/webeye/scraper/scraper.py | 3 ++- skyvern/webeye/utils/page.py | 37 +++++++++++++++++------------- 3 files changed, 45 insertions(+), 17 deletions(-) diff --git a/skyvern/webeye/scraper/domUtils.js b/skyvern/webeye/scraper/domUtils.js index 76d97e94..fca09636 100644 --- a/skyvern/webeye/scraper/domUtils.js +++ b/skyvern/webeye/scraper/domUtils.js @@ -2642,6 +2642,28 @@ async function getIncrementElements(wait_until_finished = true) { return [Array.from(idToElement.values()), cleanedTreeList]; } +function isAnimationFinished() { + const animations = document.getAnimations({ subtree: true }); + const unfinishedAnimations = animations.filter( + (a) => a.playState !== "finished", + ); + if (!unfinishedAnimations || unfinishedAnimations.length == 0) { + return true; + } + const unfinishedAnimationsWithoutBlocked = unfinishedAnimations.filter( + (a) => { + const element = a.effect?.target; + if (!element) { + _jsConsoleLog("Unfinished animation without element:", a); + return false; + } + const result = getBlockElementUniqueID(element); + return !result[1]; + }, + ); + return unfinishedAnimationsWithoutBlocked.length === 0; +} + /** // How to run the code: diff --git a/skyvern/webeye/scraper/scraper.py b/skyvern/webeye/scraper/scraper.py index c4cfdbb9..79751139 100644 --- a/skyvern/webeye/scraper/scraper.py +++ b/skyvern/webeye/scraper/scraper.py @@ -557,7 +557,8 @@ async def scrape_web_unsafe( try: await page.wait_for_load_state("load", timeout=3000) - await SkyvernFrame.wait_for_animation_end(page) + skyvern_frame = await SkyvernFrame.create_instance(page) + await skyvern_frame.safe_wait_for_animation_end() except Exception: LOG.warning("Failed to wait for load state, will continue scraping", exc_info=True) diff --git a/skyvern/webeye/utils/page.py b/skyvern/webeye/utils/page.py index 885d9df5..04ac68d5 100644 --- a/skyvern/webeye/utils/page.py +++ b/skyvern/webeye/utils/page.py @@ -161,7 +161,7 @@ async def _scrolling_screenshots_helper( if mode == ScreenshotMode.DETAILED: # wait until animation ends, which is triggered by scrolling - await SkyvernFrame.wait_for_animation_end(skyvern_page.frame) + await skyvern_page.safe_wait_for_animation_end() else: if draw_boxes: await skyvern_page.build_elements_and_draw_bounding_boxes(frame=frame, frame_index=frame_index) @@ -214,21 +214,6 @@ def _merge_images_by_position(images: list[Image.Image], positions: list[int]) - class SkyvernFrame: - @staticmethod - async def wait_for_animation_end(page: Page, timeout: float = 3000) -> None: - try: - await page.wait_for_function( - """ - () => { - const animations = document.getAnimations(); - return animations.every(a => a.playState === 'finished'); - } - """, - timeout=timeout, - ) - except Exception: - LOG.warning("Failed to wait for animation end, but continue", exc_info=True) - @staticmethod async def evaluate( frame: Page | Frame, @@ -514,3 +499,23 @@ class SkyvernFrame: return await self.evaluate( frame=self.frame, expression=js_script, timeout_ms=timeout_ms, arg=[wait_until_finished] ) + + async def safe_wait_for_animation_end(self, timeout_ms: float = 3000) -> None: + try: + async with asyncio.timeout(timeout_ms / 1000): + while True: + try: + is_finished = await self.evaluate( + frame=self.frame, + expression="() => isAnimationFinished()", + timeout_ms=timeout_ms, + ) + if is_finished: + return + await asyncio.sleep(0.1) + except Exception: + LOG.warning("Failed to wait for animation end, but ignore it", exc_info=True) + return + except asyncio.TimeoutError: + LOG.debug("Timeout while waiting for animation end, but ignore it", exc_info=True) + return