Skip to content

Commit

Permalink
API: implement APIRequest
Browse files Browse the repository at this point in the history
`Playwright.launch/2` now returns a tuple that includes `session`. This, in turn, may be passed to the newly implemented `Playwright.request/1`, which returns an instance of `Playwright.APIRequest`.

`APIRequest` implements `new_context/2` and `new_context!/2`, which create instances of `Playwright.APIRequestContext`.
  • Loading branch information
coreyti committed Oct 1, 2024
1 parent 311608d commit 95485d7
Show file tree
Hide file tree
Showing 11 changed files with 626 additions and 18 deletions.
18 changes: 13 additions & 5 deletions lib/playwright.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ defmodule Playwright do
alias Playwright.API.{Browser, Page, Response}
{:ok, browser} = Playwright.launch(:chromium)
{:ok, page} = Browser.new_page(browser)
{:ok, session, browser} = Playwright.launch(:chromium)
{:ok, page} = Browser.new_page(browser)
{:ok, response} = Page.goto(browser, "http://example.com")
assert Response.ok(response)
Expand All @@ -19,6 +19,7 @@ defmodule Playwright do

use Playwright.SDK.ChannelOwner
alias Playwright
alias Playwright.APIRequest
alias Playwright.SDK.Channel
alias Playwright.SDK.Config

Expand All @@ -35,6 +36,9 @@ defmodule Playwright do
# @typedoc "Options for `launch`."
# @type launch_options :: Playwright.SDK.Config.launch_options()

# API
# ---------------------------------------------------------------------------

@doc """
Initiates an instance of `Playwright.Browser` use the WebSocket transport.
Expand All @@ -57,7 +61,7 @@ defmodule Playwright do
options = Map.merge(Config.connect_options(), options)
{:ok, session} = new_session(Playwright.SDK.Transport.WebSocket, options)
{:ok, browser} = new_browser(session, client, options)
{:ok, browser}
{:ok, session, browser}
end

@doc """
Expand All @@ -69,7 +73,7 @@ defmodule Playwright do
## Arguments
| key/name | typ | | description |
| key/name | type | | description |
| ----------| ----- | ----------- | ----------- |
| `client` | param | `client()` | The type of client (browser) to launch. |
| `options` | param | `options()` | `Playwright.SDK.Config.launch_options()` |
Expand All @@ -79,7 +83,11 @@ defmodule Playwright do
options = Map.merge(Config.launch_options(), options)
{:ok, session} = new_session(Playwright.SDK.Transport.Driver, options)
{:ok, browser} = new_browser(session, client, options)
{:ok, browser}
{:ok, session, browser}
end

def request(session) do
APIRequest.new(session)
end

# private
Expand Down
247 changes: 243 additions & 4 deletions lib/playwright/api_request.ex
Original file line number Diff line number Diff line change
@@ -1,7 +1,246 @@
defmodule Playwright.APIRequest do
@moduledoc false
use Playwright.SDK.ChannelOwner
@moduledoc """
`Playwright.APIRequest` exposes an API to be used for the web API testing.
# @spec new_context(t(), options()) :: APIRequestContext.t()
# def new_context(api_request, options \\ %{})
The module is used for creating `Playwright.APIRequestContext` instances,
which in turn may be used for sending web requests. An instance of this
modeule may be obtained via `Playwright.request/1`.
For more usage details, see `Playwright.APIRequestContext`.
"""

use Playwright.SDK.Pipeline
alias Playwright.API.Error
alias Playwright.SDK.Channel

@enforce_keys [:guid, :session]
defstruct [:guid, :session]

@typedoc """
`#{String.replace_prefix(inspect(__MODULE__), "Elixir.", "")}`
"""
@type t() :: %__MODULE__{
guid: binary(),
session: pid()
}

@typedoc "Options for calls to `new_context/1`"
@type options :: map()
# %{
# base_url: String.t(),
# extra_http_headers: map(),
# http_credentials: http_credentials(),
# ignore_https_errors: boolean(),
# proxy: proxy_settings(),
# user_agent: String.t(),
# timeout: float(),
# storage_state: storage_state() | String.t() | Path.t(),
# client_certificates: [client_certificate()]
# }

