Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-09-19 04:45:44 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-09-19 04:45:44 +0300
commit85dc423f7090da0a52c73eb66faf22ddb20efff9 (patch)
tree9160f299afd8c80c038f08e1545be119f5e3f1e1 /spec/lib/gitlab/relative_positioning
parent15c2c8c66dbe422588e5411eee7e68f1fa440bb8 (diff)
Add latest changes from gitlab-org/gitlab@13-4-stable-ee
Diffstat (limited to 'spec/lib/gitlab/relative_positioning')
-rw-r--r--spec/lib/gitlab/relative_positioning/item_context_spec.rb215
-rw-r--r--spec/lib/gitlab/relative_positioning/mover_spec.rb487
-rw-r--r--spec/lib/gitlab/relative_positioning/range_spec.rb162
3 files changed, 864 insertions, 0 deletions
diff --git a/spec/lib/gitlab/relative_positioning/item_context_spec.rb b/spec/lib/gitlab/relative_positioning/item_context_spec.rb
new file mode 100644
index 00000000000..daea8d8470d
--- /dev/null
+++ b/spec/lib/gitlab/relative_positioning/item_context_spec.rb
@@ -0,0 +1,215 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::RelativePositioning::ItemContext do
+ let_it_be(:default_user) { create_default(:user) }
+ let_it_be(:project, reload: true) { create(:project) }
+
+ def create_issue(pos)
+ create(:issue, project: project, relative_position: pos)
+ end
+
+ range = (101..107) # A deliberately small range, so we can test everything
+ indices = (0..).take(range.size)
+
+ let(:start) { ((range.first + range.last) / 2.0).floor }
+ let(:subjects) { issues.map { |i| described_class.new(i.reset, range) } }
+
+ # This allows us to refer to range in methods and examples
+ let_it_be(:full_range) { range }
+
+ context 'there are gaps at the start and end' do
+ let_it_be(:issues) { (range.first.succ..range.last.pred).map { |pos| create_issue(pos) } }
+
+ it 'is always possible to find a gap' do
+ expect(subjects)
+ .to all(have_attributes(find_next_gap_before: be_present, find_next_gap_after: be_present))
+ end
+
+ where(:index) { indices.reverse.drop(2) }
+
+ with_them do
+ subject { subjects[index] }
+
+ let(:positions) { subject.scoped_items.map(&:relative_position) }
+
+ it 'is possible to shift_right, which will consume the gap at the end' do
+ subject.shift_right
+
+ expect(subject.find_next_gap_after).not_to be_present
+
+ expect(positions).to all(be_between(range.first, range.last))
+ expect(positions).to eq(positions.uniq)
+ end
+
+ it 'is possible to create_space_right, which will move the gap to immediately after' do
+ subject.create_space_right
+
+ expect(subject.find_next_gap_after).to have_attributes(start_pos: subject.relative_position)
+ expect(positions).to all(be_between(range.first, range.last))
+ expect(positions).to eq(positions.uniq)
+ end
+
+ it 'is possible to shift_left, which will consume the gap at the start' do
+ subject.shift_left
+
+ expect(subject.find_next_gap_before).not_to be_present
+ expect(positions).to all(be_between(range.first, range.last))
+ expect(positions).to eq(positions.uniq)
+ end
+
+ it 'is possible to create_space_left, which will move the gap to immediately before' do
+ subject.create_space_left
+
+ expect(subject.find_next_gap_before).to have_attributes(start_pos: subject.relative_position)
+ expect(positions).to all(be_between(range.first, range.last))
+ expect(positions).to eq(positions.uniq)
+ end
+ end
+ end
+
+ context 'there is a gap of multiple spaces' do
+ let_it_be(:issues) { [range.first, range.last].map { |pos| create_issue(pos) } }
+
+ it 'is impossible to move the last element to the right' do
+ expect { subjects.last.shift_right }.to raise_error(Gitlab::RelativePositioning::NoSpaceLeft)
+ end
+
+ it 'is impossible to move the first element to the left' do
+ expect { subjects.first.shift_left }.to raise_error(Gitlab::RelativePositioning::NoSpaceLeft)
+ end
+
+ it 'is possible to move the last element to the left' do
+ subject = subjects.last
+
+ expect { subject.shift_left }.to change { subject.relative_position }.by(be < 0)
+ end
+
+ it 'is possible to move the first element to the right' do
+ subject = subjects.first
+
+ expect { subject.shift_right }.to change { subject.relative_position }.by(be > 0)
+ end
+
+ it 'is possible to find the gap from the right' do
+ gap = Gitlab::RelativePositioning::Gap.new(range.last, range.first)
+
+ expect(subjects.last).to have_attributes(
+ find_next_gap_before: eq(gap),
+ find_next_gap_after: be_nil
+ )
+ end
+
+ it 'is possible to find the gap from the left' do
+ gap = Gitlab::RelativePositioning::Gap.new(range.first, range.last)
+
+ expect(subjects.first).to have_attributes(
+ find_next_gap_before: be_nil,
+ find_next_gap_after: eq(gap)
+ )
+ end
+ end
+
+ context 'there are several free spaces' do
+ let_it_be(:issues) { range.select(&:even?).map { |pos| create_issue(pos) } }
+ let_it_be(:gaps) do
+ range.select(&:odd?).map do |pos|
+ rhs = pos.succ.clamp(range.first, range.last)
+ lhs = pos.pred.clamp(range.first, range.last)
+
+ {
+ before: Gitlab::RelativePositioning::Gap.new(rhs, lhs),
+ after: Gitlab::RelativePositioning::Gap.new(lhs, rhs)
+ }
+ end
+ end
+
+ def issue_at(position)
+ issues.find { |i| i.relative_position == position }
+ end
+
+ where(:current_pos) { range.select(&:even?) }
+
+ with_them do
+ let(:subject) { subjects.find { |s| s.relative_position == current_pos } }
+ let(:siblings) { subjects.reject { |s| s.relative_position == current_pos } }
+
+ def covered_by_range(pos)
+ full_range.cover?(pos) ? pos : nil
+ end
+
+ it 'finds the closest gap' do
+ closest_gap_before = gaps
+ .map { |gap| gap[:before] }
+ .select { |gap| gap.start_pos <= subject.relative_position }
+ .max_by { |gap| gap.start_pos }
+ closest_gap_after = gaps
+ .map { |gap| gap[:after] }
+ .select { |gap| gap.start_pos >= subject.relative_position }
+ .min_by { |gap| gap.start_pos }
+
+ expect(subject).to have_attributes(
+ find_next_gap_before: closest_gap_before,
+ find_next_gap_after: closest_gap_after
+ )
+ end
+
+ it 'finds the neighbours' do
+ expect(subject).to have_attributes(
+ lhs_neighbour: subject.neighbour(issue_at(subject.relative_position - 2)),
+ rhs_neighbour: subject.neighbour(issue_at(subject.relative_position + 2))
+ )
+ end
+
+ it 'finds the next relative_positions' do
+ expect(subject).to have_attributes(
+ prev_relative_position: covered_by_range(subject.relative_position - 2),
+ next_relative_position: covered_by_range(subject.relative_position + 2)
+ )
+ end
+
+ it 'finds the min/max positions' do
+ expect(subject).to have_attributes(
+ min_relative_position: issues.first.relative_position,
+ max_relative_position: issues.last.relative_position
+ )
+ end
+
+ it 'finds the min/max siblings' do
+ expect(subject).to have_attributes(
+ min_sibling: siblings.first,
+ max_sibling: siblings.last
+ )
+ end
+ end
+ end
+
+ context 'there is at least one free space' do
+ where(:free_space) { range.to_a }
+
+ with_them do
+ let(:issues) { range.reject { |x| x == free_space }.map { |p| create_issue(p) } }
+ let(:gap_rhs) { free_space.succ.clamp(range.first, range.last) }
+ let(:gap_lhs) { free_space.pred.clamp(range.first, range.last) }
+
+ it 'can always find a gap before if there is space to the left' do
+ expected_gap = Gitlab::RelativePositioning::Gap.new(gap_rhs, gap_lhs)
+
+ to_the_right_of_gap = subjects.select { |s| free_space < s.relative_position }
+
+ expect(to_the_right_of_gap)
+ .to all(have_attributes(find_next_gap_before: eq(expected_gap), find_next_gap_after: be_nil))
+ end
+
+ it 'can always find a gap after if there is space to the right' do
+ expected_gap = Gitlab::RelativePositioning::Gap.new(gap_lhs, gap_rhs)
+
+ to_the_left_of_gap = subjects.select { |s| s.relative_position < free_space }
+
+ expect(to_the_left_of_gap)
+ .to all(have_attributes(find_next_gap_before: be_nil, find_next_gap_after: eq(expected_gap)))
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/relative_positioning/mover_spec.rb b/spec/lib/gitlab/relative_positioning/mover_spec.rb
new file mode 100644
index 00000000000..c49230c2415
--- /dev/null
+++ b/spec/lib/gitlab/relative_positioning/mover_spec.rb
@@ -0,0 +1,487 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe RelativePositioning::Mover do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:one_sibling, reload: true) { create(:project, creator: user, namespace: user.namespace) }
+ let_it_be(:one_free_space, reload: true) { create(:project, creator: user, namespace: user.namespace) }
+ let_it_be(:fully_occupied, reload: true) { create(:project, creator: user, namespace: user.namespace) }
+ let_it_be(:no_issues, reload: true) { create(:project, creator: user, namespace: user.namespace) }
+ let_it_be(:three_sibs, reload: true) { create(:project, creator: user, namespace: user.namespace) }
+
+ def create_issue(pos, parent = project)
+ create(:issue, author: user, project: parent, relative_position: pos)
+ end
+
+ range = (101..105)
+ indices = (0..).take(range.size)
+
+ let(:start) { ((range.first + range.last) / 2.0).floor }
+
+ subject { described_class.new(start, range) }
+
+ let_it_be(:full_set) do
+ range.each_with_index.map do |pos, i|
+ create(:issue, iid: i.succ, project: fully_occupied, relative_position: pos)
+ end
+ end
+
+ let_it_be(:sole_sibling) { create(:issue, iid: 1, project: one_sibling, relative_position: nil) }
+ let_it_be(:one_sibling_set) { [sole_sibling] }
+ let_it_be(:one_free_space_set) do
+ indices.drop(1).map { |iid| create(:issue, project: one_free_space, iid: iid.succ) }
+ end
+ let_it_be(:three_sibs_set) do
+ [1, 2, 3].map { |iid| create(:issue, iid: iid, project: three_sibs) }
+ end
+
+ def set_positions(positions)
+ vals = issues.zip(positions).map do |issue, pos|
+ issue.relative_position = pos
+ "(#{issue.id}, #{pos})"
+ end.join(', ')
+
+ Issue.connection.exec_query(<<~SQL, 'set-positions')
+ WITH cte(cte_id, new_pos) AS (
+ SELECT * FROM (VALUES #{vals}) as t (id, pos)
+ )
+ UPDATE issues SET relative_position = new_pos FROM cte WHERE id = cte_id
+ ;
+ SQL
+ end
+
+ def ids_in_position_order
+ project.issues.reorder(:relative_position).pluck(:id)
+ end
+
+ def relative_positions
+ project.issues.pluck(:relative_position)
+ end
+
+ describe '#move_to_end' do
+ def max_position
+ project.issues.maximum(:relative_position)
+ end
+
+ def move_to_end(issue)
+ subject.move_to_end(issue)
+ issue.save!
+ end
+
+ shared_examples 'able to place a new item at the end' do
+ it 'can place any new item' do
+ existing_issues = ids_in_position_order
+ new_item = create_issue(nil)
+
+ expect do
+ move_to_end(new_item)
+ end.to change { project.issues.pluck(:id, :relative_position) }
+
+ expect(new_item.relative_position).to eq(max_position)
+ expect(relative_positions).to all(be_between(range.first, range.last))
+ expect(ids_in_position_order).to eq(existing_issues + [new_item.id])
+ end
+ end
+
+ shared_examples 'able to move existing items to the end' do
+ it 'can move any existing item' do
+ issues = project.issues.reorder(:relative_position).to_a
+ issue = issues[index]
+ other_issues = issues.reject { |i| i == issue }
+ expect(relative_positions).to all(be_between(range.first, range.last))
+
+ if issues.last == issue
+ move_to_end(issue) # May not change the positions
+ else
+ expect do
+ move_to_end(issue)
+ end.to change { project.issues.pluck(:id, :relative_position) }
+ end
+
+ project.reset
+
+ expect(relative_positions).to all(be_between(range.first, range.last))
+ expect(issue.relative_position).to eq(max_position)
+ expect(ids_in_position_order).to eq(other_issues.map(&:id) + [issue.id])
+ end
+ end
+
+ context 'all positions are taken' do
+ let(:issues) { full_set }
+ let(:project) { fully_occupied }
+
+ it 'raises an error when placing a new item' do
+ new_item = create_issue(nil)
+
+ expect { subject.move_to_end(new_item) }.to raise_error(RelativePositioning::NoSpaceLeft)
+ end
+
+ where(:index) { indices }
+
+ with_them do
+ it_behaves_like 'able to move existing items to the end'
+ end
+ end
+
+ context 'there are no siblings' do
+ let(:issues) { [] }
+ let(:project) { no_issues }
+
+ it_behaves_like 'able to place a new item at the end'
+ end
+
+ context 'there is only one sibling' do
+ where(:pos) { range.to_a }
+
+ with_them do
+ let(:issues) { one_sibling_set }
+ let(:project) { one_sibling }
+ let(:index) { 0 }
+
+ before do
+ sole_sibling.reset.update!(relative_position: pos)
+ end
+
+ it_behaves_like 'able to place a new item at the end'
+
+ it_behaves_like 'able to move existing items to the end'
+ end
+ end
+
+ context 'at least one position is free' do
+ where(:free_space, :index) do
+ is = indices.take(range.size - 1)
+
+ range.to_a.product(is)
+ end
+
+ with_them do
+ let(:issues) { one_free_space_set }
+ let(:project) { one_free_space }
+
+ before do
+ positions = range.reject { |x| x == free_space }
+ set_positions(positions)
+ end
+
+ it_behaves_like 'able to place a new item at the end'
+
+ it_behaves_like 'able to move existing items to the end'
+ end
+ end
+ end
+
+ describe '#move_to_start' do
+ def min_position
+ project.issues.minimum(:relative_position)
+ end
+
+ def move_to_start(issue)
+ subject.move_to_start(issue)
+ issue.save!
+ end
+
+ shared_examples 'able to place a new item at the start' do
+ it 'can place any new item' do
+ existing_issues = ids_in_position_order
+ new_item = create_issue(nil)
+
+ expect do
+ move_to_start(new_item)
+ end.to change { project.issues.pluck(:id, :relative_position) }
+
+ expect(relative_positions).to all(be_between(range.first, range.last))
+ expect(new_item.relative_position).to eq(min_position)
+ expect(ids_in_position_order).to eq([new_item.id] + existing_issues)
+ end
+ end
+
+ shared_examples 'able to move existing items to the start' do
+ it 'can move any existing item' do
+ issues = project.issues.reorder(:relative_position).to_a
+ issue = issues[index]
+ other_issues = issues.reject { |i| i == issue }
+ expect(relative_positions).to all(be_between(range.first, range.last))
+
+ if issues.first == issue
+ move_to_start(issue) # May not change the positions
+ else
+ expect do
+ move_to_start(issue)
+ end.to change { project.issues.pluck(:id, :relative_position) }
+ end
+
+ project.reset
+
+ expect(relative_positions).to all(be_between(range.first, range.last))
+ expect(issue.relative_position).to eq(min_position)
+ expect(ids_in_position_order).to eq([issue.id] + other_issues.map(&:id))
+ end
+ end
+
+ context 'all positions are taken' do
+ let(:issues) { full_set }
+ let(:project) { fully_occupied }
+
+ it 'raises an error when placing a new item' do
+ new_item = create(:issue, project: project, relative_position: nil)
+
+ expect { subject.move_to_start(new_item) }.to raise_error(RelativePositioning::NoSpaceLeft)
+ end
+
+ where(:index) { indices }
+
+ with_them do
+ it_behaves_like 'able to move existing items to the start'
+ end
+ end
+
+ context 'there are no siblings' do
+ let(:project) { no_issues }
+ let(:issues) { [] }
+
+ it_behaves_like 'able to place a new item at the start'
+ end
+
+ context 'there is only one sibling' do
+ where(:pos) { range.to_a }
+
+ with_them do
+ let(:issues) { one_sibling_set }
+ let(:project) { one_sibling }
+ let(:index) { 0 }
+
+ before do
+ sole_sibling.reset.update!(relative_position: pos)
+ end
+
+ it_behaves_like 'able to place a new item at the start'
+
+ it_behaves_like 'able to move existing items to the start'
+ end
+ end
+
+ context 'at least one position is free' do
+ where(:free_space, :index) do
+ range.to_a.product((0..).take(range.size - 1).to_a)
+ end
+
+ with_them do
+ let(:issues) { one_free_space_set }
+ let(:project) { one_free_space }
+
+ before do
+ set_positions(range.reject { |x| x == free_space })
+ end
+
+ it_behaves_like 'able to place a new item at the start'
+
+ it_behaves_like 'able to move existing items to the start'
+ end
+ end
+ end
+
+ describe '#move' do
+ shared_examples 'able to move a new item' do
+ let(:other_issues) { project.issues.reorder(relative_position: :asc).to_a }
+ let!(:previous_order) { other_issues.map(&:id) }
+
+ it 'can place any new item betwen two others' do
+ new_item = create_issue(nil)
+
+ subject.move(new_item, lhs, rhs)
+ new_item.save!
+ lhs.reset
+ rhs.reset
+
+ expect(new_item.relative_position).to be_between(range.first, range.last)
+ expect(new_item.relative_position).to be_between(lhs.relative_position, rhs.relative_position)
+
+ ids = project.issues.reorder(:relative_position).pluck(:id).reject { |id| id == new_item.id }
+ expect(ids).to eq(previous_order)
+ end
+
+ it 'can place any new item after another' do
+ new_item = create_issue(nil)
+
+ subject.move(new_item, lhs, nil)
+ new_item.save!
+ lhs.reset
+
+ expect(new_item.relative_position).to be_between(range.first, range.last)
+ expect(new_item.relative_position).to be > lhs.relative_position
+
+ ids = project.issues.reorder(:relative_position).pluck(:id).reject { |id| id == new_item.id }
+ expect(ids).to eq(previous_order)
+ end
+
+ it 'can place any new item before another' do
+ new_item = create_issue(nil)
+
+ subject.move(new_item, nil, rhs)
+ new_item.save!
+ rhs.reset
+
+ expect(new_item.relative_position).to be_between(range.first, range.last)
+ expect(new_item.relative_position).to be < rhs.relative_position
+
+ ids = project.issues.reorder(:relative_position).pluck(:id).reject { |id| id == new_item.id }
+ expect(ids).to eq(previous_order)
+ end
+ end
+
+ shared_examples 'able to move an existing item' do
+ let(:all_issues) { project.issues.reorder(:relative_position).to_a }
+ let(:item) { all_issues[index] }
+ let(:positions) { project.reset.issues.pluck(:relative_position) }
+ let(:other_issues) { all_issues.reject { |i| i == item } }
+ let!(:previous_order) { other_issues.map(&:id) }
+ let(:new_order) do
+ project.issues.where.not(id: item.id).reorder(:relative_position).pluck(:id)
+ end
+
+ it 'can place any item betwen two others' do
+ subject.move(item, lhs, rhs)
+ item.save!
+ lhs.reset
+ rhs.reset
+
+ expect(positions).to all(be_between(range.first, range.last))
+ expect(positions).to match_array(positions.uniq)
+ expect(item.relative_position).to be_between(lhs.relative_position, rhs.relative_position)
+
+ expect(new_order).to eq(previous_order)
+ end
+
+ def sequence(expected_sequence)
+ range = (expected_sequence.first.relative_position..expected_sequence.last.relative_position)
+
+ project.issues.reorder(:relative_position).where(relative_position: range)
+ end
+
+ it 'can place any item after another' do
+ subject.move(item, lhs, nil)
+ item.save!
+ lhs.reset
+
+ expect(positions).to all(be_between(range.first, range.last))
+ expect(positions).to match_array(positions.uniq)
+ expect(item.relative_position).to be >= lhs.relative_position
+
+ expected_sequence = [lhs, item].uniq
+
+ expect(sequence(expected_sequence)).to eq(expected_sequence)
+
+ expect(new_order).to eq(previous_order)
+ end
+
+ it 'can place any item before another' do
+ subject.move(item, nil, rhs)
+ item.save!
+ rhs.reset
+
+ expect(positions).to all(be_between(range.first, range.last))
+ expect(positions).to match_array(positions.uniq)
+ expect(item.relative_position).to be <= rhs.relative_position
+
+ expected_sequence = [item, rhs].uniq
+
+ expect(sequence(expected_sequence)).to eq(expected_sequence)
+
+ expect(new_order).to eq(previous_order)
+ end
+ end
+
+ context 'all positions are taken' do
+ let(:issues) { full_set }
+ let(:project) { fully_occupied }
+
+ where(:idx_a, :idx_b) do
+ indices.product(indices).select { |a, b| a < b }
+ end
+
+ with_them do
+ let(:lhs) { issues[idx_a].reset }
+ let(:rhs) { issues[idx_b].reset }
+
+ it 'raises an error when placing a new item anywhere' do
+ new_item = create_issue(nil)
+
+ expect { subject.move(new_item, lhs, rhs) }
+ .to raise_error(Gitlab::RelativePositioning::NoSpaceLeft)
+
+ expect { subject.move(new_item, nil, rhs) }
+ .to raise_error(Gitlab::RelativePositioning::NoSpaceLeft)
+
+ expect { subject.move(new_item, lhs, nil) }
+ .to raise_error(Gitlab::RelativePositioning::NoSpaceLeft)
+ end
+
+ where(:index) { indices }
+
+ with_them do
+ it_behaves_like 'able to move an existing item'
+ end
+ end
+ end
+
+ context 'there are no siblings' do
+ let(:project) { no_issues }
+
+ it 'raises an ArgumentError when both first and last are nil' do
+ new_item = create_issue(nil)
+
+ expect { subject.move(new_item, nil, nil) }.to raise_error(ArgumentError)
+ end
+ end
+
+ context 'there are a couple of siblings' do
+ where(:pos_movable, :pos_a, :pos_b) do
+ xs = range.to_a
+
+ xs.product(xs).product(xs).map(&:flatten)
+ .select { |vals| vals == vals.uniq && vals[1] < vals[2] }
+ end
+
+ with_them do
+ let(:issues) { three_sibs_set }
+ let(:project) { three_sibs }
+ let(:index) { 0 }
+ let(:lhs) { issues[1] }
+ let(:rhs) { issues[2] }
+
+ before do
+ set_positions([pos_movable, pos_a, pos_b])
+ end
+
+ it_behaves_like 'able to move a new item'
+ it_behaves_like 'able to move an existing item'
+ end
+ end
+
+ context 'at least one position is free' do
+ where(:free_space, :index, :pos_a, :pos_b) do
+ is = indices.reverse.drop(1)
+
+ range.to_a.product(is).product(is).product(is)
+ .map(&:flatten)
+ .select { |_, _, a, b| a < b }
+ end
+
+ with_them do
+ let(:issues) { one_free_space_set }
+ let(:project) { one_free_space }
+ let(:lhs) { issues[pos_a] }
+ let(:rhs) { issues[pos_b] }
+
+ before do
+ set_positions(range.reject { |x| x == free_space })
+ end
+
+ it_behaves_like 'able to move a new item'
+ it_behaves_like 'able to move an existing item'
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/relative_positioning/range_spec.rb b/spec/lib/gitlab/relative_positioning/range_spec.rb
new file mode 100644
index 00000000000..c3386336493
--- /dev/null
+++ b/spec/lib/gitlab/relative_positioning/range_spec.rb
@@ -0,0 +1,162 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::RelativePositioning::Range do
+ item_a = OpenStruct.new(relative_position: 100, object: :x, positioned?: true)
+ item_b = OpenStruct.new(relative_position: 200, object: :y, positioned?: true)
+
+ before do
+ allow(item_a).to receive(:lhs_neighbour) { nil }
+ allow(item_a).to receive(:rhs_neighbour) { item_b }
+
+ allow(item_b).to receive(:lhs_neighbour) { item_a }
+ allow(item_b).to receive(:rhs_neighbour) { nil }
+ end
+
+ describe 'RelativePositioning.range' do
+ it 'raises if lhs and rhs are nil' do
+ expect { Gitlab::RelativePositioning.range(nil, nil) }.to raise_error(ArgumentError)
+ end
+
+ it 'raises an error if there is no extent' do
+ expect { Gitlab::RelativePositioning.range(item_a, item_a) }.to raise_error(ArgumentError)
+ end
+
+ it 'constructs a closed range when both termini are provided' do
+ range = Gitlab::RelativePositioning.range(item_a, item_b)
+ expect(range).to be_a_kind_of(Gitlab::RelativePositioning::Range)
+ expect(range).to be_a_kind_of(Gitlab::RelativePositioning::ClosedRange)
+ end
+
+ it 'constructs a starting-from range when only the LHS is provided' do
+ range = Gitlab::RelativePositioning.range(item_a, nil)
+ expect(range).to be_a_kind_of(Gitlab::RelativePositioning::Range)
+ expect(range).to be_a_kind_of(Gitlab::RelativePositioning::StartingFrom)
+ end
+
+ it 'constructs an ending-at range when only the RHS is provided' do
+ range = Gitlab::RelativePositioning.range(nil, item_b)
+ expect(range).to be_a_kind_of(Gitlab::RelativePositioning::Range)
+ expect(range).to be_a_kind_of(Gitlab::RelativePositioning::EndingAt)
+ end
+ end
+
+ it 'infers neighbours correctly' do
+ starting_at_a = Gitlab::RelativePositioning.range(item_a, nil)
+ ending_at_b = Gitlab::RelativePositioning.range(nil, item_b)
+
+ expect(starting_at_a).to eq(ending_at_b)
+ end
+
+ describe '#open_on_left?' do
+ where(:lhs, :rhs, :expected_result) do
+ [
+ [item_a, item_b, false],
+ [item_a, nil, false],
+ [nil, item_b, false],
+ [item_b, nil, false],
+ [nil, item_a, true]
+ ]
+ end
+
+ with_them do
+ it 'is true if there is no LHS terminus' do
+ range = Gitlab::RelativePositioning.range(lhs, rhs)
+
+ expect(range.open_on_left?).to be(expected_result)
+ end
+ end
+ end
+
+ describe '#open_on_right?' do
+ where(:lhs, :rhs, :expected_result) do
+ [
+ [item_a, item_b, false],
+ [item_a, nil, false],
+ [nil, item_b, false],
+ [item_b, nil, true],
+ [nil, item_a, false]
+ ]
+ end
+
+ with_them do
+ it 'is true if there is no RHS terminus' do
+ range = Gitlab::RelativePositioning.range(lhs, rhs)
+
+ expect(range.open_on_right?).to be(expected_result)
+ end
+ end
+ end
+
+ describe '#cover?' do
+ item_c = OpenStruct.new(relative_position: 150, object: :z, positioned?: true)
+ item_d = OpenStruct.new(relative_position: 050, object: :w, positioned?: true)
+ item_e = OpenStruct.new(relative_position: 250, object: :r, positioned?: true)
+ item_f = OpenStruct.new(positioned?: false)
+ item_ax = OpenStruct.new(relative_position: 100, object: :not_x, positioned?: true)
+ item_bx = OpenStruct.new(relative_position: 200, object: :not_y, positioned?: true)
+
+ where(:lhs, :rhs, :item, :expected_result) do
+ [
+ [item_a, item_b, item_a, true],
+ [item_a, item_b, item_b, true],
+ [item_a, item_b, item_c, true],
+ [item_a, item_b, item_d, false],
+ [item_a, item_b, item_e, false],
+ [item_a, item_b, item_ax, false],
+ [item_a, item_b, item_bx, false],
+ [item_a, item_b, item_f, false],
+ [item_a, item_b, nil, false],
+
+ [nil, item_b, item_a, true],
+ [nil, item_b, item_b, true],
+ [nil, item_b, item_c, true],
+ [nil, item_b, item_d, false],
+ [nil, item_b, item_e, false],
+ [nil, item_b, item_ax, false],
+ [nil, item_b, item_bx, false],
+ [nil, item_b, item_f, false],
+ [nil, item_b, nil, false],
+
+ [item_a, nil, item_a, true],
+ [item_a, nil, item_b, true],
+ [item_a, nil, item_c, true],
+ [item_a, nil, item_d, false],
+ [item_a, nil, item_e, false],
+ [item_a, nil, item_ax, false],
+ [item_a, nil, item_bx, false],
+ [item_a, nil, item_f, false],
+ [item_a, nil, nil, false],
+
+ [nil, item_a, item_a, true],
+ [nil, item_a, item_b, false],
+ [nil, item_a, item_c, false],
+ [nil, item_a, item_d, true],
+ [nil, item_a, item_e, false],
+ [nil, item_a, item_ax, false],
+ [nil, item_a, item_bx, false],
+ [nil, item_a, item_f, false],
+ [nil, item_a, nil, false],
+
+ [item_b, nil, item_a, false],
+ [item_b, nil, item_b, true],
+ [item_b, nil, item_c, false],
+ [item_b, nil, item_d, false],
+ [item_b, nil, item_e, true],
+ [item_b, nil, item_ax, false],
+ [item_b, nil, item_bx, false],
+ [item_b, nil, item_f, false],
+ [item_b, nil, nil, false]
+ ]
+ end
+
+ with_them do
+ it 'is true when the object is within the bounds of the range' do
+ range = Gitlab::RelativePositioning.range(lhs, rhs)
+
+ expect(range.cover?(item)).to be(expected_result)
+ end
+ end
+ end
+end