Current section
Files
Jump to
Current section
Files
guides/testing.md
# Testing
`Masu.Test` reads and queries the grids of a LiveView in
`Phoenix.LiveViewTest`, with no browser.
```elixir
import Masu.Test
test "finds the late orders", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/orders")
assert total(view, "orders") == 120
view
|> filter("orders", [{"status", :eq, "late"}])
|> sort("orders", {"total", :desc})
assert [%{"number" => "A-1042", "total" => "250.50"} | _] = rows(view, "orders")
end
```
The functions send the events that the browser sends, so the grid
applies a sort, a filter or a search as it does for a person: the source
sorts, filters and counts (in the database, for an Ecto source), and
`rows/3` reads the rows it gives.
## Reading
* `rows(view, id, from: 0, count: 100)`: the rows, in order. A row is a
map of each column's key to its value as the browser gets it: numbers
and booleans as they are, decimals as strings, dates and times in ISO
8601, and for a column of your own HEEx the text of its cell.
* `ids(view, id)`: the ids of the rows.
* `total(view, id)`: how many rows the query has.
* `columns(view, id)`: the labels, in order.
* `values(view, id, key, search: nil)`: what the column's filter menu
offers, as `{value, count}`, the most frequent first.
* `place(view, id, row_id)`: where the row is in the grid's order (from
0), or `nil` when its filters leave it out: what a person who follows
someone scrolls to.
## Editing
* `edit(view, id, edits, opts \\ [])`: edits cells as a person does, with
a list of `{row_id, key, value}` (the value as the browser sends it: a
string, or `nil` to empty the cell). Gives `:ok`, `:async` (your
`on_update` saves them later), or `{:error, errors}` with `{row_id,
key, message}` for each cell that was not saved. An edit `{row_id, key,
value, before}` carries the value the person saw, and is refused when
the cell changed meanwhile. `origin: :paste` (`:fill`, `:undo`...) is
what your `on_update` sees as the write's `origin`; `:test` unless
given.
## Querying
Each returns the view, so they pipe:
* `sort(view, id, sorts)`: a key, `{key, :asc | :desc}`, or a list of
them, the first the most important. `[]` takes the order away.
* `filter(view, id, filters)`: a list of `{key, op, value}`, which
replaces the grid's filters. See `Masu.Filter` for the operators.
* `search(view, id, text)`: as the search box.
A column of a related row's field is named by its key, the path with
dots unless it has its own, and its rows' values are lists for a column
of many values:
```elixir
view
|> sort("orders", {"customer.name", :desc})
|> filter("orders", [{"tags", :all_of, ["gift", "urgent"]}])
assert [%{"customer.name" => "Ana", "tags" => ["gift", "urgent"]} | _] = rows(view, "orders")
```
A query that the grid refuses (a filter on a column that does not filter,
a value that is not of the column's type) fails the test with the
grid's reason.
## Layout and views
* `layout(view, id, changes)`: moves, hides, pins and resizes columns,
as the columns menu and the headers do: `order: keys`, `hidden: keys`,
`pinned: %{key => :start | :end}`, `widths: %{key => pixels}`.
* `view(view, id)`: the grid's view, as its params (`%{}` is the grid's
own view; see `Masu.View`).
* `apply_view(view, id, params)`: applies a view, as a link or a saved
view does; gives the keys of the parts that no longer apply.
`columns/2` and `rows/3` read the columns that show, in their order.
## Groups and totals
* `group(view, id, keys)`: groups by columns, as the header menu does;
`[]` stops.
* `groups(view, id, opts)`: the groups' rows, as maps of `:id`, `:key`,
`:value`, `:count`, `:level`, `:expanded` and `:totals`.
* `totals(view, id)`: the row of totals, by column key.
## Counts and pages
* `count(view, id)`: `{:exact, n}`, or `{:at_least, n}` while the grid
does not know its count; `total/2` gives `n`.
* `ask_count(view, id)`: asks for the count, as Ctrl+End does.
* `page(view, id, page)`: goes to a page (from 1) of a grid with
`paging={:pages}`, and gives the ids of its rows.
An editor of yours that is a form: `open_editor(view, id, row_id, key)`
renders it, which `render/1` then has, and `close_editor(view, id)`
closes it; `edit/4` saves what it would post (text, or a map of params:
`%{"from" => ..., "to" => ...}`). See [Editors of your own](editors.md).
## New and deleted rows
* `add_rows(view, id, rows, opts \\ [])`: adds rows as "+ New row" does,
each a map of column keys to values; `{:ok, ids}`, or `{:error,
results}`.
* `delete_rows(view, id, ids_or_selection, opts \\ [])` and
`restore_rows(view, id, undo)`: deletes rows (or `:selection`), and
brings them back; `:async` when your `on_delete` deletes them later.
A write saved later (`Masu.Write.async/1`) waits for its reply: the job
your function started calls `Masu.reply/2` (with Oban's testing mode,
run it with `perform_job/2`), and `Phoenix.LiveViewTest.assert_push_event/3`
sees the grid's `"masu:reply"`.
## Export and selection
* `export(view, id, format, opts \\ [])`: exports as the Export menu
does, downloads the file through your router's export route, and gives
it, `{name, binary}`; `conn:` a conn with what the route's pipeline
wants (a signed-in user).
* `export_url(view, id, format)`: the link the browser would get, to test
the route itself (that it refuses a person who is not signed in).
* `select(view, id, ids, on \\ true)`, `select_all(view, id, on \\ true)`
and `selection(view, id)`.
## Trees and details
* `nodes(view, id, opts)`: the rows of a tree, as maps of `:id`,
`:level`, `:children` and `:expanded`.
* `expand(view, id, row_id)`, `collapse(view, id, row_id)` and
`expand_siblings(view, id, row_id)`: as the arrows and `*`.
* `detail(view, id, row_id, open \\ true)`: opens a row's detail, which
`render/1` then has; a grid in it is read with its own id.
## History
* `history(view, id, row_id, key \\ nil)`: the history of a cell, or of
a row, newest first, as maps of `:id`, `:key`, `:op`, `:before`,
`:after`, `:user`, `:origin`, `:batch`, `:batch_size` and `:at`.
* `restore(view, id, entry_id)`, or `restore(view, id, {:batch, batch})`:
brings a value (or a batch) back as Restore and Undo all do;
`{:ok, count}` or `{:error, errors}`. See [History](history.md).
## Actions
* `actions(view, id, :selection)`: the labels of the actions on the rows
selected (none while no row is); `actions(view, id, :row, row_id)`:
those a row shows, available or not.
* `action(view, id, label)` and `row_action(view, id, label, row_id)`:
run one as the browser does, past its question; `:ok`, `{:ok,
message}`, `{:error, message}`, `:async`, a link's `{:navigate, path}`
or `{:patch, path}`, or `{:error, :refused}` when the server refuses
it (not offered, hidden or disabled for the row, no row selected). See
[Actions](actions.md).
* `activate_row(view, id, row_id)`: opens a row as a click on it or
Enter does (`on_row_activate`); `:ok`, the `{:patch, path}` or
`{:navigate, path}` the grid follows, or `{:error, :refused}` (no
`on_row_activate`, no such row).
## Menus of your own
* `actions(view, id, :header | :cell, key)`: the labels of the items
the grid offers for a column.
* `header_action(view, id, label, key)` and `cell_action(view, id,
label, key, row_id, opts)`: run an item, as a click on it does; its
panel is then in `render/1`. `close_panel(view, id)` closes it. See
[Menus of your own](menus.md).
## Slow sources and controlled grids
The helpers wait for a grid's tasks (`load={:async}`), as the browser
does, and fail with the grid's error when a read fails: a test reads the
same either way. In a controlled grid (`on_query`), a sort or a filter
reaches the LiveView, which runs its query: `render_async/1` waits for
a query that it runs in a task, before the rows are read.
## What these tests do not cover
`Masu.Test` does not run the browser: the drawing of the rows, the
keyboard, the selection and the clipboard are tested by Masu's own
browser tests.