Skip to content

adds support for BrowserType.connect_over_cdp() (for chromium browser only) #37

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 64 additions & 8 deletions lib/playwright/browser_type.ex
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,21 @@ defmodule Playwright.BrowserType do
"""

use Playwright.ChannelOwner
alias Playwright.{BrowserType, Config, Transport}
alias Playwright.{BrowserType, Config, Transport, Channel}
alias Playwright.Channel.Session

@typedoc "The web client type used for `launch/1` and `connect/2` functions."
@type client :: :chromium | :firefox | :webkit

@typedoc "Options for `connect/2`"
@type connect_options :: map()
@type connect_options :: %{
optional(:headers) => map(),
optional(:slow_mo) => integer(),
optional(:timeout) => integer()
}

@typedoc "Options for `connect_over_cdp/3`"
@type connect_over_cdp_options :: connect_options()

@typedoc "A map/struct providing call options"
@type options :: map()
Expand All @@ -55,12 +62,12 @@ defmodule Playwright.BrowserType do

## Arguments

| key/name | type | | description |
| key/name | type | | description |
| ------------- | ------ | --------------------------- | ----------- |
| `ws_endpoint` | param | `BrowserType.ws_endpoint()` | A browser websocket endpoint to connect to. |
| `:headers` | option | `map()` | Additional HTTP headers to be sent with websocket connect request |
| `:slow_mow` | option | `integer()` | Slows down Playwright operations by the specified amount of milliseconds. Useful so that you can see what is going on. `(default: 0)` |
| `:logger` | option | | Logger sink for Playwright logging |
| `:slow_mo` | option | `integer()` | Slows down Playwright operations by the specified amount of milliseconds. Useful so that you can see what is going on. `(default: 0)` |
| `:logger` | N/A | NOT IMPLEMENTED YET | Logger sink for Playwright logging |
| `:timeout` | option | `integer()` | Maximum time in milliseconds to wait for the connection to be established. Pass `0` to disable timeout. `(default: 30_000 (30 seconds))` |
"""
@spec connect(ws_endpoint(), connect_options()) :: {pid(), Playwright.Browser.t()}
Expand All @@ -77,10 +84,59 @@ defmodule Playwright.BrowserType do
end
end

# ---
# IMPORTANT: `Browser` instances returned from `connect_over_cdp` are effectively pointers
# to existing Chromium devtools sessions. That is, our `Catalog` will contain, for example,
# multiple `Page` instances pointing to the same Playwright server browser session.
# Or... something like that.
@spec connect_over_cdp(Playwright.Browser.t(), url(), connect_over_cdp_options()) :: Playwright.Browser.t()
def connect_over_cdp(%Playwright.Browser{} = browser, endpoint_url, options \\ %{}) do
params =
%{
"endpointURL" => endpoint_url,
"sdkLanguage" => "elixir"
}
|> Map.merge(options)

connect_over_cdp = fn browser_type, params ->
Channel.post(browser_type.session, {:guid, browser_type.guid}, "connectOverCDP", Map.merge(params, options))
end

with browser_type <- get_browser_type(browser, :chromium),
%{browser: browser} = response <- connect_over_cdp.(browser_type, params) do
if response.default_context do
Channel.patch(
browser_type.session,
{:guid, response.default_context.guid},
%{browser: browser}
)
end

browser
else
{:error, :browser_type_not_found, client} ->
raise RuntimeError,
message: """
Attempted to use #{__MODULE__}.connect_over_cdp/3 with incompatible browser
client #{inspect(client)}. It is only availabe for use with chromium
"""
end
end

defp get_browser_type(%Playwright.Browser{} = browser, client) do
find_playwright = fn -> {:playwright, Playwright.Channel.find(browser.session, {:guid, "Playwright"})} end
find_client_guid = fn playwright, client -> {:client_guid, get_in(playwright, [Access.key(client), Access.key(:guid)])} end

with {:playwright, playwright} <- find_playwright.(),
{:client_guid, client_guid} <- find_client_guid.(playwright, client) do
Playwright.Channel.find(browser.session, {:guid, client_guid})
else
{:client_guid, nil} ->
{:error, :browser_type_not_found, client}

# @spec connect_over_cdp(BrowserType.t(), url(), options()) :: Playwright.Browser.t()
# def connect_over_cdp(browser_type, endpoint_url, options \\ %{})
_ ->
{:error, :unexpected_error}
end
end

# @spec executable_path(BrowserType.t()) :: String.t()
# def executable_path(browser_type)
Expand Down
7 changes: 6 additions & 1 deletion lib/playwright/channel.ex
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,14 @@ defmodule Playwright.Channel do
Session.catalog(session) |> Catalog.get(guid, options)
end

