diff --git a/Changelog.md b/Changelog.md
index 8ce7cce912..c82ad22ecc 100644
--- a/Changelog.md
+++ b/Changelog.md
@@ -26,6 +26,7 @@
 - Add unit tests for `marks_graders_controller` (#7382)
 - Convert front-end tests from enzyme to react testing library; add `@testing-library/user-event` (#7379)
 - Refactor the `Result` component and its children to use React context API (#7380)
+- Implement `contain_message` and `have_message` custom Rspec matchers to check for flash message content (#7386)
 - Update Python version to 3.13 in seed autotest schemas (#7388)
 - Rename jupyter notebook content functions and files to generalize to html content (#7391)
 
diff --git a/spec/controllers/annotation_categories_controller_spec.rb b/spec/controllers/annotation_categories_controller_spec.rb
index ce416238cf..e9678808b5 100644
--- a/spec/controllers/annotation_categories_controller_spec.rb
+++ b/spec/controllers/annotation_categories_controller_spec.rb
@@ -702,8 +702,7 @@
 
         expect(response).to have_http_status(:found)
         expect(flash[:error]).to be_nil
-        expect(flash[:success].map { |f| extract_text f }).to eq([I18n.t('upload_success',
-                                                                         count: 2)].map { |f| extract_text f })
+        expect(flash[:success]).to have_message(I18n.t('upload_success', count: 2))
         expect(response).to redirect_to(action: 'index', assignment_id: assignment.id)
 
         expect(AnnotationCategory.all.size).to eq(2)
@@ -729,8 +728,7 @@
 
         expect(response).to have_http_status :found
         expect(flash[:error]).to be_nil
-        expect(flash[:success].map { |f| extract_text f }).to eq([I18n.t('upload_success',
-                                                                         count: 3)].map { |f| extract_text f })
+        expect(flash[:success]).to have_message(I18n.t('upload_success', count: 3))
         expect(response).to redirect_to(action: 'index', assignment_id: assignment.id)
 
         expect(AnnotationCategory.all.size).to eq 3
diff --git a/spec/controllers/assignments_controller_spec.rb b/spec/controllers/assignments_controller_spec.rb
index 50024893b1..00ff86a0c4 100644
--- a/spec/controllers/assignments_controller_spec.rb
+++ b/spec/controllers/assignments_controller_spec.rb
@@ -2277,7 +2277,7 @@
       grouping # lazy initialization
       delete_as instructor, :destroy, params: { course_id: course.id, id: assignment.id }
       expect(Assignment.exists?(assignment.id)).to be(true)
-      expect(flash[:error]).to eq(["<p>#{I18n.t('assignments.assignment_has_groupings')}</p>"])
+      expect(flash[:error]).to have_message(I18n.t('assignments.assignment_has_groupings'))
       expect(flash.to_hash.length).to eq(1)
       expect(flash[:error].length).to eq(1)
       expect(response).to have_http_status(:found)
@@ -2286,8 +2286,8 @@
     it 'should successfully DELETE assignment (no groups)' do
       delete_as instructor, :destroy, params: { course_id: course.id, id: assignment.id }
       expect(Assignment.exists?(assignment.id)).to be(false)
-      expect(flash[:success]).to eq(I18n.t('flash.actions.destroy.success',
-                                           resource_name: assignment.short_identifier))
+      expect(flash[:success]).to have_message(I18n.t('flash.actions.destroy.success',
+                                                     resource_name: assignment.short_identifier))
       expect(flash.to_hash.length).to eq(1)
       expect(response).to have_http_status(:found)
     end
@@ -2318,10 +2318,8 @@
 
       delete_as instructor, :destroy, params: { course_id: course.id, id: assignment.id }
 
-      expect(flash[:error][0]).to include(
-        I18n.t('activerecord.errors.models.assignment_deletion',
-               problem_message: 'some error')
-      )
+      expect(flash[:error]).to contain_message(I18n.t('activerecord.errors.models.assignment_deletion',
+                                                      problem_message: 'some error'))
       expect(Assignment.exists?(assignment.id)).to be true
       expect(response).to redirect_to(edit_course_assignment_path(course.id, assignment.id))
     end
diff --git a/spec/controllers/automated_tests_controller_spec.rb b/spec/controllers/automated_tests_controller_spec.rb
index d9321c06c0..c23ff792ce 100644
--- a/spec/controllers/automated_tests_controller_spec.rb
+++ b/spec/controllers/automated_tests_controller_spec.rb
@@ -279,7 +279,7 @@
         end
 
         it 'flashes an error message' do
-          expect(flash[:error].join('\n')).to include(I18n.t('errors.invalid_path'))
+          expect(flash[:error]).to contain_message(I18n.t('errors.invalid_path'))
         end
 
         it 'does not save the file' do
@@ -294,7 +294,7 @@
         end
 
         it 'flashes an error message' do
-          expect(flash[:error].join('\n')).to include(I18n.t('errors.invalid_path'))
+          expect(flash[:error]).to contain_message(I18n.t('errors.invalid_path'))
         end
 
         it 'does not create the folder' do
@@ -309,7 +309,7 @@
         end
 
         it 'flashes an error message' do
-          expect(flash[:error].join('\n')).to include(I18n.t('errors.invalid_path'))
+          expect(flash[:error]).to contain_message(I18n.t('errors.invalid_path'))
         end
 
         it 'does not delete the folder' do
@@ -324,7 +324,7 @@
         end
 
         it 'flashes an error message' do
-          expect(flash[:error].join('\n')).to include(I18n.t('errors.invalid_path'))
+          expect(flash[:error]).to contain_message(I18n.t('errors.invalid_path'))
         end
 
         it 'does not delete the file' do
diff --git a/spec/controllers/courses_controller_spec.rb b/spec/controllers/courses_controller_spec.rb
index 7b7de2d202..35b652074d 100644
--- a/spec/controllers/courses_controller_spec.rb
+++ b/spec/controllers/courses_controller_spec.rb
@@ -367,8 +367,7 @@
       test2 = Assignment.find_by(short_identifier: @test_asn2)
       expect(test2).not_to be_nil
       expect(flash[:error]).to be_nil
-      expect(flash[:success].map { |f| extract_text f }).to eq([I18n.t('upload_success',
-                                                                       count: 2)].map { |f| extract_text f })
+      expect(flash[:success]).to have_message(I18n.t('upload_success', count: 2))
       expect(response).to redirect_to(course_assignments_path(course))
     end
 
diff --git a/spec/controllers/criteria_controller_spec.rb b/spec/controllers/criteria_controller_spec.rb
index 8d8af0553b..83dde7d93a 100644
--- a/spec/controllers/criteria_controller_spec.rb
+++ b/spec/controllers/criteria_controller_spec.rb
@@ -420,8 +420,7 @@
                   params: { course_id: course.id, id: flexible_criterion.id },
                   format: :js
         expect(assigns(:criterion)).to be_truthy
-        i18t_strings = [I18n.t('flash.criteria.destroy.success')].map { |f| extract_text f }
-        expect(i18t_strings).to eql(flash[:success].map { |f| extract_text f })
+        expect(flash[:success]).to have_message(I18n.t('flash.criteria.destroy.success'))
         expect(subject).to respond_with(:success)
 
         expect { FlexibleCriterion.find(flexible_criterion.id) }.to raise_error(ActiveRecord::RecordNotFound)
@@ -818,8 +817,7 @@
           end
 
           it 'displays an error message' do
-            expect(flash[:error].map { |f| extract_text f })
-              .to eq([I18n.t('criteria.errors.criteria_not_found')].map { |f| extract_text f })
+            expect(flash[:error]).to have_message(I18n.t('criteria.errors.criteria_not_found'))
           end
         end
       end
@@ -832,8 +830,7 @@
                   params: { course_id: course.id, id: rubric_criterion.id },
                   format: :js
         expect(assigns(:criterion)).to be_truthy
-        i18t_string = [I18n.t('flash.criteria.destroy.success')].map { |f| extract_text f }
-        expect(i18t_string).to eql(flash[:success].map { |f| extract_text f })
+        expect(flash[:success]).to have_message(I18n.t('flash.criteria.destroy.success'))
         expect(subject).to respond_with(:success)
 
         expect { RubricCriterion.find(rubric_criterion.id) }.to raise_error(ActiveRecord::RecordNotFound)
@@ -878,8 +875,7 @@
         post_as instructor, :upload,
                 params: { course_id: course.id, assignment_id: assignment.id, upload_file: empty_file }
 
-        expect(flash[:error].map { |f| extract_text f })
-          .to eq([I18n.t('upload_errors.blank')].map { |f| extract_text f })
+        expect(flash[:error]).to have_message(I18n.t('upload_errors.blank'))
       end
 
       it 'deletes all criteria previously created' do
@@ -918,8 +914,7 @@
                                                                     'cr90',
                                                                     'cr40',
                                                                     'cr50')
-        expect(flash[:success].map { |f| extract_text f })
-          .to eq([I18n.t('upload_success', count: 8)].map { |f| extract_text f })
+        expect(flash[:success]).to have_message(I18n.t('upload_success', count: 8))
       end
 
       it 'creates rubric criteria with properly formatted entries' do
@@ -1043,8 +1038,7 @@
                                                                     'cr90',
                                                                     'cr40',
                                                                     'cr50')
-        expect(flash[:success].map { |f| extract_text f })
-          .to eq([I18n.t('upload_success', count: 8)].map { |f| extract_text f })
+        expect(flash[:success]).to have_message(I18n.t('upload_success', count: 8))
       end
 
       it 'creates criteria correctly when a valid yml file with the wrong extension is uploaded' do
@@ -1059,8 +1053,7 @@
                                                                     'cr90',
                                                                     'cr40',
                                                                     'cr50')
-        expect(flash[:success].map { |f| extract_text f })
-          .to eq([I18n.t('upload_success', count: 8)].map { |f| extract_text f })
+        expect(flash[:success]).to have_message(I18n.t('upload_success', count: 8))
       end
 
       it 'does not create criteria with format errors in entries' do
@@ -1068,8 +1061,8 @@
                                                upload_file: invalid_mixed_file }
 
         expect(assignment.criteria.pluck(:name)).not_to include('cr40', 'cr50', 'cr70')
-        expect(flash[:error].map { |f| extract_text f })
-          .to eq([I18n.t('criteria.errors.invalid_format') + ' cr40, cr70, cr50'].map { |f| extract_text f })
+        expect(flash[:error]).to contain_message(I18n.t('criteria.errors.invalid_format'))
+        expect(flash[:error]).to contain_message(' cr40, cr70, cr50')
       end
 
       it 'does not create criteria with an invalid mark' do
@@ -1084,8 +1077,8 @@
                                                upload_file: missing_levels_file }
 
         expect(assignment.criteria.where(name: %w[no_levels empty_levels])).to be_empty
-        expect(flash[:error].map { |f| extract_text f })
-          .to eq([I18n.t('criteria.errors.invalid_format') + ' no_levels, empty_levels'].map { |f| extract_text f })
+        expect(flash[:error]).to contain_message(I18n.t('criteria.errors.invalid_format'))
+        expect(flash[:error]).to contain_message(' no_levels, empty_levels')
       end
 
       it 'does not create criteria that have both visibility options set to false' do
diff --git a/spec/controllers/exam_templates_controller_spec.rb b/spec/controllers/exam_templates_controller_spec.rb
index 6985881f98..b87135b6f9 100644
--- a/spec/controllers/exam_templates_controller_spec.rb
+++ b/spec/controllers/exam_templates_controller_spec.rb
@@ -50,7 +50,7 @@
       before { post_as user, :create, params: params }
 
       it 'flashes an exam template create failure error message' do
-        expect(flash[:error].map { |f| extract_text f }).to eq [I18n.t('exam_templates.create.failure')]
+        expect(flash[:error]).to have_message(I18n.t('exam_templates.create.failure'))
       end
     end
 
@@ -113,7 +113,7 @@
         end
 
         it 'displays a flash error message' do
-          expect(flash[:error].map { |f| extract_text f }).to eq [I18n.t('exam_templates.update.failure')]
+          expect(flash[:error]).to have_message(I18n.t('exam_templates.update.failure'))
         end
 
         it 'responds with 302' do
@@ -160,8 +160,7 @@
         end
 
         it 'should send appropriate error message' do
-          expect(flash[:error].map { |f| extract_text f })
-            .to eq([I18n.t('exam_templates.upload_scans.search_failure')].map { |f| extract_text f })
+          expect(flash[:error]).to have_message(I18n.t('exam_templates.upload_scans.search_failure'))
         end
       end
 
@@ -177,8 +176,7 @@
         end
 
         it 'should send appropriate error message' do
-          expect(flash[:error].map { |f| extract_text f })
-            .to eq([I18n.t('exam_templates.upload_scans.missing')].map { |f| extract_text f })
+          expect(flash[:error]).to have_message(I18n.t('exam_templates.upload_scans.missing'))
         end
       end
 
@@ -195,8 +193,7 @@
         end
 
         it 'should send appropriate error message' do
-          expect(flash[:error].map { |f| extract_text f })
-            .to eq([I18n.t('exam_templates.upload_scans.invalid')].map { |f| extract_text f })
+          expect(flash[:error]).to have_message(I18n.t('exam_templates.upload_scans.invalid'))
         end
       end
     end
diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb
index 2029a142da..b7bb450347 100644
--- a/spec/controllers/groups_controller_spec.rb
+++ b/spec/controllers/groups_controller_spec.rb
@@ -46,7 +46,7 @@
           end
 
           it 'assigns the error message to flash[:error]' do
-            expect(flash[:error][0]).to include("Group #{group_name} already exists")
+            expect(flash[:error]).to contain_message("Group #{group_name} already exists")
           end
         end
       end
@@ -181,9 +181,7 @@
             new_groupname: 'placeholder_group'
           }
 
