# Locators, Frames, Shadow DOM, Scroll, and Trusted Input

Target SDK: `helixwright==1.0.0`, Browser RPC protocol `6`, core contract `0075`.

## Structured selector families

Unprefixed text passed to `locator()` is CSS. Supported compact forms are:

```python
page.locator("#submit")
page.locator("css=.card button")
page.locator("text=Continue")
page.locator("role=button@@name:Continue")
page.locator("label=Email")
page.locator("placeholder=Search")
page.locator("alt=Product photo")
page.locator("title=Open settings")
page.locator("testid=checkout-submit")
```

Prefer typed constructors when writing Python:

```python
page.get_by_role("button", name="Continue")
page.get_by_label("Email")
page.get_by_placeholder("Search")
page.get_by_text("Order complete", exact=True)
page.get_by_alt_text("Product photo")
page.get_by_title("Open settings")
page.get_by_test_id("checkout-submit")
```

Quoted compact text/name values mean exact matching. Locator composition is immutable:

```python
row = page.locator("#orders").locator(".order-row").nth(2)
button = row.get_by_role("button", name="Open")
```

`first`, `last`, `nth(index)`, `all()`, and `count()` operate on the terminal
collection. An element-bound action without an explicit index uses the first match;
production code should prefer selectors that are unique by design.

## One-epoch reads

These reads use `state.snapshot` and return evidence from one immutable
target/frame/document epoch:

| Call | Return |
| --- | --- |
| `count()` | `int` |
| `exists()` | `bool` |
| `text()` | `str` |
| `value()` / `input_value()` | current value |
| `attr(name)` | attribute value or `None` |
| `visible()` / `enabled()` / `checked()` | `bool` |
| `rect()` | global rect when available, otherwise local rect |
| `explain()` | structured status/count/element/error envelope |
| `resolve()` | one uniquely matched structured element record |

`resolve()` raises `ElementNotFoundError` unless exactly one element matches. Do not
turn a failed resolve into a guessed coordinate action.

## Waits

```python
page.locator("#result").wait("visible", timeout_ms=15_000)
```

Supported states are `attached`, `detached`, `visible`, `hidden`, `enabled`, and
`editable`. The implementation uses snapshot -> private event subscription ->
confirming snapshot. It does not use a shared cursor or fixed busy poll.

## Atomic element actions

Element actions return `RuntimeActionEffect`:

| Action | Meaning |
| --- | --- |
| `click()` | one trusted click |
| `double_click()` | one trusted double-click transaction |
| `hover()` | move to the resolved target |
| `fill(text)` | replace the editable control's value and verify it |
| `type(text)` | append trusted per-key input without clearing |
| `press(key)` | send a key to the resolved element |
| `select(value=...)` | select by value |
| `select(label=...)` | select by label |
| `select(index=...)` | select by zero-based index |
| `drag_to(target, native=False)` | target-bound drag transaction |
| `upload(files, verify=True)` | native file-input action |

`select()` requires exactly one of `value`, `label`, or `index`.

Every action calls `element.await_ready_and_act` with immutable context, structured
selector, readiness contract, motion policy, and deadline. The core owns:

1. target resolution and stable identity;
2. iframe/OOPIF and Shadow DOM routing;
3. target-owned document/container scrolling and materialisation;
4. mechanical actionability;
5. final identity, geometry, and hit-test revalidation;
6. trusted input dispatch and compensating release;
7. the authoritative 22-field receipt.

## Business readiness

Mechanical checks cannot be disabled. Add only the site-specific conditions learned
from Trace:

```python
ready = hw.ReadinessContract(
    all_of=(
        hw.ReadinessSignal.element_state("#save", "enabled"),
        hw.ReadinessSignal.attribute(
            "#save", "data-hydrated", equals="true"
        ),
    ),
    stable_frames=3,
    quiet_ms=100,
    lease_ms=250,
)

effect = page.locator("#save").click(ready=ready, timeout_ms=20_000)
```

The target must remain valid in the same epoch until dispatch. A skeleton disappearing,
a button becoming visible, or one animation frame is not sufficient when the widget
still requires hydration, listener installation, network state, or pointer capture.

## iframe and OOPIF

Use an immutable frame scope when the flow logically belongs to a frame:

```python
payment = page.frame("iframe#payment")
payment.get_by_label("Card number").fill("4111111111111111")
payment.get_by_role("button", name="Pay").click()

nested = payment.frame("iframe#challenge")
nested.get_by_role("button", name="Continue").click()
```

The frame chain is frozen on each `Frame`, `Locator`, `Input`, and `ProbeController`.
No operation changes a shared frame cursor. `page.frames()` exposes the current frame
inventory for diagnostics.

For an ordinary root locator, the core may still route to the resolved cross-process
frame. Use an explicit `Frame` when state, repeated actions, diagnostics, or scrolling
must remain scoped to that frame.

## Shadow DOM

Use the final semantic or CSS selector. The browser core resolves the composed tree,
including supported closed-shadow paths, as part of the same action transaction. Do
not inject JavaScript into a shadow root to synthesize actions.

When a host rerenders, the previous element identity is invalid. The transaction must
resolve and validate the new identity; it may not reuse a stale node or coordinate.

## Scrolling

Normal locator actions scroll automatically. Explicit controls exist for workflows
whose intended operation is scrolling:

```python
page.locator("#details").scroll_into_view(timeout_ms=10_000)

item = page.scroll_until(
    "text=Load more",
    max_rounds=20,
    timeout_ms=30_000,
)

frame_item = page.frame("iframe#catalog").scroll_until(
    "text=Checkout",
    max_rounds=20,
)
```

`scroll_until()` returns the located `Locator`. The core/SDK route wheels to the
target-owning document or container and require observable movement/convergence. Do
not scroll the top document when the target belongs to an iframe scroller.

Raw wheel control is deliberate coordinate input:

```python
effect = page.input.wheel(dy=720, x=1200, y=700)
assert effect.receipt_confirmed
```

## Low-level input and gestures

Use `page.input` only when coordinates or an explicitly supplied trajectory are the
interface:

```python
page.input.move(200, 180)
page.input.click(200, 180)
page.input.drag(200, 180, 520, 180, profile="smooth")
```

Use one balanced gesture for explicit held-input control:

```python
gesture = page.input.gesture(timeout_ms=15_000)
gesture.move(200, 180).down().pause(80).move(520, 180).up()
effect = gesture.commit()
```

All buttons and keys must be released before commit. A malformed/unbalanced gesture is
rejected rather than partially dispatched.

## Element screenshots

```python
png = page.locator("#receipt").screenshot("evidence/receipt.png")
```

The screenshot is bound to one snapshot id, context, element identity, and geometry.
A clip-only fallback is not used when that proof cannot be established.

## Failure policy

On a reproducible locator, frame, shadow, scroll, actionability, or trusted-input gap,
record `explain()`, one-epoch snapshots, frame inventory, Trace events, and the action's
own receipt. Stop and report the framework gap. Do not hide it with JavaScript,
coordinates derived from a stale rect, repeated clicks, or another browser session.
