/spec/lib/gitlab/relative_positioning/mover_spec.rb

https://gitlab.com/klml/gitlab-ee · Ruby · 481 lines · 363 code · 117 blank · 1 comment · 15 complexity · a7e71b9503b58523cf82d9fcaf756e34 MD5 · raw file

  1. # frozen_string_literal: true
  2. require 'spec_helper'
  3. RSpec.describe RelativePositioning::Mover do
  4. let_it_be(:user) { create(:user) }
  5. let_it_be(:one_sibling, reload: true) { create(:project, creator: user, namespace: user.namespace) }
  6. let_it_be(:one_free_space, reload: true) { create(:project, creator: user, namespace: user.namespace) }
  7. let_it_be(:fully_occupied, reload: true) { create(:project, creator: user, namespace: user.namespace) }
  8. let_it_be(:no_issues, reload: true) { create(:project, creator: user, namespace: user.namespace) }
  9. let_it_be(:three_sibs, reload: true) { create(:project, creator: user, namespace: user.namespace) }
  10. def create_issue(pos, parent = project)
  11. create(:issue, author: user, project: parent, relative_position: pos)
  12. end
  13. range = (101..105)
  14. indices = (0..).take(range.size)
  15. let(:start) { ((range.first + range.last) / 2.0).floor }
  16. subject { described_class.new(start, range) }
  17. let_it_be(:full_set) do
  18. range.each_with_index.map do |pos, i|
  19. create(:issue, iid: i.succ, project: fully_occupied, relative_position: pos)
  20. end
  21. end
  22. let_it_be(:sole_sibling) { create(:issue, iid: 1, project: one_sibling, relative_position: nil) }
  23. let_it_be(:one_sibling_set) { [sole_sibling] }
  24. let_it_be(:one_free_space_set) do
  25. indices.drop(1).map { |iid| create(:issue, project: one_free_space, iid: iid.succ) }
  26. end
  27. let_it_be(:three_sibs_set) do
  28. [1, 2, 3].map { |iid| create(:issue, iid: iid, project: three_sibs) }
  29. end
  30. def set_positions(positions)
  31. mapping = issues.zip(positions).to_h do |issue, pos|
  32. [issue, { relative_position: pos }]
  33. end
  34. ::Gitlab::Database::BulkUpdate.execute([:relative_position], mapping)
  35. end
  36. def ids_in_position_order
  37. project.issues.reorder(:relative_position).pluck(:id)
  38. end
  39. def relative_positions
  40. project.issues.pluck(:relative_position)
  41. end
  42. describe '#move_to_end' do
  43. def max_position
  44. project.issues.maximum(:relative_position)
  45. end
  46. def move_to_end(issue)
  47. subject.move_to_end(issue)
  48. issue.save!
  49. end
  50. shared_examples 'able to place a new item at the end' do
  51. it 'can place any new item' do
  52. existing_issues = ids_in_position_order
  53. new_item = create_issue(nil)
  54. expect do
  55. move_to_end(new_item)
  56. end.to change { project.issues.pluck(:id, :relative_position) }
  57. expect(new_item.relative_position).to eq(max_position)
  58. expect(relative_positions).to all(be_between(range.first, range.last))
  59. expect(ids_in_position_order).to eq(existing_issues + [new_item.id])
  60. end
  61. end
  62. shared_examples 'able to move existing items to the end' do
  63. it 'can move any existing item' do
  64. issues = project.issues.reorder(:relative_position).to_a
  65. issue = issues[index]
  66. other_issues = issues.reject { |i| i == issue }
  67. expect(relative_positions).to all(be_between(range.first, range.last))
  68. if issues.last == issue
  69. move_to_end(issue) # May not change the positions
  70. else
  71. expect do
  72. move_to_end(issue)
  73. end.to change { project.issues.pluck(:id, :relative_position) }
  74. end
  75. project.reset
  76. expect(relative_positions).to all(be_between(range.first, range.last))
  77. expect(issue.relative_position).to eq(max_position)
  78. expect(ids_in_position_order).to eq(other_issues.map(&:id) + [issue.id])
  79. end
  80. end
  81. context 'all positions are taken' do
  82. let(:issues) { full_set }
  83. let(:project) { fully_occupied }
  84. it 'raises an error when placing a new item' do
  85. new_item = create_issue(nil)
  86. expect { subject.move_to_end(new_item) }.to raise_error(RelativePositioning::NoSpaceLeft)
  87. end
  88. where(:index) { indices }
  89. with_them do
  90. it_behaves_like 'able to move existing items to the end'
  91. end
  92. end
  93. context 'there are no siblings' do
  94. let(:issues) { [] }
  95. let(:project) { no_issues }
  96. it_behaves_like 'able to place a new item at the end'
  97. end
  98. context 'there is only one sibling' do
  99. where(:pos) { range.to_a }
  100. with_them do
  101. let(:issues) { one_sibling_set }
  102. let(:project) { one_sibling }
  103. let(:index) { 0 }
  104. before do
  105. sole_sibling.reset.update!(relative_position: pos)
  106. end
  107. it_behaves_like 'able to place a new item at the end'
  108. it_behaves_like 'able to move existing items to the end'
  109. end
  110. end
  111. context 'at least one position is free' do
  112. where(:free_space, :index) do
  113. is = indices.take(range.size - 1)
  114. range.to_a.product(is)
  115. end
  116. with_them do
  117. let(:issues) { one_free_space_set }
  118. let(:project) { one_free_space }
  119. before do
  120. positions = range.reject { |x| x == free_space }
  121. set_positions(positions)
  122. end
  123. it_behaves_like 'able to place a new item at the end'
  124. it_behaves_like 'able to move existing items to the end'
  125. end
  126. end
  127. end
  128. describe '#move_to_start' do
  129. def min_position
  130. project.issues.minimum(:relative_position)
  131. end
  132. def move_to_start(issue)
  133. subject.move_to_start(issue)
  134. issue.save!
  135. end
  136. shared_examples 'able to place a new item at the start' do
  137. it 'can place any new item' do
  138. existing_issues = ids_in_position_order
  139. new_item = create_issue(nil)
  140. expect do
  141. move_to_start(new_item)
  142. end.to change { project.issues.pluck(:id, :relative_position) }
  143. expect(relative_positions).to all(be_between(range.first, range.last))
  144. expect(new_item.relative_position).to eq(min_position)
  145. expect(ids_in_position_order).to eq([new_item.id] + existing_issues)
  146. end
  147. end
  148. shared_examples 'able to move existing items to the start' do
  149. it 'can move any existing item' do
  150. issues = project.issues.reorder(:relative_position).to_a
  151. issue = issues[index]
  152. other_issues = issues.reject { |i| i == issue }
  153. expect(relative_positions).to all(be_between(range.first, range.last))
  154. if issues.first == issue
  155. move_to_start(issue) # May not change the positions
  156. else
  157. expect do
  158. move_to_start(issue)
  159. end.to change { project.issues.pluck(:id, :relative_position) }
  160. end
  161. project.reset
  162. expect(relative_positions).to all(be_between(range.first, range.last))
  163. expect(issue.relative_position).to eq(min_position)
  164. expect(ids_in_position_order).to eq([issue.id] + other_issues.map(&:id))
  165. end
  166. end
  167. context 'all positions are taken' do
  168. let(:issues) { full_set }
  169. let(:project) { fully_occupied }
  170. it 'raises an error when placing a new item' do
  171. new_item = create(:issue, project: project, relative_position: nil)
  172. expect { subject.move_to_start(new_item) }.to raise_error(RelativePositioning::NoSpaceLeft)
  173. end
  174. where(:index) { indices }
  175. with_them do
  176. it_behaves_like 'able to move existing items to the start'
  177. end
  178. end
  179. context 'there are no siblings' do
  180. let(:project) { no_issues }
  181. let(:issues) { [] }
  182. it_behaves_like 'able to place a new item at the start'
  183. end
  184. context 'there is only one sibling' do
  185. where(:pos) { range.to_a }
  186. with_them do
  187. let(:issues) { one_sibling_set }
  188. let(:project) { one_sibling }
  189. let(:index) { 0 }
  190. before do
  191. sole_sibling.reset.update!(relative_position: pos)
  192. end
  193. it_behaves_like 'able to place a new item at the start'
  194. it_behaves_like 'able to move existing items to the start'
  195. end
  196. end
  197. context 'at least one position is free' do
  198. where(:free_space, :index) do
  199. range.to_a.product((0..).take(range.size - 1).to_a)
  200. end
  201. with_them do
  202. let(:issues) { one_free_space_set }
  203. let(:project) { one_free_space }
  204. before do
  205. set_positions(range.reject { |x| x == free_space })
  206. end
  207. it_behaves_like 'able to place a new item at the start'
  208. it_behaves_like 'able to move existing items to the start'
  209. end
  210. end
  211. end
  212. describe '#move' do
  213. shared_examples 'able to move a new item' do
  214. let(:other_issues) { project.issues.reorder(relative_position: :asc).to_a }
  215. let!(:previous_order) { other_issues.map(&:id) }
  216. it 'can place any new item betwen two others' do
  217. new_item = create_issue(nil)
  218. subject.move(new_item, lhs, rhs)
  219. new_item.save!
  220. lhs.reset
  221. rhs.reset
  222. expect(new_item.relative_position).to be_between(range.first, range.last)
  223. expect(new_item.relative_position).to be_between(lhs.relative_position, rhs.relative_position)
  224. ids = project.issues.reorder(:relative_position).pluck(:id).reject { |id| id == new_item.id }
  225. expect(ids).to eq(previous_order)
  226. end
  227. it 'can place any new item after another' do
  228. new_item = create_issue(nil)
  229. subject.move(new_item, lhs, nil)
  230. new_item.save!
  231. lhs.reset
  232. expect(new_item.relative_position).to be_between(range.first, range.last)
  233. expect(new_item.relative_position).to be > lhs.relative_position
  234. ids = project.issues.reorder(:relative_position).pluck(:id).reject { |id| id == new_item.id }
  235. expect(ids).to eq(previous_order)
  236. end
  237. it 'can place any new item before another' do
  238. new_item = create_issue(nil)
  239. subject.move(new_item, nil, rhs)
  240. new_item.save!
  241. rhs.reset
  242. expect(new_item.relative_position).to be_between(range.first, range.last)
  243. expect(new_item.relative_position).to be < rhs.relative_position
  244. ids = project.issues.reorder(:relative_position).pluck(:id).reject { |id| id == new_item.id }
  245. expect(ids).to eq(previous_order)
  246. end
  247. end
  248. shared_examples 'able to move an existing item' do
  249. let(:all_issues) { project.issues.reorder(:relative_position).to_a }
  250. let(:item) { all_issues[index] }
  251. let(:positions) { project.reset.issues.pluck(:relative_position) }
  252. let(:other_issues) { all_issues.reject { |i| i == item } }
  253. let!(:previous_order) { other_issues.map(&:id) }
  254. let(:new_order) do
  255. project.issues.where.not(id: item.id).reorder(:relative_position).pluck(:id)
  256. end
  257. it 'can place any item betwen two others' do
  258. subject.move(item, lhs, rhs)
  259. item.save!
  260. lhs.reset
  261. rhs.reset
  262. expect(positions).to all(be_between(range.first, range.last))
  263. expect(positions).to match_array(positions.uniq)
  264. expect(item.relative_position).to be_between(lhs.relative_position, rhs.relative_position)
  265. expect(new_order).to eq(previous_order)
  266. end
  267. def sequence(expected_sequence)
  268. range = (expected_sequence.first.relative_position..expected_sequence.last.relative_position)
  269. project.issues.reorder(:relative_position).where(relative_position: range)
  270. end
  271. it 'can place any item after another' do
  272. subject.move(item, lhs, nil)
  273. item.save!
  274. lhs.reset
  275. expect(positions).to all(be_between(range.first, range.last))
  276. expect(positions).to match_array(positions.uniq)
  277. expect(item.relative_position).to be >= lhs.relative_position
  278. expected_sequence = [lhs, item].uniq
  279. expect(sequence(expected_sequence)).to eq(expected_sequence)
  280. expect(new_order).to eq(previous_order)
  281. end
  282. it 'can place any item before another' do
  283. subject.move(item, nil, rhs)
  284. item.save!
  285. rhs.reset
  286. expect(positions).to all(be_between(range.first, range.last))
  287. expect(positions).to match_array(positions.uniq)
  288. expect(item.relative_position).to be <= rhs.relative_position
  289. expected_sequence = [item, rhs].uniq
  290. expect(sequence(expected_sequence)).to eq(expected_sequence)
  291. expect(new_order).to eq(previous_order)
  292. end
  293. end
  294. context 'all positions are taken' do
  295. let(:issues) { full_set }
  296. let(:project) { fully_occupied }
  297. where(:idx_a, :idx_b) do
  298. indices.product(indices).select { |a, b| a < b }
  299. end
  300. with_them do
  301. let(:lhs) { issues[idx_a].reset }
  302. let(:rhs) { issues[idx_b].reset }
  303. it 'raises an error when placing a new item anywhere' do
  304. new_item = create_issue(nil)
  305. expect { subject.move(new_item, lhs, rhs) }
  306. .to raise_error(Gitlab::RelativePositioning::NoSpaceLeft)
  307. expect { subject.move(new_item, nil, rhs) }
  308. .to raise_error(Gitlab::RelativePositioning::NoSpaceLeft)
  309. expect { subject.move(new_item, lhs, nil) }
  310. .to raise_error(Gitlab::RelativePositioning::NoSpaceLeft)
  311. end
  312. where(:index) { indices }
  313. with_them do
  314. it_behaves_like 'able to move an existing item'
  315. end
  316. end
  317. end
  318. context 'there are no siblings' do
  319. let(:project) { no_issues }
  320. it 'raises an ArgumentError when both first and last are nil' do
  321. new_item = create_issue(nil)
  322. expect { subject.move(new_item, nil, nil) }.to raise_error(ArgumentError)
  323. end
  324. end
  325. context 'there are a couple of siblings' do
  326. where(:pos_movable, :pos_a, :pos_b) do
  327. xs = range.to_a
  328. xs.product(xs).product(xs).map(&:flatten)
  329. .select { |vals| vals == vals.uniq && vals[1] < vals[2] }
  330. end
  331. with_them do
  332. let(:issues) { three_sibs_set }
  333. let(:project) { three_sibs }
  334. let(:index) { 0 }
  335. let(:lhs) { issues[1] }
  336. let(:rhs) { issues[2] }
  337. before do
  338. set_positions([pos_movable, pos_a, pos_b])
  339. end
  340. it_behaves_like 'able to move a new item'
  341. it_behaves_like 'able to move an existing item'
  342. end
  343. end
  344. context 'at least one position is free' do
  345. where(:free_space, :index, :pos_a, :pos_b) do
  346. is = indices.reverse.drop(1)
  347. range.to_a.product(is).product(is).product(is)
  348. .map(&:flatten)
  349. .select { |_, _, a, b| a < b }
  350. end
  351. with_them do
  352. let(:issues) { one_free_space_set }
  353. let(:project) { one_free_space }
  354. let(:lhs) { issues[pos_a] }
  355. let(:rhs) { issues[pos_b] }
  356. before do
  357. set_positions(range.reject { |x| x == free_space })
  358. end
  359. it_behaves_like 'able to move a new item'
  360. it_behaves_like 'able to move an existing item'
  361. end
  362. end
  363. end
  364. end