-          expect(flash[:error].map do |f|
-            extract_text f
-          end).to eq [I18n.t('groups.group_name_already_in_use_diff_assignment')]
+          expect(flash[:error]).to have_message(I18n.t('groups.group_name_already_in_use_diff_assignment'))
         end
       end
 
@@ -198,9 +196,7 @@
             new_groupname: 'placeholder_group'
           }
 
-          expect(flash[:error].map do |f|
-            extract_text f
-          end).to eq [I18n.t('groups.group_name_already_in_use_diff_assignment')]
+          expect(flash[:error]).to have_message(I18n.t('groups.group_name_already_in_use_diff_assignment'))
         end
       end
     end
diff --git a/spec/controllers/job_messages_controller_spec.rb b/spec/controllers/job_messages_controller_spec.rb
index bb878bc7a0..73de0ab514 100644
--- a/spec/controllers/job_messages_controller_spec.rb
+++ b/spec/controllers/job_messages_controller_spec.rb
@@ -51,7 +51,7 @@
           let(:status_type) { :queued }
 
           it 'should flash a notice message' do
-            expect(flash[:notice]).to include "<p>#{I18n.t('poll_job.queued')}</p>"
+            expect(flash[:notice]).to contain_message(I18n.t('poll_job.queued'))
           end
 
           it 'should not set the session[:job_id] to nil' do
