Harlock.Test (harlock v0.8.0)

Copy Markdown View Source

Test harness for Harlock apps.

Boots an app under a :test backend — no /dev/tty required — then exposes synchronous helpers for driving events and inspecting state.

test "tab moves focus to next field" do
  h = Harlock.Test.start_app(MyApp, %{...})
  Harlock.Test.send_key(h, :tab)
  assert Harlock.Test.focused(h) == :email_field
  Harlock.Test.stop(h)
end

Anything that requires a real terminal lives behind the backend abstraction; everything tested here is the same code path that runs in production except for the bytes-in/bytes-out boundary.

Summary

Functions

Returns the current cell buffer struct.

Returns the current Frame.cursor — {row, col} or nil if hidden.

Returns the id of the currently focused element, or nil.

Returns the current model from the runtime.

Returns true if the app has signaled :quit from its update callback. The runtime exits but the supervisor stays alive until you call stop/1.

Returns the raw bytes the writer has received so far.

Returns the rendered cell buffer as a single string (rows joined by \n).

Inject a resize event. Stand-in for SIGWINCH in headless tests — the runtime updates its dimensions, clears the screen, and redraws at the new size. The test writer's cell buffer is resized first so the new frame has somewhere to land.

Inject a synthetic event into the runtime. The event uses the same shape the input parser emits (e.g. {:key, :tab, []}).

Convenience for {:key, key, mods} events.

Convenience for mouse events, in the shape the input parser emits. col and row are 1-indexed, as the terminal reports them. The app must be started with mouse: true for them to be routed.

Start an app under the test backend and return a handle for the helpers in this module.

Stop the app cleanly. Safe to call after the app has already quit on its own (e.g. via update returning :quit). Drains any pending :harlock_done message from the calling process's mailbox.

Types

handle()

@type handle() :: %{
  sup: pid(),
  name: atom(),
  writer: atom(),
  reader: atom(),
  runtime: atom()
}

Functions

cells(handle)

@spec cells(handle()) :: Harlock.Render.Buffer.t()

Returns the current cell buffer struct.

cursor(handle)

@spec cursor(handle()) :: {non_neg_integer(), non_neg_integer()} | nil

Returns the current Frame.cursor — {row, col} or nil if hidden.

focused(handle)

@spec focused(handle()) :: any()

Returns the id of the currently focused element, or nil.

model(handle)

@spec model(handle()) :: any()

Returns the current model from the runtime.

quit?(handle)

@spec quit?(handle()) :: boolean()

Returns true if the app has signaled :quit from its update callback. The runtime exits but the supervisor stays alive until you call stop/1.

raw_writes(handle)

@spec raw_writes(handle()) :: binary()

Returns the raw bytes the writer has received so far.

render(handle)

@spec render(handle()) :: String.t()

Returns the rendered cell buffer as a single string (rows joined by \n).

resize(handle, rows, cols)

@spec resize(handle(), pos_integer(), pos_integer()) :: :ok

Inject a resize event. Stand-in for SIGWINCH in headless tests — the runtime updates its dimensions, clears the screen, and redraws at the new size. The test writer's cell buffer is resized first so the new frame has somewhere to land.

send_event(handle, event)

@spec send_event(handle(), any()) :: :ok

Inject a synthetic event into the runtime. The event uses the same shape the input parser emits (e.g. {:key, :tab, []}).

send_key(handle, key, mods \\ [])

@spec send_key(handle(), any(), [atom()]) :: :ok

Convenience for {:key, key, mods} events.

Harlock.Test.send_key(h, :tab)
Harlock.Test.send_key(h, {:char, ?q})
Harlock.Test.send_key(h, :enter, [:ctrl])

send_mouse(handle, action, button, col, row, mods \\ [])

@spec send_mouse(handle(), atom(), atom() | nil, pos_integer(), pos_integer(), [
  atom()
]) :: :ok

Convenience for mouse events, in the shape the input parser emits. col and row are 1-indexed, as the terminal reports them. The app must be started with mouse: true for them to be routed.

Harlock.Test.send_mouse(h, :press, :left, 5, 2)
Harlock.Test.send_mouse(h, :wheel_down, nil, 5, 2)

start_app(app, init_arg \\ nil, opts \\ [])

@spec start_app(module(), any(), keyword()) :: handle()

Start an app under the test backend and return a handle for the helpers in this module.

Options:

  • :rows, :cols — the size of the test screen (default 24×80).

  • :name — base name for the app's processes (default: a unique one), useful when a test needs to address them.

  • :theme — as for Harlock.run/3.

  • :exec — stands in for the programs Harlock.Cmd.exec/3 would run, since there is no terminal to hand over. A function receiving the program, args and options the app passed, returning the result the app should get:

    h = Harlock.Test.start_app(Editor, nil,
          exec: fn "vim", [path], _opts ->
            send(test_pid, {:edited, path})
            {:ok, 0}
          end)

    Without it, every exec returns {:error, :no_terminal}.

  • :mouse — route mouse events as Harlock.run/3 does with mouse: true, for send_mouse/6.

  • :suspend — stands in for Harlock.Cmd.suspend/0 in the same way: a 0-arity function returning the result the app should get, typically {:ok, :resumed}. Without it, suspend returns {:error, :no_terminal}.

stop(handle)

@spec stop(handle()) :: :ok

Stop the app cleanly. Safe to call after the app has already quit on its own (e.g. via update returning :quit). Drains any pending :harlock_done message from the calling process's mailbox.