Skip to content

Commit

Permalink
API: some APIRequestContext & APIResponse impl
Browse files Browse the repository at this point in the history
Including:

- `APIRequestContext.fetch/3`
- `APIResponse.body/1`
- `APIResponse.dispose/1`
- `APIResponse.header/2`
- `APIResponse.json/1`
- `APIResponse.text/1`

Also, removes:

- `APIRequestContext.body/1`. The function has moved to `APIResponse`, where it belongs.
- `APIRequestContext.post/3` for the time being; it needs to be TDD'd and the implementation should simply call `APIRequestContext.fetch/3`.

Incidentally, the placeholder for `Tracing` has been moved to `Playwright.Tracing`.
  • Loading branch information
coreyti committed Oct 2, 2024
1 parent beb1e3e commit c7f5442
Show file tree
Hide file tree
Showing 10 changed files with 383 additions and 348 deletions.
19 changes: 15 additions & 4 deletions lib/playwright/api/error.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,32 @@ defmodule Playwright.API.Error do
# that is in response to a `Message` previously sent.

@enforce_keys [:type, :message]
defstruct [:type, :message]
defstruct [:type, :message, :wrapped]

@type t() :: %__MODULE__{
type: String.t(),
message: String.t()
message: String.t(),
wrapped: any()
}

def new(%{error: %{name: name, message: message, wrapped: original} = _error}, _catalog) do
%__MODULE__{
type: name,
message: String.split(message, "\n") |> List.first(),
wrapped: original
}
end

def new(%{error: %{name: name, message: message} = _error}, _catalog) do
%__MODULE__{
type: name,
message: String.split(message, "\n") |> List.first()
}
end
def new(%{error: %{message: message} = error}, _catalog) do
dbg(error)

def new(%{error: %{message: message} = _error}, _catalog) do
# dbg(error)