@@ -67,7 +67,7 @@
           let(:status_type) { :working }
 
           it 'should flash a notice message' do
-            expect(flash[:notice]).to include "<p>#{ApplicationJob.show_status(status)}</p>"
+            expect(flash[:notice]).to contain_message(ApplicationJob.show_status(status))
           end
 
           it 'should not set the session[:job_id] to nil' do
diff --git a/spec/controllers/main_controller_spec.rb b/spec/controllers/main_controller_spec.rb
index 607b5d9c66..907e73bf66 100644
--- a/spec/controllers/main_controller_spec.rb
+++ b/spec/controllers/main_controller_spec.rb
@@ -9,12 +9,12 @@
   context 'A non-authenticated user' do
     it 'should not be able to login with a blank username' do
       post :login, params: { user_login: '', user_password: 'a' }
-      expect(ActionController::Base.helpers.strip_tags(flash[:error][0])).to eq(I18n.t('main.username_not_blank'))
+      expect(flash[:error]).to have_message(I18n.t('main.username_not_blank'))
     end
 
     it 'should not be able to login with a blank password' do
       post :login, params: { user_login: 'a', user_password: '' }
-      expect(ActionController::Base.helpers.strip_tags(flash[:error][0])).to eq(I18n.t('main.password_not_blank'))
+      expect(flash[:error]).to have_message(I18n.t('main.password_not_blank'))
     end
 
     describe 'login_remote_auth' do
@@ -143,7 +143,7 @@
     context 'after logging in with a bad username' do
       it 'should not be able to login with an incorrect username' do
         post :login, params: { user_login: instructor.user_name + 'BAD', user_password: 'a' }
-        expect(ActionController::Base.helpers.strip_tags(flash[:error][0])).to eq(I18n.t('main.login_failed'))
+        expect(flash[:error]).to have_message(I18n.t('main.login_failed'))
       end
     end
 
