# `PhoenixTest.Playwright`
[🔗](https://github.com/ftes/phoenix_test_playwright/blob/v0.15.0/lib/phoenix_test/playwright.ex#L1)

Playwright driver for [PhoenixTest](https://hexdocs.pm/phoenix_test).

This module implements the PhoenixTest driver protocol, running tests in a real browser
via Playwright. The `conn` in tests is not a `Plug.Conn` but a `%PhoenixTest.Playwright{}`
struct holding the Playwright session state (page, frame, browser context, etc.).

It also provides browser-specific functions beyond the standard PhoenixTest API,
such as `screenshot/3`, `evaluate/2`, `type/3`, `press/3`, and `drag/3`.

See the [README](README.md) for getting started, configuration, and troubleshooting.

## Missing Playwright features

See the [README](README.md#missing-playwright-features).

# `css_selector`

```elixir
@type css_selector() :: String.t()
```

# `playwright_selector`

```elixir
@type playwright_selector() :: String.t()
```

# `selector`

```elixir
@type selector() :: playwright_selector() | css_selector()
```

# `t`

```elixir
@opaque t()
```

# `add_cookies`

Add cookies to the browser context, using `Plug.Conn.put_resp_cookie/3`

Note that for signed cookies the signing salt is **not** configurable.
As such, this function is not appropriate for signed `Plug.Session` cookies.
For signed session cookies, use `add_session_cookie/3`

  A cookie's value must be a binary unless the cookie is signed/encrypted

## Cookie fields

| key          | type        | description |
| -----------  | ----------- | ----------- |
| `:name`      | `binary()`  | |
| `:value`     | `binary()`  | |
| `:url`       | `binary()`  | *(optional)* either url or domain / path are required |
| `:domain`    | `binary()`  | *(optional)* either url or domain / path are required |
| `:path`      | `binary()`  | *(optional)* either url or domain / path are required |
| `:max_age`   | `float()`   | *(optional)* The cookie max age, in seconds. |
| `:http_only` | `boolean()` | *(optional)* |
| `:secure`    | `boolean()` | *(optional)* |
| `:encrypt`   | `boolean()` | *(optional)* |
| `:sign`      | `boolean()` | *(optional)* |
| `:same_site` | `binary()`  | *(optional)* one of "Strict", "Lax", "None" |

Two of the cookie fields mean nothing to Playwright. These are:

1. `:encrypt`
2. `:sign`

The `:max_age` cookie field means the same thing as documented in `Plug.Conn.put_resp_cookie/4`.
The `:max_age` value is used to infer the correct `expires` value that Playwright requires.

See https://playwright.dev/docs/api/class-browsercontext#browser-context-add-cookies

# `add_session_cookie`

Add a `Plug.Session` cookie to the browser context.

This is useful for emulating a logged-in user.

Note that that the cookie `:value` must be a map, since we are using
`Plug.Conn.put_session/3` to write each of value's key-value pairs
to the cookie.

The `session_options` are exactly the same as the opts used when
writing `plug Plug.Session` in your router/endpoint module.

## Examples
    |> add_session_cookie(
      [value: %{user_token: Accounts.generate_user_session_token(user)}],
      MyAppWeb.Endpoint.session_options()
    )

# `assert_download`

# `assert_screenshot`

```elixir
@spec assert_screenshot(t(), String.t(),
  full_page: boolean(),
  omit_background: boolean(),
  animations: term(),
  caret: term(),
  clip: map(),
  selector: binary(),
  max_diff_pixels: non_neg_integer(),
  max_diff_pixel_ratio: float(),
  threshold: float(),
  scale: term(),
  mask: [binary()],
  mask_color: binary(),
  snapshot_dir: binary(),
  timeout: non_neg_integer()
) :: t()
```

Visual regression assertion: compares a screenshot of the current page against a stored baseline.

`name` is a relative path within `:snapshot_dir` (see `PhoenixTest.Playwright.Config`), including
the file extension — for example `"home.png"` or `"auth/login.png"`. Subdirectories are created
automatically.

On first run (no baseline exists), the screenshot is saved as the new baseline and the test passes.
On subsequent runs, the live screenshot is compared pixel-by-pixel against the baseline.
On mismatch, a diff image is saved to `<snapshot_dir>/__diff__/<name>` and the test fails.

To update a baseline, delete the snapshot file and re-run the test.

## Options
* `:full_page` (`t:boolean/0`) - When true, takes a screenshot of the full scrollable page, instead of the currently visible viewport. Defaults to `false`.

* `:omit_background` (`t:boolean/0`) - Hides default white background and allows capturing screenshots with transparency. Not applicable to `jpeg` images. Defaults to `false`.

* `:animations` - When set to `"disabled"`, stops CSS animations, CSS transitions and Web Animations. Animations get different treatment depending on their duration: finite animations are fast-forwarded to completion, so they'll fire `transitionend` event. Infinite animations are canceled to initial state, and then played over after the screenshot. Defaults to `"disabled"`.

* `:caret` - When set to `"hide"`, screenshot will hide text caret. When set to `"initial"`, text caret behavior will not be changed. Defaults to `"hide"`.

* `:clip` (`t:map/0`) - An object which specifies clipping of the resulting image. `%{x: float, y: float, width: float, height: float}`.

* `:selector` (`t:String.t/0`) - Scope the screenshot to a CSS or Playwright selector instead of the full page.

* `:max_diff_pixels` (`t:non_neg_integer/0`) - An acceptable amount of pixels that could be different. Unset by default.

* `:max_diff_pixel_ratio` (`t:float/0`) - An acceptable ratio of pixels that are different to the total amount of pixels, between `0` and `1`. Unset by default.

* `:threshold` (`t:float/0`) - An acceptable perceived color difference in the YIQ color space between the same pixel in compared images, between zero (strict) and one (lax). Defaults to `0.2`.

* `:scale` - When set to `"css"`, screenshot will have a single pixel per each css pixel on the page. For high-dpi devices, this will keep screenshots small. Using `"device"` option will produce a single pixel per each device pixel. Defaults to `"css"`.

* `:mask` (list of `t:String.t/0`) - Specify selectors that should be masked when the screenshot is taken. Masked elements will be overlaid with a pink box `#FF00FF` (customized by `mask_color`) that completely covers its bounding box.

* `:mask_color` (`t:String.t/0`) - Specify the color of the overlay box for masked elements, in CSS color format. Default color is pink `#FF00FF`.

* `:snapshot_dir` (`t:String.t/0`) - Override the global `:snapshot_dir` config for this call.

* `:timeout` (`t:non_neg_integer/0`) - Maximum wait time in milliseconds. Defaults to the configured timeout.

## Examples
    |> assert_screenshot("home.png")
    |> assert_screenshot("auth/login.png", full_page: false)
    |> assert_screenshot("hero.png", selector: ".hero-section")

# `clear_cookies`

```elixir
@spec clear_cookies(t(), [{:timeout, non_neg_integer()}]) :: t()
```

Removes all cookies from the context.

## Options
* `:timeout` (`t:non_neg_integer/0`) - Maximum wait time in milliseconds. Defaults to the configured timeout.

# `click`

```elixir
@spec click(t(), selector()) :: t()
```

See `click/4`.

# `click`

```elixir
@spec click(t(), selector(), String.t(), exact: boolean(), timeout: non_neg_integer()) ::
  t()
```

Click an element that is not a link or button.
Otherwise, use `click_link/4` and `click_button/4`.

## Options
* `:exact` (`t:boolean/0`) - Exact or substring text match. The default value is `false`.

* `:timeout` (`t:non_neg_integer/0`) - Maximum wait time in milliseconds. Defaults to the configured timeout.

## Examples
    |> click(Selector.menuitem("Edit"))
    |> click("summary", "(expand)", exact: false)

# `click_button`

Like `PhoenixTest.click_button/3`, but allows exact text match.

## Options
* `:exact` (`t:boolean/0`) - Exact or substring text match. The default value is `false`.

* `:timeout` (`t:non_neg_integer/0`) - Maximum wait time in milliseconds. Defaults to the configured timeout.

# `click_link`

Like `PhoenixTest.click_link/3`, but allows exact text match.

## Options
* `:exact` (`t:boolean/0`) - Exact or substring text match. The default value is `false`.

* `:timeout` (`t:non_neg_integer/0`) - Maximum wait time in milliseconds. Defaults to the configured timeout.

# `drag`

```elixir
@spec drag(t(), selector(),
  to: selector(),
  playwright: keyword(),
  timeout: non_neg_integer()
) :: t()
```

Drag and drop a source element to a target element.

## Options
* `:to` (`t:selector/0`) - Required. The target selector.

* `:playwright` (`t:keyword/0`) - Additional options passed to [frame.dragAndDrop](https://playwright.dev/docs/api/class-frame#frame-drag-and-drop). The default value is `[]`.

* `:timeout` (`t:non_neg_integer/0`) - Maximum wait time in milliseconds. Defaults to the configured timeout.

## Examples
    |> drag("#source", to: "#target")
    |> drag(Selector.text("Draggable"), to: Selector.text("Target"))

# `evaluate`

```elixir
@spec evaluate(t(), String.t()) :: t()
```

Evaluates a JavaScript expression in the page context.

When a callback function is given, it receives the JavaScript result. This
is useful for assertions or side effects without breaking the pipe chain.

## Options
* `:timeout` (`t:non_neg_integer/0`) - Maximum wait time in milliseconds. Defaults to the configured timeout.

* `:is_function` (`t:boolean/0`) - Whether the expression is a function. The default value is `false`.

* `:arg` (`t:term/0`) - Optional argument to pass to the function. The default value is `nil`.

## Examples
    conn
    |> evaluate("window.scrollTo(0, document.body.scrollHeight)")

    conn
    |> evaluate("selectors => selectors.forEach(s => document.querySelector(s).remove())", is_function: true, arg: ["h1"])

    conn
    |> evaluate("document.title", & assert &1 =~ "Dashboard")

# `evaluate`

```elixir
@spec evaluate(
  t(),
  String.t(),
  [timeout: non_neg_integer(), is_function: boolean(), arg: term()]
  | (any() -&gt; any())
) :: t()
```

# `evaluate`

```elixir
@spec evaluate(
  t(),
  String.t(),
  [timeout: non_neg_integer(), is_function: boolean(), arg: term()],
  (any() -&gt; any())
) :: t()
```

# `press`

```elixir
@spec press(t(), selector(), String.t(),
  delay: non_neg_integer(),
  timeout: non_neg_integer()
) :: t()
```

Focuses the matching element and presses a combination of the keyboard keys.

Use `type/4` if you don't need to press special keys.

Examples of [supported keys](https://developer.mozilla.org/en-US/docs/Web/API/UI_Events/Keyboard_event_key_values):
`F1 - F12, Digit0- Digit9, KeyA- KeyZ, Backquote, Minus, Equal, Backslash, Backspace, Tab, Delete, Escape, ArrowDown, End, Enter, Home, Insert, PageDown, PageUp, ArrowRight, ArrowUp`

Modifiers are also supported:
`Shift, Control, Alt, Meta, ShiftLeft, ControlOrMeta`

Combinations are also supported:
`Control+o, Control++, Control+Shift+T`

## Options
* `:delay` (`t:non_neg_integer/0`) - Time to wait between keydown and keyup in milliseconds. The default value is `0`.

* `:timeout` (`t:non_neg_integer/0`) - Maximum wait time in milliseconds. Defaults to the configured timeout.

## Examples
    |> press("#id", "Control+Shift+T")
    |> press(Selector.button("Submit"), "Enter")

# `screenshot`

```elixir
@spec screenshot(t(), String.t(),
  full_page: boolean(),
  omit_background: boolean(),
  timeout: non_neg_integer()
) :: t()
```

Takes a screenshot of the current page and saves it to the given file path.

The file type will be inferred from the file extension on the path you provide.
The file is saved in `:screenshot_dir`, see `PhoenixTest.Playwright.Config`.

## Options
* `:full_page` (`t:boolean/0`) - The default value is `true`.

* `:omit_background` (`t:boolean/0`) - Only applicable to .png images. The default value is `false`.

* `:timeout` (`t:non_neg_integer/0`) - Maximum wait time in milliseconds. Defaults to the configured timeout.

## Examples
    |> screenshot("my-screenshot.png")
    |> screenshot("my-test/my-screenshot.jpg")

# `step`
*macro* 

Label a step in the Playwright trace.

This is useful for marking custom helper functions or complex multi-step operations
so they appear as distinct steps in the trace viewer for easier debugging.
Steps can be nested.
Their source location is noted in the trace.

## Examples

    def complete_checkout(conn, user_email) do
      conn
      |> sign_in_as(user_email)
      |> step("Check out", fn conn ->
        conn
        |> step("Fill shipping information", fn conn ->
          conn
          |> fill_in("Address", with: "123 Main St")
          |> fill_in("City", with: "Portland")
          |> click_button("Continue")
        end)
        |> step("Fill payment information", fn conn ->
          conn
          |> fill_in("Card number", with: "4242424242424242")
          |> fill_in("CVV", with: "123")
          |> click_button("Place order")
        end)
      end)
    end

    defp sign_in_as(conn, user_email) do
      conn
      |> step("Sign in as #{user_email}", fn conn ->
        conn
        |> fill_in("Email", with: user_email)
        |> fill_in("Password", with: "password123")
        |> click_button("Sign In")
      end)
    end

# `type`

```elixir
@spec type(t(), selector(), String.t(),
  delay: non_neg_integer(),
  timeout: non_neg_integer()
) :: t()
```

Focuses the matching element and simulates user typing.

In most cases, you should use `PhoenixTest.fill_in/4` instead.

## Options
* `:delay` (`t:non_neg_integer/0`) - Time to wait between key presses in milliseconds. The default value is `0`.

* `:timeout` (`t:non_neg_integer/0`) - Maximum wait time in milliseconds. Defaults to the configured timeout.

## Examples
    |> type("#id", "some text")
    |> type(Selector.role("heading", "Untitled"), "New title")

# `unwrap`

```elixir
@spec unwrap(t(), (%{context_id: any(), page_id: any(), frame_id: any()} -&gt; any())) ::
  t()
```

See `PhoenixTest.unwrap/2`.

Invokes `fun` with various Playwright IDs.
These can be used to interact with the Playwright
[`BrowserContext`](`PlaywrightEx.BrowserContext`),
[`Page`](`PlaywrightEx.Page`) and
[`Frame`](`PlaywrightEx.Frame`).

## Examples
    |> unwrap(fn %{page_id: page_id} -> PlaywrightEx.subscribe(page_id) end)

# `visit`

```elixir
@spec visit(t(), String.t(),
  timeout: non_neg_integer(),
  referer: binary(),
  wait_until: term()
) :: t()
```

Like `PhoenixTest.visit/2`, but with a custom `timeout`.

## Options
* `:timeout` (`t:non_neg_integer/0`) - Maximum wait time in milliseconds. Defaults to the configured timeout.

* `:referer` (`t:String.t/0`) - Referer header value. If provided, takes preference over the referer header set via the `extra_http_headers` option to `PlaywrightEx.Browser.new_context/2`.

* `:wait_until` - When to consider the operation succeeded. One of:
  - `"load"` (default) — fires when the `load` event is fired.
  - `"domcontentloaded"` — fires when the `DOMContentLoaded` event is fired.
  - `"networkidle"` — **discouraged** — fires when there are no network connections for at least 500 ms. Rely on web assertions instead for testing readiness.
  - `"commit"` — fires when the network response is received and the document has started loading. The default value is `"load"`.

# `with_dialog`

Handle browser dialogs (`alert()`, `confirm()`, `prompt()`) while executing the inner function.

*Note:* Add `@tag accept_dialogs: false` before tests that call this function.
Otherwise, all dialogs are accepted by default.

## Callback return values
The callback may return one of these values:
- `:accept` -> accepts confirmation dialog
- `{:accept, prompt_text}` -> accepts prompt dialog with text
- `:dismiss` -> dismisses dialog
- Any other value will ignore the dialog

## Examples
    @tag accept_dialogs: false
    test "conditionally handle dialog", %{conn: conn} do
    conn
      |> visit("/")
      |> with_dialog(
        fn
          %{message: "Are you sure?"} -> :accept
          %{message: "Enter the magic number"} -> {:accept, "42"}
          %{message: "Self destruct?"} -> :dismiss
        end,
        fn conn ->
          conn
          |> click_button("Delete")
          |> assert_has(".flash", text: "Deleted")
        end
      end)
    end

---

*Consult [api-reference.md](api-reference.md) for complete listing*