def list(session, type) do
Catalog.list(Session.catalog(session), %{
type: type
})
end
def list(session, {:guid, guid}, type) do
Catalog.list(Session.catalog(session), %{
parent: guid,
parent: %{guid: guid},
type: type
})
end
Expand Down
7 changes: 7 additions & 0 deletions lib/playwright/channel/response.ex
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,13 @@ defmodule Playwright.Channel.Response do
result
end

defp parse([browser: %{guid: browser_guid}, defaultContext: %{guid: context_guid}], catalog) do
%{
browser: Catalog.get(catalog, browser_guid),
default_context: Catalog.get(catalog, context_guid)
}
end

defp parse([{:binary, value}], _catalog) do
value
end
Expand Down
1 change: 1 addition & 0 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ defmodule Playwright.MixProject do
{:dialyxir, "~> 1.1", only: [:dev, :test], runtime: false},
{:ex_doc, "~> 0.25", only: :dev, runtime: false},
{:esbuild, "~> 0.4", runtime: Mix.env() == :dev},
{:httpoison, "~> 1.8", only: [:test]},
{:gun, "~> 1.3.3"},
{:jason, "~> 1.2"},
{:mix_audit, "~> 1.0", only: [:dev, :test], runtime: false},
Expand Down
9 changes: 9 additions & 0 deletions mix.lock
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
%{
"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm", "7af5c7e09fe1d40f76c8e4f9dd2be7cebd83909f31fee7cd0e9eadc567da8353"},
"castore": {:hex, :castore, "0.1.13", "ccf3ab251ffaebc4319f41d788ce59a6ab3f42b6c27e598ad838ffecee0b04f9", [:mix], [], "hexpm", "a14a7eecfec7e20385493dbb92b0d12c5d77ecfd6307de10102d58c94e8c49c0"},
"certifi": {:hex, :certifi, "2.8.0", "d4fb0a6bb20b7c9c3643e22507e42f356ac090a1dcea9ab99e27e0376d695eba", [:rebar3], [], "hexpm", "6ac7efc1c6f8600b08d625292d4bbf584e14847ce1b6b5c44d983d273e1097ea"},
"cowboy": {:hex, :cowboy, "2.6.3", "99aa50e94e685557cad82e704457336a453d4abcb77839ad22dbe71f311fcc06", [:rebar3], [{:cowlib, "~> 2.7.3", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "e5580029080f3f1ad17436fb97b0d5ed2ed4e4815a96bac36b5a992e20f58db6"},
"cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"},
"cowlib": {:hex, :cowlib, "2.7.3", "a7ffcd0917e6d50b4d5fb28e9e2085a0ceb3c97dea310505f7460ff5ed764ce9", [:rebar3], [], "hexpm", "1e1a3d176d52daebbecbbcdfd27c27726076567905c2a9d7398c54da9d225761"},
Expand All @@ -12,21 +13,29 @@
"ex_doc": {:hex, :ex_doc, "0.25.5", "ac3c5425a80b4b7c4dfecdf51fa9c23a44877124dd8ca34ee45ff608b1c6deb9", [:mix], [{:earmark_parser, "~> 1.4.0", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "688cfa538cdc146bc4291607764a7f1fcfa4cce8009ecd62de03b27197528350"},
"file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"},
"gun": {:hex, :gun, "1.3.3", "cf8b51beb36c22b9c8df1921e3f2bc4d2b1f68b49ad4fbc64e91875aa14e16b4", [:rebar3], [{:cowlib, "~> 2.7.0", [hex: :cowlib, repo: "hexpm", optional: false]}], "hexpm", "3106ce167f9c9723f849e4fb54ea4a4d814e3996ae243a1c828b256e749041e0"},
"hackney": {:hex, :hackney, "1.18.0", "c4443d960bb9fba6d01161d01cd81173089686717d9490e5d3606644c48d121f", [:rebar3], [{:certifi, "~>2.8.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "9afcda620704d720db8c6a3123e9848d09c87586dc1c10479c42627b905b5c5e"},
"httpoison": {:hex, :httpoison, "1.8.0", "6b85dea15820b7804ef607ff78406ab449dd78bed923a49c7160e1886e987a3d", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "28089eaa98cf90c66265b6b5ad87c59a3729bea2e74e9d08f9b51eb9729b3c3a"},
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
"jason": {:hex, :jason, "1.3.0", "fa6b82a934feb176263ad2df0dbd91bf633d4a46ebfdffea0c8ae82953714946", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "53fc1f51255390e0ec7e50f9cb41e751c260d065dcba2bf0d08dc51a4002c2ac"},
"json_diff": {:hex, :json_diff, "0.1.3", "c80d5ca5416e785867e765e906e9a91b7efc35bfd505af276654d108f4995736", [:mix], [], "hexpm", "a5332e8293e7e9f384d34ea44645d7961334db73739165178fd4a7728d06f7d1"},
"makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"},
"makeup_elixir": {:hex, :makeup_elixir, "0.15.2", "dc72dfe17eb240552857465cc00cce390960d9a0c055c4ccd38b70629227e97c", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "fd23ae48d09b32eff49d4ced2b43c9f086d402ee4fd4fcb2d7fad97fa8823e75"},
"makeup_erlang": {:hex, :makeup_erlang, "0.1.1", "3fcb7f09eb9d98dc4d208f49cc955a34218fc41ff6b84df7c75b3e6e533cc65f", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "174d0809e98a4ef0b3309256cbf97101c6ec01c4ab0b23e926a9e17df2077cbb"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
"mime": {:hex, :mime, "2.0.2", "0b9e1a4c840eafb68d820b0e2158ef5c49385d17fb36855ac6e7e087d4b1dcc5", [:mix], [], "hexpm", "e6a3f76b4c277739e36c2e21a2c640778ba4c3846189d5ab19f97f126df5f9b7"},
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
"mix_audit": {:hex, :mix_audit, "1.0.0", "d2b5adbd69f34ba6b5b7d52812b1ba06f9110367e196d3ba5dba7753124cf8be", [:make, :mix], [{:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 2.8.0", [hex: :yaml_elixir, repo: "hexpm", optional: false]}], "hexpm", "1b1ff6694e6eb12818ce5dcc276a39bbe03e27fcd11376c381bfe6b4900f2aa8"},
"nimble_parsec": {:hex, :nimble_parsec, "1.1.0", "3a6fca1550363552e54c216debb6a9e95bd8d32348938e13de5eda962c0d7f89", [:mix], [], "hexpm", "08eb32d66b706e913ff748f11694b17981c0b04a33ef470e33e11b3d3ac8f54b"},
"parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"},
"playwright_assets": {:hex, :playwright_assets, "1.18.1", "3398a85f0281f05cbe12c89709ff60f831f262356f6275e536ab8d2e0f4fb6ec", [:mix], [{:cowlib, "~> 2.7.3", [hex: :cowlib, repo: "hexpm", optional: false]}, {:plug, "~> 1.12", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.1.3", [hex: :plug_cowboy, repo: "hexpm", optional: false]}], "hexpm", "78e6940c3bac4f21f206597b9c264db005a6dc54b3135976ef2723042147e54e"},
"plug": {:hex, :plug, "1.13.2", "33aba8e2b43ddd68d9d49b818ed2fb46da85f4ec3229bc4bcd0c981a640a4e71", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a95cdfe599e3524b98684376c3f3494cbfbc1f41fcddefc380cac3138dd7619d"},
"plug_cowboy": {:hex, :plug_cowboy, "2.1.3", "38999a3e85e39f0e6bdfdf820761abac61edde1632cfebbacc445cdcb6ae1333", [:mix], [{:cowboy, "~> 2.5", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "056f41f814dbb38ea44613e0f613b3b2b2f2c6afce64126e252837669eba84db"},
"plug_crypto": {:hex, :plug_crypto, "1.2.2", "05654514ac717ff3a1843204b424477d9e60c143406aa94daf2274fdd280794d", [:mix], [], "hexpm", "87631c7ad914a5a445f0a3809f99b079113ae4ed4b867348dd9eec288cecb6db"},
"ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm", "451d8527787df716d99dc36162fca05934915db0b6141bbdac2ea8d3c7afc7d7"},
"recase": {:hex, :recase, "0.7.0", "3f2f719f0886c7a3b7fe469058ec539cb7bbe0023604ae3bce920e186305e5ae", [:mix], [], "hexpm", "36f5756a9f552f4a94b54a695870e32f4e72d5fad9c25e61bc4a3151c08a4e0c"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"},
"telemetry": {:hex, :telemetry, "1.0.0", "0f453a102cdf13d506b7c0ab158324c337c41f1cc7548f0bc0e130bbf0ae9452", [:rebar3], [], "hexpm", "73bc09fa59b4a0284efb4624335583c528e07ec9ae76aca96ea0673850aec57a"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},
"uuid": {:hex, :uuid, "1.1.8", "e22fc04499de0de3ed1116b770c7737779f226ceefa0badb3592e64d5cfb4eb9", [:mix], [], "hexpm", "c790593b4c3b601f5dc2378baae7efaf5b3d73c4c6456ba85759905be792f2ac"},
"yamerl": {:hex, :yamerl, "0.8.1", "07da13ffa1d8e13948943789665c62ccd679dfa7b324a4a2ed3149df17f453a4", [:rebar3], [], "hexpm", "96cb30f9d64344fed0ef8a92e9f16f207de6c04dfff4f366752ca79f5bceb23f"},
"yaml_elixir": {:hex, :yaml_elixir, "2.8.0", "c7ff0034daf57279c2ce902788ce6fdb2445532eb4317e8df4b044209fae6832", [:mix], [{:yamerl, "~> 0.8", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "4b674bd881e373d1ac6a790c64b2ecb69d1fd612c2af3b22de1619c15473830b"},
Expand Down
122 changes: 118 additions & 4 deletions test/integration/browser_type/connect_cdp_test.exs
Original file line number Diff line number Diff line change
@@ -1,7 +1,121 @@
defmodule Playwright.BrowserType.ConnectCDPTest do
use Playwright.TestCase, async: true
@remote_debug_port "9222"

# test_connect_to_an_existing_cdp_session
# test_connect_to_an_existing_cdp_session_twice
# test_conect_over_a_ws_endpoint
use Playwright.TestCase, async: true, args: ["--remote-debugging-port=#{@remote_debug_port}"]

alias Playwright.Browser
alias Playwright.BrowserContext
alias Playwright.BrowserType
alias Playwright.Page

@tag :skip
test "can only connect to CDP session if using chromium client" do
# For some reason this still launches a chromium browser... I could not
# figure out why.

browser = Playwright.launch(:firefox)
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coreyti any thoughts about how to better test this error case? I couldn't quite figure out why but it appears that no matter whether I call Playwright.launch() with (ie :webkit or :firefox) I still get a chromium %Playwright.Browser{} back.

Perhaps this is just something to note and move on from since technically this library only supports chromium at the moment.


expected_message = "Attempted to use Playwright.BrowserType.connect_over_cdp/3 with incompatible browser client"

assert_raise RuntimeError, expected_message, fn ->
BrowserType.connect_over_cdp(
browser,
"http://localhost:#{@remote_debug_port}/"
)
end
end

test "can connect to an existing CDP session via http endpoint", %{browser: browser} do
cdp_browser =
BrowserType.connect_over_cdp(
browser,
"http://localhost:#{@remote_debug_port}/"
)

assert length(Browser.contexts(cdp_browser)) == 1

Browser.close(cdp_browser)
end

@tag exclude: [:page]
test "can connect to an existing CDP session twice", %{browser: browser, assets: assets} do
cdp_browser1 =
BrowserType.connect_over_cdp(
browser,
"http://localhost:#{@remote_debug_port}/"
)

cdp_browser2 =
BrowserType.connect_over_cdp(
browser,
"http://localhost:#{@remote_debug_port}/"
)

cdp_browser3 =
BrowserType.connect_over_cdp(
browser,
"http://localhost:#{@remote_debug_port}/"
)

assert length(contexts(cdp_browser1)) == 1

page1 =
contexts(cdp_browser1)
|> List.first()
|> BrowserContext.new_page()

Page.goto(page1, assets.empty)

assert length(contexts(cdp_browser2)) == 1

page2 =
contexts(cdp_browser2)
|> List.first()
|> BrowserContext.new_page()

Page.goto(page2, assets.empty)

assert contexts(cdp_browser1)
|> List.first()
|> pages()
|> length() == 2

assert contexts(cdp_browser2)
|> List.first()
|> pages()
|> length() == 2

# NOTE: no `Page` was explicitly created off of `cdp_browser3`, but its context includes
# those created by the other sessions. See docs in `BrowserType.connect_over_cdp/3`
assert contexts(cdp_browser3)
|> List.first()
|> pages()
|> length() == 2

Browser.close(cdp_browser1)
Browser.close(cdp_browser2)
Browser.close(cdp_browser3)
end

test "can connect over a websocket endpoint", %{browser: browser} do
ws_endpoint = ws_endpoint_for_url("http://localhost:#{@remote_debug_port}/json/version")

cdp_browser1 = BrowserType.connect_over_cdp(browser, ws_endpoint)
assert contexts(cdp_browser1) |> length() == 1
Browser.close(cdp_browser1)

cdp_browser2 = BrowserType.connect_over_cdp(browser, ws_endpoint)
assert contexts(cdp_browser2) |> length() == 1
Browser.close(cdp_browser2)
end

defp ws_endpoint_for_url(url) do
{:ok, %{body: body}} = HTTPoison.get(url)

Jason.decode!(body)
|> Map.get("webSocketDebuggerUrl")
end

defp contexts(browser), do: Browser.contexts(browser)
defp pages(context), do: BrowserContext.pages(context)
end