diff --git a/spec/controllers/marks_graders_controller_spec.rb b/spec/controllers/marks_graders_controller_spec.rb
index 615d9e803c..3f41729aca 100644
--- a/spec/controllers/marks_graders_controller_spec.rb
+++ b/spec/controllers/marks_graders_controller_spec.rb
@@ -43,7 +43,7 @@
         expect(flash[:error]).to be_nil
         expect(response).to redirect_to(action: 'index',
                                         grade_entry_form_id:
-                                            grade_entry_form_with_data.id)
+                                          grade_entry_form_with_data.id)
 
         # check that the ta was assigned to each student
         @student_user_names.each do |name|
@@ -80,7 +80,7 @@
         @student_user_names.each do |name|
           expect(
             GradeEntryStudentTa.joins(grade_entry_student: :user)
-              .exists?(grade_entry_student: { users: { user_name: name } })
+                               .exists?(grade_entry_student: { users: { user_name: name } })
           ).to be true
         end
         expect(ges.tas.count).to eq 0
@@ -167,7 +167,7 @@
         }
 
         expect(response).to have_http_status(:bad_request)
-        expect(flash[:error]).to eq(["<p>#{I18n.t('groups.select_a_student')}</p>"])
+        expect(flash[:error]).to have_message(I18n.t('groups.select_a_student'))
       end
     end
 
@@ -181,7 +181,7 @@
         }
 
         expect(response).to have_http_status(:bad_request)
-        expect(flash[:error]).to eq(["<p>#{I18n.t('graders.select_a_grader')}</p>"])
+        expect(flash[:error]).to have_message(I18n.t('graders.select_a_grader'))
       end
     end
 
@@ -194,7 +194,7 @@
         }
 
         expect(response).to have_http_status(:bad_request)
-        expect(flash[:error]).to eq(["<p>#{I18n.t('groups.select_a_student')}</p>"])
+        expect(flash[:error]).to have_message(I18n.t('groups.select_a_student'))
       end
     end
 
@@ -207,7 +207,7 @@
         }
 
         expect(response).to have_http_status(:bad_request)
-        expect(flash[:error]).to eq(["<p>#{I18n.t('graders.select_a_grader')}</p>"])
+        expect(flash[:error]).to have_message(I18n.t('graders.select_a_grader'))
       end
     end
   end
@@ -250,7 +250,7 @@
         }
 
         expect(response).to have_http_status(:bad_request)
-        expect(flash[:error]).to eq(["<p>#{I18n.t('groups.select_a_student')}</p>"])
+        expect(flash[:error]).to have_message(I18n.t('groups.select_a_student'))
       end
     end
 
@@ -264,7 +264,7 @@
         }
 
         expect(response).to have_http_status(:bad_request)
-        expect(flash[:error]).to eq(["<p>#{I18n.t('graders.select_a_grader')}</p>"])
+        expect(flash[:error]).to have_message(I18n.t('graders.select_a_grader'))
       end
     end
 
@@ -277,7 +277,7 @@
         }
 
         expect(response).to have_http_status(:bad_request)
-        expect(flash[:error]).to eq(["<p>#{I18n.t('groups.select_a_student')}</p>"])
+        expect(flash[:error]).to have_message(I18n.t('groups.select_a_student'))
       end
     end
 
@@ -290,7 +290,7 @@
         }
 
         expect(response).to have_http_status(:bad_request)
-        expect(flash[:error]).to eq(["<p>#{I18n.t('graders.select_a_grader')}</p>"])
+        expect(flash[:error]).to have_message(I18n.t('graders.select_a_grader'))
       end
     end
   end
@@ -333,7 +333,7 @@
         }
 
         expect(response).to have_http_status(:bad_request)
-        expect(flash[:error]).to eq(["<p>#{I18n.t('groups.select_a_student')}</p>"])
+        expect(flash[:error]).to have_message(I18n.t('groups.select_a_student'))
       end
     end
 
@@ -347,7 +347,7 @@
         }
 
         expect(response).to have_http_status(:bad_request)
-        expect(flash[:error]).to eq(["<p>#{I18n.t('graders.select_a_grader')}</p>"])
+        expect(flash[:error]).to have_message(I18n.t('graders.select_a_grader'))
       end
     end
 
@@ -360,7 +360,7 @@
         }
 
         expect(response).to have_http_status(:bad_request)
-        expect(flash[:error]).to eq(["<p>#{I18n.t('groups.select_a_student')}</p>"])
+        expect(flash[:error]).to have_message(I18n.t('groups.select_a_student'))
       end
     end
 
@@ -372,7 +372,7 @@
           student_id: 1
         }
         expect(response).to have_http_status(:bad_request)
-        expect(flash[:error]).to eq(["<p>#{I18n.t('graders.select_a_grader')}</p>"])
+        expect(flash[:error]).to have_message(I18n.t('graders.select_a_grader'))
       end
     end
   end
diff --git a/spec/controllers/notes_controller_spec.rb b/spec/controllers/notes_controller_spec.rb
index b0aa4a72fb..b66d27ce32 100644
--- a/spec/controllers/notes_controller_spec.rb
+++ b/spec/controllers/notes_controller_spec.rb
@@ -143,7 +143,8 @@
                 params: { course_id: course.id, noteable_type: 'Grouping',
                           note: { noteable_id: grouping.id, notes_message: @message } }
         expect(assigns(:note)).not_to be_nil
-        expect(flash[:success]).to eq I18n.t('flash.actions.create.success', resource_name: Note.model_name.human)
+        expect(flash[:success]).to have_message(I18n.t('flash.actions.create.success',
+                                                       resource_name: Note.model_name.human))
         expect(response).to redirect_to(controller: 'notes')
         expect(Note.count).to eq @notes + 1
       end
