# `Drafter.Test`
[🔗](https://github.com/jaman/drafter/blob/main/lib/drafter/test.ex#L1)

Headless testing of Drafter applications under ExUnit.

`start_headless/3` starts an app against an in-memory terminal and returns a
context map. Every other function in this module takes that context as its first
argument. `stop/1` shuts the instance down.

Each function that sends input blocks until the app has finished handling it, so a
send and the assertion that follows need no sleep between them.

    defmodule CounterTest do
      use ExUnit.Case, async: false
      import Drafter.Test

      setup do
        ctx = start_headless(Counter)
        on_exit(fn -> stop(ctx) end)
        %{ctx: ctx}
      end

      test "increment", %{ctx: ctx} do
        send_click(ctx, query_one(ctx, "Button"))
        assert get_state(ctx).count == 1
        assert screen_text(ctx) =~ "Count: 1"
      end
    end

Selectors take the forms `Drafter.query_one/1` documents: a widget type as the
module's last segment in CamelCase or snake_case (`"Button"`, `"TextInput"`,
`"text_input"`), `"#id"`, and `".class"`.

## What has to be running

The `:drafter` application must be started before `start_headless/3` is called:
the harness starts its own session services, but the widget supervisor, the
skin manager, the stylesheet loader and the PubSub the widgets use belong to
`:drafter`'s own supervision tree. In a Mix project that lists `:drafter` as a
dependency this is automatic — `mix test` starts the applications of every dep,
and nothing extra goes in `test_helper.exs`. No terminal, tty, or `TERM` is
needed; the in-memory driver replaces all of it.

A single-file test outside a project works the same way once `Mix.install/2` has
pulled the dependency in and started it. Building it compiles a C NIF and a
helper binary through `elixir_make`, so `make` and a C compiler must be present.
`ExUnit.start/1` runs the tests when the script ends, so `elixir the_file.exs`
is the whole command. The app module — `Counter` below — is defined in the same
file or loaded by it.

    Mix.install([{:drafter, github: "jaman/drafter"}])

    ExUnit.start()

    defmodule CounterTest do
      use ExUnit.Case, async: false
      import Drafter.Test

      test "starts" do
        ctx = start_headless(Counter)
        assert screen_text(ctx) =~ "Count: 0"
        stop(ctx)
      end
    end

# `assert_widget_present`
*macro* 

Asserts that a widget matching `selector` is in the hierarchy.

`ctx` is the context from `start_headless/3`; `selector` is a `String.t()` in the
forms `Drafter.query_one/1` documents. Returns the matched widget's id, so it can
be bound and used for a follow-up interaction. Raises `ExUnit.AssertionError` when
nothing matches.

A macro: `import Drafter.Test` or `require Drafter.Test` before calling it.

    id = assert_widget_present(ctx, "Button.primary")
    send_click(ctx, id)

# `assert_widget_value`
*macro* 

Asserts that the first widget matching `selector` has the value `expected`.

Compares `expected` with `get_widget_value/2` using `==`; the per-widget value
types are the ones `Drafter.get_widget_value/1` lists. Returns `:ok`. Raises
`ExUnit.AssertionError` when nothing matches the selector and when the value
differs. A macro, as `assert_widget_present/2` is.

    assert_widget_value(ctx, "#name", "Ada")

# `await_render`

```elixir
@spec await_render(
  map(),
  keyword()
) :: :ok | :timeout
```

Waits for the app to render.

`opts`:

  * `:timeout` - milliseconds to wait. Default `1000`.
  * `:min_count` - wait until the driver's total render count reaches this
    integer, polling every 10 ms. Default `nil`, which instead waits for one
    `{:render, count}` message in the calling process's mailbox.

Returns `:ok`, or `:timeout` if the wait expired. The context argument is accepted
and unused.

`:min_count` is the reliable form. The driver sends `{:render, count}` to the
`:test_pid` on every frame it writes and nothing ever drains that mailbox:
`send_key/3`, `send_char/2`, `send_click/2`, `send_click/3` and `send_mouse/2`
synchronise with the app but leave their render messages queued. A bare
`await_render(ctx)` after any of them therefore takes one of those stale
messages and returns `:ok` without a new frame having been drawn — a test that
passes whether or not the thing it waits for happened. Read the count first and
wait for one more:

    before = Drafter.Test.HeadlessDriver.get_render_count()
    send_key(ctx, :enter)
    :ok = await_render(ctx, min_count: before + 1)

The mailbox form is only usable in the process `start_headless/3` was given as
`:test_pid`, since that is the only one notifications reach, and each call
consumes one message. The input functions already wait for the app to finish, so
neither form is needed after an input; this is for frames that arrive on their
own — a timer tick, a pushed data channel.

# `get_rendered_output`

```elixir
@spec get_rendered_output(map()) :: iodata()
```

Everything the app has written to the terminal since it started, as iodata.

