add undo/redo to page editors with keyboard shortcuts
All checks were successful
deploy / deploy (push) Successful in 1m29s

History stacks (@history/@future) on both admin editor and live sidebar,
capped at 50 entries. All mutations routed through apply_mutation for
consistent history tracking. EditorKeyboard JS hook combines DirtyGuard
with Ctrl+Z/Ctrl+Shift+Z. Settings panel fade-in animation. 10 new tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
jamey
2026-02-28 12:16:15 +00:00
parent 22d7b0e92b
commit 79b5161e02
8 changed files with 379 additions and 37 deletions

View File

@@ -215,6 +215,88 @@ defmodule BerrypodWeb.PageEditorHookTest do
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, "/?edit=true")
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, "/?edit=true")
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, "/?edit=true")
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, "/?edit=true")
# 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 (deferred init)" do
setup %{conn: conn, user: user} do
%{conn: log_in_user(conn, user)}