diff --git a/modules/boards/spec/features/board_highlighting_spec.rb b/modules/boards/spec/features/board_highlighting_spec.rb
index b0b35d16bf2..80e8d87c550 100644
--- a/modules/boards/spec/features/board_highlighting_spec.rb
+++ b/modules/boards/spec/features/board_highlighting_spec.rb
@@ -87,17 +87,17 @@ RSpec.describe "Work Package boards spec", :js, :selenium do
expect(page).to have_css(".__hl_inline_type_#{type2.id}")
# Highlight whole card by priority
- board_page.change_board_highlighting "inline", "Priority"
+ board_page.change_board_highlighting "Entire card by", "Priority"
expect(page).to have_css(".__hl_background_priority_#{priority.id}")
expect(page).to have_css(".__hl_background_priority_#{priority2.id}")
# Highlight whole card by type
- board_page.change_board_highlighting "inline", "Type"
+ board_page.change_board_highlighting "Entire card by", "Type"
expect(page).to have_css(".__hl_background_type_#{type.id}")
expect(page).to have_css(".__hl_background_type_#{type2.id}")
# Disable highlighting
- board_page.change_board_highlighting "none"
+ board_page.change_board_highlighting "No highlighting"
expect(page).to have_no_css(".__hl_background_type_#{type.id}")
expect(page).to have_no_css(".__hl_background_type_#{type2.id}")
diff --git a/modules/boards/spec/features/support/board_page.rb b/modules/boards/spec/features/support/board_page.rb
index 6df9283083c..b63f2f0e9a8 100644
--- a/modules/boards/spec/features/support/board_page.rb
+++ b/modules/boards/spec/features/support/board_page.rb
@@ -374,9 +374,8 @@ module Pages
def change_board_highlighting(mode, attribute = nil)
click_dropdown_entry "Configure view"
- if attribute.nil?
- choose(option: mode)
- else
+ choose mode
+ unless attribute.nil?
select attribute, from: "selected_attribute"
end
diff --git a/spec/controllers/work_packages/bulk_controller_spec.rb b/spec/controllers/work_packages/bulk_controller_spec.rb
index 72c297a2b98..888994da169 100644
--- a/spec/controllers/work_packages/bulk_controller_spec.rb
+++ b/spec/controllers/work_packages/bulk_controller_spec.rb
@@ -141,6 +141,39 @@ RSpec.describe WorkPackages::BulkController, with_settings: { journal_aggregatio
it { assert_select "input", attributes: { name: "work_package[parent_id]" } }
end
+ context "with work package list" do
+ context "with classic (numeric) identifiers" do
+ it "displays a hash-prefixed numeric id link for each work package" do
+ assert_select "ul li a", text: /\A#{Regexp.escape(work_package1.type.to_s)} ##{work_package1.id}\z/
+ assert_select "ul li a", text: /\A#{Regexp.escape(work_package2.type.to_s)} ##{work_package2.id}\z/
+ end
+ end
+
+ context "with semantic identifiers" do
+ let(:semantic_prefix) { "TESTPROJ" }
+
+ before do
+ allow(Setting::WorkPackageIdentifier).to receive_messages(semantic?: true, classic?: false)
+ work_package1.update_columns(identifier: "#{semantic_prefix}-1", sequence_number: 1)
+ work_package2.update_columns(identifier: "#{semantic_prefix}-2", sequence_number: 2)
+ end
+
+ it "displays the semantic identifier in each link" do
+ get :edit, params: { ids: [work_package1.id, work_package2.id] }
+
+ assert_select "ul li a", text: /#{semantic_prefix}-1/
+ assert_select "ul li a", text: /#{semantic_prefix}-2/
+ end
+
+ it "does not display a bare numeric id in the links" do
+ get :edit, params: { ids: [work_package1.id, work_package2.id] }
+
+ assert_select "ul li a", text: /##{work_package1.id}/, count: 0
+ assert_select "ul li a", text: /##{work_package2.id}/, count: 0
+ end
+ end
+ end
+
context "custom_field" do
describe "#type" do
it { assert_select "input", attributes: { name: "work_package[custom_field_values][#{custom_field1.id}]" } }
diff --git a/spec/services/project_identifiers/revert_project_to_classic_service_spec.rb b/spec/services/project_identifiers/revert_project_to_classic_service_spec.rb
index d75790155a1..8ab8640f559 100644
--- a/spec/services/project_identifiers/revert_project_to_classic_service_spec.rb
+++ b/spec/services/project_identifiers/revert_project_to_classic_service_spec.rb
@@ -109,5 +109,38 @@ RSpec.describe ProjectIdentifiers::RevertProjectToClassicService do
expect(project.reload.identifier).to match(/\Aproject-[a-z0-9]{5}\z/)
end
end
+
+ context "when the classic slug from FriendlyId history is already taken by another project" do
+ let!(:blocking_project) { create(:project, identifier: "my-app") }
+
+ let!(:project) do
+ create(:project).tap do |p|
+ p.update_columns(identifier: "MYAPP", wp_sequence_counter: 0)
+ # Remove p's own initial slug so "my-app" is the only entry in its slug history.
+ # Without this, the factory slug (newer created_at) would be returned first by
+ # restore_identifier, the update would succeed, and the conflict path would never fire.
+ FriendlyId::Slug.where(sluggable_id: p.id, sluggable_type: "Project").delete_all
+ # blocking_project already owns the "my-app" FriendlyId slug; reassign it so that
+ # restore_identifier returns "my-app" and project.update! conflicts with blocking_project.
+ FriendlyId::Slug.where(slug: "my-app", sluggable_type: "Project").update_all(sluggable_id: p.id)
+ end
+ end
+
+ it "does not raise" do
+ expect { described_class.new(project).call }.not_to raise_error
+ end
+
+ it "assigns a project-NNNNN fallback identifier" do
+ described_class.new(project).call
+ expect(project.reload.identifier).to match(/\Aproject-[a-z0-9]{5}\z/)
+ end
+
+ it "logs a warning containing the project id and the conflicting identifier" do
+ allow(Rails.logger).to receive(:warn)
+ described_class.new(project).call
+ expect(Rails.logger).to have_received(:warn)
+ .with(a_string_including(project.id.to_s, "my-app"))
+ end
+ end
end
end
diff --git a/spec/support/components/work_packages/table_configuration/highlighting.rb b/spec/support/components/work_packages/table_configuration/highlighting.rb
index 2a1fdd59f7f..48cc72b039e 100644
--- a/spec/support/components/work_packages/table_configuration/highlighting.rb
+++ b/spec/support/components/work_packages/table_configuration/highlighting.rb
@@ -47,7 +47,7 @@ module Components
choose "Entire row by"
# Open select field
- within(page.all(".form--field")[1]) do
+ within(page.all(".form--field")[3]) do
page.find(".ng-input input").click
end
page.find(".ng-dropdown-panel .ng-option", text: label).click
@@ -59,7 +59,7 @@ module Components
choose "Highlighted attribute(s)"
# Open select field
- within(page.all(".form--field")[0]) do
+ within(page.all(".form--field")[1]) do
page.find(".ng-input input").click
end
diff --git a/spec/workers/project_identifiers/revert_instance_to_classic_ids_job_spec.rb b/spec/workers/project_identifiers/revert_instance_to_classic_ids_job_spec.rb
index 2ff40ac9f34..0b65c2d61bb 100644
--- a/spec/workers/project_identifiers/revert_instance_to_classic_ids_job_spec.rb
+++ b/spec/workers/project_identifiers/revert_instance_to_classic_ids_job_spec.rb
@@ -112,6 +112,53 @@ RSpec.describe ProjectIdentifiers::RevertInstanceToClassicIdsJob do
.to match(Projects::Identifier::CLASSIC_FORMAT)
end
end
+
+ context "when one project has a conflicting restored identifier" do
+ # project_before and project_after bracket the conflict project to confirm
+ # the find_each loop is not aborted — both must be processed.
+ let!(:project_before) do
+ create(:project).tap do |p|
+ p.update_columns(identifier: "BEFORE1")
+ FriendlyId::Slug.create!(sluggable: p, slug: "project-before")
+ end
+ end
+
+ # Holds "conflict-slug" as its current identifier, blocking project_conflict
+ # from reclaiming it.
+ let!(:blocking_project) { create(:project, identifier: "conflict-slug") }
+
+ let!(:project_conflict) do
+ create(:project).tap do |p|
+ p.update_columns(identifier: "CONFLICT1")
+ FriendlyId::Slug.where(sluggable_id: p.id, sluggable_type: "Project").delete_all
+ FriendlyId::Slug.where(slug: "conflict-slug", sluggable_type: "Project").update_all(sluggable_id: p.id)
+ end
+ end
+
+ let!(:project_after) do
+ create(:project).tap do |p|
+ p.update_columns(identifier: "AFTER1")
+ FriendlyId::Slug.create!(sluggable: p, slug: "project-after")
+ end
+ end
+
+ before do
+ allow(Setting::WorkPackageIdentifier).to receive_messages(classic?: true, semantic?: false)
+ described_class.new.perform
+ end
+
+ it "still reverts project_before (loop not aborted before the conflict)" do
+ expect(project_before.reload.identifier).to eq("project-before")
+ end
+
+ it "still reverts project_after (loop not aborted after the conflict)" do
+ expect(project_after.reload.identifier).to eq("project-after")
+ end
+
+ it "assigns project_conflict a project-NNNNN fallback identifier" do
+ expect(project_conflict.reload.identifier).to match(/\Aproject-[a-z0-9]{5}\z/)
+ end
+ end
end
end
end