lib/kino/test.ex

defmodule Kino.Test do
  @moduledoc """
  Conveniences for testing custom Kino components.

  In practice, `Kino.JS.Live` kinos communicate with Livebook via
  the group leader. During tests, Livebook is out of the equation,
  so we need to mimic this side of the communication. To do so, add
  the following setup to your test module:

      import Kino.Test

      setup :configure_livebook_bridge

  """

  import ExUnit.Callbacks
  import ExUnit.Assertions

  def configure_livebook_bridge(_context) do
    gl = start_supervised!({Kino.Test.GroupLeader, self()})
    Process.group_leader(self(), gl)
    :ok
  end

  @doc """
  Asserts the given output is sent within `timeout`.

  ## Examples

      assert_output({:markdown, "_hey_"})

  """
  defmacro assert_output(output, timeout \\ 100) do
    quote do
      assert_receive {:livebook_put_output, unquote(output)}, unquote(timeout)
    end
  end

  @doc """
  Asserts the given output is sent directly to the given client within
  `timeout`.

  ## Examples

      assert_output_to("client1", {:markdown, "_hey_"})

  """
  defmacro assert_output_to(client_id, output, timeout \\ 100) do
    quote do
      assert_receive {:livebook_put_output_to, unquote(client_id), unquote(output)},
                     unquote(timeout)
    end
  end

  @doc """
  Asserts the given output is sent directly to all clients within `timeout`.

  ## Examples

      assert_output_to("client1", {:markdown, "_hey_"})

  """
  defmacro assert_output_to_clients(output, timeout \\ 100) do
    quote do
      assert_receive {:livebook_put_output_to_clients, unquote(output)}, unquote(timeout)
    end
  end

  @doc """
  Asserts a `Kino.JS.Live` kino will broadcast an event within
  `timeout`.

  ## Examples

      assert_broadcast_event(kino, "bump", %{by: 2})

  """
  defmacro assert_broadcast_event(kino, event, payload, timeout \\ 100) do
    quote do
      %{ref: ref} = unquote(kino)

      assert_receive {:runtime_broadcast, "js_live", ^ref,
                      {:event, unquote(event), unquote(payload), %{ref: ^ref}}},
                     unquote(timeout)
    end
  end

  @doc """
  Asserts a `Kino.JS.Live` kino will send an event within `timeout`
  to the caller.

  ## Examples

      assert_send_event(kino, "pong", %{})

  """
  defmacro assert_send_event(kino, event, payload, timeout \\ 100) do
    quote do
      %{ref: ref} = unquote(kino)

      assert_receive {:event, unquote(event), unquote(payload), %{ref: ^ref}}, unquote(timeout)
    end
  end

  @doc """
  Sends a client event to a `Kino.JS.Live` kino.

  ## Examples

      push_event(kino, "bump", %{"by" => 2})

  """
  def push_event(kino, event, payload) do
    send(kino.pid, {:event, event, payload, %{origin: inspect(self())}})
  end

  @doc """
  Connects to a `Kino.JS.Live` kino and returns the initial data.

  If `resolve_fun` is given, it runs after sending the connection
  request and before awaiting for the reply.

  ## Examples

      data = connect(kino)
      assert data == %{count: 1}

  """
  def connect(kino, resolve_fun \\ nil, timeout \\ 100) do
    ref = kino.ref
    send(kino.pid, {:connect, self(), %{ref: ref, origin: inspect(self())}})
    if resolve_fun, do: resolve_fun.()
    assert_receive {:connect_reply, data, %{ref: ^ref}}, timeout
    data
  end

  @doc """
  Starts a smart cell defined by the given module.

  Returns a `Kino.JS.Live` kino for interacting with the cell, as
  well as the initial source.

  ## Examples

      {kino, source} = start_smart_cell!(Kino.SmartCell.Custom, %{"key" => "value"})

  """
  def start_smart_cell!(module, attrs) do
    ref = Kino.Output.random_ref()
    spec_arg = %{ref: ref, attrs: attrs, target_pid: self()}
    %{start: {mod, fun, args}} = module.child_spec(spec_arg)
    {:ok, pid, info} = apply(mod, fun, args)

    kino = %Kino.JS.Live{module: module, pid: pid, ref: info.js_view.ref}

    {kino, info.source}
  end

  @doc ~S'''
  Asserts a smart cell update will be broadcasted within `timeout`.

  Matches against the source and attribute that are reported as part
  of the update.

  If the `source` argument is a string, that string is compared in an
  exact match against the Kino's source.

  Alternatively, the `source` argument can be used to bind a variable
  to the Kino's source, allowing for custom assertions against the
  source.

  ## Examples

      assert_smart_cell_update(kino, %{"variable" => "x", "number" => 10}, "x = 10")

      assert_smart_cell_update(kino, %{"variable" => "x", "number" => 10}, source)
      assert source =~ "10"

  '''
  defmacro assert_smart_cell_update(kino, attrs, source, timeout \\ 100) do
    quote do
      %{ref: ref} = unquote(kino)

      assert_receive {:runtime_smart_cell_update, ^ref, unquote(attrs), unquote(source), _info},
                     unquote(timeout)
    end
  end
end