All checks were successful
deploy / deploy (push) Successful in 1m32s
Split the editor sheet into two distinct elements: - .editor-fab: floating action button, always a pill in the corner - .editor-panel: sliding panel that animates in/out independently This enables proper CSS keyframe animations (slide-up/down on mobile, slide-in/out on desktop) with a closing class for exit transitions. Simplified the JS hook to only handle close behaviour. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
330 lines
10 KiB
Elixir
330 lines
10 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 "editor sheet is not shown for non-admins", %{conn: conn} do
|
|
{:ok, _view, html} = live(conn, "/")
|
|
|
|
refute html =~ "editor-panel"
|
|
refute html =~ "Edit page"
|
|
end
|
|
end
|
|
|
|
describe "editor sheet visibility" do
|
|
test "admin sees editor sheet with edit button", %{conn: conn, user: user} do
|
|
conn = log_in_user(conn, user)
|
|
{:ok, view, _html} = live(conn, "/")
|
|
|
|
assert has_element?(view, ".editor-panel")
|
|
assert has_element?(view, "button", "Edit page")
|
|
end
|
|
|
|
test "non-admin does not see editor sheet", %{conn: conn} do
|
|
{:ok, view, _html} = live(conn, "/")
|
|
|
|
refute has_element?(view, ".editor-panel")
|
|
refute has_element?(view, "button", "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 "clicking edit button enters edit mode", %{conn: conn} do
|
|
{:ok, view, _html} = live(conn, "/")
|
|
|
|
# Sheet starts collapsed
|
|
assert has_element?(view, ".editor-panel[data-state='collapsed']")
|
|
|
|
# Click edit button (the specific edit button class)
|
|
view |> element(".editor-fab") |> render_click()
|
|
|
|
# Now editing, sheet expanded
|
|
assert has_element?(view, ".editor-panel[data-editing='true']")
|
|
assert has_element?(view, ".editor-panel-content")
|
|
assert has_element?(view, ".block-card")
|
|
end
|
|
|
|
test "sheet shows the page title when editing", %{conn: conn} do
|
|
{:ok, view, _html} = live(conn, "/")
|
|
|
|
view |> element(".editor-fab") |> render_click()
|
|
|
|
assert has_element?(view, ".editor-panel-page-title", "Home page")
|
|
end
|
|
|
|
test "sheet state changes when entering edit mode and collapsing", %{conn: conn} do
|
|
{:ok, view, _html} = live(conn, "/")
|
|
|
|
# Starts collapsed
|
|
assert has_element?(view, ".editor-panel[data-state='collapsed']")
|
|
|
|
# Enter edit mode - expands to open
|
|
view |> element(".editor-fab") |> render_click()
|
|
assert has_element?(view, ".editor-panel[data-state='open']")
|
|
|
|
# Collapse sheet (still in edit mode, just previewing)
|
|
render_click(view, "editor_set_sheet_state", %{"state" => "collapsed"})
|
|
|
|
assert has_element?(view, ".editor-panel[data-state='collapsed']")
|
|
# Still in edit mode
|
|
assert has_element?(view, ".editor-panel[data-editing='true']")
|
|
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, "/")
|
|
|
|
# Enter edit mode
|
|
view |> element(".editor-fab") |> render_click()
|
|
|
|
# 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, "/")
|
|
|
|
# Enter edit mode
|
|
view |> element(".editor-fab") |> render_click()
|
|
|
|
refute has_element?(view, ".editor-panel-dirty")
|
|
|
|
# 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, ".editor-panel-dirty")
|
|
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, "/")
|
|
|
|
# Enter edit mode
|
|
view |> element(".editor-fab") |> render_click()
|
|
|
|
# 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()
|
|
|
|
# Verify persistence
|
|
updated = Pages.get_page("home")
|
|
refute List.first(updated.blocks)["type"] == original_first_type
|
|
end
|
|
|
|
test "reset restores default blocks and is undoable", %{conn: conn} do
|
|
# First, save a modified page (reverse block order)
|
|
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, "/")
|
|
|
|
# Enter edit mode
|
|
view |> element(".editor-fab") |> render_click()
|
|
|
|
# Get the first block type before reset (should be reversed, so last default)
|
|
first_before_reset = view |> element(".block-card:first-child") |> render()
|
|
|
|
# Reset
|
|
view |> element("button[phx-click='editor_reset_defaults']") |> render_click()
|
|
|
|
# First block should now be the default first (Hero)
|
|
first_after_reset = view |> element(".block-card:first-child") |> render()
|
|
assert first_after_reset =~ "Hero"
|
|
refute first_after_reset == first_before_reset
|
|
|
|
# Reset should be undoable
|
|
refute has_element?(view, "button[phx-click='editor_undo'][disabled]")
|
|
|
|
# Undo the reset
|
|
view |> element("button[phx-click='editor_undo']") |> render_click()
|
|
|
|
# Should be back to the reversed order
|
|
first_after_undo = view |> element(".block-card:first-child") |> render()
|
|
refute first_after_undo =~ "Hero"
|
|
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, "/")
|
|
|
|
# Enter edit mode
|
|
view |> element(".editor-fab") |> render_click()
|
|
|
|
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()
|
|
|
|
# After move_up, second block is now first
|
|
first_html = view |> element(".block-card:first-child") |> render()
|
|
refute first_html =~ "Hero"
|
|
|
|
# Undo
|
|
view |> element("button[phx-click='editor_undo']") |> render_click()
|
|
|
|
# Hero should be back as first block
|
|
restored_first = view |> element(".block-card:first-child") |> render()
|
|
assert restored_first =~ "Hero"
|
|
end
|
|
|
|
test "redo restores an undone change", %{conn: conn} do
|
|
{:ok, view, _html} = live(conn, "/")
|
|
|
|
# Enter edit mode
|
|
view |> element(".editor-fab") |> render_click()
|
|
|
|
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()
|
|
|
|
view |> element("button[phx-click='editor_undo']") |> render_click()
|
|
view |> element("button[phx-click='editor_redo']") |> render_click()
|
|
|
|
# After redo, hero should no longer be first
|
|
first_html = view |> element(".block-card:first-child") |> render()
|
|
refute first_html =~ "Hero"
|
|
end
|
|
|
|
test "history clears on save", %{conn: conn} do
|
|
{:ok, view, _html} = live(conn, "/")
|
|
|
|
# Enter edit mode
|
|
view |> element(".editor-fab") |> render_click()
|
|
|
|
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()
|
|
|
|
view |> element("button[phx-click='editor_save']") |> render_click()
|
|
|
|
# After save, undo should be disabled
|
|
assert has_element?(view, "button[phx-click='editor_undo'][disabled]")
|
|
end
|
|
|
|
test "undo/redo buttons reflect stack state", %{conn: conn} do
|
|
{:ok, view, _html} = live(conn, "/")
|
|
|
|
# Enter edit mode
|
|
view |> element(".editor-fab") |> render_click()
|
|
|
|
# Initially both disabled
|
|
assert has_element?(view, "button[phx-click='editor_undo'][disabled]")
|
|
assert has_element?(view, "button[phx-click='editor_redo'][disabled]")
|
|
|
|
# Make a change
|
|
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()
|
|
|
|
# Undo enabled, redo still disabled
|
|
refute has_element?(view, "button[phx-click='editor_undo'][disabled]")
|
|
assert has_element?(view, "button[phx-click='editor_redo'][disabled]")
|
|
end
|
|
end
|
|
|
|
describe "content pages" do
|
|
setup %{conn: conn, user: user} do
|
|
%{conn: log_in_user(conn, user)}
|
|
end
|
|
|
|
test "editing works on about page", %{conn: conn} do
|
|
{:ok, view, _html} = live(conn, "/about")
|
|
|
|
# Editor sheet visible for admin
|
|
assert has_element?(view, ".editor-panel")
|
|
|
|
# Enter edit mode
|
|
view |> element(".editor-fab") |> render_click()
|
|
|
|
assert has_element?(view, ".editor-panel-content")
|
|
assert has_element?(view, ".editor-panel-page-title", "About")
|
|
assert has_element?(view, ".block-card")
|
|
end
|
|
end
|
|
end
|