Packages

Boa-powered JavaScript runtimes for Elixir with BEAM dispatch hooks.

Current section

Files

Jump to
boam test boam_test.exs
Raw

test/boam_test.exs

defmodule BoamTest do
use ExUnit.Case, async: false
import ExUnit.CaptureLog
alias Boam.JS
alias Boam.Runtime
doctest Boam
test "evaluates JavaScript values through the runtime wrapper" do
runtime = start_supervised!({Runtime, []})
assert {:ok, 3} = Runtime.eval(runtime, "1 + 2")
assert {:ok, :undefined} = Runtime.eval(runtime, "undefined")
assert {:ok, %{"nested" => [1, true, nil]}} =
Runtime.eval(runtime, "({ nested: [1, true, null] })")
end
test "dispatches into exported BEAM handlers" do
runtime =
start_supervised!(
{Runtime,
[
exports: %{
sum: fn [left, right] -> left + right end
}
]}
)
assert {:ok, 5} = Runtime.eval(runtime, "beam.call('sum', 2, 3)")
end
test "uses a fallback matcher for unregistered names" do
runtime =
start_supervised!(
{Runtime,
[
fallback: fn name, args -> "#{name}:#{Enum.join(args, ",")}" end
]}
)
assert {:ok, "echo:1,two"} = Runtime.eval(runtime, "beam.call('echo', 1, 'two')")
end
test "turns dispatcher crashes into JavaScript errors instead of hanging" do
runtime =
start_supervised!(
{Runtime,
[
exports: %{
explode: fn _args -> raise "boom" end
}
]}
)
assert {:error, message} = Runtime.eval(runtime, "beam.call('explode')")
assert message =~ "boom"
end
test "rejects unsupported bridge arguments loudly" do
runtime =
start_supervised!(
{Runtime,
[
exports: %{
noop: fn _args -> :ok end
}
]}
)
assert {:error, message} = Runtime.eval(runtime, "beam.call('noop', undefined)")
assert message =~ "undefined"
end
test "returns a descriptive error for missing handlers" do
runtime = start_supervised!({Runtime, []})
assert {:error, message} = Runtime.eval(runtime, "beam.call('missing')")
assert message =~ "no handler registered"
end
test "runs prelude code during startup" do
runtime =
start_supervised!(
{Runtime,
[
prelude: "globalThis.console = { prefix: 'ready:' };"
]}
)
assert {:ok, "ready:ok"} = Runtime.eval(runtime, "console.prefix + 'ok'")
end
test "exposes nested Elixir handlers as JS functions" do
runtime =
start_supervised!(
{Runtime,
[
expose: %{
console: %{
log: fn [message] -> "logged:" <> message end,
warn: {:dispatch, "logger.warn", fn [message] -> "warn:" <> message end}
}
}
]}
)
assert {:ok, "logged:hello"} = Runtime.eval(runtime, "console.log('hello')")
assert {:ok, "warn:careful"} = Runtime.eval(runtime, "console.warn('careful')")
end
test "separates dispatcher exports from JS exposure" do
runtime = start_supervised!({Runtime, []})
dispatcher = Runtime.dispatcher(runtime)
assert :ok =
Boam.Dispatcher.register(dispatcher, "math.sum", fn [left, right] -> left + right end)
assert :ok = Runtime.expose_js(runtime, %{math: %{sum: true}})
assert {:ok, 5} = Runtime.eval(runtime, "math.sum(2, 3)")
assert :ok = Boam.Dispatcher.unregister(dispatcher, "math.sum")
assert {:error, message} = Runtime.eval(runtime, "math.sum(2, 3)")
assert message =~ "no handler registered"
end
test "allows JS exposure with an explicit dispatcher" do
dispatcher =
start_supervised!(
{Boam.Dispatcher,
[
fallback: fn name, args ->
"#{name}:#{Enum.join(Enum.map(args, &to_string/1), ",")}"
end
]}
)
runtime =
start_supervised!(
{Runtime,
[
dispatcher: dispatcher,
js_expose: %{console: %{log: true}}
]}
)
assert {:ok, "console.log:1,two"} = Runtime.eval(runtime, "console.log(1, 'two')")
end
test "accepts a named dispatcher server" do
dispatcher_name = __MODULE__.NamedDispatcher
dispatcher =
start_supervised!(
{Boam.Dispatcher,
[
name: dispatcher_name,
fallback: fn name, args ->
"#{name}:#{Enum.join(Enum.map(args, &to_string/1), ",")}"
end
]}
)
runtime =
start_supervised!(
{Runtime,
[
dispatcher: dispatcher_name,
js_expose: %{console: %{log: true}}
]}
)
assert dispatcher == Runtime.dispatcher(runtime)
assert {:ok, "console.log:1,two"} = Runtime.eval(runtime, "console.log(1, 'two')")
end
test "fails startup when a named dispatcher is not running" do
missing_dispatcher = __MODULE__.MissingDispatcher
{pid, monitor} =
spawn_monitor(fn ->
Runtime.start_link(dispatcher: missing_dispatcher)
end)
assert_receive {:DOWN, ^monitor, :process, ^pid, message}
assert message =~ "dispatcher is not running"
assert message =~ inspect(missing_dispatcher)
end
test "fails startup when dispatcher is not a valid server reference" do
{pid, monitor} =
spawn_monitor(fn ->
Runtime.start_link(dispatcher: 123)
end)
assert_receive {:DOWN, ^monitor, :process, ^pid, message}
assert message =~ "expected :dispatcher to be a pid, registered name"
assert message =~ "123"
end
test "can use a manually generated shim prelude" do
runtime =
start_supervised!(
{Runtime,
[
fallback: fn name, args ->
"#{name}:#{Enum.join(Enum.map(args, &to_string/1), ",")}"
end,
prelude: JS.export_prelude(%{console: %{log: true}})
]}
)
assert {:ok, "console.log:1,two"} = Runtime.eval(runtime, "console.log(1, 'two')")
end
test "can queue JS exposure from a dispatch handler by sending the runtime a message" do
holder = start_supervised!({Agent, fn -> nil end})
runtime =
start_supervised!(
{Runtime,
[
fallback: fn
"register", [name] ->
runtime = Agent.get(holder, & &1)
send(runtime, {:boam_expose_js, %{api: %{hello: {:dispatch, name}}}})
"registered"
name, args ->
"#{name}:#{Enum.join(Enum.map(args, &to_string/1), ",")}"
end
]}
)
Agent.update(holder, fn _ -> runtime end)
assert {:ok, "registered"} = Runtime.eval(runtime, "beam.call('register', 'greeter.hello')")
assert {:ok, "greeter.hello:world"} = Runtime.eval(runtime, "api.hello('world')")
end
test "returns an error for invalid synchronous JS exposure requests" do
runtime = start_supervised!({Runtime, []})
assert {:error, message} = Runtime.expose_js(runtime, %{console: %{log: fn _args -> :ok end}})
assert message =~ "expected JS exposure leaves"
assert {:ok, 3} = Runtime.eval(runtime, "1 + 2")
end
test "ignores invalid asynchronous JS exposure messages without crashing the runtime" do
runtime = start_supervised!({Runtime, []})
log =
capture_log(fn ->
send(runtime, {:boam_expose_js, %{console: %{log: fn _args -> :ok end}}})
assert {:ok, 3} = Runtime.eval(runtime, "1 + 2")
end)
assert log =~ "failed to expose JS functions at runtime"
end
test "fails startup when the prelude is invalid JavaScript" do
{pid, monitor} =
spawn_monitor(fn ->
Runtime.start_link(prelude: "let = ;")
end)
assert_receive {:DOWN, ^monitor, :process, ^pid, {:startup_prelude_failed, message}}
assert message != ""
end
test "rejects legacy expose handlers when an external dispatcher is supplied" do
dispatcher = start_supervised!({Boam.Dispatcher, []})
{pid, monitor} =
spawn_monitor(fn ->
Runtime.start_link(
dispatcher: dispatcher,
expose: %{console: %{log: fn _args -> :ok end}}
)
end)
assert_receive {:DOWN, ^monitor, :process, ^pid, message}
assert message =~ "cannot attach Elixir handlers"
end
end