Skip to content

Scenarios

A scenario is a Python file that declares a Scenario object, registers a test function via the @scenario.test decorator, and (optionally) declares Locator objects for elements the test interacts with.

from scout.runner import Locator, Page, Scenario

scenario = Scenario(
    name="login",
    base_url="https://app.example.com",
)

email = Locator(name="email", tag="input", bbox=(640, 320, 280, 32))
submit = Locator(name="submit", tag="button", bbox=(640, 428, 280, 40))


@scenario.test
async def test(page: Page) -> None:
    await page.goto("/login")
    await page.fill(email, "user@example.com")
    await page.click(submit)

Scenario

Scenario(
    *,
    name: str,
    base_url: str,
    viewport_width: int = 1280,
    viewport_height: int = 900,
    wait_ms: int = 0,
)
Parameter Description
name Human-readable scenario identifier (recorded with each run)
base_url URL the scenario navigates against. Overridden by scout run --web-base-url
viewport_width / viewport_height Browser viewport size — must match the size used at recording time (Locator bboxes are viewport-relative)
wait_ms Default wait after each action, in milliseconds. Useful for SPA route transitions. Per-call page.wait() overrides

Decorators

  • @scenario.setup — runs once before @scenario.test. Use for login, fixture setup, navigation to a starting page
  • @scenario.test — the actual test logic. Required
@scenario.setup
async def setup(page: Page) -> None:
    await page.goto("/login")
    await page.fill(email, "...")
    await page.click(submit)

@scenario.test
async def test(page: Page) -> None:
    await page.goto("/dashboard")
    # ...

Locator

Locator(
    *,
    name: str,
    tag: str,
    bbox: tuple[int, int, int, int],
    scroll_y: int = 0,
    pos_type: str = "abs",
    parent: str | None = None,
    pos_offset: dict | None = None,
    dynamic: dict | None = None,
    filter: str | None = None,
)
Parameter Description
name Identifier for logging — also appears in the diff report
tag Expected HTML tag (button, input, a, ...). Used to disambiguate when multiple elements occupy similar coordinates
bbox (x, y, width, height) in viewport coordinates, captured at recording time
scroll_y Page scroll offset at recording time. scout scrolls back to this position before resolving the element
pos_type "abs" (default), "rel", or "dxy". See Positioning modes below
parent Name of the parent Locator (for rel / dxy modes)
pos_offset Offset specification — {"left": N, "top": N} for rel, {"dx": N, "dy": N} for dxy
dynamic {"w": bool, "h": bool} — flag width/height as dynamic; scout re-resolves at runtime
filter CSS or XPath selector to narrow the bbox to a specific child element

Positioning modes

abs (absolute) — bbox is in viewport coordinates. The element is expected at exactly that position relative to the viewport top-left.

submit = Locator(
    name="submit", tag="button",
    bbox=(640, 428, 280, 40),  # viewport-relative
)

rel (relative-to-parent) — bbox is relative to the parent Locator's bbox, anchored by pos_offset. Use when a child element is laid out by container offset (e.g., a dropdown menu item relative to its trigger button).

menu = Locator(name="menu", tag="div", bbox=(800, 100, 200, 300))
item = Locator(
    name="menu-item", tag="a",
    bbox=(810, 140, 180, 36),
    pos_type="rel",
    parent="menu",
    pos_offset={"left": 10, "top": 40},  # offset from menu top-left
)

dxy (delta from parent center) — bbox positioned by pixel delta from parent's center. Useful for cluster-positioned elements.

chip = Locator(
    name="chip", tag="span",
    bbox=(0, 0, 60, 24),
    pos_type="dxy",
    parent="cluster",
    pos_offset={"dx": -40, "dy": 0},
)

Dynamic dimensions

Width or height of an element changes between runs (variable-length text, expandable panels). Flag the dimension to make scout re-measure at runtime:

text_block = Locator(
    name="status-text", tag="div",
    bbox=(100, 200, 400, 24),
    dynamic={"w": True, "h": True},  # both dimensions re-resolved
)

Filter

When the bbox covers a region but you want a specific child element, narrow with a CSS or XPath selector:

row = Locator(
    name="user-row", tag="tr",
    bbox=(0, 100, 1200, 48),
    filter="button.delete",  # CSS: pick the delete button inside this row
)

Page

The Page object wraps a Playwright page with Locator-aware actions. It's passed into your setup and test functions.

Method What it does
await page.goto(url) Navigate (relative to base_url)
await page.click(locator) Resolve Locator and click
await page.fill(locator, value) Resolve Locator and fill text
await page.select_option(locator, value) Resolve <select> and pick option by value
await page.hover(locator) Resolve Locator and hover
await page.wait(ms=None) Wait ms milliseconds (or scenario.wait_ms if None)
page.pw() Escape hatch — return the underlying Playwright Page for advanced needs

All actions take screenshots before/after, captured under .scout/runs/<run-id>/<scenario>/screenshots/. The "before" screenshot also draws the resolved bbox so you can see where scout aimed.