Harlock.Test (harlock v0.2.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, discards prev_frame, and re-renders. 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.

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, discards prev_frame, and re-renders. 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])

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.

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.