From f5598988d4f89af942e14835a839d81238b369b7 Mon Sep 17 00:00:00 2001 From: DualFroz Date: Sat, 12 Sep 2026 18:03:46 +0200 Subject: [PATCH] test: cover OrmListPositionTest and teardown --- test/test_orm_list_position.rb | 161 +++++++++++++++++++++++++++++++++ 1 file changed, 161 insertions(+) create mode 100644 test/test_orm_list_position.rb diff --git a/test/test_orm_list_position.rb b/test/test_orm_list_position.rb new file mode 100644 index 0000000..bb69ab3 --- /dev/null +++ b/test/test_orm_list_position.rb @@ -0,0 +1,161 @@ +require 'minitest/autorun' +require 'prism' +require 'tmpdir' +require 'fileutils' + +class OrmListPositionTest < Minitest::Test + def setup + @dir = Dir.mktmpdir('prism-list-position-store') + @store = Prism::Orm::Store.new(File.join(@dir, 'test.pstore')) + @store.create_table(:tasks, %w[id title position deleted_at]) + @store.create_table(:cards, %w[id title position board_id]) + + task_class = Class.new(Prism::Orm::Model) do + table_name :tasks + acts_as_paranoid + acts_as_list column: :position + end + card_class = Class.new(Prism::Orm::Model) do + table_name :cards + acts_as_list column: :position, scope: :board_id + end + plain_class = Class.new(Prism::Orm::Model) { table_name :tasks } + + [task_class, card_class].each { |klass| klass.store = @store } + plain_class.store = @store + Object.const_set(:Task, task_class) + Object.const_set(:Card, card_class) + Object.const_set(:PlainTask, plain_class) + end + + def teardown + FileUtils.remove_entry(@dir) + %i[Task Card PlainTask].each { |name| Object.send(:remove_const, name) if Object.const_defined?(name) } + end + + def test_a_model_that_never_called_acts_as_list_is_unaffected + refute PlainTask.acts_as_list? + assert Task.acts_as_list? + end + + def test_fresh_records_are_appended_to_the_bottom_in_creation_order + a = Task.create!(title: 'a') + b = Task.create!(title: 'b') + c = Task.create!(title: 'c') + assert_equal [1, 2, 3], [a, b, c].map(&:list_position) + end + + def test_move_higher_swaps_with_the_previous_item_and_is_a_noop_at_the_top + a = Task.create!(title: 'a') + b = Task.create!(title: 'b') + b.move_higher + assert_equal 1, b.list_position + assert_equal 2, a.reload.list_position + + b.reload.move_higher # b is already first - nothing above it to swap with + assert_equal 1, b.list_position + assert_equal 2, a.reload.list_position + end + + def test_move_lower_swaps_with_the_next_item_and_is_a_noop_at_the_bottom + a = Task.create!(title: 'a') + b = Task.create!(title: 'b') + a.move_lower + assert_equal 2, a.list_position + assert_equal 1, b.reload.list_position + + b.reload.move_lower # b is now first (pos 1), a is last (pos 2) - moving b lower swaps back + assert_equal 2, b.list_position + assert_equal 1, a.reload.list_position + end + + def test_move_lower_on_the_last_item_is_a_noop + a = Task.create!(title: 'a') + Task.create!(title: 'b') + b2 = Task.find_by!(title: 'b') + b2.move_lower + assert_equal 2, b2.list_position + assert_equal 1, a.reload.list_position + end + + def test_move_higher_on_the_first_item_is_a_noop + a = Task.create!(title: 'a') + Task.create!(title: 'b') + a.move_higher + assert_equal 1, a.list_position + end + + def test_first_and_last_in_list_predicates + a = Task.create!(title: 'a') + b = Task.create!(title: 'b') + c = Task.create!(title: 'c') + assert a.first_in_list? + refute a.last_in_list? + assert c.last_in_list? + refute c.first_in_list? + refute b.first_in_list? + refute b.last_in_list? + end + + def test_move_to_top_and_move_to_bottom_reposition_and_shift_everyone_between + a = Task.create!(title: 'a') + b = Task.create!(title: 'b') + c = Task.create!(title: 'c') + d = Task.create!(title: 'd') + + c.move_to_top + assert_equal [1, 2, 3, 4], [c, a, b, d].map { |t| t.reload.list_position } + + a.reload.move_to_bottom + assert_equal [1, 2, 3, 4], [c, b, d, a].map { |t| t.reload.list_position } + end + + def test_insert_at_moves_an_item_to_an_arbitrary_middle_slot + a = Task.create!(title: 'a') + b = Task.create!(title: 'b') + c = Task.create!(title: 'c') + d = Task.create!(title: 'd') + + d.insert_at(2) + assert_equal [1, 2, 3, 4], [a, d, b, c].map { |t| t.reload.list_position } + end + + def test_insert_at_on_a_record_that_is_not_persisted_raises + task = Task.new(title: 'unsaved') + assert_raises(Prism::Orm::RecordNotFound) { task.insert_at(1) } + end + + def test_destroying_an_item_closes_the_gap_for_everyone_below_it + a = Task.create!(title: 'a') + b = Task.create!(title: 'b') + c = Task.create!(title: 'c') + b.destroy + assert_equal 1, a.reload.list_position + assert_equal 2, c.reload.list_position + end + + def test_soft_deleting_a_paranoid_item_also_closes_the_gap + a = Task.create!(title: 'a') + b = Task.create!(title: 'b') + c = Task.create!(title: 'c') + b.destroy # paranoid: soft-delete, not a real row removal + assert b.deleted? + assert_equal 1, a.reload.list_position + assert_equal 2, c.reload.list_position + end + + def test_restoring_a_soft_deleted_item_reappends_it_to_the_bottom_not_its_old_slot + a = Task.create!(title: 'a') + b = Task.create!(title: 'b') + c = Task.create!(title: 'c') + b.destroy + d = Task.create!(title: 'd') + assert_equal [1, 2, 3], [a.reload, c.reload, d.reload].map(&:list_position) + + restored = Task.only_deleted.find_by!(title: 'b') + restored.restore + assert_equal 4, restored.list_position + # nobody else collides with the reappended slot + assert_equal [1, 2, 3, 4], [a.reload, c.reload, d.reload, restored.reload].map(&:list_position).sort + end +