defmodule BoamTest do use ExUnit.Case, async: false 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 "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 "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 automatic exposure 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 use :expose" end end