diff options
| author | erdgeist <erdgeist@erdgeist.org> | 2026-07-18 02:15:29 +0200 |
|---|---|---|
| committer | erdgeist <erdgeist@erdgeist.org> | 2026-07-18 02:15:29 +0200 |
| commit | d41ee504caedbe858e24ab2a23c7a804454c64c8 (patch) | |
| tree | eb3ec7c8f44d15fea40292d0a9e7358e41640edc /test | |
| parent | 5dced8b4b624aabf4215ba21b13957080345c326 (diff) | |
Add Trash affordances: cockpit, listing, dashboard entry
nodes#show gains a Trash section on trashed nodes: provenance from
the trash entry, a restore form whose parent picker pre-fills the
old parent while it still lives, and permanent deletion. A
Move-to-Trash button joins the status actions on living nodes.
nodes#trashed lists trashed subtree roots with weight, provenance,
and deletion; the dashboard housekeeping row links to it, and
trash/destroy redirect there.
Deletion from Trash now removes the whole subtree, deepest first,
each node through a real destroy! so every per-node cascade runs --
amending the never-recursive rule for this one sanctioned path
(both confirms state the count; the root entry carries
destroyed_descendants). Bare Node#destroy still refuses children.
Diffstat (limited to 'test')
| -rw-r--r-- | test/controllers/nodes_controller_test.rb | 31 | ||||
| -rw-r--r-- | test/models/node_trash_test.rb | 41 |
2 files changed, 62 insertions, 10 deletions
diff --git a/test/controllers/nodes_controller_test.rb b/test/controllers/nodes_controller_test.rb index 7e5a990..6e2ddb3 100644 --- a/test/controllers/nodes_controller_test.rb +++ b/test/controllers/nodes_controller_test.rb | |||
| @@ -427,11 +427,11 @@ class NodesControllerTest < ActionController::TestCase | |||
| 427 | test "show never renders a destroy link for events" do | 427 | test "show never renders a destroy link for events" do |
| 428 | login_as :quentin | 428 | login_as :quentin |
| 429 | node = create_node_with_published_page | 429 | node = create_node_with_published_page |
| 430 | Event.create!(node_id: node.id, start_time: Time.now, end_time: Time.now + 1.hour) | 430 | event = Event.create!(node_id: node.id, start_time: Time.now, end_time: Time.now + 1.hour) |
| 431 | 431 | ||
| 432 | get :show, params: { id: node.id } | 432 | get :show, params: { id: node.id } |
| 433 | assert_response :success | 433 | assert_response :success |
| 434 | assert_select "form.button_to.destructive", count: 0 | 434 | assert_select "form[action=?]", event_path(event), count: 0 |
| 435 | end | 435 | end |
| 436 | 436 | ||
| 437 | test "reverting from nodes#show returns to the show page, not the editor, even if a draft remains" do | 437 | test "reverting from nodes#show returns to the show page, not the editor, even if a draft remains" do |
| @@ -479,7 +479,8 @@ class NodesControllerTest < ActionController::TestCase | |||
| 479 | login_as :quentin | 479 | login_as :quentin |
| 480 | get :show, params: { :id => node.id } | 480 | get :show, params: { :id => node.id } |
| 481 | assert_response :success | 481 | assert_response :success |
| 482 | assert_select "form.destructive", :count => 0 | 482 | assert_select "form[action=?]", revert_node_path(node), count: 0 |
| 483 | assert_select "form[action=?]", trash_node_path(node), count: 1 | ||
| 483 | end | 484 | end |
| 484 | 485 | ||
| 485 | test "drafts includes a never-published node with only a draft" do | 486 | test "drafts includes a never-published node with only a draft" do |
| @@ -679,7 +680,7 @@ class NodesControllerTest < ActionController::TestCase | |||
| 679 | 680 | ||
| 680 | put :trash, params: { :id => node.id } | 681 | put :trash, params: { :id => node.id } |
| 681 | 682 | ||
| 682 | assert_redirected_to node_path(Node.trash) | 683 | assert_redirected_to trashed_nodes_path |
| 683 | assert node.reload.in_trash? | 684 | assert node.reload.in_trash? |
| 684 | end | 685 | end |
| 685 | 686 | ||
| @@ -722,6 +723,26 @@ class NodesControllerTest < ActionController::TestCase | |||
| 722 | delete :destroy, params: { :id => node.id } | 723 | delete :destroy, params: { :id => node.id } |
| 723 | 724 | ||
| 724 | assert_not Node.exists?(node.id) | 725 | assert_not Node.exists?(node.id) |
| 725 | assert_redirected_to node_path(Node.trash) | 726 | assert_redirected_to trashed_nodes_path |
| 727 | end | ||
| 728 | |||
| 729 | test "trash lists trashed subtree roots" do | ||
| 730 | login_as :quentin | ||
| 731 | node = Node.root.children.create!(:slug => "listed_in_trash") | ||
| 732 | node.trash!(users(:quentin)) | ||
| 733 | |||
| 734 | get :trashed | ||
| 735 | assert_response :success | ||
| 736 | assert_select "a[href=?]", node_path(node) | ||
| 737 | end | ||
| 738 | |||
| 739 | test "trashed rows carry provenance and a delete for childless roots" do | ||
| 740 | login_as :quentin | ||
| 741 | node = Node.root.children.create!(:slug => "provenance_test") | ||
| 742 | node.trash!(users(:quentin)) | ||
| 743 | |||
| 744 | get :trashed | ||
| 745 | assert_select "td", /quentin/ | ||
| 746 | assert_select "form[action=?]", node_path(node), count: 1 | ||
| 726 | end | 747 | end |
| 727 | end | 748 | end |
diff --git a/test/models/node_trash_test.rb b/test/models/node_trash_test.rb index b72770b..07e5bc6 100644 --- a/test/models/node_trash_test.rb +++ b/test/models/node_trash_test.rb | |||
| @@ -102,12 +102,25 @@ class NodeTrashTest < ActiveSupport::TestCase | |||
| 102 | assert Node.exists?(node.id) | 102 | assert Node.exists?(node.id) |
| 103 | end | 103 | end |
| 104 | 104 | ||
| 105 | test "destroy_from_trash! refuses nodes with children" do | 105 | test "destroy_from_trash! removes the whole subtree bottom-up with one entry" do |
| 106 | node = create_node_with_published_page | 106 | parent = create_node_with_published_page |
| 107 | node.children.create!(:slug => "still_here") | 107 | child = parent.children.create!(:slug => "doomed_child") |
| 108 | node.trash!(@user1) | 108 | grandchild = child.children.create!(:slug => "doomed_grandchild") |
| 109 | page_ids = [parent, child, grandchild].flat_map { |n| n.pages.pluck(:id) } | ||
| 110 | parent.trash!(@user1) | ||
| 111 | |||
| 112 | assert_difference "NodeAction.count", 1 do | ||
| 113 | parent.reload.destroy_from_trash!(@user1) | ||
| 114 | end | ||
| 115 | |||
| 116 | assert_not Node.exists?(parent.id) | ||
| 117 | assert_not Node.exists?(child.id) | ||
| 118 | assert_not Node.exists?(grandchild.id) | ||
| 119 | assert_equal 0, Page.where(:id => page_ids).count | ||
| 109 | 120 | ||
| 110 | assert_raises(ActiveRecord::RecordNotDestroyed) { node.reload.destroy_from_trash!(@user1) } | 121 | action = NodeAction.where(:action => "destroy").last |
| 122 | assert_equal 2, action.metadata["destroyed_descendants"] | ||
| 123 | assert_nil action.node_id | ||
| 111 | end | 124 | end |
| 112 | 125 | ||
| 113 | test "destroy_from_trash! logs an entry that survives the node" do | 126 | test "destroy_from_trash! logs an entry that survives the node" do |
| @@ -124,4 +137,22 @@ class NodeTrashTest < ActiveSupport::TestCase | |||
| 124 | assert_equal "Doomed", action.subject_name | 137 | assert_equal "Doomed", action.subject_name |
| 125 | assert_equal @user1, action.user | 138 | assert_equal @user1, action.user |
| 126 | end | 139 | end |
| 140 | |||
| 141 | test "suggested_restore_parent finds the old parent while it lives" do | ||
| 142 | parent = Node.root.children.create!(:slug => "old_home") | ||
| 143 | node = parent.children.create!(:slug => "comes_back") | ||
| 144 | node.trash!(@user1) | ||
| 145 | |||
| 146 | assert_equal parent, node.reload.suggested_restore_parent | ||
| 147 | |||
| 148 | parent.trash!(@user1) | ||
| 149 | assert_nil node.reload.suggested_restore_parent | ||
| 150 | end | ||
| 151 | |||
| 152 | test "suggested_restore_parent is root for trashed top-level nodes" do | ||
| 153 | node = Node.root.children.create!(:slug => "was_top_level") | ||
| 154 | node.trash!(@user1) | ||
| 155 | |||
| 156 | assert_equal Node.root, node.reload.suggested_restore_parent | ||
| 157 | end | ||
| 127 | end | 158 | end |