@@ -156,7 +157,8 @@
                 params: { course_id: course.id, noteable_type: 'Student',
                           note: { noteable_id: student.id, notes_message: @message } }
         expect(assigns(:note)).not_to be_nil
-        expect(flash[:success]).to eq I18n.t('flash.actions.create.success', resource_name: Note.model_name.human)
+        expect(flash[:success]).to have_message(I18n.t('flash.actions.create.success',
+                                                       resource_name: Note.model_name.human))
         expect(response).to redirect_to(controller: 'notes')
         expect(Note.count).to eq @notes + 1
       end
@@ -169,7 +171,8 @@
                 params: { course_id: course.id, noteable_type: 'Assignment',
                           note: { noteable_id: assignment.id, notes_message: @message } }
         expect(assigns(:note)).not_to be_nil
-        expect(flash[:success]).to eq I18n.t('flash.actions.create.success', resource_name: Note.model_name.human)
+        expect(flash[:success]).to have_message(I18n.t('flash.actions.create.success',
+                                                       resource_name: Note.model_name.human))
         expect(response).to redirect_to(controller: 'notes')
         expect(Note.count).to eq @notes + 1
       end
@@ -245,7 +248,8 @@
                   :update,
                   params: { course_id: course.id, id: @note.id, note: { notes_message: @new_message } }
           expect(assigns(:note)).not_to be_nil
-          expect(flash[:success]).to eq I18n.t('flash.actions.update.success', resource_name: Note.model_name.human)
+          expect(flash[:success]).to have_message(I18n.t('flash.actions.update.success',
+                                                         resource_name: Note.model_name.human))
           expect(response).to redirect_to(controller: 'notes')
         end
       end
@@ -265,15 +269,15 @@
         @note = create(:note, creator_id: @ta.id)
         delete_as @ta, :destroy, params: { course_id: course.id, id: @note.id }
         expect(assigns(:note)).not_to be_nil
-        expect(flash[:success]).to eq I18n.t('flash.actions.destroy.success', resource_name: Note.model_name.human)
+        expect(flash[:success]).to have_message(I18n.t('flash.actions.destroy.success',
+                                                       resource_name: Note.model_name.human))
       end
 
       it 'for a note belonging to someone else (delete as TA)' do
         @note = create(:note)
         delete_as @ta, :destroy, params: { course_id: course.id, id: @note.id }
         expect(assigns(:note)).not_to be_nil
-        i18t_string = [I18n.t('action_policy.policy.note.modify?')].map { |f| extract_text f }
-        expect(flash[:error].map { |f| extract_text f }).to eq(i18t_string)
+        expect(flash[:error]).to have_message(I18n.t('action_policy.policy.note.modify?'))
       end
     end
   end
@@ -313,8 +317,7 @@
 
     it 'for invalid type' do
       get_as @instructor, :noteable_object_selector, params: { course_id: course.id, noteable_type: 'gibberish' }
-      i18t_string = [I18n.t('notes.new.invalid_selector')].map { |f| extract_text f }
-      expect(flash[:error].map { |f| extract_text f }).to eq(i18t_string)
+      expect(flash[:error]).to have_message(I18n.t('notes.new.invalid_selector'))
       expect(assigns(:assignments)).not_to be_nil
       expect(assigns(:groupings)).not_to be_nil
       expect(assigns(:students)).to be_nil
@@ -373,7 +376,8 @@
                 params: { course_id: course.id, noteable_type: 'Grouping',
                           note: { noteable_id: grouping.id, notes_message: @message } }
         expect(assigns(:note)).not_to be_nil
-        expect(flash[:success]).to eq I18n.t('flash.actions.create.success', resource_name: Note.model_name.human)
+        expect(flash[:success]).to have_message(I18n.t('flash.actions.create.success',
+                                                       resource_name: Note.model_name.human))
         expect(response).to redirect_to(controller: 'notes')
         expect(Note.count).to eq @notes + 1
       end
@@ -386,7 +390,8 @@
                 params: { course_id: course.id, noteable_type: 'Student',
                           note: { noteable_id: student.id, notes_message: @message } }
         expect(assigns(:note)).not_to be_nil
-        expect(flash[:success]).to eq I18n.t('flash.actions.create.success', resource_name: Note.model_name.human)
+        expect(flash[:success]).to have_message(I18n.t('flash.actions.create.success',
+                                                       resource_name: Note.model_name.human))
         expect(response).to redirect_to(controller: 'notes')
         expect(Note.count).to eq @notes + 1
       end
@@ -399,7 +404,8 @@
                 params: { course_id: course.id, noteable_type: 'Assignment',
                           note: { noteable_id: assignment.id, notes_message: @message } }
         expect(assigns(:note)).not_to be_nil
-        expect(flash[:success]).to eq I18n.t('flash.actions.create.success', resource_name: Note.model_name.human)
+        expect(flash[:success]).to have_message(I18n.t('flash.actions.create.success',
+                                                       resource_name: Note.model_name.human))
         expect(response).to redirect_to(controller: 'notes')
         expect(Note.count).to eq @notes + 1
       end
@@ -442,7 +448,8 @@
         post_as @instructor, :update,
                 params: { course_id: course.id, id: @note.id, note: { notes_message: @new_message } }
         expect(assigns(:note)).not_to be_nil
-        expect(flash[:success]).to eq I18n.t('flash.actions.update.success', resource_name: Note.model_name.human)
+        expect(flash[:success]).to have_message(I18n.t('flash.actions.update.success',
+                                                       resource_name: Note.model_name.human))
         expect(response).to redirect_to(controller: 'notes')
       end
 
