Packages

Package that helps automate browser testing, similar to puppeteer.

Current section

Files

Jump to
speed_test lib speed_test.ex
Raw

lib/speed_test.ex

defmodule SpeedTest do
@moduledoc """
High level wrapper for the chrome debug protocol and managing
headless chrome browsers.
"""
require Logger
@timeout :timer.seconds(30)
alias SpeedTest.{Cookie, Retry}
alias SpeedTest.Page.{Registry, Session, Supervisor}
@type options() :: [timeout: timeout(), retry: SpeedTest.Retry.t()]
@type node_id :: integer()
@spec launch(atom | %{id: any}) :: pid
@doc ~S"""
Launch creates a new browser session inside headless chrome.
### Examples
iex> page = SpeedTest.launch()
iex> is_pid(page)
true
"""
def launch(page \\ %{id: UUID.uuid4()}) do
case Registry.lookup(page) do
{:ok, {pid, _page_data}} ->
pid
_ ->
{:ok, pid} = Supervisor.start_child(Session, %{page: page, server: nil})
pid
end
end
@doc ~S"""
Closes a page process.
*Note* this does NOT stop the underlying chroxy chrome process
### Examples
iex> page = SpeedTest.launch()
iex> SpeedTest.close(page)
true
"""
@spec close(pid) :: true
def close(page) do
Process.exit(page, :normal)
end
@doc ~S"""
Changes the dimensions of a given page
### Examples
iex> page = SpeedTest.launch()
iex> page |> SpeedTest.dimensions(%{width: 1920, height: 1080})
:ok
"""
@spec dimensions(pid, %{width: number(), height: number()}, nil | options()) ::
:ok | {:error, any()}
def dimensions(server, params, options \\ []) do
GenServer.call(server, {:dimensions, params, options}, options[:timeout] || @timeout)
end
@doc ~S"""
Navigates to a given URL. This URL can be shorted by providing the configuration value :base_url
i.e. `config :speed_test, base_url: "http://localhost:4003"`
### Examples
iex> page = SpeedTest.launch()
iex> page |> SpeedTest.goto("/")
:ok
"""
@spec goto(pid, binary(), options()) :: :ok | {:error, :timeout} | {:error, any()}
def goto(server, url, options \\ []) do
GenServer.call(server, {:visit, url, options}, options[:timeout] || @timeout)
end
@doc ~S"""
Creates a PDF from the current page. Optionally can be saved to a provided path.
# Parameters
* scale = 1,
* displayHeaderFooter = false,
* headerTemplate = '',
* footerTemplate = '',
* printBackground = false,
* landscape = false,
* pageRanges = '',
* preferCSSPageSize = false,
* margin = {},
* path = null
### Examples
iex> page = SpeedTest.launch()
iex> page |> SpeedTest.goto("/")
iex> {:ok, pdf} = page |> SpeedTest.pdf("/")
iex> is_binary(pdf) and String.length(pdf) > 0
true
"""
@spec pdf(pid, %{path: binary()} | %{}, options) :: :ok | {:ok, binary()} | {:error, any()}
def pdf(server, params \\ %{}, options \\ []) do
GenServer.call(server, {:pdf, params, options}, options[:timeout] || @timeout)
end
@doc ~S"""
Captures a screenshot of the current window. Optionally saves the output to a given path
### Examples
iex> page = SpeedTest.launch()
iex> page |> SpeedTest.goto("/")
iex> {:ok, png} = page |> SpeedTest.screenshot("/")
iex> is_binary(png) and String.length(png) > 0
true
"""
@spec screenshot(pid, %{path: binary()} | %{}, options) ::
:ok | {:ok, binary()} | {:error, any()}
def screenshot(server, params \\ %{}, options \\ []) do
GenServer.call(server, {:screenshot, params, options}, options[:timeout] || @timeout)
end
@doc ~S"""
Gets a `t:node_id/0` to a given selector on the page.
### Examples
iex> page = SpeedTest.launch()
iex> :ok = page |> SpeedTest.goto("/")
iex> {:ok, id} = page |> SpeedTest.get("input")
iex> is_integer(id)
true
iex> {:ok, id} = page |> SpeedTest.get("[data-test=login_email]")
iex> is_integer(id)
true
"""
@spec get(pid, node_id(), options()) :: {:ok, node_id} | {:error, :timeout} | {:error, any()}
def get(server, selector, options \\ []) do
GenServer.call(
server,
{:get, %{selector: selector, retry: options[:retry] || %Retry{}},
Keyword.delete(options, :retry)},
options[:timeout] || @timeout
)
end
@doc ~S"""
Focuses on a given node.
### Examples
iex> page = SpeedTest.launch()
iex> :ok = page |> SpeedTest.goto("/")
iex> {:ok, id} = page |> SpeedTest.get("input")
iex> page |> SpeedTest.focus(id)
:ok
"""
@spec focus(pid, node_id(), options()) :: :ok | {:error, any()}
def focus(server, node_id, options \\ []) do
GenServer.call(
server,
{:focus, %{node_id: node_id}, options},
options[:timeout] || @timeout
)
end
@doc ~S"""
Types text in a given node.
### Examples
iex> page = SpeedTest.launch()
iex> :ok = page |> SpeedTest.goto("/")
iex> {:ok, email_input} = page |> SpeedTest.get("[data-test=login_email]")
iex> :ok = page |> SpeedTest.type(email_input, "testing@test.com")
iex> page |> SpeedTest.value(email_input)
{:ok, "testing@test.com"}
"""
@spec type(pid, node_id(), binary(), options()) :: :ok | {:error, any()}
def type(server, node_id, text, options \\ []) do
GenServer.call(
server,
{:type, %{node_id: node_id, text: text}, options},
options[:timeout] || @timeout
)
end
@doc ~S"""
Gets the "value" property from a given node.
### Examples
iex> page = SpeedTest.launch()
iex> :ok = page |> SpeedTest.goto("/")
iex> {:ok, email_input} = page |> SpeedTest.get("[data-test=login_email]")
iex> page |> SpeedTest.value(email_input)
{:ok, ""}
"""
@spec value(pid, node_id(), options()) :: binary() | :notfound
def value(server, node_id, options \\ []) do
GenServer.call(
server,
{:property, %{node_id: node_id, property: "value"}, options},
options[:timeout] || @timeout
)
end
@doc ~S"""
Gets arbitrary properties from a dom node.
### Examples
iex> page = SpeedTest.launch()
iex> :ok = page |> SpeedTest.goto("/")
iex> {:ok, email_input} = page |> SpeedTest.get("[data-test=login_email]")
iex> page |> SpeedTest.property(email_input, "type")
{:ok, "email"}
"""
@spec property(pid, node_id(), binary(), options) :: {:ok, binary()} | {:error, :notfound}
def property(server, node_id, property, options \\ []) do
GenServer.call(
server,
{:property, %{node_id: node_id, property: property}, options},
options[:timeout] || @timeout
)
end
@doc ~S"""
Utility function for returning the text of a DOM node ("textContent" property)
### Examples
iex> page = SpeedTest.launch()
iex> :ok = page |> SpeedTest.goto("/")
iex> {:ok, header} = page |> SpeedTest.get("h1")
iex> page |> SpeedTest.text(header)
{:ok, "I am a website"}
"""
@spec text(pid, node_id(), options()) :: {:ok, binary()} | {:error, :notfound}
def text(server, node_id, options \\ []) do
GenServer.call(
server,
{:property, %{node_id: node_id, property: "textContent"}, options},
options[:timeout] || @timeout
)
end
@doc ~S"""
Returns all attributes for a dom node as a map
### Examples
iex> page = SpeedTest.launch()
iex> :ok = page |> SpeedTest.goto("/")
iex> {:ok, input} = page |> SpeedTest.get("input")
iex> page |> SpeedTest.attributes(input)
{:ok, %{"type" => "email", "data-test" => "login_email", "name" => "test"}}
"""
@spec attributes(pid, node_id(), options()) :: {:ok, map()} | :notfound
def attributes(server, node_id, options \\ []) do
GenServer.call(
server,
{:attribute, %{node_id: node_id}, options},
options[:timeout] || @timeout
)
end
@doc ~S"""
Returns a single attribute for a node
### Examples
iex> page = SpeedTest.launch()
iex> :ok = page |> SpeedTest.goto("/")
iex> {:ok, input} = page |> SpeedTest.get("input")
iex> page |> SpeedTest.attribute(input, "name")
{:ok, "test"}
"""
@spec attribute(pid, node_id(), binary(), options()) :: {:ok, binary()} | :notfound
def attribute(server, node_id, attribute, options \\ []) do
GenServer.call(
server,
{:attribute, %{node_id: node_id, attribute: attribute}, options},
options[:timeout] || @timeout
)
end
@doc ~S"""
Clicks on a dom node. Can take in a params object with the number of times to click.
### Examples
iex> page = SpeedTest.launch()
iex> :ok = page |> SpeedTest.goto("/")
iex> {:ok, button} = page |> SpeedTest.get("button")
iex> page |> SpeedTest.click(button)
:ok
iex> page |> SpeedTest.click(button, %{click_count: 2})
:ok
"""
@spec click(pid, node_id(), %{click_count: integer()} | %{}, options()) :: :ok | {:error, any()}
def click(server, node_id, params \\ %{}, options \\ [])
def click(server, node_id, %{click_count: count}, options) do
GenServer.call(
server,
{:click, %{node_id: node_id, click_count: count}, options},
options[:timeout] || @timeout
)
end
def click(server, node_id, _params, options) do
GenServer.call(
server,
{:click, %{node_id: node_id}, options},
options[:timeout] || @timeout
)
end
@doc ~S"""
Waits for the current URL to equal the provided one. Useful if you have a redirect that
occurs after some user action.
### Examples
iex> page = SpeedTest.launch()
iex> :ok = page |> SpeedTest.goto("/")
iex> :ok = page |> SpeedTest.wait_for_url("/", retry: %SpeedTest.Retry{timeout: :timer.seconds(2)})
:ok
"""
@spec wait_for_url(pid(), binary(), options()) ::
{:ok, any()} | {:error, :timeout} | {:error, any()}
def wait_for_url(server, url, options \\ []) do
GenServer.call(
server,
{:wait_for_url, %{url: url, retry: options[:retry] || %Retry{}}},
options[:timeout] || @timeout
)
end
@doc ~S"""
Sets a cookie on the page.
### Examples
iex> page = SpeedTest.launch()
iex> :ok = page |> SpeedTest.goto("/")
iex> page |> SpeedTest.set_cookie(%SpeedTest.Cookie{domain: "localhost", name: "testing", value: "123"})
:ok
"""
@spec set_cookie(
pid(),
SpeedTest.Cookie.t(),
options()
) :: :ok | any()
def set_cookie(server, %Cookie{} = cookie, options \\ []) do
GenServer.call(
server,
{:set_cookies,
%{
cookies: [
%{
name: cookie.name,
value: cookie.value,
url: cookie.url,
domain: cookie.domain,
path: cookie.path,
secure: cookie.secure,
httpOnly: cookie.http_only,
sameSite: cookie.same_site,
expires: cookie.expires
}
]
}, options},
options[:timeout] || @timeout
)
end
@doc ~S"""
Gets a cookie from the page
### Examples
iex> page = SpeedTest.launch()
iex> :ok = page |> SpeedTest.goto("/")
iex> page |> SpeedTest.set_cookie(%SpeedTest.Cookie{domain: "localhost", name: "testing", value: "123"})
:ok
iex> page |> SpeedTest.get_cookie("testing")
%SpeedTest.Cookie{domain: "localhost", name: "testing", value: "123"}
"""
@spec get_cookie(pid(), binary(), options()) ::
{:ok, SpeedTest.Cookie.t()} | {:error, :notfound}
def get_cookie(server, name, options \\ []) do
GenServer.call(
server,
{:get_cookie, name, options},
options[:timeout] || @timeout
)
end
@doc ~S"""
Returns the current URL
### Examples
iex> page = SpeedTest.launch()
iex> :ok = page |> SpeedTest.goto("/")
iex> page |> SpeedTest.current_url()
"http://localhost:8081/"
"""
@spec current_url(pid(), options()) :: binary()
def current_url(server, options \\ []) do
GenServer.call(
server,
{:url},
options[:timeout] || @timeout
)
end
@doc ~S"""
Waits for a given network request to happen. Can be anything that shows up in the network tab from XHR
request to CSS to javascript.
*NOTE* you do not want to use this on the home page since the goto() function already waits for page load
and will not record that HTTP request.
### Examples
iex> page = SpeedTest.launch()
iex> :ok = page |> SpeedTest.goto("/")
iex> {:ok, %{"request" => %{"url" => "http://localhost:8081/app.js"}, "response" => %{"status" => 200}}} = page |> SpeedTest.intercept_request("GET", "/app.js")
"""
@spec intercept_request(
pid,
binary,
binary,
options()
) :: {:ok, map()} | {:error, :timeout}
def intercept_request(server, method, url, options \\ []) do
GenServer.call(
server,
{:network, %{url: url, method: String.upcase(method), retry: options[:retry] || %Retry{}}},
options[:timeout] || @timeout
)
end
@doc ~S"""
Clears a text input. Wrapper fro calling click on an input 3 times and typing backspace.
### Examples
iex> page = SpeedTest.launch()
iex> :ok = page |> SpeedTest.goto("/")
iex> {:ok, email_input} = page |> SpeedTest.get("[data-test=login_email]")
iex> :ok = page |> SpeedTest.type(email_input, "testing@test.com")
iex> page |> SpeedTest.value(email_input)
{:ok, "testing@test.com"}
iex> page |> SpeedTest.clear(email_input)
:ok
iex> page |> SpeedTest.value(email_input)
{:ok, ""}
"""
@spec clear(pid, node_id(), options()) :: :ok | {:error, :timeout}
def clear(server, node, options \\ []) do
with :ok <- server |> click(node, %{click_count: 3}, options),
:ok <-
server
|> type(node, "\b", options) do
:ok
end
end
end