Raw bytes including ANSI escape sequences and cursor moves. Use `screen_text/1` for
the rendered characters. The context argument is accepted and unused; the buffer
belongs to the single headless driver.

# `get_state`

```elixir
@spec get_state(map()) :: term()
```

The app's current state — whatever its `mount/1` and callbacks have produced.

Raises `RuntimeError` if the app does not answer within 1000 ms.

# `get_widget_hierarchy`

```elixir
@spec get_widget_hierarchy(map()) :: Drafter.WidgetHierarchy.t() | nil
```

The app's current `Drafter.WidgetHierarchy` struct.

Returns `nil` when the app fails to answer within 1000 ms. The struct's thirteen
fields:

  * `:root` - the id of the outermost widget, or `nil` before the first render.
    The whole tree is walked from here through `:children`.
  * `:widgets` - id to `%{module:, state:, parent:, children:, order:, pid:}`.
    `:state` is the widget's own state term, `:pid` is set only for a widget
    running in its own process, and `:order` is its position
    among its siblings.
  * `:widget_rects` - id to `%{x:, y:, width:, height:}` in absolute screen
    cells. A widget that was not laid out has no entry.
  * `:focused_widget` - the id holding focus, or `nil`.
  * `:hover_widget` - the id under the pointer, or `nil`.
  * `:widget_counter` - the number of ids generated so far, used to name
    anonymous widgets.
  * `:scroll_containers` - id to `%{viewport_rect:, content_height:,
    content_width:, click_to_scroll:, scroll_exceptions:}` for each scrollable
    container.
  * `:widget_scroll_parents` - id to the id of the scroll container that clips it.
  * `:drag_capture_widget` - the id receiving drag events until the button is
    released, or `nil`.
  * `:preferred_sizes` - id to the height the widget asked for.
  * `:hidden_widgets` - a `MapSet` of ids that are mounted but not drawn.
    Defaults to an empty set.
  * `:event_consumed` - `boolean()`, whether the last event dispatched into the
    hierarchy was claimed by a widget. Defaults to `false`. This is the field to
    assert on when checking that a key or click reached a widget rather than
    falling through to the app.
  * `:widget_overflow` - id to `:ellipsis` for the widgets whose text is
    ellipsised when it does not fit. `:clip` is the default and is not stored,
    so the map holds only the exceptions. Defaults to `%{}`.

    hierarchy = get_widget_hierarchy(ctx)
    assert hierarchy.event_consumed
    assert Map.has_key?(hierarchy.widget_rects, hierarchy.root)

# `get_widget_state`

```elixir
@spec get_widget_state(map(), term()) :: term() | nil
```

The full state struct of the widget with `widget_id`.

Returns `nil` when no widget has that id, and also when the app fails to answer
within 1000 ms.

# `get_widget_value`

```elixir
@spec get_widget_value(map(), term()) :: term() | nil
```

The primary value of the widget with `widget_id`.

The per-widget types are the ones `Drafter.get_widget_value/1` lists. Returns `nil`
when no widget has that id, and also when the app fails to answer within 1000 ms.

# `query_all`

```elixir
@spec query_all(map(), String.t()) :: [term()]
```

The ids of every widget matching `selector`.

`selector` is a `String.t()` in the forms `Drafter.query_one/1` documents. Returns
`[]` when nothing matches, and also when the app fails to answer within 1000 ms.

# `query_one`

```elixir
@spec query_one(map(), String.t()) :: term() | nil
```

The id of the first widget matching `selector`, or `nil`.

`selector` is a `String.t()` in the forms `Drafter.query_one/1` documents.
Also returns `nil` when the app fails to answer within 1000 ms.

# `refute_widget_present`
*macro* 

Asserts that no widget matching `selector` is in the hierarchy.

Returns `:ok`, or raises `ExUnit.AssertionError` when one matches. A macro, as
`assert_widget_present/2` is.

# `screen_lines`

```elixir
@spec screen_lines(map()) :: [String.t()]
```

The visible characters of each screen row, trailing blanks removed.

# `screen_text`

```elixir
@spec screen_text(map()) :: String.t()
```

What the app currently has on screen, as plain text.

Replays the terminal writes onto a blank grid and returns the visible
characters, one screen row per line.

    assert Drafter.Test.screen_text(ctx) =~ "Counter: 3"

# `send_char`

```elixir
@spec send_char(map(), integer() | binary()) :: :ok
```

Injects a `{:char, codepoint}` event and returns once the app has handled it.

`char` is a codepoint integer, or a binary whose **first byte** is taken as the
codepoint — so only single-byte ASCII binaries round-trip correctly. Pass a
codepoint integer for anything above U+007F: `send_char(ctx, "中")` injects the
first UTF-8 byte, `228`, while `send_char(ctx, ?中)` injects the character.
Returns `:ok`.

