Current section

Files

Jump to
wallaby lib wallaby webdriver_client.ex
Raw

lib/wallaby/webdriver_client.ex

defmodule Wallaby.WebdriverClient do
@moduledoc false
# Client implementation for the WebDriver Wire Protocol
# documented on https://github.com/SeleniumHQ/selenium/wiki/JsonWireProtocol
alias Wallaby.{Driver, Element, Query, Session}
alias Wallaby.Helpers.KeyCodes
import Wallaby.HTTPClient
@type http_method :: :post | :get | :delete
@type url :: String.t()
@type parent ::
Element.t()
| Session.t()
@web_element_identifier "element-6066-11e4-a52e-4f735466cecf"
@button_mapping %{left: 0, middle: 1, right: 2}
@doc """
Create a session with the base url.
"""
@spec create_session(String.t(), map) :: {:ok, map}
def create_session(base_url, capabilities) do
params = %{desiredCapabilities: capabilities}
request(:post, "#{base_url}session", params)
end
@doc """
Deletes a session with the driver.
"""
@spec delete_session(Session.t() | Element.t()) :: {:ok, map}
def delete_session(session) do
request(:delete, session.session_url, %{})
rescue
_ -> {:ok, %{}}
end
@doc """
Finds an element on the page for a session. If an element is provided then
the query will be scoped to within that element.
"""
@spec find_elements(Session.t() | Element.t(), Query.compiled()) :: {:ok, [Element.t()]}
def find_elements(parent, locator) do
with {:ok, resp} <- request(:post, parent.url <> "/elements", to_params(locator)),
{:ok, elements} <- Map.fetch(resp, "value"),
elements <- Enum.map(elements || [], &cast_as_element(parent, &1)),
do: {:ok, elements}
end
@doc """
Sets the value of an element.
"""
@spec set_value(Element.t(), String.t()) :: {:ok, nil} | {:error, Driver.reason()}
def set_value(%Element{url: url}, value) do
case request(:post, "#{url}/value", %{value: [value]}) do
{:ok, resp} -> {:ok, Map.get(resp, "value")}
{:error, reason} -> {:error, reason}
end
end
@doc """
Clears the value in an element
"""
@spec clear(Element.t()) :: {:ok, nil} | {:error, Driver.reason()}
def clear(%Element{url: url}) do
case request(:post, "#{url}/clear") do
{:ok, resp} -> {:ok, Map.get(resp, "value")}
{:error, reason} -> {:error, reason}
end
end
@doc """
Invoked to accept one alert triggered within `open_dialog_fn` and return the alert message.
"""
def accept_alert(session, fun) do
fun.(session)
with {:ok, value} <- alert_text(session),
{:ok, _r} <- request(:post, "#{session.url}/accept_alert"),
do: value
end
@doc """
Invoked to accept one alert triggered within `open_dialog_fn` and return the alert message.
"""
def dismiss_alert(session, fun) do
accept_alert(session, fun)
end
def accept_confirm(session, fun) do
accept_alert(session, fun)
end
def dismiss_confirm(session, fun) do
fun.(session)
with {:ok, value} <- alert_text(session),
{:ok, _r} <- request(:post, "#{session.url}/dismiss_alert") do
value
end
end
def accept_prompt(session, input, fun) when is_nil(input) do
fun.(session)
with {:ok, value} <- alert_text(session),
{:ok, _r} <- request(:post, "#{session.url}/accept_alert") do
value
end
end
def accept_prompt(session, input, fun) do
fun.(session)
with {:ok, _r} <- request(:post, "#{session.url}/alert_text", %{text: input}),
{:ok, value} <- alert_text(session),
{:ok, _r} <- request(:post, "#{session.url}/accept_alert") do
value
end
end
def dismiss_prompt(session, fun) do
dismiss_confirm(session, fun)
end
@doc """
Clicks an element.
"""
@spec click(Element.t()) :: {:ok, map}
def click(%Element{url: url}) do
with {:ok, resp} <- request(:post, "#{url}/click") do
Map.fetch(resp, "value")
end
end
@doc """
Clicks given mouse button on the current cursor position.
"""
# Doesn't work for Firefox with middle and right mouse button
@spec click(parent, atom) :: {:ok, map}
def click(parent, button) when button in [:left, :middle, :right] do
with {:ok, resp} <-
request(:post, "#{parent.session_url}/click", %{button: @button_mapping[button]}) do
Map.fetch(resp, "value")
end
end
@doc """
Double-clicks left mouse button at the current mouse coordinates.
"""
@spec double_click(parent) :: {:ok, map}
def double_click(parent) do
with {:ok, resp} <- request(:post, "#{parent.session_url}/doubleclick") do
Map.fetch(resp, "value")
end
end
@doc """
Clicks and holds the given mouse button at the current mouse coordinates.
"""
# Doesn't work for Firefox with middle and right mouse button.
@spec button_down(parent, atom) :: {:ok, map}
def button_down(parent, button) when button in [:left, :middle, :right] do
with {:ok, resp} <-
request(:post, "#{parent.session_url}/buttondown", %{button: @button_mapping[button]}) do
Map.fetch(resp, "value")
end
end
@doc """
Releases given previously held mouse button.
"""
# Doesn't work for Firefox with middle and right mouse button.
@spec button_up(parent, atom) :: {:ok, map}
def button_up(parent, button) when button in [:left, :middle, :right] do
with {:ok, resp} <-
request(:post, "#{parent.session_url}/buttonup", %{button: @button_mapping[button]}) do
Map.fetch(resp, "value")
end
end
@doc """
Move the mouse by an offset of the specified element.
If no element is specified, the move is relative to the current mouse cursor.
If an element is provided but no offset, the mouse will be moved to the center of the element.
Gets keyword list with element, xoffset and yoffset specified as an argument.
"""
@spec move_mouse_to(parent | nil, Element.t() | nil, integer | nil, integer | nil) :: {:ok, map}
def move_mouse_to(session, element, x_offset \\ nil, y_offset \\ nil) do
params =
%{element: element, xoffset: x_offset, yoffset: y_offset}
|> Enum.filter(fn {_key, value} -> not is_nil(value) end)
|> Enum.into(%{})
params =
if Map.has_key?(params, :element) do
Map.put(params, :element, params[:element].id)
else
params
end
session_url =
if is_nil(element) do
session.session_url
else
element.session_url
end
with {:ok, resp} <- request(:post, "#{session_url}/moveto", params) do
Map.fetch(resp, "value")
end
end
@doc """
Touches the screen at the given position.
"""
@spec touch_down(parent | nil, Element.t() | nil, integer, integer) :: {:ok, map}
def touch_down(session, element, x_or_offset \\ 0, y_or_offset \\ 0) do
session_url =
if is_nil(element) do
session.session_url
else
element.session_url
end
{x, y} =
if is_nil(element) do
{x_or_offset, y_or_offset}
else
{:ok, {x, y}} = element_location(element)
{x + x_or_offset, y + y_or_offset}
end
with {:ok, resp} <- request(:post, "#{session_url}/touch/down", %{x: x, y: y}) do
Map.fetch(resp, "value")
end
end
@doc """
Stops touching the screen.
"""
@spec touch_up(parent) :: {:ok, map}
def touch_up(parent) do
# in fact x and y (in request params) can be any coordinates not greater than page size
with {:ok, resp} <- request(:post, "#{parent.session_url}/touch/up", %{x: 0, y: 0}) do
Map.fetch(resp, "value")
end
end
@doc """
Taps the element.
"""
@spec tap(Element.t()) :: {:ok, map}
def tap(element) do
with {:ok, resp} <-
request(:post, "#{element.session_url}/touch/click", %{element: element.id}) do
Map.fetch(resp, "value")
end
end
@doc """
Moves the touch pointer (finger, stylus etc.) on the screen to the point determined by the given coordinates.
"""
@spec touch_move(parent, non_neg_integer, non_neg_integer) :: {:ok, map}
def touch_move(parent, x, y) do
with {:ok, resp} <- request(:post, "#{parent.session_url}/touch/move", %{x: x, y: y}) do
Map.fetch(resp, "value")
end
end
@doc """
Scroll on the screen from the given element by the given offset using touch events.
"""
@spec touch_scroll(Element.t(), integer, integer) :: {:ok, map}
def touch_scroll(element, x_offset, y_offset) do
params = %{
element: element.id,
xoffset: x_offset,
yoffset: y_offset
}
with {:ok, resp} <- request(:post, "#{element.session_url}/touch/scroll", params) do
Map.fetch(resp, "value")
end
end
@doc """
Gets the text for an element.
"""
@spec text(Element.t()) :: {:ok, String.t()}
def text(element) do
with {:ok, resp} <- request(:get, "#{element.url}/text") do
Map.fetch(resp, "value")
end
end
@doc """
Gets the title of the current page.
"""
@spec page_title(Session.t()) :: {:ok, String.t()}
def page_title(session) do
with {:ok, resp} <- request(:get, "#{session.url}/title") do
Map.fetch(resp, "value")
end
end
@doc """
Gets the value of an elements attribute.
"""
@spec attribute(Element.t(), String.t()) :: {:ok, String.t()}
def attribute(element, name) do
with {:ok, resp} <- request(:get, "#{element.url}/attribute/#{name}") do
Map.fetch(resp, "value")
end
end
@doc """
Visit a specific page.
"""
@spec visit(Session.t(), String.t()) :: :ok
def visit(session, path) do
with {:ok, resp} <- request(:post, "#{session.url}/url", %{url: path}),
{:ok, _} <- Map.fetch(resp, "value") do
:ok
end
end
@doc """
Gets the current url.
"""
@spec current_url(Session.t()) :: {:ok, String.t()} | {:error, any()}
def current_url(session) do
with {:ok, resp} <- request(:get, "#{session.url}/url") do
Map.fetch(resp, "value")
end
end
@doc """
Gets the current path or nil.
"""
@spec current_path(Session.t()) :: {:ok, String.t()} | {:error, any()}
def current_path(session) do
with {:ok, url} <- current_url(session) do
url
|> URI.parse()
|> Map.fetch(:path)
end
end
@doc """
Gets the selected value of the element.
For Checkboxes and Radio buttons it returns the selected option.
For options selects it returns the selected option
"""
@spec selected(Element.t()) :: {:ok, boolean} | {:error, :stale_reference}
def selected(element) do
with {:ok, resp} <- request(:get, "#{element.url}/selected") do
Map.fetch(resp, "value")
end
end
@doc """
Checks if the element is being displayed.
"""
@spec displayed(Element.t()) :: {:ok, boolean} | {:error, :stale_reference}
def displayed(element) do
with {:ok, resp} <- request(:get, "#{element.url}/displayed") do
Map.fetch(resp, "value")
end
end
@doc """
Gets the size of a element.
"""
@spec size(Element.t()) :: {:ok, any}
def size(element) do
with {:ok, resp} <- request(:get, "#{element.url}/size") do
Map.fetch(resp, "value")
end
end
@doc """
Gets the height, width, x, and y position of an Element.
"""
@spec rect(Element.t()) :: {:ok, any}
def rect(element) do
with {:ok, resp} <- request(:get, "#{element.url}/rect") do
Map.fetch(resp, "value")
end
end
@doc """
Takes a screenshot.
"""
@spec take_screenshot(Session.t() | Element.t()) :: binary
def take_screenshot(session) do
with {:ok, resp} <- request(:get, "#{session.session_url}/screenshot"),
{:ok, value} <- Map.fetch(resp, "value") do
:base64.decode(value)
end
end
@doc """
Gets the cookies for a session.
"""
@spec cookies(Session.t()) :: {:ok, [map]}
def cookies(session) do
with {:ok, resp} <- request(:get, "#{session.url}/cookie") do
Map.fetch(resp, "value")
end
end
@doc """
Sets a cookie for the session.
It accepts a list of optional cookie attributes per the
[WebDriver specification](https://www.w3.org/TR/webdriver1/#cookies):
* path (string)
* domain (string)
* secure (boolean)
* httpOnly (boolean)
* expiry (integer; number of seconds since Unix Epoch)
"""
@spec set_cookie(Session.t(), String.t(), String.t()) :: {:ok, []}
@spec set_cookie(Session.t(), String.t(), String.t(), keyword()) :: {:ok, []}
def set_cookie(session, key, value, attributes \\ []) do
cookie_params =
attributes
|> Enum.into(%{})
|> Map.take(~w[path domain secure httpOnly expiry]a)
|> Map.merge(%{name: key, value: value})
with {:ok, resp} <- request(:post, "#{session.url}/cookie", %{cookie: cookie_params}) do
Map.fetch(resp, "value")
end
end
@doc """
Sets the size of the window
"""
@spec set_window_size(parent, non_neg_integer, non_neg_integer) :: {:ok, map}
def set_window_size(session, width, height) do
params = %{width: width, height: height}
with {:ok, resp} <- request(:post, "#{session.url}/window/current/size", params) do
Map.fetch(resp, "value")
end
end
@doc """
Gets the size of the window
"""
@spec get_window_size(parent) :: {:ok, map}
def get_window_size(session) do
with {:ok, resp} <- request(:get, "#{session.url}/window/current/size") do
Map.fetch(resp, "value")
end
end
@doc """
Changes the position of the window
"""
@spec set_window_position(parent, non_neg_integer, non_neg_integer) :: {:ok, map}
def set_window_position(session, x_coordinate, y_coordinate) do
params = %{
x: x_coordinate,
y: y_coordinate
}
with {:ok, resp} <- request(:post, "#{session.url}/window/current/position", params) do
Map.fetch(resp, "value")
end
end
@doc """
Gets the position of the window
"""
@spec get_window_position(parent) :: {:ok, map}
def get_window_position(session) do
with {:ok, resp} <- request(:get, "#{session.url}/window/current/position") do
Map.fetch(resp, "value")
end
end
@doc """
Maximizes the window if not already maximized
"""
@spec maximize_window(parent) :: {:ok, map}
def maximize_window(session) do
with {:ok, resp} <- request(:post, "#{session.url}/window/current/maximize") do
Map.fetch(resp, "value")
end
end
@doc """
Executes JavaScript synchronously, taking as arguments the script to execute,
and optionally a list of arguments available in the script via `arguments`
"""
@spec execute_script(Session.t() | Element.t(), String.t(), Keyword.t()) :: {:ok, any}
def execute_script(session, script, arguments \\ []) do
params = %{script: script, args: arguments}
with {:ok, resp} <- request(:post, "#{session.session_url}/execute", params) do
Map.fetch(resp, "value")
end
end
@doc """
Executes asynchronous JavaScript, taking as arguments the script to execute,
and optionally a list of arguments available in the script via `arguments`
"""
@spec execute_script_async(Session.t() | Element.t(), String.t(), Keyword.t()) :: {:ok, any}
def execute_script_async(session, script, arguments \\ []) do
params = %{
script: script,
args: arguments
}
with {:ok, resp} <- request(:post, "#{session.session_url}/execute_async", params) do
Map.fetch(resp, "value")
end
end
@doc """
Sends a list of key strokes to active element.
"""
@spec send_keys(parent(), [String.t() | atom]) :: {:ok, any}
def send_keys(%Session{} = session, keys) when is_list(keys) do
params = KeyCodes.json(keys)
opts = [encode_json: false]
with {:ok, resp} <- request(:post, "#{session.session_url}/keys", params, opts) do
Map.fetch(resp, "value")
end
end
def send_keys(%Element{} = element, keys) when is_list(keys) do
params = KeyCodes.json(keys)
opts = [encode_json: false]
with {:ok, resp} <- request(:post, "#{element.url}/value", params, opts) do
Map.fetch(resp, "value")
end
end
@doc """
Retrieves logs from the browser
"""
@spec log(Session.t() | Element.t()) :: {:ok, [map]}
def log(session) do
with {:ok, resp} <- request(:post, "#{session.session_url}/log", %{type: "browser"}) do
Map.fetch(resp, "value")
end
end
@doc """
Retrieves the current page source from session
"""
@spec page_source(Session.t()) :: {:ok, String.t()}
def page_source(session) do
with {:ok, resp} <- request(:get, "#{session.url}/source") do
Map.fetch(resp, "value")
end
end
@doc """
Retrieves the list of window handles of all windows (or tabs) available to the session
"""
@spec window_handles(parent) :: {:ok, list(String.t())}
def window_handles(session) do
with {:ok, resp} <- request(:get, "#{session.url}/window_handles") do
Map.fetch(resp, "value")
end
end
@doc """
Retrieves the window handle for the currently focused window (or tab) for the session
"""
@spec window_handle(parent) :: {:ok, String.t()}
def window_handle(session) do
with {:ok, resp} <- request(:get, "#{session.url}/window_handle") do
Map.fetch(resp, "value")
end
end
@doc """
Changes focus to another window (or tab)
You may specify the the window by its server-assigned window handle
or by the value of its name attribute.
"""
@spec focus_window(parent, String.t()) :: {:ok, map}
def focus_window(session, window_handle_or_name) do
with {:ok, resp} <-
request(:post, "#{session.url}/window", %{
name: window_handle_or_name,
handle: window_handle_or_name
}) do
# In the Selenium WebDriver Protocol, the parameter is called name:
# https://github.com/SeleniumHQ/selenium/wiki/JsonWireProtocol#sessionsessionidwindow
# In the new W3C protocol, the parameter is called handle:
# https://w3c.github.io/webdriver/#switch-to-window
# Browsers are starting to support only the new W3C protocol,
# so we're adding `handle` as well to have it working until
# we implement a separate client module for the new protocol.
Map.fetch(resp, "value")
end
end
@doc """
Closes the currently focused window (or tab)
"""
@spec close_window(parent) :: {:ok, map}
def close_window(session) do
with {:ok, resp} <- request(:delete, "#{session.url}/window") do
Map.fetch(resp, "value")
end
end
@doc """
Changes focus to another frame
You may specify the the frame by passing the frame Element, its index or id.
When passed `nil`, the server should switch to the page's default (top level) frame.
"""
@spec focus_frame(parent, String.t() | number | nil | Element.t()) :: {:ok, map}
def focus_frame(session, %Element{} = frame_element) do
params = %{
id: %{
"ELEMENT" => frame_element.id,
@web_element_identifier => frame_element.id
}
}
with {:ok, resp} <- request(:post, "#{session.url}/frame", params) do
Map.fetch(resp, "value")
end
end
def focus_frame(session, frame) do
with {:ok, resp} <- request(:post, "#{session.url}/frame", %{id: frame}) do
Map.fetch(resp, "value")
end
end
@doc """
Changes focus to parent frame
"""
@spec focus_parent_frame(parent) :: {:ok, map}
def focus_parent_frame(session) do
with {:ok, resp} <- request(:post, "#{session.url}/frame/parent") do
Map.fetch(resp, "value")
end
end
@doc """
Returns a tuple `{width, height}` with the size of the given element.
"""
@spec element_size(Element.t()) :: {:ok, tuple}
def element_size(element) do
with {:ok, resp} <- request(:get, "#{element.session_url}/element/#{element.id}/size"),
{:ok, value} <- Map.fetch(resp, "value"),
do: {:ok, {value["width"], value["height"]}}
end
@doc """
Returns a tuple `{x, y}` with the coordinates of the top-left corner of given element.
"""
@spec element_location(Element.t()) :: {:ok, tuple}
def element_location(element) do
with {:ok, resp} <- request(:get, "#{element.session_url}/element/#{element.id}/location"),
{:ok, value} <- Map.fetch(resp, "value"),
do: {:ok, {value["x"], value["y"]}}
end
@spec cast_as_element(Session.t() | Element.t(), map) :: Element.t()
defp cast_as_element(parent, %{"ELEMENT" => id}) do
# In the Selenium WebDriver Protocol, the identifier is "ELEMENT":
# https://github.com/SeleniumHQ/selenium/wiki/JsonWireProtocol#webelement-json-object
# In the new W3C protocol, the identifier is a specific constant:
# https://w3c.github.io/webdriver/#elements
# Browsers are starting to support only the new W3C protocol,
# so we're reading the constant as well to have it working until
# we implement a separate client module for the new protocol.
cast_as_element(parent, %{@web_element_identifier => id})
end
defp cast_as_element(parent, %{@web_element_identifier => id}) do
%Wallaby.Element{
id: id,
session_url: parent.session_url,
url: parent.session_url <> "/element/#{id}",
parent: parent,
driver: parent.driver
}
end
# Retrieves the text from an alert, prompt or confirm.
@spec alert_text(Session.t()) :: {:ok, String.t()}
defp alert_text(session) do
with {:ok, resp} <- request(:get, "#{session.url}/alert_text") do
Map.fetch(resp, "value")
end
end
end