diff --git a/CHANGELOG.md b/CHANGELOG.md index 2860189..4c84d86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). +## Unreleased +### Fixed +- Launch JavaScript Playwright CLI files through Node on Windows or when `PLAYWRIGHT_NODEJS_PATH` is configured, while preserving shebang execution on Unix. + ## [0.7.1] 2026-06-19 ### Added - Warn about playwright version < 1.61.0 (port transport only). Commit [cd9e98e] diff --git a/lib/playwright_ex/processes/port_transport.ex b/lib/playwright_ex/processes/port_transport.ex index b18917d..a7e77f5 100644 --- a/lib/playwright_ex/processes/port_transport.ex +++ b/lib/playwright_ex/processes/port_transport.ex @@ -33,7 +33,7 @@ defmodule PlaywrightEx.PortTransport do def start_link(opts) do opts = Keyword.validate!(opts, [:executable, :name, :connection_name, env: %{}]) name = Keyword.get(opts, :name, @default_name) - check_version(opts[:executable]) + check_version(opts[:executable], opts[:env]) GenServer.start_link(__MODULE__, Map.new(opts), name: name) end @@ -44,8 +44,15 @@ defmodule PlaywrightEx.PortTransport do @impl GenServer def init(%{executable: executable, env: env} = opts) do + {command, args} = executable_command(executable, ["run-driver"], env) env = Enum.map(env, fn {k, v} -> {String.to_charlist(k), String.to_charlist(v)} end) - port = Port.open({:spawn_executable, executable}, [:binary, :stderr_to_stdout, args: ["run-driver"], env: env]) + + port = + Port.open( + {:spawn_executable, String.to_charlist(command)}, + [:binary, :stderr_to_stdout, args: args, env: env] + ) + connection_name = Map.get(opts, :connection_name, Connection) {:ok, %__MODULE__{port: port, connection_name: connection_name}} end @@ -108,8 +115,9 @@ defmodule PlaywrightEx.PortTransport do |> Map.update(:method, nil, &Serialization.underscore/1) end - defp check_version(executable) do - {"Version " <> version, 0} = executable |> Path.expand() |> System.cmd(~w(--version)) + defp check_version(executable, env) do + {command, args} = executable_command(Path.expand(executable), ["--version"], env) + {"Version " <> version, 0} = System.cmd(command, args) version = version |> String.trim() |> Version.parse!() recommended = PlaywrightEx.recommended_min_version() @@ -117,4 +125,26 @@ defmodule PlaywrightEx.PortTransport do IO.warn("Playwright version #{version} is below recommended #{recommended}") end end + + defp executable_command(executable, args, env) do + if javascript_file?(executable) and (windows?() or not is_nil(node_override(env))) do + {node_executable!(env), [executable | args]} + else + {executable, args} + end + end + + defp javascript_file?(executable), do: String.downcase(Path.extname(executable)) == ".js" + + defp windows?, do: :os.type() == {:win32, :nt} + + defp node_executable!(env) do + node_override(env) || + System.find_executable("node") || + raise "Node.js executable not found; set PLAYWRIGHT_NODEJS_PATH or add node to PATH" + end + + defp node_override(env) do + Map.get(env, "PLAYWRIGHT_NODEJS_PATH") || System.get_env("PLAYWRIGHT_NODEJS_PATH") + end end diff --git a/test/playwright_ex/processes/port_transport_test.exs b/test/playwright_ex/processes/port_transport_test.exs new file mode 100644 index 0000000..8ba5d43 --- /dev/null +++ b/test/playwright_ex/processes/port_transport_test.exs @@ -0,0 +1,90 @@ +defmodule PlaywrightEx.PortTransportTest do + use ExUnit.Case, async: true + + alias PlaywrightEx.PortTransport + + test "honors PLAYWRIGHT_NODEJS_PATH for JavaScript CLI files" do + node = System.find_executable("node") || flunk("Node.js executable not found on PATH") + {executable, marker_path} = create_cli_fixture() + + pid = + start_transport(executable, + env: %{"PLAYWRIGHT_NODEJS_PATH" => node} + ) + + assert_eventually(fn -> File.read(marker_path) == {:ok, "run-driver"} end) + GenServer.stop(pid) + end + + test "preserves JavaScript shebang execution on Unix" do + if :os.type() == {:win32, :nt} do + :ok + else + node = System.find_executable("node") || flunk("Node.js executable not found on PATH") + {executable, marker_path} = create_cli_fixture("#!#{node}\n") + File.chmod!(executable, 0o755) + + pid = start_transport(executable) + + assert_eventually(fn -> File.read(marker_path) == {:ok, "run-driver"} end) + GenServer.stop(pid) + end + end + + defp create_cli_fixture(shebang \\ "") do + test_dir = + Path.join( + System.tmp_dir!(), + "playwright port transport #{System.unique_integer([:positive])}" + ) + + File.mkdir_p!(test_dir) + on_exit(fn -> File.rm_rf!(test_dir) end) + + marker_path = Path.join(test_dir, "invocation.txt") + executable = Path.join(test_dir, "playwright cli.js") + marker_base64 = Base.encode64(marker_path) + + File.write!(executable, """ + #{shebang}const fs = require('node:fs') + const marker = Buffer.from('#{marker_base64}', 'base64').toString() + + if (process.argv[2] === '--version') { + console.log('Version 1.61.1') + } else if (process.argv[2] === 'run-driver') { + fs.writeFileSync(marker, 'run-driver') + process.stdin.resume() + } + """) + + {executable, marker_path} + end + + defp start_transport(executable, opts \\ []) do + name = String.to_atom("port_transport_test_#{System.unique_integer([:positive])}") + connection_name = String.to_atom("port_transport_connection_#{System.unique_integer([:positive])}") + + assert {:ok, pid} = + PortTransport.start_link( + [ + executable: executable, + name: name, + connection_name: connection_name + ] ++ opts + ) + + pid + end + + defp assert_eventually(fun, attempts \\ 50) + defp assert_eventually(fun, attempts) when attempts <= 0, do: assert(fun.()) + + defp assert_eventually(fun, attempts) do + if fun.() do + :ok + else + Process.sleep(10) + assert_eventually(fun, attempts - 1) + end + end +end