A real terminal sends printable ASCII as `{:key, key}`, not `{:char, codepoint}`;
use `send_key/3` to reproduce a plain letter press and this to reproduce a
non-ASCII one.

    send_char(ctx, ?x)
    send_char(ctx, 0x4E2D)

# `send_click`

```elixir
@spec send_click(map(), atom()) :: :ok
```

Clicks a widget by id and returns once the app has handled it.

`widget_id` is an atom — the `:id` given to an element, or the id `query_one/2`
returned. A left-button `mouse_up` is injected at the centre of the widget's
current rect, so the click lands on whatever is drawn on top there. Returns `:ok`;
an id that is not in the hierarchy is ignored and nothing is clicked.

    send_click(ctx, query_one(ctx, "Button.primary"))

# `send_click`

```elixir
@spec send_click(map(), integer(), integer()) :: :ok
```

Clicks at a screen cell and returns once the app has handled it.

`x` and `y` are zero-based column and row. Injects a left-button
`%{type: :mouse_up, ...}` event, which is what activates a widget. Returns `:ok`.

# `send_key`

```elixir
@spec send_key(map(), atom(), [atom()]) :: :ok
```

Injects a key press and returns once the app has handled it.

`key` is a key atom as `Drafter.Terminal.ANSI` produces them: `:enter`, `:up`,
`:f1`, or a printable ASCII character as an atom (`:q`, `:" "`). `modifiers` is a
list drawn from `:ctrl`, `:alt`, and `:shift`; default `[]`.

Injects `{:key, key}` when `modifiers` is empty and `{:key, key, modifiers}`
otherwise — the same shapes `handle_event/2` and `keybinding/3` match. Returns `:ok`.

    send_key(ctx, :enter)
    send_key(ctx, :s, [:ctrl])

# `send_mouse`

```elixir
@spec send_mouse(map(), map()) :: :ok
```

Injects a raw mouse event and returns once the app has handled it.

`event` is the payload map, wrapped as `{:mouse, event}`. Its shapes are those
`Drafter.Terminal.ANSI` documents: `%{type: :mouse_down | :mouse_up | :drag,
button: button, x: x, y: y}`, `%{type: :move, x: x, y: y}`, and
`%{type: :scroll, direction: :up | :down | :left | :right, x: x, y: y}`.
Returns `:ok`.

    send_mouse(ctx, %{type: :scroll, direction: :down, x: 10, y: 5})

# `start_headless`

```elixir
@spec start_headless(module(), map(), keyword()) :: map()
```

Starts `app_module` against an in-memory terminal and returns the test context.

`props` is the map handed to the app's `mount/1`; default `%{}`.

`opts` is a keyword list:

  * `:size` - the terminal size as a `{columns, rows}` tuple. Default `{80, 24}`.
  * `:test_pid` - the process the headless driver notifies on each render.
    Default `self()`, which is what `await_render/2` waits on.

No other keys are read; unknown ones are silently ignored.

Returns a context map carrying `:app_module`, `:app_pid`, `:app_monitor`,
`:props`, `:test_pid`, and `:session_pids`. Raises `RuntimeError` if the app fails
to start. The app has completed its first render before this returns.

The headless driver is a globally registered process, so only one instance can run
at a time: a test using this must be `async: false`, and `stop/1` must run before
the next one starts.

    ctx = Drafter.Test.start_headless(MyApp, %{user_id: 7}, size: {120, 40})

# `stop`

```elixir
@spec stop(map()) :: :ok
```

Stops the app and every service `start_headless/3` started. Returns `:ok`.

Safe to call on an app that has already exited. Call it from `on_exit/1` so a
failing test still frees the globally named services for the next one.

# `sync`

```elixir
@spec sync(map()) :: :ok
```

Returns once every event injected before this call has been handled by the app
and every frame the app drew for it is on the headless driver.

An injected event passes from the headless driver to the event manager to the
app loop, and the frame it produces passes from the loop to the compositor to
the driver. This makes a synchronous round trip to each of those in that order,
so by the time it returns the event has been processed by the loop and the
driver's buffer and render count include its frame. A frame the app itself is
still holding back under `frame_pacing: :always` is not waited for.

Call this between injecting an event and asserting on state or screen contents.

# `wait_for`

```elixir
@spec wait_for(map(), (map() -&gt; as_boolean(term())), keyword()) :: :ok | :timeout
```

Polls `condition_fn` until it returns a truthy value.

`condition_fn` is a one-arity function called with `ctx`.

`opts`:

  * `:timeout` - milliseconds before giving up. Default `1000`.
  * `:interval` - milliseconds between calls. Default `50`.

Returns `:ok` once the condition holds, or `:timeout`. The condition is evaluated
before the first sleep, so a condition that already holds returns immediately.

    wait_for(ctx, fn ctx -> get_state(ctx).loaded end, timeout: 5000)

---

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