berrypod/test/berrypod_web/live/admin/pages_test.exs
jamey 93ff66debc
Some checks failed
deploy / deploy (push) Has been cancelled
add legal page editor integration and media library polish
Legal pages (privacy, delivery, terms) now auto-populate content from
shop settings on mount, show auto-generated vs customised badges, and
have a regenerate button. Theme editor gains alt text fields for logo,
header, and icon images. Image picker in page builder now has an upload
button and alt text warning badges. Clearing unused image references
shows an orphan info flash.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 17:55:02 +00:00

1071 lines
35 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 "undo and redo" do
setup %{conn: conn, user: user} do
%{conn: log_in_user(conn, user)}
end
test "undo reverts the last change", %{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"]})
# Block should be gone
refute has_element?(view, ".block-card-name", "Category navigation")
# Undo
render_click(view, "undo")
# Block should be back
assert has_element?(view, ".block-card-name", "Category navigation")
assert has_element?(view, "[aria-live='polite']", "Undone")
end
test "redo restores an undone change", %{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, "undo")
assert has_element?(view, ".block-card-name", "Category navigation")
render_click(view, "redo")
refute has_element?(view, ".block-card-name", "Category navigation")
assert has_element?(view, "[aria-live='polite']", "Redone")
end
test "new mutation clears redo stack", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/pages/home")
page = Pages.get_page("home")
hero = Enum.at(page.blocks, 0)
cat_nav = Enum.at(page.blocks, 1)
# Make a change, undo, then make a different change
render_click(view, "remove_block", %{"id" => cat_nav["id"]})
render_click(view, "undo")
render_click(view, "move_down", %{"id" => hero["id"]})
# Redo should do nothing (stack was cleared)
render_click(view, "redo")
# Hero should still be at position 2 (the move_down result)
assert has_element?(view, "[aria-live='polite']", "Hero banner moved to position 2")
end
test "undo all the way back clears dirty flag", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/pages/home")
page = Pages.get_page("home")
hero = Enum.at(page.blocks, 0)
render_click(view, "move_down", %{"id" => hero["id"]})
assert has_element?(view, ".admin-badge-warning", "Unsaved changes")
render_click(view, "undo")
refute has_element?(view, ".admin-badge-warning", "Unsaved changes")
end
test "undo when history empty does nothing", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/pages/home")
# No changes made, undo should be fine
render_click(view, "undo")
# Should still render normally
assert has_element?(view, ".block-card-name", "Hero banner")
end
test "undo/redo buttons reflect stack state", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/pages/home")
# Initially both disabled
assert has_element?(view, "button[phx-click='undo'][disabled]")
assert has_element?(view, "button[phx-click='redo'][disabled]")
# Make a change
page = Pages.get_page("home")
hero = Enum.at(page.blocks, 0)
render_click(view, "move_down", %{"id" => hero["id"]})
# Undo enabled, redo still disabled
refute has_element?(view, "button[phx-click='undo'][disabled]")
assert has_element?(view, "button[phx-click='redo'][disabled]")
# Undo
render_click(view, "undo")
# Undo disabled again, redo now enabled
assert has_element?(view, "button[phx-click='undo'][disabled]")
refute has_element?(view, "button[phx-click='redo'][disabled]")
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
describe "block settings editing" do
setup %{conn: conn, user: user} do
%{conn: log_in_user(conn, user)}
end
test "edit button shown only for blocks with settings", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/pages/home")
page = Pages.get_page("home")
hero = Enum.at(page.blocks, 0)
category_nav = Enum.at(page.blocks, 1)
# Hero has settings — edit button present
assert has_element?(view, "#block-edit-btn-#{hero["id"]}")
# Category nav has no settings — no edit button
refute has_element?(view, "#block-edit-btn-#{category_nav["id"]}")
end
test "toggle expand shows settings form", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/pages/home")
page = Pages.get_page("home")
hero = Enum.at(page.blocks, 0)
# Settings panel not visible initially
refute has_element?(view, "#block-settings-#{hero["id"]}")
# Click edit to expand
render_click(view, "toggle_expand", %{"id" => hero["id"]})
# Settings panel now visible
assert has_element?(view, "#block-settings-#{hero["id"]}")
assert has_element?(view, "[aria-live='polite']", "Hero banner settings expanded")
end
test "toggle collapse hides settings form", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/pages/home")
page = Pages.get_page("home")
hero = Enum.at(page.blocks, 0)
# Expand then collapse
render_click(view, "toggle_expand", %{"id" => hero["id"]})
assert has_element?(view, "#block-settings-#{hero["id"]}")
render_click(view, "toggle_expand", %{"id" => hero["id"]})
refute has_element?(view, "#block-settings-#{hero["id"]}")
assert has_element?(view, "[aria-live='polite']", "Hero banner settings collapsed")
end
test "aria-expanded reflects toggle state", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/pages/home")
page = Pages.get_page("home")
hero = Enum.at(page.blocks, 0)
assert has_element?(view, "#block-edit-btn-#{hero["id"]}[aria-expanded='false']")
render_click(view, "toggle_expand", %{"id" => hero["id"]})
assert has_element?(view, "#block-edit-btn-#{hero["id"]}[aria-expanded='true']")
end
test "settings form renders fields from schema", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/pages/home")
page = Pages.get_page("home")
hero = Enum.at(page.blocks, 0)
render_click(view, "toggle_expand", %{"id" => hero["id"]})
# Hero has text fields: title, description, cta_text, etc.
assert has_element?(view, "#block-#{hero["id"]}-title")
assert has_element?(view, "#block-#{hero["id"]}-description")
assert has_element?(view, "#block-#{hero["id"]}-cta_text")
# And a select field: variant
assert has_element?(view, "#block-#{hero["id"]}-variant")
end
test "editing settings updates working state and sets dirty", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/pages/home")
page = Pages.get_page("home")
hero = Enum.at(page.blocks, 0)
render_click(view, "toggle_expand", %{"id" => hero["id"]})
# Edit the title
render_change(view, "update_block_settings", %{
"block_id" => hero["id"],
"block_settings" => %{"title" => "New hero title"}
})
# Dirty flag should appear
assert has_element?(view, ".admin-badge-warning", "Unsaved changes")
end
test "edited settings persist after save", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/pages/home")
page = Pages.get_page("home")
hero = Enum.at(page.blocks, 0)
render_click(view, "toggle_expand", %{"id" => hero["id"]})
render_change(view, "update_block_settings", %{
"block_id" => hero["id"],
"block_settings" => %{"title" => "Updated title", "description" => "Updated desc"}
})
render_click(view, "save")
saved = Pages.get_page("home")
saved_hero = Enum.find(saved.blocks, &(&1["type"] == "hero"))
assert saved_hero["settings"]["title"] == "Updated title"
assert saved_hero["settings"]["description"] == "Updated desc"
end
test "number fields are coerced to integers", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/pages/home")
page = Pages.get_page("home")
featured = Enum.find(page.blocks, &(&1["type"] == "featured_products"))
render_click(view, "toggle_expand", %{"id" => featured["id"]})
render_change(view, "update_block_settings", %{
"block_id" => featured["id"],
"block_settings" => %{"product_count" => "4"}
})
render_click(view, "save")
saved = Pages.get_page("home")
saved_featured = Enum.find(saved.blocks, &(&1["type"] == "featured_products"))
assert saved_featured["settings"]["product_count"] == 4
end
test "select fields render with options", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/pages/home")
page = Pages.get_page("home")
hero = Enum.at(page.blocks, 0)
render_click(view, "toggle_expand", %{"id" => hero["id"]})
html = render(view)
# Variant select should have the expected options
assert html =~ "default"
assert html =~ "sunken"
end
test "multiple blocks can be expanded simultaneously", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/pages/home")
page = Pages.get_page("home")
hero = Enum.at(page.blocks, 0)
featured = Enum.find(page.blocks, &(&1["type"] == "featured_products"))
render_click(view, "toggle_expand", %{"id" => hero["id"]})
render_click(view, "toggle_expand", %{"id" => featured["id"]})
assert has_element?(view, "#block-settings-#{hero["id"]}")
assert has_element?(view, "#block-settings-#{featured["id"]}")
end
test "expanded card has expanded class", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/pages/home")
page = Pages.get_page("home")
hero = Enum.at(page.blocks, 0)
refute has_element?(view, "#block-#{hero["id"]}.block-card-expanded")
render_click(view, "toggle_expand", %{"id" => hero["id"]})
assert has_element?(view, "#block-#{hero["id"]}.block-card-expanded")
end
end
describe "repeater fields" do
setup %{conn: conn, user: user} do
%{conn: log_in_user(conn, user)}
end
test "repeater items render with input fields", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/pages/contact")
page = Pages.get_page("contact")
info_card = Enum.find(page.blocks, &(&1["type"] == "info_card"))
render_click(view, "toggle_expand", %{"id" => info_card["id"]})
# Should render 3 default items with label/value inputs
html = render(view)
assert html =~ "Printing"
assert html =~ "Delivery"
assert html =~ "Issues"
end
test "editing a repeater item field sets dirty flag", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/pages/contact")
page = Pages.get_page("contact")
info_card = Enum.find(page.blocks, &(&1["type"] == "info_card"))
render_click(view, "toggle_expand", %{"id" => info_card["id"]})
render_change(view, "update_block_settings", %{
"block_id" => info_card["id"],
"block_settings" => %{
"title" => "Handy to know",
"items" => %{
"0" => %{"label" => "Shipping", "value" => "1-3 business days"},
"1" => %{
"label" => "Delivery",
"value" => "Example: 3-7 business days after printing"
},
"2" => %{"label" => "Issues", "value" => "Example: Reprints for any defects"}
}
}
})
assert has_element?(view, ".admin-badge-warning", "Unsaved changes")
end
test "adding a repeater item appends an empty item", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/pages/contact")
page = Pages.get_page("contact")
info_card = Enum.find(page.blocks, &(&1["type"] == "info_card"))
render_click(view, "toggle_expand", %{"id" => info_card["id"]})
# Should have 3 items initially
assert render(view) |> count_repeater_items() == 3
render_click(view, "repeater_add", %{
"block-id" => info_card["id"],
"field" => "items"
})
# Now 4 items
assert render(view) |> count_repeater_items() == 4
assert has_element?(view, ".admin-badge-warning", "Unsaved changes")
end
test "removing a repeater item removes it from the list", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/pages/contact")
page = Pages.get_page("contact")
info_card = Enum.find(page.blocks, &(&1["type"] == "info_card"))
render_click(view, "toggle_expand", %{"id" => info_card["id"]})
render_click(view, "repeater_remove", %{
"block-id" => info_card["id"],
"field" => "items",
"index" => "0"
})
html = render(view)
assert count_repeater_items(html) == 2
# "Printing" was item 0, should be gone
refute html =~ "Printing"
assert html =~ "Delivery"
end
test "moving a repeater item changes order", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/pages/contact")
page = Pages.get_page("contact")
info_card = Enum.find(page.blocks, &(&1["type"] == "info_card"))
render_click(view, "toggle_expand", %{"id" => info_card["id"]})
render_click(view, "repeater_move", %{
"block-id" => info_card["id"],
"field" => "items",
"index" => "0",
"dir" => "down"
})
# Save and check the persisted order
render_click(view, "save")
saved = Pages.get_page("contact")
saved_info = Enum.find(saved.blocks, &(&1["type"] == "info_card"))
labels = Enum.map(saved_info["settings"]["items"], & &1["label"])
# "Printing" moved from 0 to 1
assert labels == ["Delivery", "Printing", "Issues"]
end
test "repeater changes persist after save", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/pages/contact")
page = Pages.get_page("contact")
info_card = Enum.find(page.blocks, &(&1["type"] == "info_card"))
render_click(view, "toggle_expand", %{"id" => info_card["id"]})
# Edit item via phx-change
render_change(view, "update_block_settings", %{
"block_id" => info_card["id"],
"block_settings" => %{
"title" => "Good to know",
"items" => %{
"0" => %{"label" => "Shipping", "value" => "Fast shipping"},
"1" => %{"label" => "Returns", "value" => "Easy returns"}
}
}
})
render_click(view, "save")
saved = Pages.get_page("contact")
saved_info = Enum.find(saved.blocks, &(&1["type"] == "info_card"))
assert saved_info["settings"]["title"] == "Good to know"
assert length(saved_info["settings"]["items"]) == 2
assert Enum.at(saved_info["settings"]["items"], 0)["label"] == "Shipping"
assert Enum.at(saved_info["settings"]["items"], 0)["value"] == "Fast shipping"
end
end
describe "live preview" do
setup %{conn: conn, user: user} do
%{conn: log_in_user(conn, user)}
end
test "preview pane renders page content", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/admin/pages/home")
# Preview pane should be in the DOM with the themed class
assert html =~ "page-editor-preview themed"
# Should render the page via PageRenderer (hero block is on home)
assert html =~ "page-editor-preview-pane"
end
test "toggle preview switches between edit and preview on mobile", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/pages/home")
# Initially: editor visible, preview hidden on mobile
assert has_element?(view, ".page-editor-pane:not(.page-editor-pane-hidden-mobile)")
assert has_element?(view, ".page-editor-preview-hidden-mobile")
# Toggle to preview
render_click(view, "toggle_preview")
assert has_element?(view, ".page-editor-pane-hidden-mobile")
assert has_element?(
view,
".page-editor-preview-pane:not(.page-editor-preview-hidden-mobile)"
)
# Toggle back to edit
render_click(view, "toggle_preview")
assert has_element?(view, ".page-editor-pane:not(.page-editor-pane-hidden-mobile)")
assert has_element?(view, ".page-editor-preview-hidden-mobile")
end
test "preview updates when block settings change", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/pages/home")
page = Pages.get_page("home")
hero = Enum.at(page.blocks, 0)
render_click(view, "toggle_expand", %{"id" => hero["id"]})
render_change(view, "update_block_settings", %{
"block_id" => hero["id"],
"block_settings" => %{"title" => "Preview test title"}
})
# The preview should show the updated title
html = render(view)
assert html =~ "Preview test title"
end
test "preview updates after block reorder", %{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"]})
# Should still render without errors
html = render(view)
assert html =~ "page-editor-preview themed"
end
test "preview toggle button shows in header", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/pages/home")
# Toggle button should be present
assert has_element?(view, ".page-editor-toggle-preview")
end
end
describe "custom pages on index" do
setup %{conn: conn, user: user} do
%{conn: log_in_user(conn, user)}
end
test "shows new page button", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/pages")
assert has_element?(view, "a[href='/admin/pages/new']", "New page")
end
test "shows custom pages section when pages exist", %{conn: conn} do
{:ok, _} = Pages.create_custom_page(%{slug: "faq", title: "FAQ"})
{:ok, view, _html} = live(conn, ~p"/admin/pages")
assert has_element?(view, ".page-group-title", "Custom pages")
assert has_element?(view, ".page-card-title", "FAQ")
end
test "does not show custom pages section when empty", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/pages")
refute has_element?(view, ".page-group-title", "Custom pages")
end
test "shows draft badge for unpublished pages", %{conn: conn} do
{:ok, _} =
Pages.create_custom_page(%{slug: "draft-page", title: "Draft", published: false})
{:ok, view, _html} = live(conn, ~p"/admin/pages")
assert has_element?(view, ".admin-badge-warning", "Draft")
end
test "custom page card links to editor", %{conn: conn} do
{:ok, _} = Pages.create_custom_page(%{slug: "faq", title: "FAQ"})
{:ok, view, _html} = live(conn, ~p"/admin/pages")
assert has_element?(view, "a[href='/admin/pages/faq']")
end
test "shows slug in card meta", %{conn: conn} do
{:ok, _} = Pages.create_custom_page(%{slug: "our-story", title: "Our story"})
{:ok, view, _html} = live(conn, ~p"/admin/pages")
assert has_element?(view, ".page-card-meta", "/our-story")
end
test "delete removes page from list", %{conn: conn} do
{:ok, _} = Pages.create_custom_page(%{slug: "faq", title: "FAQ"})
{:ok, view, _html} = live(conn, ~p"/admin/pages")
assert has_element?(view, ".page-card-title", "FAQ")
view
|> element("button[phx-click='delete_custom_page'][phx-value-slug='faq']")
|> render_click()
refute has_element?(view, ".page-card-title", "FAQ")
assert render(view) =~ "Page deleted"
end
end
describe "custom page creation" do
setup %{conn: conn, user: user} do
%{conn: log_in_user(conn, user)}
end
test "renders creation form", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/pages/new")
assert has_element?(view, "h1", "New page")
assert has_element?(view, "#custom-page-form")
end
test "creating with valid data redirects to editor", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/pages/new")
view
|> form("#custom-page-form", page: %{title: "FAQ", slug: "faq"})
|> render_submit()
assert_redirect(view, ~p"/admin/pages/faq")
end
test "creating with empty title shows error", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/pages/new")
view
|> form("#custom-page-form", page: %{title: "", slug: "faq"})
|> render_submit()
assert has_element?(view, "#custom-page-form")
end
test "creating with reserved slug shows error", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/pages/new")
html =
view
|> form("#custom-page-form", page: %{title: "Admin", slug: "admin"})
|> render_submit()
assert html =~ "is reserved"
end
test "creating with duplicate slug shows error", %{conn: conn} do
{:ok, _} = Pages.create_custom_page(%{slug: "faq", title: "FAQ"})
{:ok, view, _html} = live(conn, ~p"/admin/pages/new")
html =
view
|> form("#custom-page-form", page: %{title: "FAQ 2", slug: "faq"})
|> render_submit()
assert html =~ "has already been taken"
end
test "auto-slugifies title during validation", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/pages/new")
html =
view
|> form("#custom-page-form", page: %{title: "Our Story"})
|> render_change()
assert html =~ ~s(value="our-story")
end
end
describe "custom page settings" do
setup %{conn: conn, user: user} do
{:ok, _} = Pages.create_custom_page(%{slug: "help-page", title: "Help"})
%{conn: log_in_user(conn, user)}
end
test "renders settings form", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/pages/help-page/settings")
assert has_element?(view, "h1", "Page settings")
assert has_element?(view, "#custom-page-form")
end
test "updating title saves correctly", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/pages/help-page/settings")
view
|> form("#custom-page-form", page: %{title: "Help centre"})
|> render_submit()
assert_redirect(view, ~p"/admin/pages/help-page")
page = Pages.get_page("help-page")
assert page.title == "Help centre"
end
test "updating slug redirects to new editor path", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/pages/help-page/settings")
view
|> form("#custom-page-form", page: %{slug: "support"})
|> render_submit()
assert_redirect(view, ~p"/admin/pages/support")
end
test "settings for nonexistent page redirects", %{conn: conn} do
assert {:error, {:live_redirect, %{to: "/admin/pages"}}} =
live(conn, ~p"/admin/pages/nonexistent/settings")
end
end
describe "editor for custom pages" do
setup %{conn: conn, user: user} do
{:ok, _} = Pages.create_custom_page(%{slug: "size-guide", title: "Size guide"})
%{conn: log_in_user(conn, user)}
end
test "shows settings button for custom pages", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/pages/size-guide")
assert has_element?(view, "a[href='/admin/pages/size-guide/settings']", "Settings")
end
test "does not show reset to defaults for custom pages", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/pages/size-guide")
refute has_element?(view, "button", "Reset to defaults")
end
test "shows reset to defaults for system pages", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/pages/home")
assert has_element?(view, "button", "Reset to defaults")
refute has_element?(view, "a[href='/admin/pages/home/settings']")
end
end
describe "legal page editor" do
setup %{conn: conn, user: user} do
%{conn: log_in_user(conn, user)}
end
test "shows regenerate button for privacy page", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/pages/privacy")
assert has_element?(view, "button[phx-click='regenerate_legal']", "Regenerate")
end
test "shows regenerate button for delivery page", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/pages/delivery")
assert has_element?(view, "button[phx-click='regenerate_legal']", "Regenerate")
end
test "does not show regenerate button for home page", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/pages/home")
refute has_element?(view, "button[phx-click='regenerate_legal']")
end
test "shows auto-generated badge for legal pages on mount", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/pages/privacy")
assert has_element?(view, ".admin-badge-info", "Auto-generated from settings")
end
test "auto-populates content_body on mount for legal pages", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/pages/privacy")
# The content_body block should be expanded to see the content
page = Pages.get_page("privacy")
content_body = Enum.find(page.blocks, &(&1["type"] == "content_body"))
render_click(view, "toggle_expand", %{"id" => content_body["id"]})
html = render(view)
# Should contain generated legal content
assert html =~ "What we collect"
end
test "shows customised badge after manual edit", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/admin/pages/privacy")
page = Pages.get_page("privacy")
content_body = Enum.find(page.blocks, &(&1["type"] == "content_body"))
render_click(view, "toggle_expand", %{"id" => content_body["id"]})
render_change(view, "update_block_settings", %{
"block_id" => content_body["id"],
"block_settings" => %{"content" => "Custom privacy content here"}
})
assert has_element?(view, ".admin-badge", "Customised")
refute has_element?(view, ".admin-badge-info", "Auto-generated from settings")
end
end
defp count_repeater_items(html) do
Regex.scan(~r/class="repeater-item"/, html) |> length()
end
end