@doc """
Returns a new `Playwright.APIRequest`.
## Returns
- `Playwright.APIRequest.t()`
## Parameters
- session ...
"""
@spec new(pid()) :: t()
def new(session) do
%__MODULE__{
guid: "Playwright",
session: session
}
end

@doc """
Creates a new instance of `Playwright.APIRequestContext`.
## Returns
- `Playwright.APIRequestContext.t()`
- `{:error, Playwright.API.Error.t()}`
## Parameters
- request ...
- options
## Options
| name | type |
| ---------------------- | ------------------------ |
| `:base_url` | `String.t()` |
| `:extra_http_headers` | `map()` |
| `:http_credentials` | `[http_credential()]` |
| `:ignore_https_errors` | `boolean()` |
| `:proxy` | `proxy()` |
| `:user_agent` | `String.t()` |
| `:timeout` | `float()` |
| `:storage_state` | `storage_state()` |
| `:client_certififates` | `[client_certificate()]` |
---
### Option: `:base_url`
Functions such as `Playwright.APIRequestContext.get/3` take the base URL into
consideration by using the [`URL()`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL)
constructor for building the corresponding require URL.
Examples:
- With `base_url: http://localhost:3000`, sending a request to `/bar.html`
results in `http://localhost:3000/bar.html`.
- With `base_url: http://localhost:3000/foo/`, sending a request to `/bar.html`
results in `http://localhost:3000/foo/bar.html`.
- With `base_url: http://localhost:3000/foo` (without the trailing slash),
navigating to `./bar.html` results in `http://localhost:3000/bar.html`.
---
### Option: `:extra_http_headers`
A `map` containing additional HTTP headers to be sent with every request.
### Option: `:http_credentials`
Credentials for [HTTP authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication).
If no `:origin` is specified, the `:username` and `:password` are sent to any
servers upon unauthorized responses.
| name | type |
| ---------------------- | -------------------------------------- |
| `:username` | `String.t()` |
| `:password` | `String.t()` |
| `:origin` | `String.t()` (optional) |
| `:send` | `"always", "unauthorized"` (optional) |
---
### Option: `:ignore_https_errors`
Whether to ignore HTTPS errors when sending network requests. Defaults to
`false`.
---
### Option: `:proxy`
Network proxy settings.
| name | type |
| ----------- | ----------------------- |
| `:server` | `String.t()` |
| `:bypass` | `String.t()` (optional) |
| `:username` | `String.t()` (optional) |
| `:password` | `String.t()` (optional) |
---
### Option: `:user_agent`
Specific user agent to use in this context.
---
### Option: `:timeout`
Maximum time in milliseconds to wait for the response. Defaults to `30_000`
(30 seconds). Pass `0` to disable the timeout.
---
### Option: `:storage_state`
Populates context with given storage state.
This option can be used to initialize context with logged-in information
obtained via, either, a path to the file with saved storage, or the value
returned by one of `BrowserContext.storage_state/2` or
`APIRequestContext.storage_state/2`.
One of:
- `String.t()`
- `Path.t()`
- Storage state
Where storage state has the following shape:
| name | type |
| ----------- | --------- |
| `:cookies` | **TODO** |
| `:origins` | **TODO** |
---
### Option: `:client_certificates`
TLS client authentication allows the server to request a client certificate
and verify it.
**Details**
An array of client certificates to be used. Each certificate object must have
both `:cert_path` and `:key_path` or a single `:pfx_path` to load the client
certificate.
Optionally, the `:passphrase` property should be provided if the certficiate
is encrypted. The `:origin` property should be provided with an exact match to
the request origin for which the certificate is valid.
**NOTES:**
- Using client certificates in combination with proxy servers is not supported.
- When using WebKit on macOS, accessing `localhost` will not pick up client
certificates. As a work-around: replace `localhost` with `local.playwright`.
Where each client certificate has the following shape:
| name | type |
| ------------- | --------------------------------- |
| `:origin` | `String.t()` |
| `:cert_path` | `Path.t(), String.t()` (optional) |
| `:key_path` | `Path.t(), String.t()` (optional) |
| `:pfx_path` | `Path.t(), String.t()` (optional) |
| `:passphrase` | `String.t()` (optional) |
"""
@pipe {:new_context, [:request]}
@pipe {:new_context, [:request, :options]}
@spec new_context(t(), options()) :: t() | {:error, Error.t()}
def new_context(request, options \\ %{}) do
Channel.post({request, :new_request}, options)
end