@@ -452,7 +459,8 @@
         post_as @instructor, :update,
                 params: { course_id: course.id, id: @note.id, note: { notes_message: @new_message } }
         expect(assigns(:note)).not_to be_nil
-        expect(flash[:success]).to eq I18n.t('flash.actions.update.success', resource_name: Note.model_name.human)
+        expect(flash[:success]).to have_message(I18n.t('flash.actions.update.success',
+                                                       resource_name: Note.model_name.human))
         expect(response).to redirect_to(controller: 'notes')
       end
 
@@ -460,7 +468,8 @@
         @note = create(:note, creator_id: @instructor.id)
         delete_as @instructor, :destroy, params: { course_id: course.id, id: @note.id }
         expect(assigns(:note)).not_to be_nil
-        expect(flash[:success]).to eq I18n.t('flash.actions.destroy.success', resource_name: Note.model_name.human)
+        expect(flash[:success]).to have_message(I18n.t('flash.actions.destroy.success',
+                                                       resource_name: Note.model_name.human))
         expect(response).to redirect_to(controller: 'notes')
       end
 
@@ -468,7 +477,8 @@
         @note = create(:note, creator_id: create(:ta).id)
         delete_as @instructor, :destroy, params: { course_id: course.id, id: @note.id }
         expect(assigns(:note)).not_to be_nil
-        expect(flash[:success]).to eq I18n.t('flash.actions.destroy.success', resource_name: Note.model_name.human)
+        expect(flash[:success]).to have_message(I18n.t('flash.actions.destroy.success',
+                                                       resource_name: Note.model_name.human))
         expect(response).to redirect_to(controller: 'notes')
       end
 
diff --git a/spec/controllers/peer_reviews_controller_spec.rb b/spec/controllers/peer_reviews_controller_spec.rb
index e25bb80d46..7a29e63603 100644
--- a/spec/controllers/peer_reviews_controller_spec.rb
+++ b/spec/controllers/peer_reviews_controller_spec.rb
@@ -263,9 +263,8 @@
         end
 
         it 'flashes the correct message' do
-          expect(flash[:success].map { |f| extract_text f }).to eq [I18n.t(
-            'peer_reviews.unassigned_reviewers_successfully', deleted_count: 1.to_s
-          )]
+          expect(flash[:success]).to have_message(I18n.t('peer_reviews.unassigned_reviewers_successfully',
+                                                         deleted_count: 1.to_s))
         end
 
         it 'removes selected reviewer as reviewer for selected reviewee' do
@@ -296,9 +295,8 @@
             end
 
             it 'flashes the correct message' do
-              expect(flash[:success].map { |f| extract_text f }).to eq [I18n.t(
-                'peer_reviews.unassigned_reviewers_successfully', deleted_count: 8.to_s
-              )]
+              expect(flash[:success]).to have_message(I18n.t('peer_reviews.unassigned_reviewers_successfully',
+                                                             deleted_count: 8.to_s))
             end
           end
         end
@@ -331,9 +329,7 @@
           end
 
           it 'flashes the correct message' do
-            expect(flash[:error].map { |f| extract_text f }).to eq [I18n.t(
-              'peer_reviews.errors.cannot_unassign_any_reviewers'
-            )]
+            expect(flash[:error]).to have_message(I18n.t('peer_reviews.errors.cannot_unassign_any_reviewers'))
           end
 
           it 'does not delete any peer reviews' do
@@ -361,9 +357,7 @@
             end
 
             it 'flashes the correct message' do
-              expect(flash[:error].map { |f| extract_text f }).to eq [I18n.t(
-                'peer_reviews.errors.cannot_unassign_any_reviewers'
-              )]
+              expect(flash[:error]).to have_message(I18n.t('peer_reviews.errors.cannot_unassign_any_reviewers'))
             end
 
             it 'does not delete any peer reviews' do
@@ -389,9 +383,9 @@
                      reviewer_group_name: review.reviewer.group.group_name,
                      reviewee_group_name: review.result.grouping.group.group_name)
             end
-            flashed_error = flash[:error].map { |f| extract_text f }[0]
-            expect(flashed_error).to include('Successfully unassigned 1 peer reviewer(s)')
-            expect(flashed_error).to include(I18n.t('additional_not_shown', count: undeleted_reviews.length - 6))
+            expect(flash[:error]).to contain_message('Successfully unassigned 1 peer reviewer(s)')
+            expect(flash[:error]).to contain_message(I18n.t('additional_not_shown',
+                                                            count: undeleted_reviews.length - 6))
           end
 
           it 'deletes the correct number of peer reviews' do
@@ -426,9 +420,9 @@
                        reviewee_group_name: review.result.grouping.group.group_name)
               end
 
-              flashed_error = flash[:error].map { |f| extract_text f }[0]
-              expect(flashed_error).to include('Successfully unassigned 1 peer reviewer(s)')
-              expect(flashed_error).to include(I18n.t('additional_not_shown', count: undeleted_reviews.length - 6))
+              expect(flash[:error]).to contain_message('Successfully unassigned 1 peer reviewer(s)')
+              expect(flash[:error]).to contain_message(I18n.t('additional_not_shown',
+                                                              count: undeleted_reviews.length - 6))
             end
 
             it 'deletes the correct number of peer reviews' do
@@ -451,8 +445,7 @@
           end
 
           it 'flashes the correct message' do
-            flashed_error = flash[:error].map { |f| extract_text f }[0]
-            expect(flashed_error).to include('Successfully unassigned 6 peer reviewer(s)')
+            expect(flash[:error]).to contain_message('Successfully unassigned 6 peer reviewer(s)')
           end
 
           it 'deletes the correct number of peer reviews' do
@@ -486,9 +479,10 @@
           end
 
           it 'flashes the correct message' do