%__MODULE__{
type: "UnknownError",
message: String.split(message, "\n") |> List.first()
Expand Down
5 changes: 3 additions & 2 deletions lib/playwright/api_request.ex
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ defmodule Playwright.APIRequest do

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

@enforce_keys [:guid, :session]
Expand Down Expand Up @@ -54,7 +55,7 @@ defmodule Playwright.APIRequest do
@type http_headers :: %{required(String.t()) => String.t()}

@typedoc "HTTP authetication credentials."
@type http_credentials() :: %{
@type http_credentials :: %{
required(:username) => String.t(),
required(:password) => String.t(),
optional(:origin) => String.t(),
Expand Down Expand Up @@ -276,7 +277,7 @@ defmodule Playwright.APIRequest do
@pipe {:new_context, [:request]}
@pipe {:new_context, [:request, :options]}
@spec new_context(t(), options()) :: t() | {:error, Error.t()}
def new_context(request, options \\ %{}) do
def new_context(%APIRequest{} = request, options \\ %{}) do
Channel.post({request, :new_request}, options)
end
end
170 changes: 142 additions & 28 deletions lib/playwright/api_request_context.ex
Original file line number Diff line number Diff line change
@@ -1,33 +1,95 @@
defmodule Playwright.APIRequestContext do
@moduledoc """
This API is used for the Web API testing. You can use it to trigger API
endpoints, configure micro-services, prepare environment or the server to your
e2e test.
Use this at caution as has not been tested.
`Playwright.APIRequestContext` is useful for testing of web APIs.
The module may be used to trigger API endpoints, configure micro-services,
prepare environment or services in end-to-end (e2e) tests.
Each `Playwright.BrowserContext` (browser context) has an associated
`Playwright.APIRequestContext` (API context) instance that shares cookie
storage with the browser context and can be accessed via
`Playwright.BrowserContext.request/1` or `Playwright.Page.request/1`.
It is also possible to create a new `Playwright.APIRequestContext` instance
via `Playwright.APIRequest.newContext/2`.
## Cookie management
An `Playwright.APIRequestContext` returned by `Playwright.BrowserContext.request/1`
or `Playwright.Page.request/1` shares cookie storage with the corresponding
`Playwright.BrowserContext`. Each API request will have a cookie HTTP header
populated with the values from the browser context. If the API response
contains a `Set-Cookie` header, it will automatically update `Playwright.BrowserContext`
cookies and requests made from the page will pick up the changes. This means
that if you authenticate using this API, your e2e test will be authenticated.
If you want API requests to not interfere with the browser cookies, create a
new `Playwright.APIRequestContext` via `Playwright.APIRequest.new_context/1`.
Such API contexts will have isolated cookie storage.
## Shared options
The following options are available for all forms of request:
| name | | description |
| ---------------------- | ---------- | --------------------------------- |
| `:data` | (optional) | Sets post data of the request. If the `:data` parameter is a `serializable()`, it will be serialized as a JSON string and the `content-type` HTTP header will be set to `application/json`, if not explicitly set. Otherwise the `content-type` header will be set to `application/octet-stream` if not explicitly set. |
| `:fail_on_status_code` | (optional) | Whether to raise an error for response codes other than `2xx` and `3xx`. By default, a `Playwright.APIResponse` is returned for all status codes. |
| `:form` | (optional) | Provides content that will be serialized as an HTML form using `application/x-www-form-urlencoded` encoding and sent as the request body. If this parameter is specified, the `content-type` HTTP header will be set to `application/x-www-form-urlencoded` unless explicitly provided. |
| `:headers` | (optional) | Set HTTP headers. These headers will apply to the fetched request as well as any redirects initiated by it. |
| `:ignore_https_errors` | (optional) | Whether to ignore HTTPS errors when sending network requests. Defaults to `false`. |
| `:max_redirects` | (optional) | Maximum number of request redirects that will be followed automatically. An error will be thrown if the number is exceeded. Defaults to `20`. Pass `0` to not follow redirects. |
| `:max_retries` | (optional) | Maximum number of times network errors should be retried. Currently only `ECONNRESET` error is retried. Does not retry based on HTTP response codes. An error will be thrown if the limit is exceeded. Defaults to `0` - no retries. |
| `:method` | (optional) | If set changes the fetch method (e.g. [`PUT`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/PUT) or [`POST`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST)). If not specified, [`GET`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/GET) method is used. |
| `:multipart` | (optional) | Provides content that will be serialized as an HTML form using `multipart/form-data` encoding and sent as the request body. If this parameter is specified, the `content-type` header will be set to `multipart/form-data` unless explicitly provided. File values can be passed either as a `Multipart.t()` or as file-like object containing file name, mime-type and content. |
| `:params` | (optional) | Query parameters to be sent with the URL. |
| `:timeout` | (optional) | Request timeout in milliseconds. Defaults to `30_000` (30 seconds). Pass `0` to disable timeout. |
When constructing a `:multipart` parameter as a `form()`, the following fields
should be defined:application
- `:name` - File name (`String.t()`)
- `:mime_type` - File type (`String.t()`)
- `:buffer` - File content (`binary()`)
"""

use Playwright.SDK.ChannelOwner
alias Playwright.APIRequestContext
alias Playwright.APIResponse
alias Playwright.Request
alias Playwright.API.Error
alias Playwright.SDK.Channel

# types
# ----------------------------------------------------------------------------

@type fetch_options() :: %{
optional(:params) => any(),
optional(:method) => binary(),
optional(:headers) => any(),
optional(:post_data) => any(),
optional(:json_data) => any(),
optional(:form_data) => any(),
optional(:multipart_data) => any(),
optional(:timeout) => non_neg_integer(),
@typedoc "Options for the various request types."
@type options :: %{
# TODO: support the equivalent of TypeScript's `Buffer`
optional(:data) => serializable() | String.t(),
optional(:fail_on_status_code) => boolean(),
optional(:ignore_HTTPS_errors) => boolean()
optional(:form) => form(),
optional(:headers) => http_headers(),
optional(:ignore_https_errors) => boolean(),
optional(:max_redirects) => number(),
optional(:max_retries) => number(),
optional(:method) => String.t(),
# TODO: support the equivalent of TypeScript's `ReadStream`
optional(:multipart) => Multipart.t() | form(),
optional(:params) => form() | String.t(),
optional(:timeout) => float()
}

@typedoc "A data structure for form content."
@type form :: %{
required(String.t()) => binary() | boolean() | float() | String.t()
}

@typedoc "A `map` containing additional HTTP headers to be sent with every request."
@type http_headers :: %{required(String.t()) => String.t()}

@typedoc "Data serializable as JSON."
@type serializable :: list() | map()

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

Expand All @@ -37,8 +99,66 @@ defmodule Playwright.APIRequestContext do
# @spec dispose(t()) :: t()
# def dispose(api_request_context)

# @spec fetch(t(), binary() | Request.t(), options()) :: APIResponse.t()
# def fetch(context, url_or_request, options \\ %{})
@doc """
Sends an HTTP(S) request and returns the response (`Playwright.APIResponse`).
The function will populate request cookies from the context, and update
context cookies from the response.
## Usage
JSON objects may be passed directly to the request:
request = Playwright.request(session)
context = APIRequest.new_context(request)
APIRequest.fetch(context, "https://example.com/api/books", %{
method: "POST",
data: %{
author: "Jane Doe",
title: "Book Title"
}
})
A common way to send file(s) in the body of a request is to upload them as
form fields with `multipart/form-data` encoding.
Use [`FormData`](https://developer.mozilla.org/en-US/docs/Web/API/FormData) to
construct the request body and pass that to the request via the `multipart`
parameter:
data = Multipart.new()
|> Multipart.add_field("author", "Jane Doe")
|> Multipart.add_field("title", "Book Title")
|> Multipart.add_file("path/to/manuscript.md", name: "manuscript.md")
APIRequest.fetch(context, "https://example.com/api/books", %{
method: "POST",
multipart: data
})
## Arguments
| name | | description |
| ---------------- | ---------- | ----------------------------- |
| `url_or_request` | | The "subject" `APIRequest` |
| `options` | (optional) | `APIRequestContext.options()` |
## Options
See "Shared options" above.
"""
@spec fetch(t(), binary() | Request.t(), options()) :: APIResponse.t() | {:error, Error.t()}
def fetch(context, url_or_request, options \\ %{})

def fetch(%APIRequestContext{} = context, url, options) when is_binary(url) do
case Channel.post({context, :fetch}, %{url: url, method: "GET"}, options) do
{:error, _} = error ->
error

response ->
APIResponse.new(Map.merge(response, %{context: context}))
end
end

# @spec get(t(), binary(), options()) :: APIResponse.t()
# def get(context, url, options \\ %{})
Expand All @@ -49,20 +169,14 @@ defmodule Playwright.APIRequestContext do
# @spec patch(t(), binary(), options()) :: APIResponse.t()
# def patch(context, url, options \\ %{})

@spec post(t(), binary(), fetch_options()) :: Playwright.APIResponse.t()
def post(%APIRequestContext{} = context, url, options \\ %{}) do
Channel.post({context, :fetch}, %{url: url, method: "POST"}, options)
end
# @spec post(t(), binary(), options()) :: Playwright.APIResponse.t()
# def post(%APIRequestContext{} = context, url, options \\ %{}) do
# Channel.post({context, :fetch}, %{url: url, method: "POST"}, options)
# end

# @spec put(t(), binary(), options()) :: APIResponse.t()
# def put(context, url, options \\ %{})

# @spec storage_state(t(), options()) :: StorageState.t()
# def storage_state(context, options \\ %{})

# TODO: move to `APIResponse.body`, probably.
@spec body(t(), Playwright.APIResponse.t()) :: any()
def body(%APIRequestContext{} = context, response) do
Channel.post({context, :fetch_response_body}, %{fetchUid: response.fetchUid})
end
end
Loading

0 comments on commit c7f5442

Please sign in to comment.