From 12bb3608893f2d0c3e78211f5a6aaa06c73578f3 Mon Sep 17 00:00:00 2001 From: Andrew Date: Mon, 2 Mar 2026 14:36:34 -0500 Subject: [PATCH 01/10] Add browser_launch_opts config option This adds a new `browser_launch_opts` configuration option that allows passing additional arguments to Playwright's browserType.launch(). This is useful for scenarios like: - Enabling fake media streams for testing audio/video capture - Passing custom browser flags for specific test requirements Example usage: ```elixir config :phoenix_test, playwright: [ browser_launch_opts: [ args: [ "--use-fake-ui-for-media-stream", "--use-fake-device-for-media-stream" ] ] ] ``` --- README.md | 22 ++++++++++++++++ lib/phoenix_test/playwright/config.ex | 13 ++++++++-- .../playwright/internal/browser.ex | 2 ++ .../playwright/browser_launch_opts_test.exs | 18 +++++++++++++ test/phoenix_test/playwright/config_test.exs | 26 +++++++++++++++++++ 5 files changed, 79 insertions(+), 2 deletions(-) create mode 100644 test/phoenix_test/playwright/browser_launch_opts_test.exs create mode 100644 test/phoenix_test/playwright/config_test.exs diff --git a/README.md b/README.md index 04b52d6..25ee67f 100644 --- a/README.md +++ b/README.md @@ -153,6 +153,28 @@ defmodule DebuggingFeatureTest do end ``` +### Browser Launch Options + +Pass additional arguments to the browser via `browser_launch_opts`. This is useful for +testing features that require specific browser flags, like fake media streams for +audio/video capture tests: + +```elixir +# config/test.exs +config :phoenix_test, + playwright: [ + browser_launch_opts: [ + args: [ + "--use-fake-ui-for-media-stream", + "--use-fake-device-for-media-stream" + ] + ] + ] +``` + +See [Playwright's browserType.launch() docs](https://playwright.dev/docs/api/class-browsertype#browser-type-launch) +for all available options. + ## Remote Playwright Server diff --git a/lib/phoenix_test/playwright/config.ex b/lib/phoenix_test/playwright/config.ex index ced80b2..8f2e2af 100644 --- a/lib/phoenix_test/playwright/config.ex +++ b/lib/phoenix_test/playwright/config.ex @@ -12,6 +12,14 @@ browser_opts = [ type: {:in, browsers}, type_doc: "`#{Enum.map_join(browsers, " | ", &":#{&1}")}`" ], + browser_launch_opts: [ + default: [], + type: {:or, [:map, :keyword_list]}, + doc: """ + Additional arguments passed to Playwright [browserType.launch](https://playwright.dev/docs/api/class-browsertype#browser-type-launch). + E.g. `[args: ["--use-fake-ui-for-media-stream", "--use-fake-device-for-media-stream"]]`. + """ + ], browser_launch_timeout: [ default: to_timeout(second: 4), type: :non_neg_integer @@ -60,6 +68,7 @@ schema_opts = [ """ ], browser: browser_opts[:browser], + browser_launch_opts: browser_opts[:browser_launch_opts], browser_context_opts: [ default: [], type: {:or, [:map, :keyword_list]}, @@ -172,9 +181,9 @@ schema_opts = [ schema = NimbleOptions.new!(schema_opts) -setup_all_keys = ~w(browser_pool browser browser_launch_timeout executable_path headless slow_mo)a +setup_all_keys = ~w(browser_pool browser browser_launch_opts browser_launch_timeout executable_path headless slow_mo)a setup_keys = ~w(accept_dialogs ecto_sandbox_stop_owner_delay screenshot trace browser_context_opts browser_page_opts)a -merge_global_into_browser_pool_keys = ~w(browser browser_launch_timeout headless slow_mo)a +merge_global_into_browser_pool_keys = ~w(browser browser_launch_opts browser_launch_timeout headless slow_mo)a defmodule PhoenixTest.Playwright.Config do @moduledoc """ diff --git a/lib/phoenix_test/playwright/internal/browser.ex b/lib/phoenix_test/playwright/internal/browser.ex index 6a9eaa3..eb2309e 100644 --- a/lib/phoenix_test/playwright/internal/browser.ex +++ b/lib/phoenix_test/playwright/internal/browser.ex @@ -7,7 +7,9 @@ defmodule PhoenixTest.Playwright.Browser do def launch_browser!(config) do {launch_timeout, opts} = Keyword.pop!(config, :browser_launch_timeout) {browser, opts} = Keyword.pop!(opts, :browser) + {launch_opts, opts} = Keyword.pop(opts, :browser_launch_opts, []) opts = opts |> Keyword.put(:timeout, launch_timeout) |> Keyword.delete(:browser_pool) + opts = Keyword.merge(opts, launch_opts) case PlaywrightEx.launch_browser(browser, opts) do {:ok, browser} -> diff --git a/test/phoenix_test/playwright/browser_launch_opts_test.exs b/test/phoenix_test/playwright/browser_launch_opts_test.exs new file mode 100644 index 0000000..fa429ef --- /dev/null +++ b/test/phoenix_test/playwright/browser_launch_opts_test.exs @@ -0,0 +1,18 @@ +defmodule PhoenixTest.Playwright.BrowserLaunchOptsTest do + @moduledoc """ + Tests that browser_launch_opts are passed through to Playwright. + """ + + use PhoenixTest.Playwright.Case, + async: true, + browser_pool: false, + browser_launch_opts: [args: ["--disable-background-networking"]] + + test "launches browser with custom launch opts", %{conn: conn} do + # If browser_launch_opts weren't handled correctly, the browser launch would fail. + # This test verifies the option is properly passed through the launch pipeline. + conn + |> visit("/pw/live") + |> assert_has("h1") + end +end diff --git a/test/phoenix_test/playwright/config_test.exs b/test/phoenix_test/playwright/config_test.exs new file mode 100644 index 0000000..4fc98e8 --- /dev/null +++ b/test/phoenix_test/playwright/config_test.exs @@ -0,0 +1,26 @@ +defmodule PhoenixTest.Playwright.ConfigTest do + use ExUnit.Case, async: true + + alias PhoenixTest.Playwright.Config + + describe "browser_launch_opts" do + test "accepts keyword list" do + config = Config.validate!(browser_launch_opts: [args: ["--disable-gpu"]]) + assert config[:browser_launch_opts] == [args: ["--disable-gpu"]] + end + + test "accepts map" do + config = Config.validate!(browser_launch_opts: %{args: ["--disable-gpu"]}) + assert config[:browser_launch_opts] == %{args: ["--disable-gpu"]} + end + + test "defaults to empty list" do + config = Config.validate!([]) + assert config[:browser_launch_opts] == [] + end + + test "is included in setup_all_keys" do + assert :browser_launch_opts in Config.setup_all_keys() + end + end +end From c8c2dac7237930d5dfe0a5b7e1a27fef04d055ce Mon Sep 17 00:00:00 2001 From: Andrew Date: Mon, 2 Mar 2026 15:25:57 -0500 Subject: [PATCH 02/10] Fix misleading test comment --- test/phoenix_test/playwright/browser_launch_opts_test.exs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/phoenix_test/playwright/browser_launch_opts_test.exs b/test/phoenix_test/playwright/browser_launch_opts_test.exs index fa429ef..8d575f9 100644 --- a/test/phoenix_test/playwright/browser_launch_opts_test.exs +++ b/test/phoenix_test/playwright/browser_launch_opts_test.exs @@ -9,8 +9,8 @@ defmodule PhoenixTest.Playwright.BrowserLaunchOptsTest do browser_launch_opts: [args: ["--disable-background-networking"]] test "launches browser with custom launch opts", %{conn: conn} do - # If browser_launch_opts weren't handled correctly, the browser launch would fail. - # This test verifies the option is properly passed through the launch pipeline. + # Verifies that browser_launch_opts is accepted by the config and doesn't + # break browser launching. The args are passed through to Playwright. conn |> visit("/pw/live") |> assert_has("h1") From c3afa2e0fe7ac208193ad4e69b6052af303d816a Mon Sep 17 00:00:00 2001 From: Andrew Date: Mon, 2 Mar 2026 15:43:32 -0500 Subject: [PATCH 03/10] Add stronger test proving browser_launch_opts flags work Test getUserMedia with and without fake media device flags to verify the browser launch options actually affect browser behavior. --- .../playwright/browser_launch_opts_test.exs | 58 +++++++++++++++++-- 1 file changed, 54 insertions(+), 4 deletions(-) diff --git a/test/phoenix_test/playwright/browser_launch_opts_test.exs b/test/phoenix_test/playwright/browser_launch_opts_test.exs index 8d575f9..df94e35 100644 --- a/test/phoenix_test/playwright/browser_launch_opts_test.exs +++ b/test/phoenix_test/playwright/browser_launch_opts_test.exs @@ -1,18 +1,68 @@ defmodule PhoenixTest.Playwright.BrowserLaunchOptsTest do @moduledoc """ Tests that browser_launch_opts are passed through to Playwright. + + These tests verify that browser launch flags actually affect browser behavior + by testing getUserMedia with and without fake media device flags. """ use PhoenixTest.Playwright.Case, async: true, browser_pool: false, - browser_launch_opts: [args: ["--disable-background-networking"]] + browser_launch_opts: [ + args: [ + "--use-fake-ui-for-media-stream", + "--use-fake-device-for-media-stream" + ] + ] - test "launches browser with custom launch opts", %{conn: conn} do - # Verifies that browser_launch_opts is accepted by the config and doesn't - # break browser launching. The args are passed through to Playwright. + test "getUserMedia succeeds with fake media device flags", %{conn: conn} do conn |> visit("/pw/live") |> assert_has("h1") + |> unwrap(fn %{frame_id: frame_id} -> + {:ok, result} = + PlaywrightEx.Frame.evaluate(frame_id, + expression: """ + navigator.mediaDevices.getUserMedia({ audio: true }) + .then(() => "success") + .catch(e => "error: " + e.name) + """, + timeout: timeout() + ) + + assert result == "success" + end) + end +end + +defmodule PhoenixTest.Playwright.BrowserLaunchOptsWithoutFlagsTest do + @moduledoc """ + Tests that getUserMedia fails WITHOUT the fake media device flags. + This proves the flags in BrowserLaunchOptsTest actually have an effect. + """ + + use PhoenixTest.Playwright.Case, + async: true, + browser_pool: false + + test "getUserMedia fails without fake media device flags", %{conn: conn} do + conn + |> visit("/pw/live") + |> assert_has("h1") + |> unwrap(fn %{frame_id: frame_id} -> + {:ok, result} = + PlaywrightEx.Frame.evaluate(frame_id, + expression: """ + navigator.mediaDevices.getUserMedia({ audio: true }) + .then(() => "success") + .catch(e => "error: " + e.name) + """, + timeout: timeout() + ) + + # Without fake device flags, getUserMedia should fail in headless mode + assert result =~ "error:" + end) end end From 7937ff685b11637f0203d9395f7e67eb544097cf Mon Sep 17 00:00:00 2001 From: Andrew Date: Mon, 2 Mar 2026 15:51:05 -0500 Subject: [PATCH 04/10] Use pop! since NimbleOptions provides default --- lib/phoenix_test/playwright/internal/browser.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/phoenix_test/playwright/internal/browser.ex b/lib/phoenix_test/playwright/internal/browser.ex index eb2309e..afda16e 100644 --- a/lib/phoenix_test/playwright/internal/browser.ex +++ b/lib/phoenix_test/playwright/internal/browser.ex @@ -7,7 +7,7 @@ defmodule PhoenixTest.Playwright.Browser do def launch_browser!(config) do {launch_timeout, opts} = Keyword.pop!(config, :browser_launch_timeout) {browser, opts} = Keyword.pop!(opts, :browser) - {launch_opts, opts} = Keyword.pop(opts, :browser_launch_opts, []) + {launch_opts, opts} = Keyword.pop!(opts, :browser_launch_opts) opts = opts |> Keyword.put(:timeout, launch_timeout) |> Keyword.delete(:browser_pool) opts = Keyword.merge(opts, launch_opts) From fec6545cceefddd10036b56f2c5aea72c68f56c0 Mon Sep 17 00:00:00 2001 From: Andrew Date: Mon, 2 Mar 2026 15:52:50 -0500 Subject: [PATCH 05/10] Use keyword_list type since Keyword.merge is used --- lib/phoenix_test/playwright/config.ex | 2 +- test/phoenix_test/playwright/config_test.exs | 5 ----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/lib/phoenix_test/playwright/config.ex b/lib/phoenix_test/playwright/config.ex index 8f2e2af..1939a61 100644 --- a/lib/phoenix_test/playwright/config.ex +++ b/lib/phoenix_test/playwright/config.ex @@ -14,7 +14,7 @@ browser_opts = [ ], browser_launch_opts: [ default: [], - type: {:or, [:map, :keyword_list]}, + type: :keyword_list, doc: """ Additional arguments passed to Playwright [browserType.launch](https://playwright.dev/docs/api/class-browsertype#browser-type-launch). E.g. `[args: ["--use-fake-ui-for-media-stream", "--use-fake-device-for-media-stream"]]`. diff --git a/test/phoenix_test/playwright/config_test.exs b/test/phoenix_test/playwright/config_test.exs index 4fc98e8..d45f2b6 100644 --- a/test/phoenix_test/playwright/config_test.exs +++ b/test/phoenix_test/playwright/config_test.exs @@ -9,11 +9,6 @@ defmodule PhoenixTest.Playwright.ConfigTest do assert config[:browser_launch_opts] == [args: ["--disable-gpu"]] end - test "accepts map" do - config = Config.validate!(browser_launch_opts: %{args: ["--disable-gpu"]}) - assert config[:browser_launch_opts] == %{args: ["--disable-gpu"]} - end - test "defaults to empty list" do config = Config.validate!([]) assert config[:browser_launch_opts] == [] From 545e311400f7804d7dfae3e447946402ad2099ef Mon Sep 17 00:00:00 2001 From: Andrew Date: Mon, 2 Mar 2026 15:53:25 -0500 Subject: [PATCH 06/10] Remove this alltogether --- test/phoenix_test/playwright/config_test.exs | 21 -------------------- 1 file changed, 21 deletions(-) delete mode 100644 test/phoenix_test/playwright/config_test.exs diff --git a/test/phoenix_test/playwright/config_test.exs b/test/phoenix_test/playwright/config_test.exs deleted file mode 100644 index d45f2b6..0000000 --- a/test/phoenix_test/playwright/config_test.exs +++ /dev/null @@ -1,21 +0,0 @@ -defmodule PhoenixTest.Playwright.ConfigTest do - use ExUnit.Case, async: true - - alias PhoenixTest.Playwright.Config - - describe "browser_launch_opts" do - test "accepts keyword list" do - config = Config.validate!(browser_launch_opts: [args: ["--disable-gpu"]]) - assert config[:browser_launch_opts] == [args: ["--disable-gpu"]] - end - - test "defaults to empty list" do - config = Config.validate!([]) - assert config[:browser_launch_opts] == [] - end - - test "is included in setup_all_keys" do - assert :browser_launch_opts in Config.setup_all_keys() - end - end -end From f2006330a98039547d634da0b45ab3db68f53c7a Mon Sep 17 00:00:00 2001 From: Andrew Date: Tue, 3 Mar 2026 12:51:32 -0500 Subject: [PATCH 07/10] Use evaluate --- .../playwright/browser_launch_opts_test.exs | 42 +++++++------------ 1 file changed, 16 insertions(+), 26 deletions(-) diff --git a/test/phoenix_test/playwright/browser_launch_opts_test.exs b/test/phoenix_test/playwright/browser_launch_opts_test.exs index df94e35..c2b9027 100644 --- a/test/phoenix_test/playwright/browser_launch_opts_test.exs +++ b/test/phoenix_test/playwright/browser_launch_opts_test.exs @@ -20,19 +20,14 @@ defmodule PhoenixTest.Playwright.BrowserLaunchOptsTest do conn |> visit("/pw/live") |> assert_has("h1") - |> unwrap(fn %{frame_id: frame_id} -> - {:ok, result} = - PlaywrightEx.Frame.evaluate(frame_id, - expression: """ - navigator.mediaDevices.getUserMedia({ audio: true }) - .then(() => "success") - .catch(e => "error: " + e.name) - """, - timeout: timeout() - ) - - assert result == "success" - end) + |> evaluate( + """ + navigator.mediaDevices.getUserMedia({ audio: true }) + .then(() => "success") + .catch(e => "error: " + e.name) + """, + &assert(&1 == "success") + ) end end @@ -50,19 +45,14 @@ defmodule PhoenixTest.Playwright.BrowserLaunchOptsWithoutFlagsTest do conn |> visit("/pw/live") |> assert_has("h1") - |> unwrap(fn %{frame_id: frame_id} -> - {:ok, result} = - PlaywrightEx.Frame.evaluate(frame_id, - expression: """ - navigator.mediaDevices.getUserMedia({ audio: true }) - .then(() => "success") - .catch(e => "error: " + e.name) - """, - timeout: timeout() - ) - + |> evaluate( + """ + navigator.mediaDevices.getUserMedia({ audio: true }) + .then(() => "success") + .catch(e => "error: " + e.name) + """, # Without fake device flags, getUserMedia should fail in headless mode - assert result =~ "error:" - end) + &assert(&1 =~ "error:") + ) end end From ce0730f7cc9e13af8e2343dd71826a8a5b782286 Mon Sep 17 00:00:00 2001 From: Andrew Date: Tue, 3 Mar 2026 13:03:17 -0500 Subject: [PATCH 08/10] Revert README changes --- README.md | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/README.md b/README.md index 25ee67f..04b52d6 100644 --- a/README.md +++ b/README.md @@ -153,28 +153,6 @@ defmodule DebuggingFeatureTest do end ``` -### Browser Launch Options - -Pass additional arguments to the browser via `browser_launch_opts`. This is useful for -testing features that require specific browser flags, like fake media streams for -audio/video capture tests: - -```elixir -# config/test.exs -config :phoenix_test, - playwright: [ - browser_launch_opts: [ - args: [ - "--use-fake-ui-for-media-stream", - "--use-fake-device-for-media-stream" - ] - ] - ] -``` - -See [Playwright's browserType.launch() docs](https://playwright.dev/docs/api/class-browsertype#browser-type-launch) -for all available options. - ## Remote Playwright Server From 1683010860c41a6ceb2f51591f4d98b1699baaeb Mon Sep 17 00:00:00 2001 From: Fredrik Teschke Date: Tue, 3 Mar 2026 20:22:03 +0100 Subject: [PATCH 09/10] Format --- lib/phoenix_test/playwright/config.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/phoenix_test/playwright/config.ex b/lib/phoenix_test/playwright/config.ex index 1939a61..4800b7c 100644 --- a/lib/phoenix_test/playwright/config.ex +++ b/lib/phoenix_test/playwright/config.ex @@ -68,7 +68,6 @@ schema_opts = [ """ ], browser: browser_opts[:browser], - browser_launch_opts: browser_opts[:browser_launch_opts], browser_context_opts: [ default: [], type: {:or, [:map, :keyword_list]}, @@ -77,6 +76,7 @@ schema_opts = [ E.g. `[http_credentials: %{username: "a", password: "b"}]`. """ ], + browser_launch_opts: browser_opts[:browser_launch_opts], browser_launch_timeout: browser_opts[:browser_launch_timeout], browser_page_opts: [ default: [], From 3563ac972059c65d7a13a3289e1c4fe07ac48491 Mon Sep 17 00:00:00 2001 From: Fredrik Teschke Date: Tue, 3 Mar 2026 20:35:09 +0100 Subject: [PATCH 10/10] Skip for websocket --- lib/phoenix_test/playwright/config.ex | 1 + test/phoenix_test/playwright/browser_launch_opts_test.exs | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/lib/phoenix_test/playwright/config.ex b/lib/phoenix_test/playwright/config.ex index 4800b7c..3d791e8 100644 --- a/lib/phoenix_test/playwright/config.ex +++ b/lib/phoenix_test/playwright/config.ex @@ -18,6 +18,7 @@ browser_opts = [ doc: """ Additional arguments passed to Playwright [browserType.launch](https://playwright.dev/docs/api/class-browsertype#browser-type-launch). E.g. `[args: ["--use-fake-ui-for-media-stream", "--use-fake-device-for-media-stream"]]`. + Can't be used with remote browser (ws_endpoint). """ ], browser_launch_timeout: [ diff --git a/test/phoenix_test/playwright/browser_launch_opts_test.exs b/test/phoenix_test/playwright/browser_launch_opts_test.exs index c2b9027..807184b 100644 --- a/test/phoenix_test/playwright/browser_launch_opts_test.exs +++ b/test/phoenix_test/playwright/browser_launch_opts_test.exs @@ -16,6 +16,8 @@ defmodule PhoenixTest.Playwright.BrowserLaunchOptsTest do ] ] + @moduletag skip: !!Application.compile_env!(:phoenix_test, :playwright)[:ws_endpoint] + test "getUserMedia succeeds with fake media device flags", %{conn: conn} do conn |> visit("/pw/live") @@ -41,6 +43,8 @@ defmodule PhoenixTest.Playwright.BrowserLaunchOptsWithoutFlagsTest do async: true, browser_pool: false + @moduletag skip: !!Application.compile_env!(:phoenix_test, :playwright)[:ws_endpoint] + test "getUserMedia fails without fake media device flags", %{conn: conn} do conn |> visit("/pw/live")