-            flashed_error = flash[:error].map { |f| extract_text f }[0]
-            expect(flashed_error).to include('Successfully unassigned 2 peer reviewer(s), but could not unassign the ' \
-                                             'following due to existing marks or annotations: ')
+            expect(flash[:error]).to contain_message(
+              'Successfully unassigned 2 peer reviewer(s), but could not unassign the ' \
+              'following due to existing marks or annotations: '
+            )
           end
 
           it 'deletes the correct number of peer reviews' do
diff --git a/spec/controllers/sections_controller_spec.rb b/spec/controllers/sections_controller_spec.rb
index fa4740ac9d..ffaaf679ea 100644
--- a/spec/controllers/sections_controller_spec.rb
+++ b/spec/controllers/sections_controller_spec.rb
@@ -52,26 +52,21 @@
       post_as @instructor, :create, params: { course_id: course.id, section: { name: 'section_01' } }
 
       expect(response).to be_redirect
-      i18t_string = [I18n.t('sections.create.success', name: 'section_01')].map { |f| extract_text f }
-      expect(flash[:success].map { |f| extract_text f }).to eq(i18t_string)
+      expect(flash[:success]).to have_message(I18n.t('sections.create.success', name: 'section_01'))
       expect(Section.find_by(name: 'section_01')).to be_truthy
     end
 
     it 'not be able to create a section with the same name as a existing one' do
       post_as @instructor, :create, params: { course_id: course.id, section: { name: section.name } }
       expect(response).to have_http_status(:ok)
-      expect(flash[:error].map { |f| extract_text f }).to eq([I18n.t('sections.create.error')].map do |f|
-                                                               extract_text f
-                                                             end)
+      expect(flash[:error]).to have_message(I18n.t('sections.create.error'))
     end
 
     it 'not be able to create a section with a blank name' do
       post_as @instructor, :create, params: { course_id: course.id, section: { name: '' } }
       expect(Section.find_by(name: '')).to be_nil
       expect(response).to have_http_status(:ok)
-      expect(flash[:error].map { |f| extract_text f }).to eq([I18n.t('sections.create.error')].map do |f|
-                                                               extract_text f
-                                                             end)
+      expect(flash[:error]).to have_message(I18n.t('sections.create.error'))
     end
 
     it 'on edit section' do
@@ -83,8 +78,7 @@
       put_as @instructor, :update, params: { course_id: course.id, id: section.id, section: { name: 'no section' } }
 
       expect(response).to be_redirect
-      i18t_string = [I18n.t('sections.update.success', name: 'no section')].map { |f| extract_text f }
-      expect(flash[:success].map { |f| extract_text f }).to eq(i18t_string)
+      expect(flash[:success]).to have_message(I18n.t('sections.update.success', name: 'no section'))
       expect(Section.find_by(name: 'no section')).to be_truthy
     end
 
@@ -96,24 +90,20 @@
     it 'not be able to edit a section name to an existing name' do
       put_as @instructor, :update, params: { course_id: course.id, id: section.id, section: { name: section2.name } }
       expect(response).to have_http_status(:ok)
-      expect(flash[:error].map { |f| extract_text f }).to eq([I18n.t('sections.update.error')].map do |f|
-                                                               extract_text f
-                                                             end)
+      expect(flash[:error]).to have_message(I18n.t('sections.update.error'))
     end
 
     context 'with an already created section' do
       it 'be able to delete a section' do
         delete_as @instructor, :destroy, params: { course_id: course.id, id: section.id }
-        i18t_string = [I18n.t('sections.destroy.success')].map { |f| extract_text f }
-        expect(flash[:success].map { |f| extract_text f }).to eq(i18t_string)
+        expect(flash[:success]).to have_message(I18n.t('sections.destroy.success'))
       end
 
       it 'not be able to delete a section with students in it' do
         student = create(:student)
         section.students << student
         delete_as @instructor, :destroy, params: { course_id: course.id, id: section.id }
-        i18t_string = [I18n.t('sections.destroy.not_empty')].map { |f| extract_text f }
-        expect(flash[:error].map { |f| extract_text f }).to eq(i18t_string)
+        expect(flash[:error]).to have_message(I18n.t('sections.destroy.not_empty'))
         expect(Section.find(section.id)).to be_truthy
       end
     end
diff --git a/spec/controllers/starter_file_groups_controller_spec.rb b/spec/controllers/starter_file_groups_controller_spec.rb
index 0864044ec0..2eacbbf7c3 100644
--- a/spec/controllers/starter_file_groups_controller_spec.rb
+++ b/spec/controllers/starter_file_groups_controller_spec.rb
@@ -234,7 +234,7 @@
         let(:new_folders) { %w[../../hello] }
 
         it 'flashes an error message' do
-          expect(flash[:error].join('\n')).to include(I18n.t('errors.invalid_path'))
+          expect(flash[:error]).to contain_message(I18n.t('errors.invalid_path'))
         end
 
         it 'does not create the folder' do
@@ -270,7 +270,7 @@
         let(:delete_files) { %w[../../../../../LICENSE] }
 
         it 'flashes an error message' do
-          expect(flash[:error].join('\n')).to include(I18n.t('errors.invalid_path'))
+          expect(flash[:error]).to contain_message(I18n.t('errors.invalid_path'))
         end
 
         it 'does not delete the file' do
@@ -310,7 +310,7 @@
         let(:delete_folders) { %w[../../../../../doc] }
 
         it 'flashes an error message' do
-          expect(flash[:error].join('\n')).to include(I18n.t('errors.invalid_path'))
+          expect(flash[:error]).to contain_message(I18n.t('errors.invalid_path'))
         end
 
         it 'does not delete the folder' do
@@ -333,7 +333,7 @@
 
       it 'should flash an error message' do
         subject
