defmodule Playwright.Locator do
@moduledoc """
`Playwright.Locator` represents a view to the element(s) on the page.
It captures the logic sufficient to retrieve the element at any given moment.
`Locator` can be created with the `Playwright.Locator.new/2` function.
See also `Playwright.Page.locator/2`.
## Example
locator = Playwright.Locator.new(page, "a#exists")
Playwright.Locator.click(locator)
The difference between the `Playwright.Locator` and `Playwright.ElementHandle`
is that the latter points to a particular element, while `Playwright.Locator` captures
the logic of how to retrieve that element.
## ElementHandle Example
In the example below, `handle` points to a particular DOM element on page. If
that element changes text or is used by React to render an entirely different
component, `handle` is still pointing to that very DOM element. This can lead
to unexpected behaviors.
handle = Page.query_selector(page, "text=Submit")
ElementHandle.hover(handle)
ElementHandle.click(handle)
## Locator Example
With the locator, every time the element is used, up-to-date DOM element is
located in the page using the selector. So in the snippet below, underlying
DOM element is going to be located twice.
locator = Playwright.Locator.new(page, "a#exists")
:ok = Playwright.Locator.hover(locator)
:ok = Playwright.Locator.click(locator)
## Strictness
Locators are strict. This means that all operations on locators that imply
some target DOM element will throw if more than one element matches given
selector.
alias Playwright.Locator
locator = Locator.new(page, "button")
# Throws if there are several buttons in DOM:
Locator.click(locator)
# Works because we explicitly tell locator to pick the first element:
Locator.first(locator) |> Locator.click()
# Works because count knows what to do with multiple matches:
Locator.count(locator)
"""
alias Playwright.{Channel, ElementHandle, Frame, Locator, Page}
@enforce_keys [:frame, :selector]
defstruct [:frame, :selector]
@type t() :: %__MODULE__{
frame: Playwright.Frame.t(),
selector: selector()
}
@type options() :: %{optional(:timeout) => non_neg_integer()}
@type options_keyboard() :: %{
optional(:delay) => non_neg_integer(),
optional(:no_wait_after) => boolean(),
optional(:timeout) => non_neg_integer()
}
@type options_position() :: %{
optional(:x) => number(),
optional(:y) => number()
}
@type options_click() :: %{
optional(:button) => param_input_button(),
optional(:click_count) => number(),
optional(:delay) => number(),
optional(:force) => boolean(),
optional(:modifiers) => [:alt | :control | :meta | :shift],
optional(:no_wait_after) => boolean(),
optional(:position) => options_position(),
optional(:timeout) => number(),
optional(:trial) => boolean()
}
@type param_input_button() :: :left | :right | :middle
@type selector() :: binary()
@type serializable :: any()
@doc """
Returns a `%Playwright.Locator{}`.
## Arguments
| key/name | type | | description |
| ---------- | ------ | ---------------------- | ----------- |
| `frame` | param | `Frame.t() | Page.t()` | |
| `selector` | param | `binary()` | A Playwright selector. |
"""
@spec new(Frame.t() | Page.t(), selector()) :: Locator.t()
def new(frame, selector)
def new(%Frame{} = frame, selector) do
%__MODULE__{
frame: frame,
selector: selector
}
end
def new(%Page{} = page, selector) do
%__MODULE__{
frame: Page.main_frame(page),
selector: selector
}
end
@doc """
Returns an list of `node.innerText` values for all matching nodes.
## Returns
- `[binary()]`
"""
@spec all_inner_texts(t()) :: [binary()]
def all_inner_texts(%Locator{} = locator) do
Frame.eval_on_selector_all(locator.frame, locator.selector, "ee => ee.map(e => e.innerText)")
end
@doc """
Returns an list of `node.textContent` values for all matching nodes.
## Returns
- `[binary()]`
"""
@spec all_text_contents(t()) :: [binary()]
def all_text_contents(%Locator{} = locator) do
Frame.eval_on_selector_all(locator.frame, locator.selector, "ee => ee.map(e => e.textContent || '')")
end
@doc """
Returns the bounding box of the element, or `nil` if the element is not visible.
The bounding box is calculated relative to the main frame viewport which is
usually the same as the browser window.
Scrolling affects the returned bounding box, similarly to
[Element.getBoundingClientRect](https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect).
That means `x` and/or `y` may be negative.
Elements from child frames return the bounding box relative to the main frame,
unlike [Element.getBoundingClientRect](https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect).
Assuming the page is static, it is safe to use bounding box coordinates to perform input. For example, the following snippet should click the center of the element:options()
box = Locator.bounding_box(locator)
Page.Mouse.click(page, box.x + box.width / 2, box.y + box.height / 2)
## Returns
- `%{x: x, y: y, width: width, height: height}`
- `nil`
## Arguments
| key/name | type | | description |
| ---------- | ------ | ---------- | ----------- |
| `:timeout` | option | `number()` | Maximum time in milliseconds. Pass `0` to disable timeout. The default value can be changed via `Playwright.BrowserContext.set_default_timeout/2` or `Playwright.Page.set_default_timeout/2`. `(default: 30 seconds)` |
"""
@spec bounding_box(t(), options()) :: map() | nil
def bounding_box(%Locator{} = locator, options \\ %{}) do
with_element(locator, options, fn handle ->
ElementHandle.bounding_box(handle)
end)
end
@doc """
Checks the (checkmark) element by performing the following steps:
1. Ensure that element is a checkbox or a radio input. If not, this method
throws. If the element is already checked, this method returns immediately.
2. Wait for actionability checks on the element, unless force option is set.
3. Scroll the element into view if needed.
4. Use `Playwright.Page.Mouse` to click in the center of the element.
5. Wait for initiated navigations to either succeed or fail, unless
`option: no_wait_after` is set.
6. Ensure that the element is now checked. If not, this method throws.
If the element is detached from the DOM at any moment during the action,
this method throws.
When all steps combined have not finished during the specified timeout, this
method throws a `TimeoutError`. Passing `0` timeout disables this.
## Returns
- `:ok`
## Arguments
| key/name | type | | description |
| ---------------- | ------ | --------------- | ----------- |
| `:force` | option | `boolean()` | Whether to bypass the actionability checks. `(default: false)` |
| `:no_wait_after` | option | `boolean()` | Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You can opt out of waiting via setting this flag. You would only need this option in the exceptional cases such as navigating to inaccessible pages. `(default: false)` |
| `:position` | option | `%{x: x, y: y}` | A point to use relative to the top-left corner of element padding box. If not specified, uses some visible point of the element. |
| `:timeout` | option | `number()` | Maximum time in milliseconds. Pass `0` to disable timeout. The default value can be changed via `Playwright.BrowserContext.set_default_timeout/2` or `Playwright.Page.set_default_timeout/2`. `(default: 30 seconds)` |
| `:trial` | option | `boolean()` | When set, this call only performs the actionability checks and skips the action. Useful to wait until the element is ready for the action without performing it. `(default: false)` |
"""
@spec check(t(), options()) :: :ok
def check(%Locator{} = locator, options \\ %{}) do
options = Map.merge(options, %{strict: true})
Frame.check(locator.frame, locator.selector, options)
end
@doc """
Clicks the element by performing the following steps:
1. Wait for actionability checks on the element, unless `option: force` is set.
2. Scroll the element into view if needed.
3. Use `Playwright.Page.Mouse` to click in the center of the element, or
the specified position.
4. Wait for initiated navigations to either succeed or fail, unless
`option: no_wait_after` is set.
If the element is detached from the DOM at any moment during the action, this method throws.
When all steps combined have not finished during the specified timeout, this method throws a
`Playwright.Channel.Error.t()`. Passing `0` timeout disables this.
## Returns
- `:ok`
## Arguments
| key/name | type | | description |
| ---------------- | ------ | --------------------------------- | ----------- |
| `:button` | option | `:left`, `:right` or `:middle` | `(default: :left)` |
| `:click_count` | option | `number()` | See [MDN: `UIEvent.detail`](https://developer.mozilla.org/en-US/docs/Web/API/UIEvent/detail) `(default: 1)` |
| `:delay` | option | `number()` | Time to wait between `mousedown` and `mouseup` in milliseconds. `(default: 0)` |
| `:force` | option | `boolean()` | Whether to bypass the actionability checks. `(default: false)` |
| `:modifiers` | option | `[:alt, :control, :meta, :shift]` | Modifier keys to press. Ensures that only these modifiers are pressed during the operation, and then restores current modifiers back. If not specified, currently pressed modifiers are used. |
| `:no_wait_after` | option | `boolean()` | Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You can opt out of waiting via setting this flag. You would only need this option in the exceptional cases such as navigating to inaccessible pages. `(default: false)` |
| `:position` | option | `%{x: x, y: y}` | A point to use relative to the top-left corner of element padding box. If not specified, uses some visible point of the element. |
| `:timeout` | option | `number()` | Maximum time in milliseconds. Pass `0` to disable timeout. The default value can be changed via `Playwright.BrowserContext.set_default_timeout/2` or `Playwright.Page.set_default_timeout/2`. `(default: 30 seconds)` |
| `:trial` | option | `boolean()` | When set, this call only performs the actionability checks and skips the action. Useful to wait until the element is ready for the action without performing it. `(default: false)` |
"""
@spec click(t(), options_click()) :: :ok
def click(%Locator{} = locator, options \\ %{}) do
options = Map.merge(options, %{strict: true})
Frame.click(locator.frame, locator.selector, options)
end
@doc """
Returns the number of elements matching given selector.
## Returns
- `number()`
"""
@spec count(t()) :: number()
def count(%Locator{} = locator) do
evaluate_all(locator, "ee => ee.length")
end
@doc """
Double clicks the element.
Performs the following steps:
1. Wait for actionability checks on the matched element, unless
`option: force` is set.
2. Scroll the element into view if needed.
3 Use `Page.Mouse` to double click in the center of the element, or the
specified `option: position`.
4. Wait for initiated navigations to either succeed or fail, unless
`option: no_wait_after` is set. Note that if the first click of the
`dblclick/3` triggers a navigation event, the call will throw.
If the element is detached from the DOM at any moment during the action,
the call throws.
When all steps combined have not finished during the specified
`option: timeout`, throws a `TimeoutError`. Passing `timeout: 0` disables
this.
> NOTE
>
> `dblclick/3` dispatches two `click` events and a single `dblclick` event.
## Returns
- `:ok`
## Arguments
| key/name | type | | description |
| ---------------- | ------ | --------------------------------- | ----------- |
| `:button` | option | `:left`, `:right` or `:middle` | `(default: :left)` |
| `:delay` | option | `number() ` | Time to wait between keydown and keyup in milliseconds. `(default: 0)` |
| `:force` | option | `boolean()` | Whether to bypass the actionability checks. `(default: false)` |
| `:modifiers` | option | `[:alt, :control, :meta, :shift]` | Modifier keys to press. Ensures that only these modifiers are pressed during the operation, and then restores current modifiers back. If not specified, currently pressed modifiers are used. |
| `:no_wait_after` | option | `boolean()` | Actions that initiate navigations are waiting for these navigations to happen and for pages to start loading. You can opt out of waiting via setting this flag. You would only need this option in the exceptional cases such as navigating to inaccessible pages. `(default: false)` |
| `:position` | option | `%{x: x, y: y}` | A point to use relative to the top-left corner of element padding box. If not specified, uses some visible point of the element. |
| `:timeout` | option | `number()` | Maximum time in milliseconds. Pass `0` to disable timeout. The default value can be changed by using the `Playwright.BrowserContext.set_default_timeout/2` or `Playwright.Page.set_default_timeout/2` functions. `(default: 30 seconds)` |
| `:trial` | option | `boolean()` | When set, this call only performs the actionability checks and skips the action. Useful to wait until the element is ready for the action without performing it. `(default: false)` |
"""
@spec dblclick(t(), options()) :: :ok
def dblclick(%Locator{} = locator, options \\ %{}) do
Frame.dblclick(locator.frame, locator.selector, options)
end
@doc """
Dispatches the `param: type` event on the element.
Regardless of the visibility state of the element, the event is dispatched.
Under the hood, creates an instance of an event based on the given type,
initializes it with the `param: event_init` properties and dispatches it on
the element.
Events are composed, cancelable and bubble by default.
The `param: event_init` is event-specific. Please refer to the events
documentation for the lists of initial properties:
- [DragEvent](https://developer.mozilla.org/en-US/docs/Web/API/DragEvent/DragEvent)
- [FocusEvent](https://developer.mozilla.org/en-US/docs/Web/API/FocusEvent/FocusEvent)
- [KeyboardEvent](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/KeyboardEvent)
- [MouseEvent](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/MouseEvent)
- [PointerEvent](https://developer.mozilla.org/en-US/docs/Web/API/PointerEvent/PointerEvent)
- [TouchEvent](https://developer.mozilla.org/en-US/docs/Web/API/TouchEvent/TouchEvent)
- [Event](https://developer.mozilla.org/en-US/docs/Web/API/Event/Event)
## Example
Dispatch a 'click' event on the element. This is equivalent to calling
`Playwright.ElementHandle.click/2`:
Locator.dispatch_event(locator, :click)
Specify a `Playwright.JSHandle` as the property value to be passed into the
event:
data_transfer = Page.evaluate_handle(page, "new DataTransfer()")
Locator.dispatch_event(locator, :dragstart, { "dataTransfer": data_transfer })
## Returns
- `:ok`
## Arguments
| key/name | type | | description |
| ---------------- | ------ | ----------------------- | ----------- |
| `type` | param | `atom()` or `binary()` | DOM event type: `:click`, `:dragstart`, etc. |
| `event_init` | param | `evaluation_argument()` | Optional event-specific initialization properties. |
| `:timeout` | option | `number()` | Maximum time in milliseconds. Pass `0` to disable timeout. The default value can be changed by using the `Playwright.BrowserContext.set_default_timeout/2` or `Playwright.Page.set_default_timeout/2` functions. `(default: 30 seconds)` |
"""
@spec dispatch_event(t(), atom() | binary(), Frame.evaluation_argument(), options()) :: :ok
def dispatch_event(%Locator{} = locator, type, event_init \\ nil, options \\ %{}) do
options = Map.merge(options, %{strict: true})
Frame.dispatch_event(locator.frame, locator.selector, type, event_init, options)
end
@doc """
Resolves the given `Playwright.Locator` to the first matching DOM element.
If no elements matching the query are visible, waits for them up to a given
timeout. If multiple elements match the selector, throws.
## Returns
- `Playwright.ElementHandle.t()`
- `{:error, Playwright.Channel.Error.t()}`
## Arguments
| key/name | type | | description |
| ---------- | ------ | ---------- | ----------- |
| `:timeout` | option | `number()` | Maximum time in milliseconds. Pass `0` to disable timeout. The default value can be changed by using the `Playwright.BrowserContext.set_default_timeout/2` or `Playwright.Page.set_default_timeout/2` functions. `(default: 30 seconds)` |
"""
@spec element_handle(t(), options()) :: ElementHandle.t() | {:error, Channel.Error.t()}
def element_handle(%Locator{} = locator, options \\ %{}) do
options = Map.merge(%{strict: true, state: "attached"}, options)
with_element(locator, options, fn handle ->
handle
end)
end
@doc """
Resolves given locator to all matching DOM elements.
## Returns
- `[Playwright.ElementHandle.t()]`
"""
@spec element_handles(t()) :: [ElementHandle.t()]
def element_handles(locator) do
Frame.query_selector_all(locator.frame, locator.selector)
end
@doc """
Returns the result of executing `param: expression`.
Passes the handle as the first argument to the expression.
## Returns
- `Serializable.t()`
## Arguments
| key/name | type | | description |
| ------------ | ------ | ---------- | ----------- |
| `expression` | param | `binary()` | JavaScript expression to be evaluated in the browser context. If it looks like a function declaration, it is interpreted as a function. Otherwise, evaluated as an expression. |
| `arg` | param | `any()` | Argument to pass to `expression` `(optional)` |
| `:timeout` | option | `number()` | Maximum time in milliseconds. Pass `0` to disable timeout. The default value can be changed by using the `Playwright.BrowserContext.set_default_timeout/2` or `Playwright.Page.set_default_timeout/2` functions. `(default: 30 seconds)` |
"""
@spec evaluate(t(), binary(), any(), options()) :: serializable()
def evaluate(locator, expression, arg \\ nil, options \\ %{})
require Logger
# NOTE: need to do all of the map-like things before a plain `map()`,
# then do `map()`, then do anything else.
def evaluate(%Locator{} = locator, expression, arg, options)
when is_struct(arg, ElementHandle) do
with_element(locator, options, fn handle ->
ElementHandle.evaluate(handle, expression, arg)
end)
end
def evaluate(%Locator{} = locator, expression, options, _)
when is_map(options) do
with_element(locator, options, fn handle ->
ElementHandle.evaluate(handle, expression)
end)
end
def evaluate(%Locator{} = locator, expression, arg, options) do
with_element(locator, options, fn handle ->
ElementHandle.evaluate(handle, expression, arg)
end)
end
@doc """
Finds all elements matching the specified locator and passes the list of
matched elements as an argument to `param: expression`.
Returns the result of expression invocation.
## Returns
- `Serializable.t()`
## Arguments
| key/name | type | | description |
| ------------ | ------ | ---------- | ----------- |
| `expression` | param | `binary()` | JavaScript expression to be evaluated in the browser context. If it looks like a function declaration, it is interpreted as a function. Otherwise, evaluated as an expression. |
| `arg` | param | `any()` | Argument to pass to `expression` `(optional)` |
"""
@spec evaluate_all(t(), binary(), any()) :: serializable()
def evaluate_all(%Locator{} = locator, expression, arg \\ nil) do
Frame.eval_on_selector_all(locator.frame, locator.selector, expression, arg)
end
@doc """
Returns the result of `param: expression` as a `Playwright.JSHandle`.
Passes the handle as the first argument to `param: expression`.
The only difference between `Playwright.Locator.evaluate/4` and
`Playwright.Locator.evaluate_handle/3` is that `evaluate_handle` returns
`JSHandle`.
See `Playwright.Page.evaluate_handle` for more details.
## Returns
- `Playwright.ElementHandle.t()`
- `{:error, Playwright.Channel.Error.t()}`
## Arguments
| key/name | type | | description |
| ------------ | ------ | ---------- | ----------- |
| `expression` | param | `binary()` | JavaScript expression to be evaluated in the browser context. If it looks like a function declaration, it is interpreted as a function. Otherwise, evaluated as an expression. |
| `arg` | param | `any()` | Argument to pass to `expression` `(optional)` |
| `:timeout` | option | `number()` | Maximum time in milliseconds. Pass `0` to disable timeout. The default value can be changed by using the `Playwright.BrowserContext.set_default_timeout/2` or `Playwright.Page.set_default_timeout/2` functions. `(default: 30 seconds)` |
"""
@spec evaluate_handle(t(), binary(), any(), options()) :: ElementHandle.t() | {:error, Channel.Error.t()}
def evaluate_handle(locator, expression, arg \\ nil, options \\ %{})
# NOTE: need to do all of the map-like things before a plain `map()`,
# then do `map()`, then do anything else.
def evaluate_handle(%Locator{} = locator, expression, arg, options)
when is_struct(arg, ElementHandle) do
options = Map.merge(%{strict: true, state: "attached"}, options)
with_element(locator, options, fn handle ->
ElementHandle.evaluate_handle(handle, expression, arg)
end)
end
def evaluate_handle(%Locator{} = locator, expression, options, _)
when is_map(options) do
options = Map.merge(%{strict: true, state: "attached"}, options)
with_element(locator, options, fn handle ->
ElementHandle.evaluate_handle(handle, expression)
end)
end
def evaluate_handle(%Locator{} = locator, expression, arg, options) do
options = Map.merge(%{strict: true, state: "attached"}, options)
with_element(locator, options, fn handle ->
ElementHandle.evaluate_handle(handle, expression, arg)
end)
end
@doc """
Fills a form field or `contenteditable` element with text.
Waits for an element matching `param: selector`, waits for "actionability
checks", focuses the element, fills it and triggers an input event after
filling.
If the target element is not an ``, `