Coelho.LiveViewTest (coelho v0.14.0)

Copy Markdown View Source

Driving and reading the editor from a LiveView test.

The editor's container carries phx-update="ignore" — ProseMirror owns that subtree — so it is invisible to render_change/2: there is no input to fill and no text to assert on. What the server sees is the hidden input the hook writes the document into, and a test has to write it itself.

Doing that by hand is three lines of JSON encoding and parameter nesting at every call site, and getting the nesting wrong fails as "the form ignored the change" rather than as a mistake. This is those three lines.

import Coelho.LiveViewTest

test "the intro is saved", %{conn: conn} do
  {:ok, view, _html} = live(conn, ~p"/portal/edit")

  type(view, "page[intro_doc]", paragraph("bonjour"))

  assert document(view, "page[intro_doc]") == paragraph("bonjour")
end

Nothing here needs the browser: it writes what the hook would have written, and reads what the server rendered back.

Summary

Types

A hidden input's name, or the form field it was rendered from.

Functions

The document an editor is currently holding, decoded.

The parameters a change carrying this document would arrive with.

Posts a document as the editor would, and returns the rendered result.

Types

name()

@type name() :: String.t() | struct()

A hidden input's name, or the form field it was rendered from.

Functions

document(view_or_html, name)

@spec document(term(), name()) :: map() | String.t() | nil

The document an editor is currently holding, decoded.

Read off the hidden input the server rendered, which is where the editor's state is visible from Elixir. Returns nil when there is no such input, and the raw string when it does not hold a document — which is what a rejected document comes back as, so that the writer can fix it rather than lose it.

params(name, document, extra \\ %{})

@spec params(name(), map(), map()) :: map()

The parameters a change carrying this document would arrive with.

type/4 sends these; this is for a test that has its own way of sending — a render_submit, a render_hook, a controller post. The nesting is the browser's: page[intro_doc] becomes %{"page" => %{"intro_doc" => json}}, which is what Plug.Conn.Query reads back and what a form expects to see.

params("page[intro_doc]", document, %{"page" => %{"title" => "Été"}})
#=> %{"page" => %{"intro_doc" => "{…}", "title" => "Été"}}

type(view, name, document, opts \\ [])

@spec type(term(), name(), map(), keyword()) :: String.t()

Posts a document as the editor would, and returns the rendered result.

name is the hidden input's name — "page[intro_doc]" — or the %Phoenix.HTML.FormField{} it was rendered from. The document is encoded and nested into parameters the same way a browser would nest them.

Options

  • :event — the phx-change event to send, "validate" by default
  • :params — the form's other fields, merged into the parameters as a browser would send them, which means nested the way the form nests them: params: %{"post" => %{"title" => "t"}}, not %{"title" => "t"}. A changeset wanting a title reports the title when a change arrives without one, so this is how the document under test stays the thing under test — and a title merged at the top level never reaches handle_event("validate", %{"post" => params}, …) at all

There used to be a :form option here, sending the change through Phoenix.LiveViewTest.form/3 instead. It cannot work for this field and never could: form/3 refuses a hidden input whose value differs from the one that was rendered, and an editor's field is hidden and differs by definition — that is what typing is. It raised value for hidden "post[body]" must be one of […] at every call. Pass the other fields through :params.