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