berrypod/test/berrypod_web/page_editor_hook_test.exs
jamey a039c8d53c
All checks were successful
deploy / deploy (push) Successful in 6m49s
add live page editor sidebar with collapsible UI
Admins can now edit pages directly on the live shop by clicking the
pencil icon in the header. A sidebar slides in with block management
controls (add, remove, reorder, edit settings, save, reset, done).

Key features:
- PageEditorHook on_mount with handle_params/event/info hooks
- BlockEditor pure functions extracted from admin editor
- Shared BlockEditorComponents with event_prefix namespacing
- Collapsible sidebar: X closes it, header pencil reopens it
- Backdrop overlay dismisses sidebar on tap
- Conditional admin.css loading for logged-in users
- content_body block now portable (textarea setting + rich text fallback)

13 integration tests, 26 unit tests, 1370 total passing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 16:22:35 +00:00

232 lines
7.2 KiB
Elixir

defmodule BerrypodWeb.PageEditorHookTest do
use BerrypodWeb.ConnCase, async: false
import Phoenix.LiveViewTest
import Berrypod.AccountsFixtures
import Berrypod.ProductsFixtures
alias Berrypod.Pages
alias Berrypod.Pages.PageCache
setup do
PageCache.invalidate_all()
user = user_fixture()
{:ok, _} = Berrypod.Settings.set_site_live(true)
provider_conn = provider_connection_fixture()
product =
product_fixture(%{
provider_connection: provider_conn,
title: "Test Product",
category: "Test Category"
})
product_variant_fixture(%{product: product, title: "Standard", price: 1999})
Berrypod.Products.recompute_cached_fields(product)
%{user: user, product: product}
end
describe "non-admin cannot access edit mode" do
test "visiting with ?edit=true shows normal page, no sidebar", %{conn: conn} do
{:ok, _view, html} = live(conn, "/?edit=true")
refute html =~ "page-editor-sidebar"
refute html =~ "page-editor-live"
end
end
describe "edit button visibility" do
test "admin sees edit pencil in shop header", %{conn: conn, user: user} do
conn = log_in_user(conn, user)
{:ok, _view, html} = live(conn, "/")
assert html =~ "Edit page"
end
test "non-admin does not see edit pencil", %{conn: conn} do
{:ok, _view, html} = live(conn, "/")
refute html =~ "Edit page"
end
end
describe "entering and exiting edit mode" do
setup %{conn: conn, user: user} do
%{conn: log_in_user(conn, user)}
end
test "admin enters edit mode with ?edit=true", %{conn: conn} do
{:ok, view, _html} = live(conn, "/?edit=true")
assert has_element?(view, ".page-editor-sidebar")
assert has_element?(view, ".page-editor-content")
assert has_element?(view, ".block-card")
end
test "sidebar shows the page title", %{conn: conn} do
{:ok, view, _html} = live(conn, "/?edit=true")
assert has_element?(view, ".page-editor-sidebar-title", "Home page")
end
test "done button exits edit mode", %{conn: conn} do
{:ok, view, _html} = live(conn, "/?edit=true")
# editor_done uses push_navigate, which causes a redirect
{:error, {:live_redirect, %{to: "/"}}} =
view |> element("button[phx-click='editor_done']") |> render_click()
end
test "toggle sidebar hides and shows via header pencil", %{conn: conn} do
{:ok, view, _html} = live(conn, "/?edit=true")
# Sidebar starts open, pencil button in header is hidden
assert has_element?(view, "[data-sidebar-open='true']")
refute has_element?(
view,
"button[phx-click='editor_toggle_sidebar'][aria-label='Show editor sidebar']"
)
# Close the sidebar via the X button
view
|> element("button[phx-click='editor_toggle_sidebar'][aria-label='Close sidebar']")
|> render_click()
assert has_element?(view, "[data-sidebar-open='false']")
# Pencil button appears in header to re-open
assert has_element?(
view,
"button[phx-click='editor_toggle_sidebar'][aria-label='Show editor sidebar']"
)
# Re-open via pencil in header
view
|> element("button[phx-click='editor_toggle_sidebar'][aria-label='Show editor sidebar']")
|> render_click()
assert has_element?(view, "[data-sidebar-open='true']")
end
test "clicking backdrop hides the sidebar", %{conn: conn} do
{:ok, view, _html} = live(conn, "/?edit=true")
# Backdrop present when sidebar is open
assert has_element?(view, ".page-editor-backdrop")
# Click backdrop to dismiss
view |> element(".page-editor-backdrop") |> render_click()
assert has_element?(view, "[data-sidebar-open='false']")
# Backdrop gone when sidebar is hidden
refute has_element?(view, ".page-editor-backdrop")
end
end
describe "block manipulation in edit mode" do
setup %{conn: conn, user: user} do
%{conn: log_in_user(conn, user)}
end
test "move block down reorders the list", %{conn: conn} do
{:ok, view, _html} = live(conn, "/?edit=true")
# Home page default: hero is first block
first_card = view |> element(".block-card:first-child")
first_html = render(first_card)
assert first_html =~ "Hero"
# Get the hero block's ID and move it down
blocks = Pages.get_page("home").blocks
hero_id = List.first(blocks)["id"]
view
|> element("button[phx-click='editor_move_down'][phx-value-id='#{hero_id}']")
|> render_click()
# After move, hero is no longer the first card
updated_first = view |> element(".block-card:first-child") |> render()
refute updated_first =~ "Hero"
end
test "dirty indicator appears after changes", %{conn: conn} do
{:ok, view, _html} = live(conn, "/?edit=true")
refute has_element?(view, ".admin-badge-warning", "Unsaved changes")
# Move a block to trigger dirty state
blocks = Pages.get_page("home").blocks
second_id = Enum.at(blocks, 1)["id"]
view
|> element("button[phx-click='editor_move_up'][phx-value-id='#{second_id}']")
|> render_click()
assert has_element?(view, ".admin-badge-warning", "Unsaved changes")
end
end
describe "save and reset" do
setup %{conn: conn, user: user} do
%{conn: log_in_user(conn, user)}
end
test "save persists block changes", %{conn: conn} do
{:ok, view, _html} = live(conn, "/?edit=true")
# Move a block to make changes
blocks = Pages.get_page("home").blocks
original_first_type = List.first(blocks)["type"]
second_id = Enum.at(blocks, 1)["id"]
view
|> element("button[phx-click='editor_move_up'][phx-value-id='#{second_id}']")
|> render_click()
# Save
view |> element("button[phx-click='editor_save']") |> render_click()
assert has_element?(view, "#shop-flash-info", "Page saved")
# Verify persistence
updated = Pages.get_page("home")
refute List.first(updated.blocks)["type"] == original_first_type
end
test "reset restores default blocks", %{conn: conn} do
# First, save a modified page
original = Pages.get_page("home")
reordered = Enum.reverse(original.blocks)
Pages.save_page("home", %{title: original.title, blocks: reordered})
PageCache.invalidate_all()
{:ok, view, _html} = live(conn, "/?edit=true")
# Reset
view |> element("button[phx-click='editor_reset_defaults']") |> render_click()
assert has_element?(view, "#shop-flash-info", "Page reset to defaults")
# Verify the blocks are back to defaults
reset_page = Pages.get_page("home")
assert List.first(reset_page.blocks)["type"] == List.first(original.blocks)["type"]
end
end
describe "content pages (deferred init)" do
setup %{conn: conn, user: user} do
%{conn: log_in_user(conn, user)}
end
test "editing works on about page via deferred init", %{conn: conn} do
{:ok, view, _html} = live(conn, "/about?edit=true")
assert has_element?(view, ".page-editor-sidebar")
assert has_element?(view, ".page-editor-sidebar-title", "About")
assert has_element?(view, ".block-card")
end
end
end