diff --git a/.agents/skills/testing/SKILL.md b/.agents/skills/testing/SKILL.md index 45e9189e..f95f3b43 100644 --- a/.agents/skills/testing/SKILL.md +++ b/.agents/skills/testing/SKILL.md @@ -52,21 +52,62 @@ mix test --max-failures 5 ## Test Styles — When to Use Each -### PhoenixTest (`assert_has`, `click_button`, `click_link`, `visit`) +### PhoenixTest (`visit`, `click_button`, `click_link`, `fill_in`, `assert_has`) -Use for **page-level LiveView tests** where you're testing navigation and DOM presence: +The **primary framework** for all LiveView page-level tests. Prefer PhoenixTest over +`Phoenix.LiveViewTest.live/2` for all new and migrated tests. ```elixir -{:ok, view, _html} = live(conn, ~p"/collection") -assert_has(view, ".record-card") +# Navigation and assertions +conn +|> visit(~p"/collection") +|> assert_has("h1", "My Collection") +|> click_link("Next Page") +|> assert_path(~p"/collection?page=2") + +# Form interactions +conn +|> visit(~p"/record-sets/new") +|> fill_in("Name", with: "Favorites") +|> click_button("Save Set") +|> assert_has("p", "Favorites") ``` -### Phoenix.LiveViewTest (`form/3`, `render_submit/1`, `element/2`, `render_click/1`) +### ConnCase Auto-Imports -Use for **LiveComponent interactions** where `phx-target={@myself}` is involved: +`MusicLibraryWeb.ConnCase` auto-imports the following LiveViewTest functions (with `only:`): +- `render_async/1`, `render_change/1`, `render_click/3` +- `render_hook/2`, `render_hook/3` +- `element/2`, `element/3` +- `form/3` + +It also imports `PhoenixTest` in full. When a test needs LiveViewTest functions NOT in +the auto-import list (e.g. `render_click/1`, `render_submit/1`), add an explicit +`only:` import to the test file rather than importing the whole module. + +### The `unwrap/2` Escape Hatch + +When PhoenixTest can't handle an interaction directly, use `unwrap/2` to access the +underlying `Phoenix.LiveViewTest.View` and call LiveViewTest functions directly. +`unwrap/2` handles redirects automatically and returns a PhoenixTest session. ```elixir -html = render_submit(form(view, "#record-form", record: attrs)) +# Trigger a JS hook event +unwrap(session, &render_hook(&1, "reorder", %{"record_ids" => [3, 1, 2]})) + +# Submit a form inside a LiveComponent (phx-target={@myself}) +unwrap(session, fn view -> + view + |> form("#record-picker-navigation form", %{query: "search term"}) + |> render_submit() +end) + +# Click a non-button/non-link element with phx-click +unwrap(session, fn view -> + view + |> element("li[phx-click='add_record'][phx-value-record-id='#{id}']") + |> render_click() +end) ``` ### Context Tests (standard ExUnit + DataCase) @@ -163,6 +204,54 @@ Don't duplicate the same assertion across every function that calls the shared c When multiple routes share the same plug/middleware behaviour (e.g., auth), test it once with a loop or parameterised approach, not N separate identical tests. +## Test File Migration Checklist (LiveViewTest → PhoenixTest) + +When converting a test file from `Phoenix.LiveViewTest.live/2` to `PhoenixTest.visit/2`: + +1. **Replace `import Phoenix.LiveViewTest`** with explicit `only:` imports for any + functions still needed (see ConnCase auto-imports above). Common additions: + `render_click: 1`, `render_submit: 1`, `render_change: 1`, `form: 3`. + +2. **Replace `live(conn, path)`** with `visit(conn, path)`. The session returned by + `visit` pipes into all PhoenixTest helpers. + +3. **Replace `form/3` + `render_submit/1`** with `fill_in/3` + `click_button/2` (for + standard inputs with labels) or `unwrap` with `form/3` + `render_submit/1` (for + forms inside LiveComponents without labeled inputs). + +4. **Replace `form/3` + `render_change/1`** with `fill_in/3` (triggers `phx-change` + automatically) or `unwrap` with `form/3` + `render_change/1` (for Fluxon custom + components or label-less inputs). + +5. **Replace `element/2` + `render_click/1`** with `click_button/2` (for `