defmodule Playwright.Frame do
@moduledoc """
At any point of time, `Playwright.Page` exposes its current frame tree via
the `Playwright.Page.main_frame/1` and `Playwright.Frame.child_frames/1`
functions.
A `Frame` instance lifecycle is governed by three events, dispatched on the
`Playwright.Page` resource:
- `Page event: :frame_attached` - fired when the frame gets attached to the
page. A Frame can be attached to the page only once.
- `Page event: :frame_navigated` - fired when the frame commits navigation
to a different URL.
- `Page event: :frame_detached` - fired when the frame gets detached from
the page. A Frame can be detached from the page only once.
"""
use Playwright.ChannelOwner
alias Playwright.Channel.Event
alias Playwright.{ChannelOwner, ElementHandle, Frame, Helpers, Response}
@property :load_states
@property :url
@type evaluation_argument :: any()
@type expression :: binary()
@type options :: map()
@type serializable :: any()
@type load_state :: atom() | binary()
# callbacks
# ---------------------------------------------------------------------------
@impl ChannelOwner
def init(%Frame{session: session} = frame, _initializer) do
Channel.bind(session, {:guid, frame.guid}, :loadstate, fn %{params: params} = event ->
target = event.target
case params do
%{add: state} ->
{:patch, %{target | load_states: target.load_states ++ [state]}}
%{remove: state} ->
{:patch, %{target | load_states: target.load_states -- [state]}}
end
end)
Channel.bind(session, {:guid, frame.guid}, :navigated, fn event ->
{:patch, %{event.target | url: event.params.url}}
end)
{:ok, frame}
end
# API
# ---------------------------------------------------------------------------
# ---
# @spec add_script_tag(Frame.t(), options()) :: ElementHandle.t()
# def add_script_tag(frame, options \\ %{})
# @spec add_style_tag(Frame.t(), options()) :: ElementHandle.t()
# def add_style_tag(frame, options \\ %{})
# ---
@spec check(t(), binary(), options()) :: :ok
def check(%Frame{session: session} = frame, selector, options \\ %{}) do
params = Map.merge(%{selector: selector}, options)
Channel.post(session, {:guid, frame.guid}, :check, params)
end
# ---
# @spec child_frames(Frame.t()) :: [Frame.t()]
# def child_frames(frame)
# ---
@doc """
Clicks an element matching `param: selector`, performing the following steps:
1. Find an element matching `param: selector`. If there is none, wait until
a matching element is attached to the DOM.
2. Wait for "actionability (guide)" checks on the matched element, unless
`option: force` option is set. If the element is detached during the
checks, the whole action is retried.
3. Scroll the element into view if needed.
4. Use `Playwright.Page.Mouse` to click the center of the element, or the
specified `option: position`.
5. Wait for initiated navigations to either succeed or fail, unless
`option: :no_wait_after` option is set.
When all steps combined have not finished during the specified
`option: timeout`, `/click/3` raises a `TimeoutError`. Passing zero for
`option: timeout` disables this.
"""
@spec click(t(), binary(), options()) :: :ok
def click(owner, selector, options \\ %{})
def click(%Frame{session: session} = frame, selector, options) do
params =
Map.merge(
%{
selector: selector,
timeout: 30_000,
wait_until: "load"
},
options
)
Channel.post(session, {:guid, frame.guid}, :click, params)
end
# ---
# @spec content(Frame.t()) :: binary()
# def content(frame)
# ---
@doc """
Double clicks an element matching selector.
Performs the following steps:
1. Find an element matching `param: selector`. If there is none, wait until
a matching element is attached to the DOM.
2. Wait for actionability checks on the matched element, unless
`option: force` is set. If the element is detached during the checks, the
whole action is retried.
3. Scroll the element into view if needed.
4. Use `Page.Mouse` to double click in the center of the element, or the
specified `option: position`.
5. 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.
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 |
| ---------------- | ------ | --------------------------------- | ----------- |
| `selector` | param | `binary()` | A selector to search for an element. If there are multiple elements satisfying the selector, the first will be used. See "working with selectors (guide)" for more details. |
| `: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. |
| `:strict` | option | `boolean()` | When true, the call requires selector to resolve to a single element. If given selector resolves to more then one element, the call throws an exception. |
| `: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(Frame.t(), binary(), options()) :: :ok
def dblclick(%Frame{session: session} = frame, selector, options \\ %{}) do
params =
Map.merge(
%{
selector: selector
},
options
)
Channel.post(session, {:guid, frame.guid}, :dblclick, params)
end
@doc """
Dispatches the `param: type` event on the `param: selector` 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`:
Frame.dispatch_event(frame, "button#submit", :click)
Specify a `Playwright.JSHandle` as the property value to be passed into the
event:
data_transfer = Frame.evaluate_handle(frame, "new DataTransfer()")
Frame.dispatch_event(frame, "#source", :dragstart, { "dataTransfer": data_transfer })
## Returns
- `:ok`
## Arguments
| key/name | type | | description |
| ---------------- | ------ | ----------------------- | ----------- |
| `selector` | param | `binary()` | A selector to search for an element. If there are multiple elements satisfying the selector, the first will be used. See "working with selectors (guide)" for more details. |
| `type` | param | `atom()` or `binary()` | DOM event type: `:click`, `:dragstart`, etc. |
| `event_init` | param | `evaluation_argument()` | Optional event-specific initialization properties. |
| `:strict` | option | `boolean()` | When true, the call requires selector to resolve to a single element. If given selector resolves to more then one element, the call throws an exception. |
| `: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(Frame.t(), binary(), binary(), evaluation_argument(), options()) :: :ok
def dispatch_event(frame, selector, type, event_init \\ nil, options \\ %{})
def dispatch_event(%Frame{} = frame, selector, type, options, _)
when is_map(options) do
dispatch_event(frame, selector, type, nil, options)
end
def dispatch_event(%Frame{session: session} = frame, selector, type, event_init, options) do
params =
Map.merge(options, %{
selector: selector,
type: type,
event_init: Helpers.Serialization.serialize(event_init)
})
Channel.post(session, {:guid, frame.guid}, :dispatch_event, params)
end
# ---
# @spec drag_and_drop(Frame.t(), binary(), binary(), options()) :: :ok
# def drag_and_drop(frame, source, target, options \\ %{})
# @spec eval_on_selector(Frame.t(), binary(), expression(), any(), options()) :: :ok
# def eval_on_selector(frame, selector, expression, arg \\ nil, options \\ %{})
# @spec eval_on_selector_all(Frame.t(), binary(), expression(), any(), options()) :: :ok
# def eval_on_selector_all(frame, selector, expression, arg \\ nil, options \\ %{})
# ---
@doc """
Returns the return value of `expression`.
!!!
"""
@spec eval_on_selector(Frame.t(), binary(), binary(), term(), map()) :: term()
def eval_on_selector(frame, selector, expression, arg \\ nil, options \\ %{})
def eval_on_selector(%Frame{session: session} = frame, selector, expression, arg, _options) do
parse_result(fn ->
Channel.post(session, {:guid, frame.guid}, :eval_on_selector, %{
selector: selector,
expression: expression,
arg: serialize(arg)
})
end)
end
def eval_on_selector_all(%Frame{session: session} = frame, selector, expression, arg \\ nil) do
parse_result(fn ->
Channel.post(session, {:guid, frame.guid}, :eval_on_selector_all, %{
selector: selector,
expression: expression,
arg: Helpers.Serialization.serialize(arg)
})
end)
end
@doc """
Returns the return value of `expression`.
!!!
"""
@spec evaluate(t(), expression(), any()) :: :ok
def evaluate(owner, expression, arg \\ nil)
def evaluate(%Frame{session: session} = frame, expression, arg) do
parse_result(fn ->
Channel.post(session, {:guid, frame.guid}, :evaluate_expression, %{
expression: expression,
arg: serialize(arg)
})
end)
end
@doc """
Returns the return value of `expression` as a `Playwright.JSHandle`.
!!!
"""
@spec evaluate_handle(t(), expression(), any()) :: serializable()
def evaluate_handle(%Frame{session: session} = frame, expression, arg \\ nil) do
Channel.post(session, {:guid, frame.guid}, :evaluate_expression_handle, %{
expression: expression,
is_function: Helpers.Expression.function?(expression),
arg: Helpers.Serialization.serialize(arg)
})
end
# ---
# @spec expect_navigation(Frame.t(), function(), options()) :: Playwright.Response.t() | nil
# def expect_navigation(frame, trigger, options \\ %{})
# ---
@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 ``, `