defmodule Playwright.ElementHandle do @moduledoc """ `ElementHandle` represents an in-page DOM element. `ElementHandles` can be created with the `Playwright.Page.query_selector/3` function, and similar. > ⚠️ DISCOURAGED > > The use of `Playwright.ElementHandle` is discouraged; use > `Playwright.Locator` instances and web-first assertions instead. ## Example handle = Page.q(page, "a") :ok = ElementHandle.click(handle) `ElementHandle` prevents DOM elements from garbage collection unless the handle is disposed with `Playwright.JSHandle.dispose/1`. `ElementHandles` are auto-disposed when their origin frame is navigated. An `ElementHandle` instance can be used as an argument in `Playwright.Page.eval_on_selector/5` and `Playwright.Page.evaluate/3`. ## NOTE > In most cases, you would want to use `Playwright.Locator` instead. You > should only use `ElementHandle` if you want to retain a handle to a > particular DOM node that you intend to pass into > `Playwright.Page.evaluate/3` as an argument. The difference between `Playwright.Locator` and `ElementHandle` is that `ElementHandle` points to a particular element, while `Playwright.Locator` captures the logic of how to retrieve an element. In the example below, `handle` points to a particular DOM element on the page. If that element changes text or is used by JavaScript to render an entirely different component, `handle` still points to that very DOM element. This can lead to unexpected behaviors. handle = Page.q("text=Submit") ElementHandle.hover(handle) ElementHandle.click(handle) With the `Playwright.Locator`, every time the `locator` is used, an up-to-date DOM element is located in the page using the selector. So, in the snippet below, the underlying DOM element is going to be located twice. locator = Page.locator("text=Submit") Locator.hover(locator) Locator.click(locator) """ use Playwright.ChannelOwner alias Playwright.{Channel, ChannelOwner, ElementHandle, Frame, JSHandle} @property :preview @typedoc "A map/struct providing call options" @type options :: map() # callbacks # --------------------------------------------------------------------------- @impl ChannelOwner def init(%ElementHandle{session: session} = handle, _initializer) do Channel.bind(session, {:guid, handle.guid}, :preview_updated, fn %{params: params} = event -> {:patch, %{event.target | preview: params.preview}} end) {:ok, handle} end # delegates # --------------------------------------------------------------------------- defdelegate evaluate(handle, expression, arg \\ nil), to: JSHandle defdelegate evaluate_handle(handle, expression, arg \\ nil), to: JSHandle defdelegate string(handle), to: JSHandle # API # --------------------------------------------------------------------------- @spec bounding_box(ElementHandle.t()) :: map() | nil def bounding_box(%ElementHandle{session: session} = handle) do Channel.post(session, {:guid, handle.guid}, :bounding_box) end # --- # @spec check(ElementHandle.t(), options()) :: :ok # def check(handle, options \\ %{}) # @spec click(ElementHandle.t(), options()) :: :ok # def click(handle, options \\ %{}) # --- @doc """ Clicks on the element, performing the following steps: 1. Wait for "actionability (guide)" checks on the element, unless `force: true` option is set. 2. Scroll the element into view, if needed. 3. Use `Playwright.Page.Mouse` to click the center of the element, or the specified option: `position`. 4. Wait for initiated navigations to either succeed or fail, unless `no_wait_after: true` option is set. If the element is detached from the DOM at any moment during the action, this function raises. When all steps combined have not finished during the specified `:timeout`, this function raises a `TimeoutError`. Passing zero (`0`) for timeout disables this. """ @spec click(t(), options()) :: :ok def click(%ElementHandle{session: session} = handle, options \\ %{}) do Channel.post(session, {:guid, handle.guid}, :click, options) end @doc """ Returns the `Playwright.Frame` for element handles referencing iframe nodes, or `nil` otherwise. """ @spec content_frame(t()) :: Frame.t() | nil def content_frame(%ElementHandle{session: session} = handle) do Channel.post(session, {:guid, handle.guid}, :content_frame) end # --- # @spec dblclick(ElementHandle.t(), options()) :: :ok # def dblclick(handle, options \\ %{}) # @spec dispatch_event(ElementHandle.t(), event(), evaluation_argument()) :: :ok # def dispatch_event(handle, type, arg \\ nil) # @spec fill(ElementHandle.t(), binary(), options()) :: :ok # def fill(handle, value, options \\ %{}) # @spec focus(ElementHandle.t()) :: :ok # def focus(handle) # --- @doc """ Returns the value of an element's attribute. """ @spec get_attribute(t(), binary()) :: binary() | nil def get_attribute(%ElementHandle{session: session} = handle, name) do Channel.post(session, {:guid, handle.guid}, :get_attribute, %{name: name}) end # --- # @spec hover(ElementHandle.t(), options()) :: :ok # def hover(handle, options \\ %{}) # @spec inner_html(ElementHandle.t()) :: binary() | nil # def inner_html(handle) # @spec inner_text(ElementHandle.t()) :: binary() | nil # def inner_text(handle) # @spec input_value(ElementHandle.t(), options()) :: binary() # def input_value(handle, options) # @spec is_checked(ElementHandle.t()) :: boolean() # def is_checked(handle) # @spec is_disabled(ElementHandle.t()) :: boolean() # def is_disabled(handle) # @spec is_editable(ElementHandle.t()) :: boolean() # def is_editable(handle) # @spec is_enabled(ElementHandle.t()) :: boolean() # def is_enabled(handle) # @spec is_hidden(ElementHandle.t()) :: boolean() # def is_hidden(handle) # --- @spec is_visible(t()) :: boolean() def is_visible(%ElementHandle{session: session} = handle) do Channel.post(session, {:guid, handle.guid}, :is_visible) end # --- # @spec press(ElementHandle.t(), binary(), options()) :: :ok # def press(handle, key, options \\ %{}) # --- @doc """ Searches within an element for a DOM element matching the given selector. Finds an element matching the specified selector within the subtree of the `ElementHandle`. See "working with selectors (guide)" for more details. If no elements match the selector, returns `nil`. """ @spec query_selector(t(), binary()) :: ElementHandle.t() | nil def query_selector(handle, selector) def query_selector(%ElementHandle{session: session} = handle, selector) do Channel.post(session, {:guid, handle.guid}, :query_selector, %{selector: selector}) end defdelegate q(handle, selector), to: __MODULE__, as: :query_selector # --- # @spec query_selector_all(ElementHandle.t(), binary()) :: [ElementHandle.t()] # def query_selector_all(handle, selector) # defdelegate qq(handle, selector), to: __MODULE__, as: :query_selector_all # --- @spec screenshot(ElementHandle.t(), options()) :: binary() def screenshot(%ElementHandle{session: session} = handle, options \\ %{}) do case Map.pop(options, :path) do {nil, params} -> encoded = Channel.post(session, {:guid, handle.guid}, :screenshot, params) Base.decode64!(encoded) {path, params} -> [_, filetype] = String.split(path, ".") encoded = Channel.post(session, {:guid, handle.guid}, :screenshot, Map.put(params, :type, filetype)) decoded = Base.decode64!(encoded) File.write!(path, decoded) decoded end end @spec scroll_into_view(ElementHandle.t(), options()) :: :ok def scroll_into_view(%ElementHandle{session: session} = handle, options \\ %{}) do Channel.post(session, {:guid, handle.guid}, :scroll_into_view_if_needed, options) end # --- # @spec select_option(ElementHandle.t(), selection(), options()) :: [binary()] # def select_option(handle, values, options \\ %{}) # --- @spec select_text(ElementHandle.t(), options()) :: :ok def select_text(%ElementHandle{session: session} = handle, options \\ %{}) do Channel.post(session, {:guid, handle.guid}, :select_text, options) end # --- # @spec set_checked(ElementHandle.t(), boolean(), options()) :: :ok # def set_checked(handle, checked, options \\ %{}) # @spec set_input_files(ElementHandle.t(), file_list(), options()) :: :ok # def set_input_files(handle, files, options \\ %{}) # @spec tap(ElementHandle.t(), options()) :: :ok # def tap(handle, options \\ %{}) # --- @doc """ Returns the `node.textContent` (all text within the element). """ @spec text_content(t()) :: binary() | nil def text_content(handle) def text_content(%ElementHandle{session: session} = handle) do Channel.post(session, {:guid, handle.guid}, :text_content) end # --- # @spec type(ElementHandle.t(), binary(), options()) :: :ok # def type(handle, text, options \\ %{}) # @spec uncheck(ElementHandle.t(), options()) :: :ok # def uncheck(handle, options \\ %{}) # @spec wait_for_element_state(ElementHandle.t(), state(), options()) :: :ok # def wait_for_element_state(handle, state, options \\ %{}) # @spec wait_for_selector(ElementHandle.t(), binary(), options()) :: ElementHandle.t() | nil # def wait_for_selector(handle, selector, options \\ %{}) # --- end