From d4263949529ce8bd8da3f4486477812057b3721d Mon Sep 17 00:00:00 2001 From: Corey Innis Date: Wed, 9 Oct 2024 19:15:29 -0700 Subject: [PATCH] API: impl some `BrowserContext` and fix/clean docs & tests --- lib/playwright/api_request_context.ex | 4 +- lib/playwright/browser_context.ex | 82 +++++++++++++++++++++++-- test/api/api_request_context_test.exs | 2 - test/api/browser_context_test.exs | 88 +++++++++++++++++++++++++-- test/api/browser_test.exs | 63 +++++++++++-------- 5 files changed, 201 insertions(+), 38 deletions(-) diff --git a/lib/playwright/api_request_context.ex b/lib/playwright/api_request_context.ex index 4e8bd67..0205ade 100644 --- a/lib/playwright/api_request_context.ex +++ b/lib/playwright/api_request_context.ex @@ -512,7 +512,7 @@ defmodule Playwright.APIRequestContext do | name | | description | | -------- | ---------- | --------------------------------- | - | `path` | (optional) | **NOT IMPLEMENTED** The file path to save the storage state to. If path is a relative path, then it is resolved relative to current working directory. If no path is provided, storage state is still returned, but won't be saved to the disk. | + | `path` | (optional) | The file path to save the storage state. If path is a relative path, then it is resolved relative to current working directory. If no path is provided, storage state is still returned, but won't be saved to the disk. | ## Returns @@ -521,7 +521,7 @@ defmodule Playwright.APIRequestContext do """ @pipe {:storage_state, [:context]} @pipe {:storage_state, [:context, :options]} - @spec storage_state(t(), opts_storage()) :: storage_state() + @spec storage_state(t(), opts_storage()) :: storage_state() | {:error, Error.t()} def storage_state(context, options \\ %{}) do {path, options} = Map.pop(options, :path) diff --git a/lib/playwright/browser_context.ex b/lib/playwright/browser_context.ex index 5bc3245..382d48c 100644 --- a/lib/playwright/browser_context.ex +++ b/lib/playwright/browser_context.ex @@ -203,6 +203,12 @@ defmodule Playwright.BrowserContext do optional(:accuracy) => number() } + @typedoc "Local storage settings." + @type local_storage :: %{ + required(:name) => String.t(), + required(:value) => String.t() + } + @typedoc "A map/struct providing generic call options" @type options :: map() @@ -228,6 +234,11 @@ defmodule Playwright.BrowserContext do optional(:times) => number() } + @typedoc "Options for `storage_state/2`." + @type opts_storage :: %{ + optional(:path) => String.t() + } + @typedoc "A permission available for `grant_permissions/3`." @type permission :: String.t() | atom() @@ -243,6 +254,17 @@ defmodule Playwright.BrowserContext do | function() | String.t() + @typedoc "Storage state settings." + @type storage_state :: %{ + required(:cookies) => [cookie()], + required(:origins) => [ + %{ + required(:origin) => String.t(), + required(:local_storage) => [local_storage()] + } + ] + } + @typedoc "A string URL" @type url :: String.t() @@ -1087,17 +1109,69 @@ defmodule Playwright.BrowserContext do Channel.post({context, :set_geolocation}) end + @doc """ + Configures whether the browser context should emulate being offline. + + ## Usage + + BrowserContext.set_offline(context, true) + BrowserContext.set_offline(context, false) + + ## Arguments + + | name | | description | + | --------- | ---------- | ------------------------------- | + | `context` | | The "subject" `BrowserContext`. | + | `offline` | | Whether to emulate the network being offline. | + + ## Returns + + - `BrowserContext.t()` + - `{:error, Error. t()}` + """ + @pipe {:set_offline, [:context, :offline]} @spec set_offline(t(), boolean()) :: t() | {:error, Error.t()} def set_offline(%BrowserContext{} = context, offline) do Channel.post({context, :set_offline}, %{offline: offline}) end - # --- + @doc """ + Returns storage state for this browser context. - # @spec storage_state(t(), String.t()) :: storage_state() - # def storage_state(context, path \\ nil) + The storage state contains current cookies and a local storage snapshot. - # --- + ## Arguments + + | name | | description | + | ---------------- | ---------- | --------------------------------- | + | `context` | | The "subject" `APIRequestContext` | + | `options` | (optional) | Options (see below) | + + ## Options + + | name | | description | + | -------- | ---------- | --------------------------------- | + | `path` | (optional) | The file path to save the storage state. If path is a relative path, then it is resolved relative to current working directory. If no path is provided, storage state is still returned, but won't be saved to the disk. | + + ## Returns + + - `storage_state()` + - `{:error, Error.t()}` + """ + @spec storage_state(t(), opts_storage()) :: storage_state() | {:error, Error.t()} + def storage_state(%BrowserContext{} = context, options \\ %{}) do + {path, options} = Map.pop(options, :path) + + case Channel.post({context, :storage_state}, options) do + {:error, _} = error -> + error + + result -> + result = Map.new(result) + path && File.write!(path, Jason.encode!(result)) + result + end + end @spec unroute(t(), binary(), function() | nil) :: t() | {:error, Error.t()} def unroute(%BrowserContext{session: session} = context, pattern, callback \\ nil) do diff --git a/test/api/api_request_context_test.exs b/test/api/api_request_context_test.exs index e4f06f5..a6d7f8a 100644 --- a/test/api/api_request_context_test.exs +++ b/test/api/api_request_context_test.exs @@ -303,8 +303,6 @@ defmodule Playwright.APIRequestContextTest do describe "APIRequestContext.storage_state/2" do test "(WIP) on success, ...", %{session: session} do - # python: test_storage_state_should_round_trip_through_file - # --- slug = DateTime.utc_now() |> DateTime.to_unix() path = "storage-state-#{slug}.json" diff --git a/test/api/browser_context_test.exs b/test/api/browser_context_test.exs index 6f49b5f..551d0ae 100644 --- a/test/api/browser_context_test.exs +++ b/test/api/browser_context_test.exs @@ -937,13 +937,14 @@ defmodule Playwright.BrowserContextTest do end # skip: See documentation comment for `BrowserContext.set_geolocation/2` - @tag :skip describe "BrowserContext.set_geolocation/2" do + @tag :skip test "on success, returns the 'subject' `BrowserContext`", %{page: page} do context = Page.context(page) assert %BrowserContext{} = BrowserContext.set_geolocation(context, nil) end + @tag :skip test "on failure, returns `{:error, error}`", %{page: page} do context = Page.context(page) context = %{context | guid: "bogus"} @@ -952,7 +953,8 @@ defmodule Playwright.BrowserContextTest do BrowserContext.set_geolocation(context, %{}) end - test "...", %{assets: assets, page: page} do + @tag :skip + test "mimics geolocation settings in the browser context", %{assets: assets, page: page} do context = Page.context(page) BrowserContext.grant_permissions(context, ["geolocation"]) @@ -966,18 +968,20 @@ defmodule Playwright.BrowserContextTest do resolve({latitude: position.coords.latitude, longitude: position.coords.longitude}); })) """) - |> IO.inspect(label: "geolocation") assert %{latitude: 10, longitude: 10} = geolocation end end + @tag :skip describe "BrowserContext.set_geolocation!/2" do + @tag :skip test "on success, returns the 'subject' `BrowserContext`", %{page: page} do context = Page.context(page) assert %BrowserContext{} = BrowserContext.set_geolocation!(context, %{latitude: 0, longitude: 0}) end + @tag :skip test "on failure, raises `RuntimeError`", %{page: page} do assert_raise RuntimeError, "Target page, context or browser has been closed", fn -> context = Page.context(page) @@ -988,12 +992,20 @@ defmodule Playwright.BrowserContextTest do end describe "BrowserContext.set_offline/2" do - test "returns 'subject'", %{page: page} do + test "on success, returns the 'subject' `BrowserContext`", %{page: page} do context = Page.context(page) assert %BrowserContext{} = BrowserContext.set_offline(context, false) assert %BrowserContext{} = BrowserContext.set_offline(context, true) end + test "on failure, returns `{:error, error}`", %{page: page} do + context = Page.context(page) + context = %{context | guid: "bogus"} + + assert {:error, %Error{message: "Target page, context or browser has been closed"}} = + BrowserContext.set_offline(context, true) + end + @tag without: [:page] test "using initial option", %{assets: assets, browser: browser} do context = Browser.new_context(browser, %{offline: true}) @@ -1022,9 +1034,77 @@ defmodule Playwright.BrowserContextTest do end describe "BrowserContext.set_offline!/2" do + test "on success, returns the 'subject' `BrowserContext`", %{page: page} do + context = Page.context(page) + assert %BrowserContext{} = BrowserContext.set_offline!(context, false) + assert %BrowserContext{} = BrowserContext.set_offline!(context, true) + end + + test "on failure, raises `RuntimeError`", %{page: page} do + assert_raise RuntimeError, "Target page, context or browser has been closed", fn -> + context = Page.context(page) + context = %{context | guid: "bogus"} + BrowserContext.set_offline!(context, true) + end + end end describe "BrowserContext.storage_state/2" do + test "on success, returns storage state JSON", %{browser: browser} do + storage = %{ + cookies: [ + %{ + name: "cookie name", + value: "cookie value", + domain: "example.com", + path: "/", + expires: -1, + httpOnly: false, + secure: false, + sameSite: "Lax" + } + ], + origins: [] + } + + context = Browser.new_context(browser, %{storage_state: storage}) + assert ^storage = BrowserContext.storage_state(context) + end + + test "on failure, returns `{:error, error}`", %{browser: browser} do + context = Browser.new_context(browser, %{storage_state: %{}}) + context = %{context | guid: "bogus"} + assert {:error, %Error{}} = BrowserContext.storage_state(context) + end + + test "given the `:path` option, writes the state to disk", %{browser: browser} do + slug = DateTime.utc_now() |> DateTime.to_unix() + path = "storage-state-#{slug}.json" + + storage = %{ + cookies: [ + %{ + name: "cookie name", + value: "cookie value", + domain: "example.com", + path: "/", + expires: -1, + httpOnly: false, + secure: false, + sameSite: "Lax" + } + ], + origins: [] + } + + context = Browser.new_context(browser, %{storage_state: storage}) + + assert ^storage = BrowserContext.storage_state(context, %{path: path}) + assert(File.exists?(path)) + assert(Jason.decode!(File.read!(path))) + + File.rm!(path) + end end describe "BrowserContext.unroute/2" do diff --git a/test/api/browser_test.exs b/test/api/browser_test.exs index bf75c64..6ddf5f2 100644 --- a/test/api/browser_test.exs +++ b/test/api/browser_test.exs @@ -223,43 +223,49 @@ defmodule Playwright.BrowserTest do # skip: the `:disconnected` event is meant to be emitted from the client-side, # upon `Browser.close/1`; we don't yet have a good mechanism for that. - @tag :skip - describe "Browser.on/3" do - # test "on success, returns the 'subject' `Browser`", %{transport: transport} do - # {_session, browser} = setup_browser(transport) - # assert %Browser{} = Browser.on(browser, :disconnected, fn -> nil end) - # end - - # test "on `:disconnected`, ...", %{transport: transport} do - # {_session, browser} = setup_browser(transport) - - # Browser.on(browser, :disconnected, fn data -> - # IO.inspect(data, label: "on(:disconnected) data ->") - # end) - - # Browser.close(browser) - # end - end + # describe "Browser.on/3" do + # @tag :skip + # test "on success, returns the 'subject' `Browser`", %{transport: transport} do + # {_session, browser} = setup_browser(transport) + # assert %Browser{} = Browser.on(browser, :disconnected, fn -> nil end) + # end + + # @tag :skip + # test "on `:disconnected`, ...", %{transport: transport} do + # {_session, browser} = setup_browser(transport) + + # Browser.on(browser, :disconnected, fn data -> + # IO.inspect(data, label: "on(:disconnected) data ->") + # end) + + # Browser.close(browser) + # end + # end describe "Browser.start_tracing/3" do - test "on success, returns the 'subject' `Browser`", %{browser: browser} do + test "on success, returns the 'subject' `Browser`", %{transport: transport} do + {_session, browser} = setup_browser(transport) assert %Browser{} = Browser.start_tracing(browser) Browser.stop_tracing(browser) end - test "on failure, returns `{:error, error}`", %{browser: browser} do + test "on failure, returns `{:error, error}`", %{transport: transport} do + {_session, browser} = setup_browser(transport) browser = %{browser | guid: "bogus"} assert {:error, %Error{type: "TargetClosedError"}} = Browser.start_tracing(browser) end end describe "Browser.start_tracing!/3" do - test "on success, returns the 'subject' `Browser`", %{browser: browser} do + test "on success, returns the 'subject' `Browser`", %{transport: transport} do + {_session, browser} = setup_browser(transport) assert %Browser{} = Browser.start_tracing!(browser) Browser.stop_tracing(browser) end - test "on failure, raises", %{browser: browser} do + test "on failure, raises", %{transport: transport} do + {_session, browser} = setup_browser(transport) + assert_raise RuntimeError, fn -> browser = %{browser | guid: "bogus"} Browser.start_tracing!(browser) @@ -268,12 +274,14 @@ defmodule Playwright.BrowserTest do end describe "Browser.stop_tracing/1" do - test "on success, returns the resultant `Artifact`", %{browser: browser} do + test "on success, returns the resultant `Artifact`", %{transport: transport} do + {_session, browser} = setup_browser(transport) Browser.start_tracing(browser) assert %Playwright.Artifact{} = Browser.stop_tracing(browser) end - test "on failure, returns `{:error, error}`", %{browser: browser} do + test "on failure, returns `{:error, error}`", %{transport: transport} do + {_session, browser} = setup_browser(transport) Browser.start_tracing(browser) browser = %{browser | guid: "bogus"} assert {:error, %Error{type: "TargetClosedError"}} = Browser.start_tracing(browser) @@ -281,14 +289,17 @@ defmodule Playwright.BrowserTest do end describe "Browser.stop_tracing!/3" do - test "on success, returns the resultant `Artifact`", %{browser: browser} do + test "on success, returns the resultant `Artifact`", %{transport: transport} do + {_session, browser} = setup_browser(transport) Browser.start_tracing(browser) assert %Playwright.Artifact{} = Browser.stop_tracing!(browser) end - test "on failure, raises", %{browser: browser} do + test "on failure, raises", %{transport: transport} do + {_session, browser} = setup_browser(transport) + Browser.start_tracing(browser) + assert_raise RuntimeError, fn -> - Browser.start_tracing(browser) browser = %{browser | guid: "bogus"} Browser.stop_tracing!(browser) end