defmodule PhoenixTestJsdom do @moduledoc """ A view-centric JSDom bridge for Phoenix LiveView testing. Mount a live view into a JSDom instance so client-side JavaScript (React hooks, etc.) executes alongside the live server-side process: {:ok, view, _html} = live(conn, "/react-counter") view = PhoenixTestJsdom.mount(view) html = view |> PhoenixTestJsdom.click("Increment", selector: "button") |> PhoenixTestJsdom.render() assert html =~ "Count: 1" Or use the pass-through tuple form: {:ok, view, html} = live(conn, "/react-counter") |> PhoenixTestJsdom.mount() ## Interaction functions All interaction functions return the view so you can pipe them: view |> PhoenixTestJsdom.click("Submit") |> PhoenixTestJsdom.render() The `render_*` variants mirror `Phoenix.LiveViewTest.render_*` names and return the HTML string directly, for compatibility with existing assertions: html = PhoenixTestJsdom.render_click(view, "button", "Increment") assert html =~ "Count: 1" ## Isolated LiveComponents Render a standalone LiveComponent into JSDom without a live URL: view = PhoenixTestJsdom.mount(MyComponent, %{id: "c", value: 0}, endpoint: MyAppWeb.Endpoint) assert PhoenixTestJsdom.render(view) =~ "value=\\"0\\"" ## Configuration All settings are read from `config :phoenix_test_jsdom, ...` in `config/test.exs`. | Option | Purpose | |--------|---------| | `node_path` | Absolute path to the Node binary. If unset, the worker tries `mise which node`, then `node` on `PATH`. | | `setup_files` | List of CommonJS module paths (or a single string path) executed in each new JSDom window—polyfills, global mocks, test setup (similar to Vitest `setupFiles`). | | `cwd` | Working directory for the Node process. Use this when scripts or `require()` must resolve packages from your app (for example your own `node_modules`). | ```elixir # config/test.exs config :phoenix_test_jsdom, node_path: "/opt/homebrew/bin/node", setup_files: [Path.expand("test/support/jsdom_setup.cjs", __DIR__)], cwd: Path.expand("../assets", __DIR__) ``` ### Setup modes (global vs per-file) **Global (recommended)** — start once before `ExUnit.start/0` so every test module shares one Node worker and request queue, while each mounted view or `PhoenixTestJsdom.Session` still gets its own JSDom instance id. ```elixir # test/test_helper.exs {:ok, _} = PhoenixTestJsdom.start() ExUnit.start() ``` **Per-file (or per-module)** — start the supervisor under the test supervisor when only some tests need JSDom, or when you need different `setup_files` / `cwd` per test module (each `setup_all` run gets its own tree—heavier than global). ```elixir defmodule MyApp.HeavyJsTest do use ExUnit.Case, async: true use MyAppWeb.ConnCase setup_all do start_supervised(PhoenixTestJsdom) :ok end # ... end ``` Use **global** for the usual case (`async: true` across the suite). Use **per-file** when isolating optional JS-heavy tests. ### Async & isolation model - **One** long-lived Node process (Erlang port) runs the bundled server; it multiplexes concurrent RPCs by request id. - **Each** `mount/1` (LiveView) or `Session.new/1` (PhoenixTest) allocates a **separate** JSDom instance id, stored in `PhoenixTestJsdom.ViewRegistry` keyed by `{test_pid, view_id}`. Parallel async tests do not share DOM state. - After server-driven updates (`render_click/2`, `render_patch/2`, `render_async/2`, etc.), HTML is **re-seeded** into the same JSDom id so the client bundle matches the LiveView-rendered markup. Pure client state that is not in the HTML snapshot is reset on each reseed—see the README note on LiveViewTest interop. **Waiting for client-rendered DOM** — heavy widgets (Monaco, charts) often appear after `mount/1` returns. Block on a stable selector before asserting: ```elixir {:ok, view, _} = live(conn, "/editor") |> PhoenixTestJsdom.mount() view |> PhoenixTestJsdom.wait_for(".monaco-editor", 10_000) |> PhoenixTestJsdom.render() ``` With `PhoenixTestJsdom.Session` (PhoenixTest driver), `wait_for/3` returns the session for piping. The default timeout is 5000 ms. Lower-level callers can use `PhoenixTestJsdom.Jsdom.wait_for_selector/3` with an instance id (see the debug tests). ## User interactions & event firing **LiveView + pipable helpers** (`PhoenixTestJsdom` on `%Phoenix.LiveViewTest.View{}`) dispatch real DOM events inside JSDom: `click/3`, `click_link/3`, `fill_in/3`, `select/3`, `check/3`, `uncheck/3`, `choose/3`, `submit/2`, `type/3`, etc. Options commonly include `:selector`, `:within`, and label-based matching as in PhoenixTest. **Low-level DOM events** — `PhoenixTestJsdom.FireEvent` mirrors many DOM event names (`click/3`, `change/3`, `key_down/3`, …) and takes a `Phoenix.LiveViewTest.Element` from `element/2` or `element/3`. Use this when you need precise `Event` fields or events not wrapped by the pipable API. Examples: - [FireEvent and DOM coverage](https://github.com/tzeyiing/phoenix_test_jsdom/blob/main/test/phoenix_test_jsdom/fire_event_test.exs) - [PhoenixTest session interactions](https://github.com/tzeyiing/phoenix_test_jsdom/blob/main/test/phoenix_test_jsdom/interactions_test.exs) - [Per-file startup + `render_click` interop](https://github.com/tzeyiing/phoenix_test_jsdom/blob/main/test/phoenix_test_jsdom/plain_liveview_per_file_test.exs) ## Using with LiveViewTest Obtain a view with `live/2` (or `live_isolated/3`), then call `mount/1` on the `%Phoenix.LiveViewTest.View{}` or on the `{:ok, view, html}` tuple so scripts run and the LiveView client can attach. ```elixir import Phoenix.LiveViewTest {:ok, view, _} = live(conn, "/counter") |> PhoenixTestJsdom.mount() html = view |> PhoenixTestJsdom.click("Increment", selector: "button") |> PhoenixTestJsdom.render() assert html =~ "Counter: 1" ``` Prefer **`PhoenixTestJsdom.render/1`** after interactions when you need the DOM as JSDom sees it (including client-only updates). It falls back to `Phoenix.LiveViewTest.render/1` when the view is not mounted in JSDom, so you can share helpers between JS and non-JS tests. When you drive the server with **`Phoenix.LiveViewTest`** (`render_click/2`, `render_patch/2`, `render_hook/3`, etc.), use the **`PhoenixTestJsdom.render_*`** wrappers on the view (same names, same arguments) instead of the plain `LiveViewTest` versions whenever the view is JSDom-mounted: they run the server handler, **re-seed** JSDom with the returned HTML, and return the HTML string. That keeps LiveView and the in-memory DOM aligned after server-driven updates. ```elixir {:ok, view, _} = live(conn, "/counter") |> PhoenixTestJsdom.mount() _html = PhoenixTestJsdom.render_click(view, selector: "button", text: "Increment") assert PhoenixTestJsdom.render(view) =~ "Counter: 1" ``` ### Using with LiveView Components Use **`mount/3`** to render a single `Phoenix.LiveComponent` into JSDom without navigating to a route. Pass the component module, assigns, and **`endpoint:`** (required) so asset URLs resolve the same way as in app tests. The library builds a synthetic `LiveViewTest.View` and seeds JSDom from `render_component/2`. Pipe `PhoenixTestJsdom.render/1` and the interaction helpers the same as for a full-page LiveView. ```elixir view = PhoenixTestJsdom.mount(MyAppWeb.MyComponent, %{id: "c", value: 0}, endpoint: MyAppWeb.Endpoint ) html = view |> PhoenixTestJsdom.click("Add", selector: "button") |> PhoenixTestJsdom.render() assert html =~ "value=\\"1\\"" ``` ## Using with PhoenixTest **`PhoenixTestJsdom.Session`** implements `PhoenixTest.Driver`. Build a session with `Session.new(MyAppWeb.Endpoint)` (typically in a `setup` block), `import PhoenixTest`, and pipe **`visit/2`**, **`click_button/2`**, **`fill_in/3`**, **`assert_has/3`**, and the rest of the PhoenixTest API as usual—the driver runs actions and assertions against JSDom instead of static HTML parsing. **`PhoenixTestJsdom.wait_for/3`**, **`type/3`**, and **`exec_js/2`** are also defined for `%PhoenixTestJsdom.Session{}` so you can wait on selectors, type like a user, or evaluate snippets in the same window after PhoenixTest steps. ```elixir import PhoenixTest setup do {:ok, session: PhoenixTestJsdom.Session.new(MyAppWeb.Endpoint)} end test "counter via PhoenixTest + JSDom", %{session: session} do session |> visit("/react-counter") |> click_button("Increment") |> assert_has("#count-display", text: "Count: 1") end ``` ```elixir session |> visit("/search") |> PhoenixTestJsdom.wait_for("#results", 5_000) |> PhoenixTestJsdom.type("hello", selector: "input[name=q]") |> assert_has("#results", text: "hello") ``` ## Advanced ### Executing Custom JS **`exec_js/2`** accepts a JavaScript string and runs it in the mounted window (LiveView view or session). It returns **`{:ok, string}`** with the last expression coerced to string, or **`{:error, message}`** on runtime errors. Use this for small assertions, toggling feature flags in `window`, or calling app globals when there is no dedicated Elixir helper. ```elixir {:ok, width} = PhoenixTestJsdom.exec_js(view, "document.querySelector('#chart').clientWidth") assert String.to_integer(width) > 0 ``` ```elixir {:ok, _} = PhoenixTestJsdom.exec_js(session, "window.__FLAGS = { skipAnalytics: true }") ``` ### Triggering custom events For **`CustomEvent`** (or any non-generated RTL name), use **`PhoenixTestJsdom.FireEvent.fire/4`**: pass the view, an `element/2` or `element/3` selector, the event type string, and an optional detail map merged into the event init (for example **`%{detail: %{id: 1}}`**). Pipe **`render/1`** afterward to read the DOM. Server-side hooks still go through **`render_hook/3`** (via the **`PhoenixTestJsdom.render_hook/3`** wrapper when JSDom-mounted). ```elixir import Phoenix.LiveViewTest, only: [element: 2] alias PhoenixTestJsdom.FireEvent html = view |> FireEvent.fire(element(view, "#sidebar"), "sidebar:toggle", %{detail: %{open: true}}) |> PhoenixTestJsdom.render() assert html =~ ~s(aria-expanded="true") ``` ```elixir _html = PhoenixTestJsdom.render_hook(view, :refresh, %{deg: 32}) ``` ### Mounting React components Load your bundle from the LiveView template or layout (for example a root hook that calls `createRoot`) and **`mount/1`** the route or static page as for any other JS. JSDom executes the scripts, the client connects over the test WebSocket, and **`click/3`** / **`wait_for/3`** exercise the result. Point **`cwd`** at your assets tree if the bundle uses `require` of packages from **`node_modules`**. ```elixir {:ok, view, _} = live(conn, "/react-counter") |> PhoenixTestJsdom.mount() html = view |> PhoenixTestJsdom.wait_for("#count-display", 5_000) |> PhoenixTestJsdom.click("Increment", selector: "button") |> PhoenixTestJsdom.render() assert html =~ "Count: 1" ``` ### Adding shims/stubs with setup files Set **`setup_files`** in **`config :phoenix_test_jsdom`** to a list of CommonJS paths. Each file runs once when a new JSDom window is created—before your page scripts—so you can assign **`globalThis.fetch` mocks**, polyfills, etc. Paths should be absolute (for example with **`Path.expand/2`**). Combine with **`cwd`** when modules need to resolve from your app’s **`node_modules`**. ```elixir # config/test.exs config :phoenix_test_jsdom, setup_files: [Path.expand("test/support/jsdom_setup.cjs", __DIR__)], cwd: Path.expand("../assets", __DIR__) ``` ```javascript // test/support/jsdom_setup.cjs globalThis.fetch = async () => ({ ok: true, status: 200, json: async () => ({ items: [] }) }); ``` """ alias PhoenixTestJsdom.{Jsdom, ViewRegistry} alias Phoenix.LiveViewTest require Phoenix.LiveViewTest # --------------------------------------------------------------------------- # Supervisor # --------------------------------------------------------------------------- @doc """ Starts the PhoenixTestJsdom supervision tree. Call from test_helper.exs. Configure via `config :phoenix_test_jsdom, key: value` in `config/test.exs`. """ defdelegate start_link(), to: PhoenixTestJsdom.Supervisor def child_spec(_opts), do: PhoenixTestJsdom.Supervisor.child_spec([]) @doc "Same as `start_link/0`." def start(), do: start_link() # --------------------------------------------------------------------------- # Mount # --------------------------------------------------------------------------- @doc """ Mounts a LiveViewTest view (or `{:ok, view, html}` tuple) into a JSDom instance and returns the view. The pass-through tuple form is convenient when chaining directly after `live/2`: {:ok, view, html} = live(conn, "/react-counter") |> PhoenixTestJsdom.mount() """ def mount({:ok, %LiveViewTest.View{} = view, html}) do mounted = do_mount_view(view, nil) {:ok, mounted, html} end def mount({:error, _} = err), do: err def mount(%LiveViewTest.View{} = view) do do_mount_view(view, nil) end @doc """ Renders an isolated LiveComponent into a JSDom instance and returns a synthetic view. Requires `:endpoint` in `opts` to derive the base URL for script resolution. view = PhoenixTestJsdom.mount(MyComponent, %{id: "c"}, endpoint: MyEndpoint) """ def mount(component, assigns \\ %{}, opts \\ []) when is_atom(component) do endpoint = opts[:endpoint] || raise ArgumentError, "PhoenixTestJsdom.mount/3 for a LiveComponent requires :endpoint option" html = LiveViewTest.render_component(component, assigns) synthetic_view = %LiveViewTest.View{ pid: self(), id: make_ref() |> inspect(), endpoint: endpoint } do_mount_view(synthetic_view, html) end defp do_mount_view(%LiveViewTest.View{proxy: nil} = view, html) do id = generate_id() url = base_url(view.endpoint) <> "/" :ok = Jsdom.mount_html(id, wrap_document(html), url) ViewRegistry.put(registry_key(view), id) view end defp do_mount_view(%LiveViewTest.View{} = view, _html) do # The proxy stores the URL with the default test conn host (www.example.com), # so we rebase the path onto the actual endpoint base URL. {:ok, proxy_url} = GenServer.call(elem(view.proxy, 2), :url) %{path: path, query: query} = URI.parse(proxy_url) url = base_url(view.endpoint) <> path <> if(query, do: "?#{query}", else: "") id = generate_id() :ok = Jsdom.visit(id, url) ViewRegistry.put(registry_key(view), id) view end # --------------------------------------------------------------------------- # Inspect (read from JSDom) # --------------------------------------------------------------------------- @doc """ Returns the current HTML for the view. When the view has a JSDom instance (mounted via `mount/1`), returns the JSDom HTML — reflecting client-side mutations like React hook renders. Falls back to `Phoenix.LiveViewTest.render/1` for views not mounted in JSDom, so importing `render/1` from this module works for both JSDom and plain LV tests without needing to distinguish them at the call site. """ def render(%LiveViewTest.View{} = view) do case ViewRegistry.fetch(view) do {:ok, id} -> {:ok, html} = Jsdom.get_html(id) html :error -> LiveViewTest.render(view) end end @doc "Returns the page title from JSDom, or nil." def page_title(%LiveViewTest.View{} = view) do id = ViewRegistry.fetch!(view) case Jsdom.get_title(id) do {:ok, ""} -> nil {:ok, title} -> title _ -> nil end end @doc "Returns the current path from JSDom." def current_path(%LiveViewTest.View{} = view) do id = ViewRegistry.fetch!(view) {:ok, path} = Jsdom.get_current_path(id) path end # --------------------------------------------------------------------------- # Lifecycle # --------------------------------------------------------------------------- @doc "Destroys the JSDom instance for the view and removes it from the registry." def unmount(%LiveViewTest.View{} = view) do ViewRegistry.delete(registry_key(view)) view end # --------------------------------------------------------------------------- # render_* wrappers (return HTML, mirror LiveViewTest names) # --------------------------------------------------------------------------- @doc "Triggers a click event via LiveViewTest and re-mounts the resulting HTML into JSDom. Returns the HTML string." def render_click(view_or_element, value_or_opts \\ %{}) def render_click(%LiveViewTest.View{} = view, opts) when is_list(opts) do element = resolve_element(view, opts) html = LiveViewTest.render_click(element) reseed!(view, html) html end def render_click(%LiveViewTest.View{} = view, value) do html = LiveViewTest.render_click(view, value) reseed!(view, html) html end def render_click(%LiveViewTest.Element{} = element, value) do LiveViewTest.render_click(element, value) end @doc "Triggers a submit event and re-mounts the HTML. Returns HTML." def render_submit(view_or_element, value \\ %{}) def render_submit(%LiveViewTest.View{} = view, value) do html = LiveViewTest.render_submit(view, value) reseed!(view, html) html end def render_submit(%LiveViewTest.Element{} = element, value) do LiveViewTest.render_submit(element, value) end @doc "Triggers a change event and re-mounts the HTML. Returns HTML." def render_change(view_or_element, value \\ %{}) def render_change(%LiveViewTest.View{} = view, value) do html = LiveViewTest.render_change(view, value) reseed!(view, html) html end def render_change(%LiveViewTest.Element{} = element, value) do LiveViewTest.render_change(element, value) end @doc "Triggers a keydown event and re-mounts the HTML into JSDom when called with a view. Returns HTML." def render_keydown(view_or_element, event, value \\ %{}) def render_keydown(%LiveViewTest.View{} = view, event, value) do html = LiveViewTest.render_keydown(view, event, value) reseed!(view, html) html end def render_keydown(%LiveViewTest.Element{} = element, event, value) do LiveViewTest.render_keydown(element, event, value) end @doc "Triggers a keyup event and re-mounts the HTML into JSDom when called with a view. Returns HTML." def render_keyup(view_or_element, event, value \\ %{}) def render_keyup(%LiveViewTest.View{} = view, event, value) do html = LiveViewTest.render_keyup(view, event, value) reseed!(view, html) html end def render_keyup(%LiveViewTest.Element{} = element, event, value) do LiveViewTest.render_keyup(element, event, value) end @doc "Triggers a blur event and re-mounts the HTML into JSDom when called with a view. Returns HTML." def render_blur(view_or_element, event, value \\ %{}) def render_blur(%LiveViewTest.View{} = view, event, value) do html = LiveViewTest.render_blur(view, event, value) reseed!(view, html) html end def render_blur(%LiveViewTest.Element{} = element, event, value) do LiveViewTest.render_blur(element, event, value) end @doc "Triggers a focus event and re-mounts the HTML into JSDom when called with a view. Returns HTML." def render_focus(view_or_element, event, value \\ %{}) def render_focus(%LiveViewTest.View{} = view, event, value) do html = LiveViewTest.render_focus(view, event, value) reseed!(view, html) html end def render_focus(%LiveViewTest.Element{} = element, event, value) do LiveViewTest.render_focus(element, event, value) end @doc "Triggers a hook push event and re-mounts the HTML into JSDom when called with a view. Returns HTML." def render_hook(view_or_element, event, value \\ %{}) def render_hook(%LiveViewTest.View{} = view, event, value) do html = LiveViewTest.render_hook(view, event, value) reseed!(view, html) html end def render_hook(%LiveViewTest.Element{} = element, event, value) do LiveViewTest.render_hook(element, event, value) end @doc "Patches to a new path and re-mounts the HTML. Returns HTML." def render_patch(%LiveViewTest.View{} = view, path) do html = LiveViewTest.render_patch(view, path) reseed!(view, html) html end @doc "Waits for async operations and re-mounts the HTML. Returns HTML." def render_async(%LiveViewTest.View{} = view, timeout \\ 200) do html = LiveViewTest.render_async(view, timeout) reseed!(view, html) html end @doc "Advances an upload and re-mounts the HTML. Returns HTML." def render_upload(%LiveViewTest.Upload{} = upload, entry_name, percent \\ 100) do view = upload.view html = LiveViewTest.render_upload(upload, entry_name, percent) reseed!(view, html) html end @doc "Renders a LiveComponent to HTML, mounts it in JSDom, and returns a synthetic view." def render_component(component, assigns \\ %{}, opts \\ []) when is_atom(component) do mount(component, assigns, opts) end # --------------------------------------------------------------------------- # Pipable interaction functions (return view) # --------------------------------------------------------------------------- @doc """ Clicks a button in JSDom and returns the view. This dispatches the click directly in the JSDom instance, so React-rendered buttons and other client-side elements are found and clicked. Any LiveView `pushEvent` fired by the click is handled by the LV client running inside JSDom. Options: - `:selector` — CSS selector restricting where to look (e.g. `"button"`) - `:within` — scope selector The text argument filters by button label. """ def click(%LiveViewTest.View{} = view, text, opts \\ []) do id = ViewRegistry.fetch!(view) selector = opts[:selector] within = opts[:within] :ok = Jsdom.click_button(id, selector, text, within) view end @doc """ Clicks a link in JSDom and returns the view. Options: `:selector`, `:within`. """ def click_link(%LiveViewTest.View{} = view, text, opts \\ []) do id = ViewRegistry.fetch!(view) selector = opts[:selector] || "a" within = opts[:within] :ok = Jsdom.click_link(id, selector, text, within) view end @doc """ Fills in an input in JSDom and returns the view. Options: `:selector`, `:within`. Use `:with` for the value. PhoenixTestJsdom.fill_in(view, "Email", with: "hello@example.com") """ def fill_in(%LiveViewTest.View{} = view, label, opts) do id = ViewRegistry.fetch!(view) value = Keyword.fetch!(opts, :with) |> to_string() selector = opts[:selector] within = opts[:within] :ok = Jsdom.fill_in(id, selector, label, value, within) view end @doc "Submits a form in JSDom and returns the view. Options: `:selector`, `:within`." def submit(%LiveViewTest.View{} = view, opts \\ []) do id = ViewRegistry.fetch!(view) selector = opts[:selector] || "form" within = opts[:within] :ok = Jsdom.submit_form(id, selector, within) view end @doc "Selects an option in JSDom and returns the view. Use `:from` for the label." def select(%LiveViewTest.View{} = view, option, opts) do id = ViewRegistry.fetch!(view) label = Keyword.fetch!(opts, :from) selector = opts[:selector] within = opts[:within] :ok = Jsdom.select_option(id, selector, label, option, within) view end @doc "Checks a checkbox in JSDom and returns the view." def check(%LiveViewTest.View{} = view, label, opts \\ []) do id = ViewRegistry.fetch!(view) :ok = Jsdom.check(id, opts[:selector], label, opts[:within]) view end @doc "Unchecks a checkbox in JSDom and returns the view." def uncheck(%LiveViewTest.View{} = view, label, opts \\ []) do id = ViewRegistry.fetch!(view) :ok = Jsdom.uncheck(id, opts[:selector], label, opts[:within]) view end @doc "Chooses a radio button in JSDom and returns the view." def choose(%LiveViewTest.View{} = view, label, opts \\ []) do id = ViewRegistry.fetch!(view) :ok = Jsdom.choose(id, opts[:selector], label, opts[:within]) view end @doc "Waits for a CSS selector to appear in JSDom and returns the view or session." def wait_for(view_or_session, selector, timeout \\ 5000) def wait_for(%LiveViewTest.View{} = view, selector, timeout) do id = ViewRegistry.fetch!(view) :ok = Jsdom.wait_for_selector(id, selector, timeout) view end def wait_for(%PhoenixTestJsdom.Session{} = session, selector, timeout) do :ok = Jsdom.wait_for_selector(session.instance_id, selector, timeout) session end @doc """ Types text into the focused element (or the element matching `selector:` if given). Dispatches keydown/input/keyup events per character, mimicking user keyboard input. Use `\\n` to insert a newline / press Enter. Call `click/3` first to ensure an element is focused, or pass `selector:` directly. Returns the view or session for piping. """ def type(view_or_session, text, opts \\ []) def type(%LiveViewTest.View{} = view, text, opts) when is_binary(text) do id = ViewRegistry.fetch!(view) :ok = Jsdom.type_text(id, text, opts[:selector]) view end def type(%PhoenixTestJsdom.Session{} = session, text, opts) when is_binary(text) do :ok = Jsdom.type_text(session.instance_id, text, opts[:selector]) session end @doc "Evaluates JavaScript in the JSDom window and returns `{:ok, string_result}` or `{:error, msg}`." def exec_js(%LiveViewTest.View{} = view, code) do id = ViewRegistry.fetch!(view) Jsdom.exec_js(id, code) end def exec_js(%PhoenixTestJsdom.Session{} = session, code) do Jsdom.exec_js(session.instance_id, code) end @doc "Patches the view to a new path (server + JSDom) and returns the view." def patch(%LiveViewTest.View{} = view, path) do render_patch(view, path) view end @doc "Waits for async server operations and re-mounts HTML in JSDom. Returns view." def async(%LiveViewTest.View{} = view, timeout \\ 200) do render_async(view, timeout) view end @doc "Advances an upload and returns the view." def upload(upload, entry_name, percent \\ 100) do render_upload(upload, entry_name, percent) upload.view end # --------------------------------------------------------------------------- # Private helpers # --------------------------------------------------------------------------- defp base_url(endpoint) do %{scheme: scheme, host: host, port: port} = endpoint.struct_url() "#{scheme}://#{host}:#{port}" end defp wrap_document(html) do "#{html}" end defp generate_id do :crypto.strong_rand_bytes(8) |> Base.encode16(case: :lower) end defp registry_key(%LiveViewTest.View{pid: pid, id: id}), do: {pid, id} defp reseed!(%LiveViewTest.View{} = view, html) do id = ViewRegistry.fetch!(view) url = base_url(view.endpoint) <> "/" :ok = Jsdom.patch_html(id, html, url) end defp resolve_element(%LiveViewTest.View{} = view, opts) do selector = opts[:selector] || raise ArgumentError, "expected :selector option" text = opts[:text] if text, do: LiveViewTest.element(view, selector, text), else: LiveViewTest.element(view, selector) end end