berrypod/test/berrypod_web/live/admin/pages_test.exs
jamey 660fda928f add admin page editor with block reordering and management
Stage 6 of the page builder: admin UI at /admin/pages for managing
page layouts. Page list shows all 14 pages grouped by category.
Editor supports reorder (up/down), add, remove, duplicate, save,
and reset to defaults. DirtyGuard JS hook warns on unsaved changes.
ARIA live regions announce block operations for screen readers.

Also: regenerate admin icons (81 rules via mix task with @layer
wrapping), add gen_smtp dep for SMTP email adapter, add :key to
page renderer block loop for correct LiveView diffing.

1309 tests, 0 failures.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 21:15:01 +00:00

301 lines
9.6 KiB
Elixir

defmodule BerrypodWeb.Admin.PagesTest do
use BerrypodWeb.ConnCase, async: false
import Phoenix.LiveViewTest
import Berrypod.AccountsFixtures
alias Berrypod.Pages
alias Berrypod.Pages.PageCache
setup do
PageCache.invalidate_all()
user = user_fixture()
%{user: user}
end
describe "unauthenticated" do
test "redirects to login", %{conn: conn} do
{:error, redirect} = live(conn, ~p"/admin/pages")
assert {:redirect, %{to: path}} = redirect
assert path == ~p"/users/log-in"
end
end
describe "page list" do
setup %{conn: conn, user: user} do
%{conn: log_in_user(conn, user)}
end
test "renders page list with groups", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/admin/pages")
assert html =~ "Pages"
assert html =~ "Marketing"
assert html =~ "Legal"
assert html =~ "Shop"
assert html =~ "Orders"
assert html =~ "System"
end
test "shows all 14 pages", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/pages")
assert has_element?(view, ".page-card-title", "Home page")
assert has_element?(view, ".page-card-title", "About")
assert has_element?(view, ".page-card-title", "Contact")
assert has_element?(view, ".page-card-title", "Product page")
assert has_element?(view, ".page-card-title", "Error")
end
test "shows block count per page", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/pages")
# Home has 4 default blocks
assert has_element?(view, ".page-card-meta", "4 blocks")
end
test "links to editor", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/pages")
view
|> element("a[href='/admin/pages/home']")
|> render_click()
assert_redirect(view, ~p"/admin/pages/home")
end
end
describe "page editor" do
setup %{conn: conn, user: user} do
%{conn: log_in_user(conn, user)}
end
test "renders editor with blocks", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/pages/home")
assert has_element?(view, ".block-card-name", "Hero banner")
assert has_element?(view, ".block-card-name", "Category navigation")
assert has_element?(view, ".block-card-name", "Featured products")
assert has_element?(view, ".block-card-name", "Image + text")
end
test "shows position numbers", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/pages/home")
assert has_element?(view, ".block-card-position", "1")
assert has_element?(view, ".block-card-position", "4")
end
test "shows back link", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/pages/home")
assert has_element?(view, "a[href='/admin/pages']", "Pages")
end
test "move block up reorders blocks", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/pages/home")
# Get the second block (category_nav) and move it up
page = Pages.get_page("home")
second_block = Enum.at(page.blocks, 1)
render_click(view, "move_up", %{"id" => second_block["id"]})
# The ARIA live region announces the move
assert has_element?(view, "[aria-live='polite']", "Category navigation moved to position 1")
end
test "move block down reorders blocks", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/pages/home")
page = Pages.get_page("home")
first_block = Enum.at(page.blocks, 0)
render_click(view, "move_down", %{"id" => first_block["id"]})
# Hero banner should now be at position 2
assert has_element?(view, "[aria-live='polite']", "Hero banner moved to position 2")
end
test "move up disabled for first block", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/pages/home")
page = Pages.get_page("home")
first_block = Enum.at(page.blocks, 0)
assert has_element?(
view,
"button[phx-value-id='#{first_block["id"]}'][phx-click='move_up'][disabled]"
)
end
test "move down disabled for last block", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/pages/home")
page = Pages.get_page("home")
last_block = Enum.at(page.blocks, -1)
assert has_element?(
view,
"button[phx-value-id='#{last_block["id"]}'][phx-click='move_down'][disabled]"
)
end
test "remove block", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/pages/home")
page = Pages.get_page("home")
block = Enum.at(page.blocks, 1)
render_click(view, "remove_block", %{"id" => block["id"]})
refute has_element?(view, ".block-card-name", "Category navigation")
assert has_element?(view, ".block-card-position", "3")
refute has_element?(view, ".block-card-position", "4")
end
test "duplicate block", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/pages/home")
page = Pages.get_page("home")
block = Enum.at(page.blocks, 0)
render_click(view, "duplicate_block", %{"id" => block["id"]})
# Should now have 5 blocks (position 5 exists)
assert has_element?(view, ".block-card-position", "5")
assert has_element?(view, "[aria-live='polite']", "Hero banner duplicated")
end
test "add block via picker", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/pages/home")
render_click(view, "show_picker")
assert has_element?(view, ".block-picker")
render_click(view, "add_block", %{"type" => "trust_badges"})
assert has_element?(view, ".block-card-name", "Trust badges")
refute has_element?(view, ".block-picker")
end
test "picker filter narrows results", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/pages/home")
render_click(view, "show_picker")
render_keyup(view, "filter_picker", %{"value" => "hero"})
assert has_element?(view, ".block-picker-item", "Hero banner")
refute has_element?(view, ".block-picker-item", "Trust badges")
end
test "picker only shows blocks allowed on page", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/pages/home")
render_click(view, "show_picker")
assert has_element?(view, ".block-picker-item", "Hero banner")
refute has_element?(view, ".block-picker-item", "Product hero")
refute has_element?(view, ".block-picker-item", "Cart items")
end
test "save persists blocks", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/pages/home")
page = Pages.get_page("home")
block = Enum.at(page.blocks, 1)
render_click(view, "remove_block", %{"id" => block["id"]})
render_click(view, "save")
assert render(view) =~ "Page saved"
saved = Pages.get_page("home")
assert length(saved.blocks) == 3
types = Enum.map(saved.blocks, & &1["type"])
refute "category_nav" in types
end
test "reset to defaults restores original blocks", %{conn: conn} do
{:ok, _} =
Pages.save_page("home", %{
title: "Home page",
blocks: [%{"id" => "blk_test", "type" => "hero", "settings" => %{}}]
})
{:ok, view, _html} = live(conn, ~p"/admin/pages/home")
# Should show only 1 block
assert has_element?(view, ".block-card-position", "1")
refute has_element?(view, ".block-card-position", "2")
render_click(view, "reset_defaults")
assert render(view) =~ "Page reset to defaults"
# Should now have 4 default blocks
assert has_element?(view, ".block-card-position", "4")
end
test "dirty flag appears after changes", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/pages/home")
refute has_element?(view, ".admin-badge-warning", "Unsaved changes")
page = Pages.get_page("home")
block = Enum.at(page.blocks, 0)
render_click(view, "move_down", %{"id" => block["id"]})
assert has_element?(view, ".admin-badge-warning", "Unsaved changes")
end
test "dirty flag clears after save", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/pages/home")
page = Pages.get_page("home")
block = Enum.at(page.blocks, 0)
render_click(view, "move_down", %{"id" => block["id"]})
assert has_element?(view, ".admin-badge-warning")
render_click(view, "save")
refute has_element?(view, ".admin-badge-warning")
end
test "save button disabled when not dirty", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/pages/home")
assert has_element?(view, "button[disabled]", "Save")
end
end
describe "page editor for page-specific pages" do
setup %{conn: conn, user: user} do
%{conn: log_in_user(conn, user)}
end
test "PDP editor shows PDP blocks", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/pages/pdp")
assert has_element?(view, ".block-card-name", "Breadcrumb")
assert has_element?(view, ".block-card-name", "Product hero")
assert has_element?(view, ".block-card-name", "Trust badges")
end
test "PDP picker shows PDP-specific blocks", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/pages/pdp")
render_click(view, "show_picker")
assert has_element?(view, ".block-picker-item", "Product hero")
assert has_element?(view, ".block-picker-item", "Hero banner")
refute has_element?(view, ".block-picker-item", "Cart items")
end
test "error page editor works", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/pages/error")
assert has_element?(view, ".block-card-name", "Hero banner")
assert has_element?(view, ".block-card-name", "Featured products")
end
end
end