# Storage State shape:
# {
# cookies: [
# {
# name: str,
# value: str,
# domain: str,
# path: str,
# expires: float,
# httpOnly: bool,
# secure: bool,
# sameSite: Union["Lax", "None", "Strict"]
# }
# ],
# origins: [
# {
# origin: str,
# localStorage: [
# {
# name: str,
# value: str
# }
# ]
# }
# ]
# }
end
2 changes: 1 addition & 1 deletion lib/playwright/api_request_context.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ defmodule Playwright.APIRequestContext do
"""

use Playwright.SDK.ChannelOwner
alias Playwright.SDK.Channel
alias Playwright.APIRequestContext
alias Playwright.SDK.Channel

# types
# ----------------------------------------------------------------------------
Expand Down
2 changes: 1 addition & 1 deletion lib/playwright/browser.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ defmodule Playwright.Browser do
alias Playwright.{Browser, Page}
{:ok, browser} = Playwright.launch(:chromium)
{:ok, session, browser} = Playwright.launch(:chromium)
page = Browser.new_page(browser)
Page.goto(page, "https://example.com")
Expand Down
2 changes: 1 addition & 1 deletion lib/playwright/browser_type.ex
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ defmodule Playwright.BrowserType do
# Use `:ignore_default_args` option to filter out `--mute-audio` from
# default arguments:
{:ok, browser} =
{:ok, session, browser} =
Playwright.launch(:chromium, %{ignore_default_args = ["--mute-audio"]})
## Returns
Expand Down
2 changes: 1 addition & 1 deletion lib/playwright/locator.ex
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ defmodule Playwright.Locator do
> list of elements changes dynamically, `Playwright.Locator.all/1` will
> produce unpredictable and flaky results. When the list of elements is
> stable, but loaded dynamically, wait for the full list to finish loading
> before calling `Playwright.Locator.all/1``.
> before calling `Playwright.Locator.all/1`.
## Returns
Expand Down
2 changes: 1 addition & 1 deletion lib/playwright/sdk/channel_owner.ex
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ defmodule Playwright.SDK.ChannelOwner do
defstruct @properties ++ [:session, :guid, :initializer, :listeners, :parent, :type]

@typedoc """
%#{String.replace_prefix(inspect(__MODULE__), "Elixir.", "")}{}
#{String.replace_prefix(inspect(__MODULE__), "Elixir.", "")}
"""
@type t() :: %__MODULE__{}

Expand Down
4 changes: 2 additions & 2 deletions lib/playwright_test/case.ex
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@ defmodule PlaywrightTest.Case do
Application.put_env(:playwright, LaunchOptions, launch_options)
{:ok, _} = Application.ensure_all_started(:playwright)

{_session, browser} = setup_browser(runner_options.transport)
[browser: browser, transport: runner_options.transport]
{session, browser} = setup_browser(runner_options.transport)
[browser: browser, session: session, transport: runner_options.transport]
end

setup(context) do
Expand Down
2 changes: 1 addition & 1 deletion man/basics/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ $ mix playwright.install
Once installed, you can `alias` and/or `import` Playwright in your Elixir module, and launch any of the 3 browsers (`chromium`, `firefox` and `webkit`).

```elixir
{:ok, browser} = Playwright.launch(:chromium)
{:ok, session, browser} = Playwright.launch(:chromium)
page =
browser |> Playwright.Browser.new_page()

Expand Down
Loading

0 comments on commit 95485d7

Please sign in to comment.