-        expect(flash[:error].join('\n')).to include(I18n.t('errors.invalid_path'))
+        expect(flash[:error]).to contain_message(I18n.t('errors.invalid_path'))
       end
     end
   end
diff --git a/spec/controllers/submissions_controller_spec.rb b/spec/controllers/submissions_controller_spec.rb
index 257e8fc954..afb8992dd5 100644
--- a/spec/controllers/submissions_controller_spec.rb
+++ b/spec/controllers/submissions_controller_spec.rb
@@ -900,7 +900,7 @@
                     params: { course_id: course.id, assignment_id: @assignment.id, grouping_id: @grouping1.id,
                               current_revision_identifier: revision_identifier }
 
-            expect(flash[:error]).to eq(["<p>#{I18n.t('submissions.collect.could_not_collect_released')}</p>"])
+            expect(flash[:error]).to have_message(I18n.t('submissions.collect.could_not_collect_released'))
           end
         end
       end
@@ -1783,7 +1783,7 @@
 
       it 'flashes an error message' do
         subject
-        expect(flash[:error].join('\n')).to include(I18n.t('errors.invalid_path'))
+        expect(flash[:error]).to have_message(I18n.t('errors.invalid_path'))
       end
     end
 
diff --git a/spec/controllers/tags_controller_spec.rb b/spec/controllers/tags_controller_spec.rb
index c85694b821..aaf6549be5 100644
--- a/spec/controllers/tags_controller_spec.rb
+++ b/spec/controllers/tags_controller_spec.rb
@@ -46,7 +46,7 @@
       post_as instructor, :create, params: { tag: { name: '', description: 'tag description' },
                                              assignment_id: assignment.id, course_id: course.id }
       expect(Tag.find_by(name: '', description: 'tag description')).to be_nil
-      expect(flash[:error][0]).to include(I18n.t('flash.actions.create.error', resource_name: Tag.model_name.human))
+      expect(flash[:error]).to have_message(I18n.t('flash.actions.create.error', resource_name: Tag.model_name.human))
     end
 
     it 'associates the new tag with a grouping when passed grouping_id' do
@@ -104,8 +104,7 @@
 
       expect(response).to have_http_status(:found)
       expect(flash[:error]).to be_nil
-      expect(flash[:success].map { |f| extract_text f }).to eq([I18n.t('upload_success',
-                                                                       count: 2)].map { |f| extract_text f })
+      expect(flash[:success]).to have_message(I18n.t('upload_success', count: 2))
       expect(response).to redirect_to course_tags_path(course, assignment_id: assignment.id)
 
       expect(Tag.find_by(name: 'tag').description).to eq('desc')
diff --git a/spec/controllers/tas_controller_spec.rb b/spec/controllers/tas_controller_spec.rb
index aa7f6d5447..705b3ec9bd 100644
--- a/spec/controllers/tas_controller_spec.rb
+++ b/spec/controllers/tas_controller_spec.rb
@@ -300,7 +300,8 @@
       it 'does not delete the TA, shows an error message, and gets a bad request response' do
         expect(Ta.count).to eq(1)
         expect(flash.now[:success]).to be_nil
-        expect(flash[:error].first).to include(I18n.t('flash.tas.destroy.error', user_name: ta.user_name, message: ''))
+        expect(flash[:error]).to contain_message(I18n.t('flash.tas.destroy.error', user_name: ta.user_name,
+                                                                                   message: ''))
         expect(response).to have_http_status(:bad_request)
       end
     end
@@ -317,8 +318,8 @@
       it 'does not delete the TA, shows an error message and gets a conflict response' do
         expect(Ta.count).to eq(1)
         expect(flash.now[:success]).to be_nil
-        expect(flash[:error].first).to include(I18n.t('flash.tas.destroy.restricted', user_name: ta.user_name,
-                                                                                      message: ''))
+        expect(flash[:error]).to contain_message(I18n.t('flash.tas.destroy.restricted', user_name: ta.user_name,
+                                                                                        message: ''))
         expect(response).to have_http_status(:conflict)
       end
     end
@@ -341,7 +342,7 @@
 
       it 'deletes TA, flashes success, and gets an ok response' do
         expect(Ta.exists?).to be(false)
-        expect(flash.now[:success].first).to include(I18n.t('flash.tas.destroy.success', user_name: ta.user_name))
+        expect(flash.now[:success]).to contain_message(I18n.t('flash.tas.destroy.success', user_name: ta.user_name))
         expect(response).to have_http_status(:ok)
       end
 
diff --git a/spec/support/custom_matchers.rb b/spec/support/custom_matchers.rb
new file mode 100644
index 0000000000..8ab8f65866
--- /dev/null
+++ b/spec/support/custom_matchers.rb
@@ -0,0 +1,27 @@
+# :nocov:
+RSpec::Matchers.define :have_message do |expected|
+  match do |actual|
+    actual_messages = Array(actual).map { |m| extract_text(m.to_s) }
+    actual_messages.any? { |m| m.strip == extract_text(expected).strip }
+  end
+
+  failure_message do |actual|
+    actual_stripped = Array(actual).map { |m| extract_text(m.to_s) }
+    "expected that #{actual_stripped.inspect} would exactly match message #{expected.inspect}"
+  end
+end
+# :nocov:
+
+# :nocov:
+RSpec::Matchers.define :contain_message do |expected|
+  match do |actual|
+    actual_messages = Array(actual).map { |m| extract_text(m.to_s) }
+    actual_messages.any? { |m| m.include?(extract_text(expected)) }
+  end
+
+  failure_message do |actual|
+    actual_stripped = Array(actual).map { |m| extract_text(m.to_s) }
+    "expected that #{actual_stripped.inspect} would contain message #{expected.inspect}"
+  end
+end
+# :nocov: