Skip to content

Page

conatus.utils.browser.page

Page module.

This module is used to manage a Playwright page. Think of a page as a tab in a browser. This module is used in conjunction with the BrowserContext module. The BrowserContext can have one or multiple pages which share the same context, such as cookies, etc.

The Page module is used to manage a single page. It is used to navigate to URLs, wait for the page to load, etc. Each time a navigation action occurs, a new Step is created.

Configuration

You can configure some parameters for the page:

  • Timeout settings: Control how long you're comfortable waiting for the page to load.

    • hard_timeout: The hard timeout for the page, as encoded in Playwright. Default is 20000 ms (20 seconds).
    • soft_timeout: The soft timeout for the page, which is a looser constraint that we manage. Default is 10000 ms (10 seconds).
    • time_delta: The time delta at which we check for page stability. Default is 0.5 seconds.
    • stability_threshold: The threshold for page stability. We consider the page stable if the DOM hasn't changed for this amount of time. Default is 0.99 seconds.

Note

You have to pass a full BrowserConfig instance during instantiation of the page, not just the page configuration.

MouseButton module-attribute

MouseButton: TypeAlias = Literal['left', 'right', 'middle']

Type for mouse buttons.

ExtendedMouseButton module-attribute

ExtendedMouseButton: TypeAlias = Literal[
    "left", "right", "middle", "back", "forward"
]

Type for mouse buttons, with back and forward.

ScrollDirection module-attribute

ScrollDirection: TypeAlias = Literal[
    "up", "down", "left", "right"
]

Type for scroll directions.

Page

Page(
    bc: BrowserContext,
    config: BrowserConfig,
    pw_page: Page | None = None,
    *,
    async_init: Literal[False] = False
)
Page(
    bc: None = None,
    config: None = None,
    pw_page: None = None,
    *,
    async_init: Literal[True] = True
)
Page(
    bc: BrowserContext | None = None,
    config: BrowserConfig | None = None,
    pw_page: Page | None = None,
    *,
    async_init: bool = False
)

Page module.

Init in sync or async context

You can initialize the page in a sync or async context:

from conatus.utils.browser.page import Page
from conatus.utils.browser.full import FullBrowser

browser = FullBrowser(headless=True)
context = browser.contexts[0]

# Sync context
page = Page(context, browser.config)

# Async context
# page = await Page.init_async(context, browser.config)
PARAMETER DESCRIPTION
bc

The browser context.

TYPE: BrowserContext | None DEFAULT: None

config

The browser configuration.

TYPE: BrowserConfig | None DEFAULT: None

pw_page

The Playwright page instance. If provided, we will use this page instance instead of creating a new one.

TYPE: Page | None DEFAULT: None

async_init

Whether this function is called in an async context. Users should not set this parameter. Defaults to False.

TYPE: bool DEFAULT: False

RAISES DESCRIPTION
ValueError

If bc or config are not provided if async_init is False.

Source code in conatus/utils/browser/page.py
def __init__(
    self,
    bc: PWBrowserContext | None = None,
    config: BrowserConfig | None = None,
    pw_page: PWPage | None = None,
    *,
    async_init: bool = False,
) -> None:
    """Initialize the page.

    # Init in sync or async context

    You can initialize the page in a sync or async context:

    ```python
    from conatus.utils.browser.page import Page
    from conatus.utils.browser.full import FullBrowser

    browser = FullBrowser(headless=True)
    context = browser.contexts[0]

    # Sync context
    page = Page(context, browser.config)

    # Async context
    # page = await Page.init_async(context, browser.config)
    ```

    Args:
        bc: The browser context.
        config: The browser configuration.
        pw_page: The Playwright page instance. If provided, we will use
            this page instance instead of creating a new one.
        async_init: Whether this function is called in an async context.
            Users should not set this parameter. Defaults to False.

    Raises:
        ValueError: If `bc` or `config` are not provided if `async_init` is
            False.
    """
    if not async_init and (bc is not None and config is not None):
        run_async(self._finish_init(bc, config, pw_page), loop=bc._loop)  # noqa: SLF001 # pyright: ignore[reportPrivateUsage, reportAny]
    elif not async_init and (bc is None or config is None):
        msg = "Either `bc` or `config` must be provided if called "
        msg += "without `async_init`."
        raise ValueError(msg)

pw_page instance-attribute

pw_page: Page

The Playwright page instance.

last_step instance-attribute

last_step: Step

The current step.

cdp_session instance-attribute

cdp_session: CDPSession

The Chrome Dev Protocol session.

config instance-attribute

config: BrowserConfig

The browser configuration.

bc instance-attribute

bc: BrowserContext

The browser context.

mouse_position instance-attribute

mouse_position: tuple[float, float] | None

The current mouse position.

Only recorded after actions

This is not updated in real-time. It is only updated when we take an action.

last_screenshot property

last_screenshot: Image

Get the last screenshot (of the last step).

title property

title: str

Get the current title of the page.

url property

url: str

Get the current URL of the page.

init_async async classmethod

init_async(
    bc: BrowserContext,
    config: BrowserConfig,
    pw_page: Page | None = None,
) -> Page

Create a page in async mode.

PARAMETER DESCRIPTION
bc

The browser context.

TYPE: BrowserContext

config

The browser configuration.

TYPE: BrowserConfig

pw_page

The Playwright page instance. If provided, we will use this page instance instead of creating a new one.

TYPE: Page | None DEFAULT: None

RETURNS DESCRIPTION
Page

The Page object.

Source code in conatus/utils/browser/page.py
@classmethod
async def init_async(
    cls,
    bc: PWBrowserContext,
    config: BrowserConfig,
    pw_page: PWPage | None = None,
) -> "Page":
    """Create a page in async mode.

    Args:
        bc: The browser context.
        config: The browser configuration.
        pw_page: The Playwright page instance. If provided, we will use
            this page instance instead of creating a new one.

    Returns:
        The `Page` object.
    """
    page = cls(async_init=True)
    await page._finish_init(bc, config, pw_page)
    return page

close_async async

close_async() -> None

Close the page in an async context.

You should also read the documentation for close .

Source code in conatus/utils/browser/page.py
async def close_async(self) -> None:
    """Close the page in an async context.

    You should also read the documentation for [`close`
    ][conatus.utils.browser.page.Page.close].
    """
    await self.pw_page.close()

close

close() -> None

Close the page.

Warning

Please try to ensure that, if you use this method, you also remove the page from the context's list of pages.

from conatus.utils.browser.full import FullBrowser

browser = FullBrowser()
context = browser.contexts[0]
bad_page = context.pages[0]
bad_page.close()
# If you use this method manually, you should also remove the page
context.pages.pop(0)
Source code in conatus/utils/browser/page.py
def close(self) -> None:
    """Close the page.

    !!! warning
        Please try to ensure that, if you use this method, you also
        remove the page from the context's list of pages.

        ```python
        from conatus.utils.browser.full import FullBrowser

        browser = FullBrowser()
        context = browser.contexts[0]
        bad_page = context.pages[0]
        bad_page.close()
        # If you use this method manually, you should also remove the page
        context.pages.pop(0)
        ```
    """
    run_async(self.close_async(), loop=self.pw_page._loop)  # noqa: SLF001 # pyright: ignore[reportPrivateUsage, reportAny]

wait_for_load_async async

wait_for_load_async() -> None

Waits for the page to load in an async context.

Note that everything in this function is expressed in seconds.

Source code in conatus/utils/browser/page.py
async def wait_for_load_async(
    self,
) -> None:
    """Waits for the page to load in an async context.

    Note that everything in this function is expressed in seconds.
    """
    dom: dict[str, Any] = {}  # pyright: ignore[reportExplicitAny]
    dt = self.config.page.time_delta
    stable_time: float = 0.0
    iterations = int(self.config.page.soft_timeout // dt)
    timeout_sec = self.config.page.soft_timeout
    logger.info("Waiting for page to load...")
    logger.debug(
        f"Hard timeout: {self.config.page.hard_timeout}s; "
        f"Soft timeout: {self.config.page.soft_timeout}s; "
        f"Hard timeout: {self.config.page.hard_timeout}s; "
        f"Stability threshold: {self.config.page.stability_threshold}s"
        f"Time delta: {dt * 1000}ms; "
        f"iterations: {iterations}"
    )
    start_time = time.time()
    for i in range(int(iterations)):  # pragma: no branch
        # We shouldn't really arrive at the last iteration, because
        # break should catch it before
        logger.debug(f"Checking for stability... {i}/{iterations}")
        time_elapsed_sec = time.time() - start_time
        if time_elapsed_sec > timeout_sec:
            logger.debug("Soft timeout reached.")
            break
        new_dom = await self.cdp_session.send(  # pyright: ignore[reportUnknownVariableType, reportUnknownMemberType]
            "DOMSnapshot.captureSnapshot",
            {
                "computedStyles": [],
                "includeDOMRects": True,
                "includePaintOrder": True,
            },
        )
        if new_dom == dom:
            logger.debug(f"Page is stable, adding {dt}s to the clock...")
            stable_time += dt
        else:
            stable_time = 0
            logger.debug("DOM changed, resetting the clock...")

        if stable_time >= self.config.page.stability_threshold:
            logger.debug("Page is stable, breaking.")
            break

        await asyncio.sleep(dt)
        dom = cast("dict[str, Any]", new_dom)  # pyright: ignore[reportExplicitAny]
    logger.info("Loading complete.")

wait_for_load

wait_for_load() -> None

Wait for the page to load.

Will throw ValueError if the CDP client is not initialized.

Source code in conatus/utils/browser/page.py
def wait_for_load(self) -> None:
    """Wait for the page to load.

    Will throw `ValueError` if the CDP client is not initialized.
    """
    run_async(self.wait_for_load_async(), loop=self.pw_page._loop)  # noqa: SLF001 # pyright: ignore[reportPrivateUsage, reportAny]

goto_async async

goto_async(url: str) -> None

Go to a URL in an async context.

PARAMETER DESCRIPTION
url

The URL to go to.

TYPE: str

Source code in conatus/utils/browser/page.py
async def goto_async(self, url: str) -> None:
    """Go to a URL in an async context.

    Args:
        url: The URL to go to.
    """
    _ = await self.pw_page.goto(
        url, timeout=self.config.page.hard_timeout * 1000
    )
    await self.wait_for_load_async()
    self.last_step = await Step.init_async(self.pw_page, self.config)

goto

goto(url: str) -> None

Go to a URL.

PARAMETER DESCRIPTION
url

The URL to go to.

TYPE: str

Source code in conatus/utils/browser/page.py
def goto(self, url: str) -> None:
    """Go to a URL.

    Args:
        url: The URL to go to.
    """
    run_async(self.goto_async(url), loop=self.pw_page._loop)  # noqa: SLF001 # pyright: ignore[reportPrivateUsage, reportAny]

mouse_move_async async

mouse_move_async(
    x: float, y: float, *, wait_for_load: bool = True
) -> None

Move the mouse to a position in an async context.

PARAMETER DESCRIPTION
x

The x coordinate to move the mouse to.

TYPE: float

y

The y coordinate to move the mouse to.

TYPE: float

wait_for_load

Whether to wait for the page to load. Defaults to True. Put False if you want to don't consider the mouse move as part of a new step.

TYPE: bool DEFAULT: True

Source code in conatus/utils/browser/page.py
async def mouse_move_async(
    self,
    x: float,
    y: float,
    *,
    wait_for_load: bool = True,
) -> None:
    """Move the mouse to a position in an async context.

    Args:
        x: The x coordinate to move the mouse to.
        y: The y coordinate to move the mouse to.
        wait_for_load: Whether to wait for the page to load. Defaults to
            `True`. Put `False` if you want to don't consider the mouse
            move as part of a new step.
    """
    await self.pw_page.mouse.move(x, y)
    self.mouse_position = (x, y)
    if wait_for_load:
        await self.wait_for_load_async()
        self.last_step = await Step.init_async(self.pw_page, self.config)

mouse_down_up_async async

mouse_down_up_async(
    event: Literal["down", "up"],
    *,
    x: float | None = None,
    y: float | None = None,
    button: MouseButton = "left",
    wait_for_load: bool = True
) -> None

Mouse down or up on an element in an async context.

PARAMETER DESCRIPTION
event

The event to perform. Can be "down" or "up".

TYPE: Literal['down', 'up']

x

The x coordinate to click on.

TYPE: float | None DEFAULT: None

y

The y coordinate to click on.

TYPE: float | None DEFAULT: None

button

The button to click on.

TYPE: MouseButton DEFAULT: 'left'

wait_for_load

Whether to wait for the page to load. Defaults to True. Put False if you want to don't consider the mouse down or up event as part of a new step.

TYPE: bool DEFAULT: True

RAISES DESCRIPTION
ValueError

if x and y are not both provided.

Source code in conatus/utils/browser/page.py
async def mouse_down_up_async(
    self,
    event: Literal["down", "up"],
    *,
    x: float | None = None,
    y: float | None = None,
    button: MouseButton = "left",
    wait_for_load: bool = True,
) -> None:
    """Mouse down or up on an element in an async context.

    Args:
        event: The event to perform. Can be `"down"` or `"up"`.
        x: The x coordinate to click on.
        y: The y coordinate to click on.
        button: The button to click on.
        wait_for_load: Whether to wait for the page to load. Defaults to
            `True`. Put `False` if you want to don't consider the mouse
            down or up event as part of a new step.

    Raises:
        ValueError: if `x` and `y` are not both provided.
    """
    if (x is None and y is not None) or (x is not None and y is None):
        msg = (
            "Both `x` and `y` must be provided, or neither."
            f" Instead, got x={x}, y={y}."
        )
        logger.error(msg)
        raise ValueError(msg)
    if x is not None and y is not None:
        await self.mouse_move_async(x, y, wait_for_load=False)
    if event == "down":
        await self.pw_page.mouse.down(button=button)
    elif event == "up":
        await self.pw_page.mouse.up(button=button)
    else:
        msg = f"Invalid event: {event}. Must be 'down' or 'up'."
        logger.error(msg)  # pyright: ignore[reportUnreachable]
        raise ValueError(msg)
    if wait_for_load:
        await self.wait_for_load_async()
        self.last_step = await Step.init_async(self.pw_page, self.config)

mouse_down_up

mouse_down_up(
    event: Literal["down", "up"],
    *,
    x: float | None = None,
    y: float | None = None,
    button: MouseButton = "left",
    wait_for_load: bool = True
) -> None

Mouse down or up on an element.

PARAMETER DESCRIPTION
event

The event to perform. Can be "down" or "up".

TYPE: Literal['down', 'up']

x

The x coordinate to click on.

TYPE: float | None DEFAULT: None

y

The y coordinate to click on.

TYPE: float | None DEFAULT: None

button

The button to click on.

TYPE: MouseButton DEFAULT: 'left'

wait_for_load

Whether to wait for the page to load. Defaults to True. Put False if you want to don't consider the mouse down or up event as part of a new step.

TYPE: bool DEFAULT: True

Source code in conatus/utils/browser/page.py
def mouse_down_up(
    self,
    event: Literal["down", "up"],
    *,
    x: float | None = None,
    y: float | None = None,
    button: MouseButton = "left",
    wait_for_load: bool = True,
) -> None:
    """Mouse down or up on an element.

    Args:
        event: The event to perform. Can be `"down"` or `"up"`.
        x: The x coordinate to click on.
        y: The y coordinate to click on.
        button: The button to click on.
        wait_for_load: Whether to wait for the page to load. Defaults to
            `True`. Put `False` if you want to don't consider the mouse
            down or up event as part of a new step.
    """
    run_async(
        self.mouse_down_up_async(
            event, x=x, y=y, button=button, wait_for_load=wait_for_load
        ),
        loop=self.pw_page._loop,  # noqa: SLF001 # pyright: ignore[reportPrivateUsage, reportAny]
    )

click_async async

click_async(
    x: float,
    y: float,
    *,
    button: ExtendedMouseButton = "left",
    click_count: int = 1,
    wait_for_load: bool = True
) -> None

Click on an element in an async context.

PARAMETER DESCRIPTION
x

The x coordinate to click on.

TYPE: float

y

The y coordinate to click on.

TYPE: float

button

The button to click on.

TYPE: ExtendedMouseButton DEFAULT: 'left'

click_count

The number of clicks to perform.

TYPE: int DEFAULT: 1

wait_for_load

Whether to wait for the page to load. Defaults to True. Put False if you want to don't consider the mouse click as part of a new step.

TYPE: bool DEFAULT: True

Source code in conatus/utils/browser/page.py
async def click_async(
    self,
    x: float,
    y: float,
    *,
    button: ExtendedMouseButton = "left",
    click_count: int = 1,
    wait_for_load: bool = True,
) -> None:
    """Click on an element in an async context.

    Args:
        x: The x coordinate to click on.
        y: The y coordinate to click on.
        button: The button to click on.
        click_count: The number of clicks to perform.
        wait_for_load: Whether to wait for the page to load. Defaults to
            `True`. Put `False` if you want to don't consider the mouse
            click as part of a new step.
    """
    logger.debug(f"Clicking at {x}, {y} with {button} button.")
    if button in {"back", "forward"}:
        button = cast("Literal['back', 'forward']", button)
        await self.mouse_move_async(x, y, wait_for_load=False)
        await self.back_forward_async(button)
    else:
        button = cast("MouseButton", button)
        try:
            logger.debug(
                f"Waiting for a new page for"
                f" {self.config.page.new_page_timeout}ms..."
            )
            async with self.bc.expect_page(
                timeout=self.config.page.new_page_timeout * 1000
            ) as new_page_info:
                await self.pw_page.mouse.click(
                    x, y, button=button, click_count=click_count
                )
            new_page: PWPage = await new_page_info.value
            await new_page.wait_for_load_state()
            logger.debug("New page found, it will be added to the context.")
        except PWTimeoutError:
            logger.debug("No new page found.")

        self.mouse_position = (x, y)
        if wait_for_load:
            await self.wait_for_load_async()
            self.last_step = await Step.init_async(
                self.pw_page, self.config
            )

click

click(
    x: float,
    y: float,
    *,
    button: ExtendedMouseButton = "left",
    click_count: int = 1,
    wait_for_load: bool = True
) -> None

Click on an element.

PARAMETER DESCRIPTION
x

The x coordinate to click on.

TYPE: float

y

The y coordinate to click on.

TYPE: float

button

The button to click on.

TYPE: ExtendedMouseButton DEFAULT: 'left'

click_count

The number of clicks to perform.

TYPE: int DEFAULT: 1

wait_for_load

Whether to wait for the page to load. Defaults to True. Put False if you want to don't consider the mouse click as part of a new step.

TYPE: bool DEFAULT: True

Source code in conatus/utils/browser/page.py
def click(
    self,
    x: float,
    y: float,
    *,
    button: ExtendedMouseButton = "left",
    click_count: int = 1,
    wait_for_load: bool = True,
) -> None:
    """Click on an element.

    Args:
        x: The x coordinate to click on.
        y: The y coordinate to click on.
        button: The button to click on.
        click_count: The number of clicks to perform.
        wait_for_load: Whether to wait for the page to load. Defaults to
            `True`. Put `False` if you want to don't consider the mouse
            click as part of a new step.
    """
    run_async(
        self.click_async(
            x,
            y,
            button=button,
            click_count=click_count,
            wait_for_load=wait_for_load,
        ),
        loop=self.pw_page._loop,  # noqa: SLF001 # pyright: ignore[reportPrivateUsage, reportAny]
    )

scroll_precise_async async

scroll_precise_async(
    delta_x: float,
    delta_y: float,
    *,
    start_x: float | None = None,
    start_y: float | None = None,
    wait_for_load: bool = True
) -> None

Scroll the page precisely in an async context.

PARAMETER DESCRIPTION
delta_x

The x coordinate of the scroll distance.

TYPE: float

delta_y

The y coordinate of the scroll distance.

TYPE: float

start_x

The x coordinate to start scrolling from.

TYPE: float | None DEFAULT: None

start_y

The y coordinate to start scrolling from.

TYPE: float | None DEFAULT: None

wait_for_load

Whether to wait for the page to load. Defaults to True.

TYPE: bool DEFAULT: True

RAISES DESCRIPTION
ValueError

if start_x and start_y are not both provided.

Source code in conatus/utils/browser/page.py
async def scroll_precise_async(
    self,
    delta_x: float,
    delta_y: float,
    *,
    start_x: float | None = None,
    start_y: float | None = None,
    wait_for_load: bool = True,
) -> None:
    """Scroll the page precisely in an async context.

    Args:
        delta_x: The x coordinate of the scroll distance.
        delta_y: The y coordinate of the scroll distance.
        start_x: The x coordinate to start scrolling from.
        start_y: The y coordinate to start scrolling from.
        wait_for_load: Whether to wait for the page to load. Defaults to
            `True`.

    Raises:
        ValueError: if `start_x` and `start_y` are not both provided.
    """
    if start_x is not None and start_y is not None:
        await self.mouse_move_async(start_x, start_y, wait_for_load=False)
    elif start_x is not None or start_y is not None:
        msg = (
            "Either `start_x` and `start_y` "
            "must be provided, but not one of them."
        )
        logger.error(msg)
        raise ValueError(msg)
    logger.info(
        f"Scrolling from {self.mouse_position} by {delta_x} {delta_y}"
    )
    await self.pw_page.mouse.wheel(delta_x, delta_y)
    if wait_for_load:
        await self.wait_for_load_async()
        self.last_step = await Step.init_async(self.pw_page, self.config)

scroll_precise

scroll_precise(
    delta_x: float,
    delta_y: float,
    *,
    start_x: float | None = None,
    start_y: float | None = None,
    wait_for_load: bool = True
) -> None

Scroll the page precisely.

PARAMETER DESCRIPTION
start_x

The x coordinate to start scrolling from.

TYPE: float | None DEFAULT: None

start_y

The y coordinate to start scrolling from.

TYPE: float | None DEFAULT: None

delta_x

The x coordinate of the scroll distance.

TYPE: float

delta_y

The y coordinate of the scroll distance.

TYPE: float

wait_for_load

Whether to wait for the page to load. Defaults to True.

TYPE: bool DEFAULT: True

Source code in conatus/utils/browser/page.py
def scroll_precise(
    self,
    delta_x: float,
    delta_y: float,
    *,
    start_x: float | None = None,
    start_y: float | None = None,
    wait_for_load: bool = True,
) -> None:
    """Scroll the page precisely.

    Args:
        start_x: The x coordinate to start scrolling from.
        start_y: The y coordinate to start scrolling from.
        delta_x: The x coordinate of the scroll distance.
        delta_y: The y coordinate of the scroll distance.
        wait_for_load: Whether to wait for the page to load. Defaults to
            `True`.
    """
    run_async(
        self.scroll_precise_async(
            delta_x,
            delta_y,
            start_x=start_x,
            start_y=start_y,
            wait_for_load=wait_for_load,
        ),
        loop=self.pw_page._loop,  # noqa: SLF001 # pyright: ignore[reportPrivateUsage, reportAny]
    )

scroll_async async

scroll_async(
    direction: ScrollDirection,
    magnitude_px: int | None = None,
    magnitude_relative: float | None = None,
    *,
    wait_for_load: bool = True
) -> None

Scroll the page in an async context.

PARAMETER DESCRIPTION
direction

The direction to scroll. Can be up or down; left or right are not supported yet.

TYPE: str

magnitude_px

The magnitude to scroll. If None, scroll the entire page. If the number is positive, scroll down or right. If the number is negative, scroll up or left.

TYPE: int | None DEFAULT: None

magnitude_relative

The magnitude to scroll relative to the current viewport size. If None, scroll the entire page. If the number is positive, scroll down or right. If the number is negative, scroll up or left.

TYPE: float | None DEFAULT: None

wait_for_load

Whether to wait for the page to load. Defaults to True. Put False if you want to don't consider the scroll as part of a new step.

TYPE: bool DEFAULT: True

RAISES DESCRIPTION
ValueError

if the viewport size is None (should never happen.)

ValueError

if the direction is invalid.

Source code in conatus/utils/browser/page.py
async def scroll_async(
    self,
    direction: ScrollDirection,
    magnitude_px: int | None = None,
    magnitude_relative: float | None = None,
    *,
    wait_for_load: bool = True,
) -> None:
    """Scroll the page in an async context.

    Args:
        direction (str): The direction to scroll. Can be `up` or `down`;
            `left` or `right` are not supported yet.
        magnitude_px: The magnitude to scroll. If `None`, scroll the
            entire page. If the number is positive, scroll down or right.
            If the number is negative, scroll up or left.
        magnitude_relative: The magnitude to scroll relative to the
            current viewport size. If `None`, scroll the entire page. If
            the number is positive, scroll down or right. If the number is
            negative, scroll up or left.
        wait_for_load: Whether to wait for the page to load. Defaults to
            `True`. Put `False` if you want to don't consider the scroll
            as part of a new step.

    Raises:
        ValueError: if the viewport size is None (should never happen.)
        ValueError: if the direction is invalid.
    """
    # Get real-time viewport size
    vp = self.pw_page.viewport_size
    if vp is None:  # pragma: no cover (this probably cannot be tested)
        msg = "Viewport size is None. Cannot scroll."
        logger.error(msg)
        raise ValueError(msg)
    height: int
    if magnitude_px is not None:
        height = magnitude_px
        if magnitude_relative is not None:
            msg = (
                f"Cannot provide both `magnitude_px` ({magnitude_px}) "
                f"and `magnitude_relative` ({magnitude_relative})."
            )
            logger.error(msg)
            raise ValueError(msg)
    elif magnitude_relative is not None:
        height = int(vp["height"] * magnitude_relative)
    else:
        height = vp["height"]
    if direction == "up":
        await self.pw_page.mouse.wheel(delta_x=0, delta_y=-height)
    elif direction == "down":
        await self.pw_page.mouse.wheel(delta_x=0, delta_y=height)
    elif direction == "left":
        await self.pw_page.mouse.wheel(delta_x=-height, delta_y=0)
    elif direction == "right":
        await self.pw_page.mouse.wheel(delta_x=height, delta_y=0)
    else:
        msg = f"Invalid direction: {direction}."  # pyright: ignore[reportUnreachable]
        logger.error(msg)
        raise ValueError(msg)
    if wait_for_load:
        await self.wait_for_load_async()
        self.last_step = await Step.init_async(self.pw_page, self.config)

scroll

scroll(
    direction: ScrollDirection,
    height: int | None = None,
    *,
    wait_for_load: bool = True
) -> None

Scroll the page.

PARAMETER DESCRIPTION
direction

The direction to scroll. Can be up or down; left or right are not supported yet.

TYPE: str

height

The height to scroll. Default is the viewport height.

TYPE: int | None DEFAULT: None

wait_for_load

Whether to wait for the page to load. Defaults to True. Put False if you want to don't consider the scroll as part of a new step.

TYPE: bool DEFAULT: True

Source code in conatus/utils/browser/page.py
def scroll(
    self,
    direction: ScrollDirection,
    height: int | None = None,
    *,
    wait_for_load: bool = True,
) -> None:
    """Scroll the page.

    Args:
        direction (str): The direction to scroll. Can be `up` or `down`;
            `left` or `right` are not supported yet.
        height: The height to scroll. Default is the viewport height.
        wait_for_load: Whether to wait for the page to load. Defaults to
            `True`. Put `False` if you want to don't consider the scroll
            as part of a new step.
    """
    run_async(
        self.scroll_async(direction, height, wait_for_load=wait_for_load),
        loop=self.pw_page._loop,  # noqa: SLF001 # pyright: ignore[reportPrivateUsage, reportAny]
    )

back_forward_async async

back_forward_async(
    direction: Literal["back", "forward"],
    *,
    wait_for_load: bool = True
) -> None

Go back or forward in the browser history in an async context.

PARAMETER DESCRIPTION
direction

The direction to go. Can be "back" or "forward".

TYPE: Literal['back', 'forward']

wait_for_load

Whether to wait for the page to load. Defaults to True. Put False if you want to don't consider the back or forward action as part of a new step.

TYPE: bool DEFAULT: True

RAISES DESCRIPTION
ValueError

if the direction is invalid.

Source code in conatus/utils/browser/page.py
async def back_forward_async(
    self,
    direction: Literal["back", "forward"],
    *,
    wait_for_load: bool = True,
) -> None:
    """Go back or forward in the browser history in an async context.

    Args:
        direction: The direction to go. Can be `"back"` or `"forward"`.
        wait_for_load: Whether to wait for the page to load. Defaults to
            `True`. Put `False` if you want to don't consider the back or
            forward action as part of a new step.

    Raises:
        ValueError: if the direction is invalid.
    """
    if direction == "back":
        maybe_response = await self.pw_page.go_back()
    elif direction == "forward":
        maybe_response = await self.pw_page.go_forward()
    else:
        msg = f"Invalid direction: {direction}."  # pyright: ignore[reportUnreachable]
        logger.error(msg)
        raise ValueError(msg)
    if maybe_response is not None and wait_for_load:
        await self.wait_for_load_async()
        self.last_step = await Step.init_async(self.pw_page, self.config)
    else:
        logger.warning(
            "Cannot go back: no previous page."
            if direction == "back"
            else "Cannot go forward: no next page."
        )

back_forward

back_forward(
    direction: Literal["back", "forward"],
    *,
    wait_for_load: bool = True
) -> None

Go back or forward in the browser history.

PARAMETER DESCRIPTION
direction

The direction to go. Can be "back" or "forward".

TYPE: Literal['back', 'forward']

wait_for_load

Whether to wait for the page to load. Defaults to True. Put False if you want to don't consider the back or forward action as part of a new step.

TYPE: bool DEFAULT: True

Source code in conatus/utils/browser/page.py
def back_forward(
    self,
    direction: Literal["back", "forward"],
    *,
    wait_for_load: bool = True,
) -> None:
    """Go back or forward in the browser history.

    Args:
        direction: The direction to go. Can be `"back"` or `"forward"`.
        wait_for_load: Whether to wait for the page to load. Defaults to
            `True`. Put `False` if you want to don't consider the back or
            forward action as part of a new step.
    """
    run_async(
        self.back_forward_async(direction, wait_for_load=wait_for_load),
        loop=self.pw_page._loop,  # noqa: SLF001 # pyright: ignore[reportPrivateUsage, reportAny]
    )

drag_async async

drag_async(
    path: list[tuple[float, float]],
    *,
    wait_for_load: bool = True
) -> None

Drag the mouse.

PARAMETER DESCRIPTION
path

The path to drag the mouse to.

TYPE: list[tuple[float, float]]

wait_for_load

Whether to wait for the page to load. Defaults to True. Put False if you want to don't consider the drag action as part of a new step.

TYPE: bool DEFAULT: True

Source code in conatus/utils/browser/page.py
async def drag_async(
    self, path: list[tuple[float, float]], *, wait_for_load: bool = True
) -> None:
    """Drag the mouse.

    Args:
        path: The path to drag the mouse to.
        wait_for_load: Whether to wait for the page to load. Defaults to
            `True`. Put `False` if you want to don't consider the drag
            action as part of a new step.
    """
    if len(path) == 0:
        logger.warning("Drag path is empty. Doing nothing.")
        return
    await self.mouse_down_up_async("down", wait_for_load=False)
    x, y = path[0]
    for i in range(len(path)):
        x, y = path[i]
        await self.mouse_move_async(x, y, wait_for_load=False)
    await self.mouse_down_up_async("up", x=x, y=y, wait_for_load=False)
    self.mouse_position = (x, y)
    if wait_for_load:
        await self.wait_for_load_async()
        self.last_step = await Step.init_async(self.pw_page, self.config)

drag

drag(
    path: list[tuple[float, float]],
    *,
    wait_for_load: bool = True
) -> None

Drag the mouse.

PARAMETER DESCRIPTION
path

The path to drag the mouse to.

TYPE: list[tuple[float, float]]

wait_for_load

Whether to wait for the page to load. Defaults to True. Put False if you want to don't consider the drag action as part of a new step.

TYPE: bool DEFAULT: True

Source code in conatus/utils/browser/page.py
def drag(
    self, path: list[tuple[float, float]], *, wait_for_load: bool = True
) -> None:
    """Drag the mouse.

    Args:
        path: The path to drag the mouse to.
        wait_for_load: Whether to wait for the page to load. Defaults to
            `True`. Put `False` if you want to don't consider the drag
            action as part of a new step.
    """
    run_async(
        self.drag_async(path, wait_for_load=wait_for_load),
        loop=self.pw_page._loop,  # noqa: SLF001 # pyright: ignore[reportPrivateUsage, reportAny]
    )

type_async async

type_async(
    text: str, *, wait_for_load: bool = True
) -> None

Type text into the page in an async context.

PARAMETER DESCRIPTION
text

The text to type.

TYPE: str

wait_for_load

Whether to wait for the page to load. Defaults to True. Put False if you want to don't consider the type action as part of a new step.

TYPE: bool DEFAULT: True

Source code in conatus/utils/browser/page.py
async def type_async(
    self,
    text: str,
    *,
    wait_for_load: bool = True,
) -> None:
    """Type text into the page in an async context.

    Args:
        text: The text to type.
        wait_for_load: Whether to wait for the page to load. Defaults to
            `True`. Put `False` if you want to don't consider the type
            action as part of a new step.
    """
    await self.pw_page.keyboard.type(text)
    if wait_for_load:
        await self.wait_for_load_async()
        self.last_step = await Step.init_async(self.pw_page, self.config)

type

type(text: str, *, wait_for_load: bool = True) -> None

Type text into the page.

PARAMETER DESCRIPTION
text

The text to type.

TYPE: str

wait_for_load

Whether to wait for the page to load. Defaults to True. Put False if you want to don't consider the type action as part of a new step.

TYPE: bool DEFAULT: True

Source code in conatus/utils/browser/page.py
def type(self, text: str, *, wait_for_load: bool = True) -> None:
    """Type text into the page.

    Args:
        text: The text to type.
        wait_for_load: Whether to wait for the page to load. Defaults to
            `True`. Put `False` if you want to don't consider the type
            action as part of a new step.
    """
    run_async(
        self.type_async(text, wait_for_load=wait_for_load),
        loop=self.pw_page._loop,  # noqa: SLF001 # pyright: ignore[reportPrivateUsage, reportAny]
    )

keypress_async async

keypress_async(
    keys: list[str],
    duration: float = 0,
    *,
    wait_for_load: bool = True
) -> None

Press keys into the page in an async context.

PARAMETER DESCRIPTION
keys

The keys to press.

TYPE: list[str]

duration

The duration to hold the key.

TYPE: float DEFAULT: 0

wait_for_load

Whether to wait for the page to load. Defaults to True. Put False if you want to don't consider the keypress action as part of a new step.

TYPE: bool DEFAULT: True

Source code in conatus/utils/browser/page.py
async def keypress_async(
    self,
    keys: list[str],
    duration: float = 0,
    *,
    wait_for_load: bool = True,
) -> None:
    """Press keys into the page in an async context.

    Args:
        keys: The keys to press.
        duration: The duration to hold the key.
        wait_for_load: Whether to wait for the page to load. Defaults to
            `True`. Put `False` if you want to don't consider the keypress
            action as part of a new step.
    """
    if len(keys) == 0:
        logger.warning("Keypress keys are empty. Doing nothing.")
        return
    new_key = self._translate_key(keys)
    await self.pw_page.keyboard.press(new_key, delay=duration)
    if wait_for_load:
        await self.wait_for_load_async()
        self.last_step = await Step.init_async(self.pw_page, self.config)

keypress

keypress(
    keys: list[str],
    duration: float = 0,
    *,
    wait_for_load: bool = True
) -> None

Press keys into the page.

PARAMETER DESCRIPTION
keys

The keys to press.

TYPE: list[str]

duration

The duration to hold the key.

TYPE: float DEFAULT: 0

wait_for_load

Whether to wait for the page to load. Defaults to True. Put False if you want to don't consider the keypress action as part of a new step.

TYPE: bool DEFAULT: True

Source code in conatus/utils/browser/page.py
def keypress(
    self,
    keys: list[str],
    duration: float = 0,
    *,
    wait_for_load: bool = True,
) -> None:
    """Press keys into the page.

    Args:
        keys: The keys to press.
        duration: The duration to hold the key.
        wait_for_load: Whether to wait for the page to load. Defaults to
            `True`. Put `False` if you want to don't consider the keypress
            action as part of a new step.
    """
    run_async(
        self.keypress_async(keys, duration, wait_for_load=wait_for_load),
        loop=self.pw_page._loop,  # noqa: SLF001 # pyright: ignore[reportPrivateUsage, reportAny]
    )

click_node_async async

click_node_async(node_id: int) -> None

Click on a node asynchronously.

PARAMETER DESCRIPTION
node_id

The id of the node to click on.

TYPE: int

RAISES DESCRIPTION
ValueError

if the node id is not found in the input click nodes.

Source code in conatus/utils/browser/page.py
async def click_node_async(self, node_id: int) -> None:
    """Click on a node asynchronously.

    Args:
        node_id: The id of the node to click on.

    Raises:
        ValueError: if the node id is not found in the input click nodes.
    """
    if node_id not in self.last_step.input_click_nodes:
        msg = f"Node id {node_id} not found in input click nodes."
        logger.error(msg)
        raise ValueError(msg)
    node = self.last_step.input_click_nodes[node_id]
    center = node["node"].center
    logger.info(f"Clicking on node {node_id} at {center}.")
    if center is None:  # pragma: no cover
        msg = f"Node {node_id} has no center."
        logger.error(msg)
        raise ValueError(msg)
    await self.click_async(center[0], center[1], wait_for_load=True)

click_node

click_node(node_id: int) -> None

Click on a node.

PARAMETER DESCRIPTION
node_id

The id of the node to click on.

TYPE: int

Source code in conatus/utils/browser/page.py
def click_node(self, node_id: int) -> None:
    """Click on a node.

    Args:
        node_id: The id of the node to click on.
    """
    run_async(self.click_node_async(node_id), loop=self.pw_page._loop)  # noqa: SLF001 # pyright: ignore[reportPrivateUsage, reportAny]