From 23ebd72d4a1112b01786aa80a6e82aa911ba9455 Mon Sep 17 00:00:00 2001 From: Stanislav Novosad Date: Mon, 10 Nov 2025 09:02:37 -0700 Subject: [PATCH] Make SkyvernPage extend Playwright (#3934) --- .../script_generations/script_skyvern_page.py | 14 +- .../core/script_generations/skyvern_page.py | 172 +++++++----------- skyvern/library/skyvern_browser.py | 19 +- skyvern/library/skyvern_browser_page.py | 117 +----------- 4 files changed, 88 insertions(+), 234 deletions(-) diff --git a/skyvern/core/script_generations/script_skyvern_page.py b/skyvern/core/script_generations/script_skyvern_page.py index 1657acd9..934e7869 100644 --- a/skyvern/core/script_generations/script_skyvern_page.py +++ b/skyvern/core/script_generations/script_skyvern_page.py @@ -123,8 +123,6 @@ class ScriptSkyvernPage(SkyvernPage): action: ActionType, *args: Any, prompt: str = "", - data: str | dict[str, Any] = "", - intention: str = "", **kwargs: Any, ) -> Any: """ @@ -150,9 +148,11 @@ class ScriptSkyvernPage(SkyvernPage): } # Backward compatibility: use intention if provided and prompt is empty + intention = kwargs.get("intention", None) if intention and not prompt: prompt = intention + data = kwargs.get("data", None) meta = ActionMetadata(prompt, data) call = ActionCall(action, args, kwargs, meta) @@ -199,7 +199,6 @@ class ScriptSkyvernPage(SkyvernPage): action_type=action, intention=prompt, status=action_status, - data=data, kwargs=kwargs, call_result=call.result, ) @@ -262,7 +261,6 @@ class ScriptSkyvernPage(SkyvernPage): action_type: ActionType, intention: str = "", status: ActionStatus = ActionStatus.pending, - data: str | dict[str, Any] = "", kwargs: dict[str, Any] | None = None, call_result: Any | None = None, ) -> Action | None: @@ -396,7 +394,7 @@ class ScriptSkyvernPage(SkyvernPage): # If screenshot creation fails, don't block execution pass - async def goto(self, url: str, timeout: float = settings.BROWSER_LOADING_TIMEOUT_MS) -> None: + async def goto(self, url: str, **kwargs: Any) -> None: url = render_template(url) url = prepend_scheme_and_validate_url(url) @@ -405,10 +403,8 @@ class ScriptSkyvernPage(SkyvernPage): if context and context.script_mode: print(f"🌐 Navigating to: {url}") - await self.page.goto( - url, - timeout=timeout, - ) + timeout = kwargs.pop("timeout", settings.BROWSER_ACTION_TIMEOUT_MS) + await self.page.goto(url, timeout=timeout, **kwargs) if context and context.script_mode: print(" ✓ Page loaded") diff --git a/skyvern/core/script_generations/skyvern_page.py b/skyvern/core/script_generations/skyvern_page.py index 4c480246..af69dda6 100644 --- a/skyvern/core/script_generations/skyvern_page.py +++ b/skyvern/core/script_generations/skyvern_page.py @@ -3,7 +3,6 @@ from __future__ import annotations import asyncio import copy from dataclasses import dataclass -from enum import StrEnum from typing import Any, Callable, Literal, overload import structlog @@ -19,10 +18,6 @@ from skyvern.webeye.actions.action_types import ActionType LOG = structlog.get_logger() -class Driver(StrEnum): - PLAYWRIGHT = "playwright" - - @dataclass class ActionMetadata: prompt: str = "" @@ -41,7 +36,7 @@ class ActionCall: error: Exception | None = None # populated if failed -class SkyvernPage: +class SkyvernPage(Page): """ A lightweight adapter for the selected driver that: 1. Executes actual browser commands @@ -54,21 +49,32 @@ class SkyvernPage: page: Page, ai: SkyvernPageAi, ) -> None: + super().__init__(page) self.page = page self.current_label: str | None = None self._ai = ai + def __getattribute__(self, name: str) -> Any: + page = object.__getattribute__(self, "page") + if hasattr(page, name): + for cls in type(self).__mro__: + if cls is Page: + break + if name in cls.__dict__: + return object.__getattribute__(self, name) + return getattr(page, name) + + return object.__getattribute__(self, name) + async def _decorate_call( self, fn: Callable, action: ActionType, *args: Any, prompt: str = "", - data: str | dict[str, Any] = "", - intention: str = "", # backward compatibility **kwargs: Any, ) -> Any: - return await fn(self, *args, prompt=prompt, data=data, intention=intention, **kwargs) + return await fn(self, *args, prompt=prompt, **kwargs) @staticmethod def action_wrap( @@ -79,20 +85,17 @@ class SkyvernPage: skyvern_page: SkyvernPage, *args: Any, prompt: str = "", - data: str | dict[str, Any] = "", - intention: str = "", # backward compatibility **kwargs: Any, ) -> Any: - return await skyvern_page._decorate_call( - fn, action, *args, prompt=prompt, data=data, intention=intention, **kwargs - ) + return await skyvern_page._decorate_call(fn, action, *args, prompt=prompt, **kwargs) return wrapper return decorator - async def goto(self, url: str, timeout: float = settings.BROWSER_LOADING_TIMEOUT_MS) -> None: - await self.page.goto(url, timeout=timeout) + async def goto(self, url: str, **kwargs: Any) -> None: + timeout = kwargs.pop("timeout", settings.BROWSER_ACTION_TIMEOUT_MS) + await self.page.goto(url, timeout=timeout, **kwargs) ######### Public Interfaces ######### @@ -103,9 +106,6 @@ class SkyvernPage: *, prompt: str | None = None, ai: str | None = "fallback", - data: str | dict[str, Any] | None = None, - timeout: float = settings.BROWSER_ACTION_TIMEOUT_MS, - intention: str | None = None, # backward compatibility **kwargs: Any, ) -> str | None: ... @@ -115,9 +115,6 @@ class SkyvernPage: *, prompt: str, ai: str | None = "fallback", - data: str | dict[str, Any] | None = None, - timeout: float = settings.BROWSER_ACTION_TIMEOUT_MS, - intention: str | None = None, # backward compatibility **kwargs: Any, ) -> str | None: ... @@ -128,9 +125,6 @@ class SkyvernPage: *, prompt: str | None = None, ai: str | None = "fallback", - data: str | dict[str, Any] | None = None, - timeout: float = settings.BROWSER_ACTION_TIMEOUT_MS, - intention: str | None = None, # backward compatibility **kwargs: Any, ) -> str | None: """Click an element using a CSS selector, AI-powered prompt matching, or both. @@ -144,8 +138,7 @@ class SkyvernPage: selector: CSS selector for the target element. prompt: Natural language description of which element to click. ai: AI behavior mode. Defaults to "fallback" which tries selector first, then AI. - data: Additional context data for AI processing. - timeout: Maximum time to wait for the click action in milliseconds. + **kwargs: All Playwright click parameters (timeout, force, modifiers, etc.) Returns: The selector string that was successfully used to click the element, or None. @@ -163,12 +156,16 @@ class SkyvernPage: ``` """ # Backward compatibility + intention = kwargs.pop("intention", None) if intention is not None and prompt is None: prompt = intention if not selector and not prompt: raise ValueError("Missing input: pass a selector and/or a prompt.") + timeout = kwargs.pop("timeout", settings.BROWSER_ACTION_TIMEOUT_MS) + data = kwargs.pop("data", None) + context = skyvern_context.current() if context and context.ai_mode_override: ai = context.ai_mode_override @@ -206,8 +203,8 @@ class SkyvernPage: ) if selector: - locator = self.page.locator(selector, **kwargs) - await locator.click(timeout=timeout) + locator = self.page.locator(selector) + await locator.click(timeout=timeout, **kwargs) return selector @@ -219,11 +216,9 @@ class SkyvernPage: *, prompt: str | None = None, ai: str | None = "fallback", - data: str | dict[str, Any] | None = None, - timeout: float = settings.BROWSER_ACTION_TIMEOUT_MS, totp_identifier: str | None = None, totp_url: str | None = None, - intention: str | None = None, # backward compatibility + **kwargs: Any, ) -> str: ... @overload @@ -234,11 +229,9 @@ class SkyvernPage: value: str | None = None, selector: str | None = None, ai: str | None = "fallback", - data: str | dict[str, Any] | None = None, - timeout: float = settings.BROWSER_ACTION_TIMEOUT_MS, totp_identifier: str | None = None, totp_url: str | None = None, - intention: str | None = None, # backward compatibility + **kwargs: Any, ) -> str: ... @action_wrap(ActionType.INPUT_TEXT) @@ -249,11 +242,9 @@ class SkyvernPage: *, prompt: str | None = None, ai: str | None = "fallback", - data: str | dict[str, Any] | None = None, - timeout: float = settings.BROWSER_ACTION_TIMEOUT_MS, totp_identifier: str | None = None, totp_url: str | None = None, - intention: str | None = None, # backward compatibility + **kwargs: Any, ) -> str: """Fill an input field using a CSS selector, AI-powered prompt matching, or both. @@ -267,8 +258,6 @@ class SkyvernPage: value: The text value to input into the field. prompt: Natural language description of which field to fill and what value. ai: AI behavior mode. Defaults to "fallback" which tries selector first, then AI. - data: Additional context data for AI processing. - timeout: Maximum time to wait for the fill action in milliseconds. totp_identifier: TOTP identifier for time-based one-time password fields. totp_url: URL to fetch TOTP codes from for authentication. @@ -293,12 +282,16 @@ class SkyvernPage: """ # Backward compatibility + intention = kwargs.pop("intention", None) if intention is not None and prompt is None: prompt = intention if not selector and not prompt: raise ValueError("Missing input: pass a selector and/or a prompt.") + timeout = kwargs.pop("timeout", settings.BROWSER_ACTION_TIMEOUT_MS) + data = kwargs.pop("data", None) + return await self._input_text( selector=selector, value=value or "", @@ -317,19 +310,21 @@ class SkyvernPage: value: str, ai: str | None = "fallback", prompt: str | None = None, - data: str | dict[str, Any] | None = None, - timeout: float = settings.BROWSER_ACTION_TIMEOUT_MS, totp_identifier: str | None = None, totp_url: str | None = None, - intention: str | None = None, # backward compatibility + **kwargs: Any, ) -> str: # Backward compatibility + intention = kwargs.pop("intention", None) if intention is not None and prompt is None: prompt = intention if not selector and not prompt: raise ValueError("Missing input: pass a selector and/or a prompt.") + timeout = kwargs.pop("timeout", settings.BROWSER_ACTION_TIMEOUT_MS) + data = kwargs.pop("data", None) + return await self._input_text( selector=selector, value=value, @@ -419,8 +414,7 @@ class SkyvernPage: *, prompt: str | None = None, ai: str | None = "fallback", - data: str | dict[str, Any] | None = None, - timeout: float = settings.BROWSER_ACTION_TIMEOUT_MS, + **kwargs: Any, ) -> str: ... @overload @@ -431,8 +425,7 @@ class SkyvernPage: files: str | None = None, selector: str | None = None, ai: str | None = "fallback", - data: str | dict[str, Any] | None = None, - timeout: float = settings.BROWSER_ACTION_TIMEOUT_MS, + **kwargs: Any, ) -> str: ... @action_wrap(ActionType.UPLOAD_FILE) @@ -443,17 +436,19 @@ class SkyvernPage: *, prompt: str | None = None, ai: str | None = "fallback", - data: str | dict[str, Any] | None = None, - timeout: float = settings.BROWSER_ACTION_TIMEOUT_MS, - intention: str | None = None, # backward compatibility + **kwargs: Any, ) -> str: # Backward compatibility + intention = kwargs.pop("intention", None) if intention is not None and prompt is None: prompt = intention if not selector and not prompt: raise ValueError("Missing input: pass a selector and/or a prompt.") + timeout = kwargs.pop("timeout", settings.BROWSER_ACTION_TIMEOUT_MS) + data = kwargs.pop("data", None) + context = skyvern_context.current() if context and context.ai_mode_override: ai = context.ai_mode_override @@ -466,7 +461,7 @@ class SkyvernPage: try: file_path = await download_file(files) locator = self.page.locator(selector) - await locator.set_input_files(file_path) + await locator.set_input_files(file_path, **kwargs) except Exception as e: error_to_raise = e selector = None @@ -501,7 +496,7 @@ class SkyvernPage: file_path = await download_file(files) locator = self.page.locator(selector) - await locator.set_input_files(file_path, timeout=timeout) + await locator.set_input_files(file_path, timeout=timeout, **kwargs) return files @overload @@ -512,9 +507,6 @@ class SkyvernPage: *, prompt: str | None = None, ai: str | None = "fallback", - data: str | dict[str, Any] | None = None, - timeout: float = settings.BROWSER_ACTION_TIMEOUT_MS, - intention: str | None = None, # backward compatibility **kwargs: Any, ) -> str | None: ... @@ -526,9 +518,6 @@ class SkyvernPage: value: str | None = None, selector: str | None = None, ai: str | None = "fallback", - data: str | dict[str, Any] | None = None, - timeout: float = settings.BROWSER_ACTION_TIMEOUT_MS, - intention: str | None = None, # backward compatibility **kwargs: Any, ) -> str | None: ... @@ -540,9 +529,6 @@ class SkyvernPage: *, prompt: str | None = None, ai: str | None = "fallback", - data: str | dict[str, Any] | None = None, - timeout: float = settings.BROWSER_ACTION_TIMEOUT_MS, - intention: str | None = None, # backward compatibility **kwargs: Any, ) -> str | None: """Select an option from a dropdown using a CSS selector, AI-powered prompt matching, or both. @@ -557,8 +543,6 @@ class SkyvernPage: value: The option value to select. prompt: Natural language description of which option to select. ai: AI behavior mode. Defaults to "fallback" which tries selector first, then AI. - data: Additional context data for AI processing. - timeout: Maximum time to wait for the select action in milliseconds. Returns: The value that was successfully selected. @@ -581,12 +565,16 @@ class SkyvernPage: """ # Backward compatibility + intention = kwargs.pop("intention", None) if intention is not None and prompt is None: prompt = intention if not selector and not prompt: raise ValueError("Missing input: pass a selector and/or a prompt.") + timeout = kwargs.pop("timeout", settings.BROWSER_ACTION_TIMEOUT_MS) + data = kwargs.pop("data", None) + context = skyvern_context.current() if context and context.ai_mode_override: ai = context.ai_mode_override @@ -631,46 +619,30 @@ class SkyvernPage: async def wait( self, seconds: float, - prompt: str | None = None, - data: str | dict[str, Any] | None = None, - intention: str | None = None, + **kwargs: Any, ) -> None: await asyncio.sleep(seconds) @action_wrap(ActionType.NULL_ACTION) - async def null_action( - self, prompt: str | None = None, data: str | dict[str, Any] | None = None, intention: str | None = None - ) -> None: + async def null_action(self, **kwargs: Any) -> None: return @action_wrap(ActionType.SOLVE_CAPTCHA) - async def solve_captcha( - self, prompt: str | None = None, data: str | dict[str, Any] | None = None, intention: str | None = None - ) -> None: + async def solve_captcha(self, prompt: str | None = None) -> None: raise NotImplementedError("Solve captcha is not supported outside server context") @action_wrap(ActionType.TERMINATE) - async def terminate( - self, - errors: list[str], - prompt: str | None = None, - data: str | dict[str, Any] | None = None, - intention: str | None = None, - ) -> None: + async def terminate(self, errors: list[str], **kwargs: Any) -> None: # TODO: update the workflow run status to terminated return @action_wrap(ActionType.COMPLETE) - async def complete( - self, prompt: str | None = None, data: str | dict[str, Any] | None = None, intention: str | None = None - ) -> None: + async def complete(self, prompt: str | None = None) -> None: """Stub for complete. Override in subclasses for specific behavior.""" @action_wrap(ActionType.RELOAD_PAGE) - async def reload_page( - self, prompt: str | None = None, data: str | dict[str, Any] | None = None, intention: str | None = None - ) -> None: - await self.page.reload() + async def reload_page(self, **kwargs: Any) -> None: + await self.page.reload(**kwargs) return @action_wrap(ActionType.EXTRACT) @@ -680,7 +652,7 @@ class SkyvernPage: schema: dict[str, Any] | list | str | None = None, error_code_mapping: dict[str, str] | None = None, intention: str | None = None, - data: str | dict[str, Any] | None = None, + **kwargs: Any, ) -> dict[str, Any] | list | str | None: """Extract structured data from the page using AI. @@ -689,7 +661,6 @@ class SkyvernPage: schema: JSON Schema defining the structure of data to extract. error_code_mapping: Mapping of error codes to custom error messages. intention: Additional context about the extraction intent. - data: Additional context data for AI processing. Returns: Extracted data matching the provided schema, or None if extraction fails. @@ -711,12 +682,11 @@ class SkyvernPage: # Returns: {"name": "...", "price": 29.99} ``` """ + data = kwargs.pop("data", None) return await self._ai.ai_extract(prompt, schema, error_code_mapping, intention, data) @action_wrap(ActionType.VERIFICATION_CODE) - async def verification_code( - self, prompt: str | None = None, data: str | dict[str, Any] | None = None, intention: str | None = None - ) -> None: + async def verification_code(self, prompt: str | None = None) -> None: return @action_wrap(ActionType.SCROLL) @@ -724,9 +694,7 @@ class SkyvernPage: self, scroll_x: int, scroll_y: int, - prompt: str | None = None, - data: str | dict[str, Any] | None = None, - intention: str | None = None, + **kwargs: Any, ) -> None: await self.page.evaluate(f"window.scrollBy({scroll_x}, {scroll_y})") @@ -736,9 +704,7 @@ class SkyvernPage: keys: list[str], hold: bool = False, duration: float = 0, - prompt: str | None = None, - data: str | dict[str, Any] | None = None, - intention: str | None = None, # backward compatibility + **kwargs: Any, ) -> None: await handler_utils.keypress(self.page, keys, hold=hold, duration=duration) @@ -747,9 +713,7 @@ class SkyvernPage: self, x: int, y: int, - prompt: str | None = None, - data: str | dict[str, Any] | None = None, - intention: str | None = None, + **kwargs: Any, ) -> None: await self.page.mouse.move(x, y) @@ -759,9 +723,7 @@ class SkyvernPage: start_x: int, start_y: int, path: list[tuple[int, int]], - prompt: str | None = None, - data: str | dict[str, Any] | None = None, - intention: str | None = None, # backward compatibility + **kwargs: Any, ) -> None: await handler_utils.drag(self.page, start_x, start_y, path) @@ -771,9 +733,7 @@ class SkyvernPage: x: int, y: int, direction: Literal["down", "up"], - prompt: str | None = None, - data: str | dict[str, Any] | None = None, - intention: str | None = None, # backward compatibility + **kwargs: Any, ) -> None: await handler_utils.left_mouse(self.page, x, y, direction) diff --git a/skyvern/library/skyvern_browser.py b/skyvern/library/skyvern_browser.py index 154b0c32..2d6441ad 100644 --- a/skyvern/library/skyvern_browser.py +++ b/skyvern/library/skyvern_browser.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any from playwright.async_api import BrowserContext, Page @@ -9,10 +9,10 @@ if TYPE_CHECKING: from skyvern.library.skyvern_sdk import SkyvernSdk -class SkyvernBrowser: +class SkyvernBrowser(BrowserContext): """A browser context wrapper that creates Skyvern-enabled pages. - This class wraps a Playwright BrowserContext and provides methods to create + This class extends Playwright BrowserContext and provides methods to create SkyvernBrowserPage instances that combine traditional browser automation with AI-powered task execution capabilities. It manages browser session state and enables persistent browser sessions across multiple pages. @@ -44,6 +44,7 @@ class SkyvernBrowser: browser_session_id: str | None = None, browser_address: str | None = None, ): + super().__init__(browser_context) self._sdk = sdk self._browser_context = browser_context self._browser_session_id = browser_session_id @@ -51,6 +52,18 @@ class SkyvernBrowser: self.workflow_run_id: None | str = None + def __getattribute__(self, name: str) -> Any: + browser_context = object.__getattribute__(self, "_browser_context") + if hasattr(browser_context, name): + for cls in type(self).__mro__: + if cls is BrowserContext: + break + if name in cls.__dict__: + return object.__getattribute__(self, name) + return getattr(browser_context, name) + + return object.__getattribute__(self, name) + @property def browser_session_id(self) -> str | None: return self._browser_session_id diff --git a/skyvern/library/skyvern_browser_page.py b/skyvern/library/skyvern_browser_page.py index b8a34800..124d0018 100644 --- a/skyvern/library/skyvern_browser_page.py +++ b/skyvern/library/skyvern_browser_page.py @@ -1,5 +1,5 @@ import asyncio -from typing import TYPE_CHECKING, Any, Pattern +from typing import TYPE_CHECKING, Any from playwright.async_api import Page @@ -8,7 +8,6 @@ from skyvern.client.types.workflow_run_response import WorkflowRunResponse from skyvern.core.script_generations.skyvern_page import SkyvernPage from skyvern.library.constants import DEFAULT_AGENT_HEARTBEAT_INTERVAL, DEFAULT_AGENT_TIMEOUT from skyvern.library.skyvern_browser_page_ai import SdkSkyvernPageAi -from skyvern.library.skyvern_locator import SkyvernLocator if TYPE_CHECKING: from skyvern.library.skyvern_browser import SkyvernBrowser @@ -249,117 +248,3 @@ class SkyvernBrowserPage(SkyvernPage): ``` """ return await self._ai.ai_act(prompt) - - async def reload(self, **kwargs: Any) -> None: - """Reload the current page. - - Args: - **kwargs: Additional options like timeout, wait_until, etc. - """ - await self.page.reload(**kwargs) - - async def screenshot(self, **kwargs: Any) -> bytes: - """Take a screenshot of the page. - - Args: - **kwargs: Additional options like path, full_page, clip, type, quality, etc. - - Returns: - bytes: The screenshot as bytes (unless path is specified, then saves to file). - """ - return await self.page.screenshot(**kwargs) - - def locator(self, selector: str, **kwargs: Any) -> SkyvernLocator: - """Find an element using a CSS selector or other selector syntax. - - Args: - selector: CSS selector or other selector syntax (xpath=, text=, etc.). - **kwargs: Additional options like has, has_text, has_not, etc. - - Returns: - SkyvernLocator object that can be used to perform actions or assertions. - """ - return SkyvernLocator(self.page.locator(selector, **kwargs)) - - def get_by_label(self, text: str | Pattern[str], **kwargs: Any) -> SkyvernLocator: - """Find an input element by its associated label text. - - Args: - text: Label text to search for (supports substring and regex matching). - **kwargs: Additional options like exact. - - Returns: - SkyvernLocator object for the labeled input element. - """ - return SkyvernLocator(self.page.get_by_label(text, **kwargs)) - - def get_by_text(self, text: str | Pattern[str], **kwargs: Any) -> SkyvernLocator: - """Find an element containing the specified text. - - Args: - text: Text content to search for (supports substring and regex matching). - **kwargs: Additional options like exact. - - Returns: - SkyvernLocator object for the element containing the text. - """ - return SkyvernLocator(self.page.get_by_text(text, **kwargs)) - - def get_by_title(self, text: str | Pattern[str], **kwargs: Any) -> SkyvernLocator: - """Find an element by its title attribute. - - Args: - text: Title attribute value to search for (supports substring and regex matching). - **kwargs: Additional options like exact. - - Returns: - SkyvernLocator object for the element with matching title. - """ - return SkyvernLocator(self.page.get_by_title(text, **kwargs)) - - def get_by_role(self, role: str, **kwargs: Any) -> SkyvernLocator: - """Find an element by its ARIA role. - - Args: - role: ARIA role (e.g., "button", "textbox", "link"). - **kwargs: Additional options like name, checked, pressed, etc. - - Returns: - SkyvernLocator object for the element with matching role. - """ - return SkyvernLocator(self.page.get_by_role(role, **kwargs)) - - def get_by_placeholder(self, text: str | Pattern[str], **kwargs: Any) -> SkyvernLocator: - """Find an input element by its placeholder text. - - Args: - text: Placeholder text to search for (supports substring and regex matching). - **kwargs: Additional options like exact. - - Returns: - SkyvernLocator object for the input element with matching placeholder. - """ - return SkyvernLocator(self.page.get_by_placeholder(text, **kwargs)) - - def get_by_alt_text(self, text: str | Pattern[str], **kwargs: Any) -> SkyvernLocator: - """Find an element by its alt text (typically images). - - Args: - text: Alt text to search for (supports substring and regex matching). - **kwargs: Additional options like exact. - - Returns: - SkyvernLocator object for the element with matching alt text. - """ - return SkyvernLocator(self.page.get_by_alt_text(text, **kwargs)) - - def get_by_test_id(self, test_id: str) -> SkyvernLocator: - """Find an element by its test ID attribute. - - Args: - test_id: Test ID value to search for. - - Returns: - SkyvernLocator object for the element with matching test ID. - """ - return SkyvernLocator(self.page.get_by_test_id(test_id))