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
path: root/spec
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-07-24 18:10:11 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-07-24 18:10:11 +0300
commit7308ec9d13fb69018200a40f287e76ef499ed47c (patch)
tree06c75f7ddceebd61d09f925a48fef2789338f3cd /spec
parentf296f23500b4b3758670ae0c5ce2e1779f533e8b (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
-rw-r--r--spec/features/admin/admin_abuse_reports_spec.rb214
-rw-r--r--spec/features/profiles/keys_spec.rb2
-rw-r--r--spec/finders/abuse_reports_finder_spec.rb225
-rw-r--r--spec/frontend/access_tokens/components/new_access_token_app_spec.js1
-rw-r--r--spec/frontend/access_tokens/components/token_spec.js1
-rw-r--r--spec/frontend/admin/abuse_reports/components/abuse_report_row_spec.js15
-rw-r--r--spec/frontend/admin/abuse_reports/components/abuse_reports_filtered_search_bar_spec.js91
-rw-r--r--spec/frontend/admin/abuse_reports/mock_data.js2
-rw-r--r--spec/frontend/authentication/two_factor_auth/components/recovery_codes_spec.js8
-rw-r--r--spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js5
-rw-r--r--spec/frontend/ci/runner/components/registration/registration_token_spec.js11
-rw-r--r--spec/frontend/diffs/components/app_spec.js43
-rw-r--r--spec/frontend/diffs/components/compare_versions_spec.js2
-rw-r--r--spec/frontend/diffs/components/diffs_file_tree_spec.js116
-rw-r--r--spec/frontend/oauth_application/components/oauth_secret_spec.js4
-rw-r--r--spec/frontend/super_sidebar/components/flyout_menu_spec.js25
-rw-r--r--spec/frontend/super_sidebar/components/menu_section_spec.js36
-rw-r--r--spec/frontend/super_sidebar/components/pinned_section_spec.js17
-rw-r--r--spec/frontend/super_sidebar/components/sidebar_menu_spec.js64
-rw-r--r--spec/frontend/vue_shared/components/form/input_copy_toggle_visibility_spec.js254
-rw-r--r--spec/lib/gitlab/regex_spec.rb12
-rw-r--r--spec/lib/gitlab/usage/metrics/instrumentations/work_items_activity_aggregated_metric_spec.rb72
-rw-r--r--spec/lib/slack_markdown_sanitizer_spec.rb17
-rw-r--r--spec/models/abuse_report_spec.rb28
-rw-r--r--spec/models/integrations/chat_message/issue_message_spec.rb10
-rw-r--r--spec/models/note_spec.rb18
-rw-r--r--spec/serializers/admin/abuse_report_entity_spec.rb1
-rw-r--r--spec/support/shared_examples/features/runners_shared_examples.rb2
-rw-r--r--spec/support/shared_examples/usage_data_counters/work_item_activity_unique_counter_shared_examples.rb34
29 files changed, 936 insertions, 394 deletions
diff --git a/spec/features/admin/admin_abuse_reports_spec.rb b/spec/features/admin/admin_abuse_reports_spec.rb
index 9739ea53f81..18bc851558d 100644
--- a/spec/features/admin/admin_abuse_reports_spec.rb
+++ b/spec/features/admin/admin_abuse_reports_spec.rb
@@ -2,27 +2,29 @@
require 'spec_helper'
-RSpec.describe "Admin::AbuseReports", :js, feature_category: :shared do
+RSpec.describe "Admin::AbuseReports", :js, feature_category: :insider_threat do
let_it_be(:user) { create(:user) }
let_it_be(:admin) { create(:admin) }
- context 'as an admin' do
- describe 'displayed reports' do
- include FilteredSearchHelpers
+ let_it_be(:open_report) { create(:abuse_report, created_at: 5.days.ago, updated_at: 2.days.ago, category: 'spam', user: user) }
+ let_it_be(:open_report2) { create(:abuse_report, created_at: 4.days.ago, updated_at: 3.days.ago, category: 'phishing') }
+ let_it_be(:closed_report) { create(:abuse_report, :closed, user: user, category: 'spam') }
- let_it_be(:open_report) { create(:abuse_report, created_at: 5.days.ago, updated_at: 2.days.ago) }
- let_it_be(:open_report2) { create(:abuse_report, created_at: 4.days.ago, updated_at: 3.days.ago, category: 'phishing') }
- let_it_be(:closed_report) { create(:abuse_report, :closed) }
+ describe 'as an admin' do
+ before do
+ sign_in(admin)
+ gitlab_enable_admin_mode_sign_in(admin)
+ end
- let(:abuse_report_row_selector) { '[data-testid="abuse-report-row"]' }
+ context 'when abuse_reports_list feature flag is enabled' do
+ include FilteredSearchHelpers
before do
- sign_in(admin)
- gitlab_enable_admin_mode_sign_in(admin)
-
visit admin_abuse_reports_path
end
+ let(:abuse_report_row_selector) { '[data-testid="abuse-report-row"]' }
+
it 'only includes open reports by default' do
expect_displayed_reports_count(2)
@@ -68,7 +70,8 @@ RSpec.describe "Admin::AbuseReports", :js, feature_category: :shared do
end
it 'can be sorted by created_at and updated_at in desc and asc order', :aggregate_failures do
- # created_at desc (default)
+ sort_by 'Created date'
+ # created_at desc
expect(report_rows[0].text).to include(report_text(open_report2))
expect(report_rows[1].text).to include(report_text(open_report))
@@ -78,25 +81,90 @@ RSpec.describe "Admin::AbuseReports", :js, feature_category: :shared do
expect(report_rows[0].text).to include(report_text(open_report))
expect(report_rows[1].text).to include(report_text(open_report2))
- # updated_at ascending
+ # updated_at asc
sort_by 'Updated date'
expect(report_rows[0].text).to include(report_text(open_report2))
expect(report_rows[1].text).to include(report_text(open_report))
- # updated_at descending
+ # updated_at desc
toggle_sort_direction
expect(report_rows[0].text).to include(report_text(open_report))
expect(report_rows[1].text).to include(report_text(open_report2))
end
+ context 'when multiple reports for the same user are created' do
+ let_it_be(:open_report3) { create(:abuse_report, category: 'spam', user: user) }
+ let_it_be(:closed_report2) { create(:abuse_report, :closed, user: user, category: 'spam') }
+
+ it 'aggregates open reports by user & category', :aggregate_failures do
+ expect_displayed_reports_count(2)
+
+ expect_aggregated_report_shown(open_report, 2)
+ expect_report_shown(open_report2)
+ end
+
+ it 'can sort aggregated reports by number_of_reports in desc order only', :aggregate_failures do
+ sort_by 'Number of Reports'
+
+ expect(report_rows[0].text).to include(aggregated_report_text(open_report, 2))
+ expect(report_rows[1].text).to include(report_text(open_report2))
+
+ toggle_sort_direction
+
+ expect(report_rows[0].text).to include(aggregated_report_text(open_report, 2))
+ expect(report_rows[1].text).to include(report_text(open_report2))
+ end
+
+ it 'can sort aggregated reports by created_at and updated_at in desc and asc order', :aggregate_failures do
+ # number_of_reports desc (default)
+ expect(report_rows[0].text).to include(aggregated_report_text(open_report, 2))
+ expect(report_rows[1].text).to include(report_text(open_report2))
+
+ # created_at desc
+ sort_by 'Created date'
+
+ expect(report_rows[0].text).to include(report_text(open_report2))
+ expect(report_rows[1].text).to include(aggregated_report_text(open_report, 2))
+
+ # created_at asc
+ toggle_sort_direction
+
+ expect(report_rows[0].text).to include(aggregated_report_text(open_report, 2))
+ expect(report_rows[1].text).to include(report_text(open_report2))
+
+ sort_by 'Updated date'
+
+ # updated_at asc
+ expect(report_rows[0].text).to include(report_text(open_report2))
+ expect(report_rows[1].text).to include(aggregated_report_text(open_report, 2))
+
+ # updated_at desc
+ toggle_sort_direction
+
+ expect(report_rows[0].text).to include(aggregated_report_text(open_report, 2))
+ expect(report_rows[1].text).to include(report_text(open_report2))
+ end
+
+ it 'does not aggregate closed reports', :aggregate_failures do
+ filter %w[Status Closed]
+
+ expect_displayed_reports_count(2)
+ expect_report_shown(closed_report, closed_report2)
+ end
+ end
+
def report_rows
page.all(abuse_report_row_selector)
end
def report_text(report)
- "#{report.user.name} reported for #{report.category}"
+ "#{report.user.name} reported for #{report.category} by #{report.reporter.name}"
+ end
+
+ def aggregated_report_text(report, count)
+ "#{report.user.name} reported for #{report.category} by #{count} users"
end
def expect_report_shown(*reports)
@@ -111,6 +179,12 @@ RSpec.describe "Admin::AbuseReports", :js, feature_category: :shared do
end
end
+ def expect_aggregated_report_shown(*reports, count)
+ reports.each do |r|
+ expect(page).to have_content(aggregated_report_text(r, count))
+ end
+ end
+
def expect_displayed_reports_count(count)
expect(page).to have_css(abuse_report_row_selector, count: count)
end
@@ -138,71 +212,30 @@ RSpec.describe "Admin::AbuseReports", :js, feature_category: :shared do
before do
stub_feature_flags(abuse_reports_list: false)
- sign_in(admin)
- gitlab_enable_admin_mode_sign_in(admin)
+ visit admin_abuse_reports_path
end
- describe 'if a user has been reported for abuse' do
- let_it_be(:abuse_report) { create(:abuse_report, user: user) }
-
- describe 'in the abuse report view' do
- before do
- visit admin_abuse_reports_path
- end
-
- it 'presents information about abuse report' do
- expect(page).to have_content('Abuse Reports')
-
- expect(page).to have_content(user.name)
- expect(page).to have_content(abuse_report.reporter.name)
- expect(page).to have_content(abuse_report.message)
- expect(page).to have_link(user.name, href: user_path(user))
- end
-
- it 'present actions items' do
- expect(page).to have_link('Remove user & report')
- expect(page).to have_link('Block user')
- expect(page).to have_link('Remove user')
- end
- end
+ it 'displays all abuse reports', :aggregate_failures do
+ expect_report_shown(open_report)
+ expect_report_actions_shown(open_report)
- describe 'in the profile page of the user' do
- it 'shows a link to view user in the admin area' do
- visit user_path(user)
+ expect_report_shown(open_report2)
+ expect_report_actions_shown(open_report2)
- expect(page).to have_link 'View user in admin area', href: admin_user_path(user)
- end
- end
+ expect_report_shown(closed_report)
+ expect_report_actions_shown(closed_report)
end
- describe 'if an admin has been reported for abuse' do
+ context 'when an admin has been reported for abuse' do
let_it_be(:admin_abuse_report) { create(:abuse_report, user: admin) }
- describe 'in the abuse report view' do
- before do
- visit admin_abuse_reports_path
- end
-
- it 'presents information about abuse report' do
- page.within(:table_row, { "User" => admin.name }) do
- expect(page).to have_content(admin.name)
- expect(page).to have_content(admin_abuse_report.reporter.name)
- expect(page).to have_content(admin_abuse_report.message)
- expect(page).to have_link(admin.name, href: user_path(admin))
- end
- end
-
- it 'does not present actions items' do
- page.within(:table_row, { "User" => admin.name }) do
- expect(page).not_to have_link('Remove user & report')
- expect(page).not_to have_link('Block user')
- expect(page).not_to have_link('Remove user')
- end
- end
+ it 'displays the abuse report without actions' do
+ expect_report_shown(admin_abuse_report)
+ expect_report_actions_not_shown(admin_abuse_report)
end
end
- describe 'if a many users have been reported for abuse' do
+ context 'when multiple users have been reported for abuse' do
let(:report_count) { AbuseReport.default_per_page + 3 }
before do
@@ -211,8 +244,8 @@ RSpec.describe "Admin::AbuseReports", :js, feature_category: :shared do
end
end
- describe 'in the abuse report view' do
- it 'presents information about abuse report' do
+ context 'in the abuse report view', :aggregate_failures do
+ it 'adds pagination' do
visit admin_abuse_reports_path
expect(page).to have_selector('.pagination')
@@ -221,12 +254,8 @@ RSpec.describe "Admin::AbuseReports", :js, feature_category: :shared do
end
end
- describe 'filtering by user' do
- let!(:user2) { create(:user) }
- let!(:abuse_report) { create(:abuse_report, user: user) }
- let!(:abuse_report_2) { create(:abuse_report, user: user2) }
-
- it 'shows only single user report' do
+ context 'when filtering reports' do
+ it 'can be filtered by reported-user', :aggregate_failures do
visit admin_abuse_reports_path
page.within '.filter-form' do
@@ -234,14 +263,39 @@ RSpec.describe "Admin::AbuseReports", :js, feature_category: :shared do
wait_for_requests
page.within '.dropdown-menu-user' do
- click_link user2.name
+ click_link user.name
end
wait_for_requests
end
- expect(page).to have_content(user2.name)
- expect(page).not_to have_content(user.name)
+ expect_report_shown(open_report)
+ expect_report_shown(closed_report)
+ end
+ end
+
+ def expect_report_shown(report)
+ page.within(:table_row, { "User" => report.user.name, "Reported by" => report.reporter.name }) do
+ expect(page).to have_content(report.user.name)
+ expect(page).to have_content(report.reporter.name)
+ expect(page).to have_content(report.message)
+ expect(page).to have_link(report.user.name, href: user_path(report.user))
+ end
+ end
+
+ def expect_report_actions_shown(report)
+ page.within(:table_row, { "User" => report.user.name, "Reported by" => report.reporter.name }) do
+ expect(page).to have_link('Remove user & report')
+ expect(page).to have_link('Block user')
+ expect(page).to have_link('Remove user')
+ end
+ end
+
+ def expect_report_actions_not_shown(report)
+ page.within(:table_row, { "User" => report.user.name, "Reported by" => report.reporter.name }) do
+ expect(page).not_to have_link('Remove user & report')
+ expect(page).not_to have_link('Block user')
+ expect(page).not_to have_link('Remove user')
end
end
end
diff --git a/spec/features/profiles/keys_spec.rb b/spec/features/profiles/keys_spec.rb
index ae61f1cf492..b49d16603b2 100644
--- a/spec/features/profiles/keys_spec.rb
+++ b/spec/features/profiles/keys_spec.rb
@@ -27,7 +27,7 @@ RSpec.describe 'Profile > SSH Keys', feature_category: :user_profile do
fill_in('Title', with: attrs[:title])
click_button('Add key')
- expect(page).to have_content("Title: #{attrs[:title]}")
+ expect(page).to have_content(format(s_('Profiles|SSH Key: %{title}'), title: attrs[:title]))
expect(page).to have_content(attrs[:key])
expect(find('[data-testid="breadcrumb-current-link"]')).to have_link(attrs[:title])
end
diff --git a/spec/finders/abuse_reports_finder_spec.rb b/spec/finders/abuse_reports_finder_spec.rb
index ee93d042ca2..0b641d0cb08 100644
--- a/spec/finders/abuse_reports_finder_spec.rb
+++ b/spec/finders/abuse_reports_finder_spec.rb
@@ -2,142 +2,205 @@
require 'spec_helper'
-RSpec.describe AbuseReportsFinder, '#execute' do
- let_it_be(:user1) { create(:user) }
- let_it_be(:user2) { create(:user) }
- let_it_be(:reporter) { create(:user) }
- let_it_be(:abuse_report_1) { create(:abuse_report, id: 20, category: 'spam', user: user1) }
- let_it_be(:abuse_report_2) do
- create(:abuse_report, :closed, id: 30, category: 'phishing', user: user2, reporter: reporter)
- end
+RSpec.describe AbuseReportsFinder, feature_category: :insider_threat do
+ let_it_be(:user_1) { create(:user) }
+ let_it_be(:user_2) { create(:user) }
- let(:params) { {} }
+ let_it_be(:reporter_1) { create(:user) }
+ let_it_be(:reporter_2) { create(:user) }
- subject { described_class.new(params).execute }
+ let_it_be(:abuse_report_1) do
+ create(:abuse_report, :open, category: 'spam', user: user_1, reporter: reporter_1, id: 1)
+ end
- context 'when params is empty' do
- it 'returns all abuse reports' do
- expect(subject).to match_array([abuse_report_1, abuse_report_2])
- end
+ let_it_be(:abuse_report_2) do
+ create(:abuse_report, :closed, category: 'phishing', user: user_2, reporter: reporter_2, id: 2)
end
- context 'when params[:user_id] is present' do
- let(:params) { { user_id: user2 } }
+ let(:params) { {} }
- it 'returns abuse reports for the specified user' do
- expect(subject).to match_array([abuse_report_2])
- end
- end
+ subject(:finder) { described_class.new(params).execute }
- shared_examples 'returns filtered reports' do |filter_field|
- it "returns abuse reports filtered by #{filter_field}_id" do
- expect(subject).to match_array(filtered_reports)
+ describe '#execute' do
+ context 'when params is empty' do
+ it 'returns all abuse reports' do
+ expect(finder).to match_array([abuse_report_1, abuse_report_2])
+ end
end
- context "when no user has username = params[:#{filter_field}]" do
- before do
- allow(User).to receive_message_chain(:by_username, :pick)
- .with(params[filter_field])
- .with(:id)
- .and_return(nil)
+ shared_examples 'returns filtered reports' do |filter_field|
+ it "returns abuse reports filtered by #{filter_field}_id" do
+ expect(finder).to match_array(filtered_reports)
end
- it 'returns all abuse reports' do
- expect(subject).to match_array([abuse_report_1, abuse_report_2])
+ context "when no user has username = params[:#{filter_field}]" do
+ before do
+ allow(User).to receive_message_chain(:by_username, :pick)
+ .with(params[filter_field])
+ .with(:id)
+ .and_return(nil)
+ end
+
+ it 'returns all abuse reports' do
+ expect(finder).to match_array([abuse_report_1, abuse_report_2])
+ end
end
end
- end
- context 'when params[:user] is present' do
- it_behaves_like 'returns filtered reports', :user do
- let(:params) { { user: user1.username } }
- let(:filtered_reports) { [abuse_report_1] }
+ context 'when params[:user] is present' do
+ it_behaves_like 'returns filtered reports', :user do
+ let(:params) { { user: user_1.username } }
+ let(:filtered_reports) { [abuse_report_1] }
+ end
end
- end
- context 'when params[:reporter] is present' do
- it_behaves_like 'returns filtered reports', :reporter do
- let(:params) { { reporter: reporter.username } }
- let(:filtered_reports) { [abuse_report_2] }
+ context 'when params[:reporter] is present' do
+ it_behaves_like 'returns filtered reports', :reporter do
+ let(:params) { { reporter: reporter_1.username } }
+ let(:filtered_reports) { [abuse_report_1] }
+ end
end
- end
- context 'when params[:status] is present' do
- context 'when value is "open"' do
+ context 'when params[:status] = open' do
let(:params) { { status: 'open' } }
it 'returns only open abuse reports' do
- expect(subject).to match_array([abuse_report_1])
+ expect(finder).to match_array([abuse_report_1])
end
end
- context 'when value is "closed"' do
+ context 'when params[:status] = closed' do
let(:params) { { status: 'closed' } }
it 'returns only closed abuse reports' do
- expect(subject).to match_array([abuse_report_2])
+ expect(finder).to match_array([abuse_report_2])
end
end
- context 'when value is not a valid status' do
+ context 'when params[:status] is not a valid status' do
let(:params) { { status: 'partial' } }
it 'defaults to returning open abuse reports' do
- expect(subject).to match_array([abuse_report_1])
+ expect(finder).to match_array([abuse_report_1])
end
end
- context 'when abuse_reports_list feature flag is disabled' do
- before do
- stub_feature_flags(abuse_reports_list: false)
- end
+ context 'when params[:category] is present' do
+ let(:params) { { category: 'phishing' } }
- it 'does not filter by status' do
- expect(subject).to match_array([abuse_report_1, abuse_report_2])
+ it 'returns abuse reports with the specified category' do
+ expect(subject).to match_array([abuse_report_2])
end
end
- end
- context 'when params[:category] is present' do
- let(:params) { { category: 'phishing' } }
+ describe 'aggregating reports' do
+ context 'when multiple open reports exist' do
+ let(:params) { { status: 'open' } }
- it 'returns abuse reports with the specified category' do
- expect(subject).to match_array([abuse_report_2])
- end
- end
+ # same category and user as abuse_report_1 -> will get aggregated
+ let_it_be(:abuse_report_3) do
+ create(:abuse_report, :open, category: abuse_report_1.category, user: abuse_report_1.user, id: 3)
+ end
- describe 'sorting' do
- let(:params) { { sort: 'created_at_asc' } }
+ # different category, but same user as abuse_report_1 -> won't get aggregated
+ let_it_be(:abuse_report_4) do
+ create(:abuse_report, :open, category: 'phishing', user: abuse_report_1.user, id: 4)
+ end
- it 'returns reports sorted by the specified sort attribute' do
- expect(subject).to eq [abuse_report_1, abuse_report_2]
- end
+ it 'aggregates open reports by user and category' do
+ expect(finder).to match_array([abuse_report_1, abuse_report_4])
+ end
+
+ it 'sorts by aggregated_count in descending order and created_at in descending order' do
+ expect(finder).to eq([abuse_report_1, abuse_report_4])
+ end
+
+ it 'returns count with aggregated reports' do
+ expect(finder[0].count).to eq(2)
+ end
+
+ context 'when a different sorting attribute is given' do
+ let(:params) { { status: 'open', sort: 'created_at_desc' } }
- context 'when sort is not specified' do
- let(:params) { {} }
+ it 'returns reports sorted by the specified sort attribute' do
+ expect(subject).to eq([abuse_report_4, abuse_report_1])
+ end
+ end
- it "returns reports sorted by #{described_class::DEFAULT_SORT}" do
- expect(subject).to eq [abuse_report_2, abuse_report_1]
+ context 'when params[:sort] is invalid' do
+ let(:params) { { status: 'open', sort: 'invalid' } }
+
+ it 'sorts reports by aggregated_count in descending order' do
+ expect(finder).to eq([abuse_report_1, abuse_report_4])
+ end
+ end
end
- end
- context 'when sort is not supported' do
- let(:params) { { sort: 'superiority' } }
+ context 'when multiple closed reports exist' do
+ let(:params) { { status: 'closed' } }
+
+ # same user and category as abuse_report_2 -> won't get aggregated
+ let_it_be(:abuse_report_5) do
+ create(:abuse_report, :closed, category: abuse_report_2.category, user: abuse_report_2.user, id: 5)
+ end
+
+ it 'does not aggregate closed reports' do
+ expect(finder).to match_array([abuse_report_2, abuse_report_5])
+ end
+
+ it 'sorts reports by created_at in descending order' do
+ expect(finder).to eq([abuse_report_5, abuse_report_2])
+ end
+
+ context 'when a different sorting attribute is given' do
+ let(:params) { { status: 'closed', sort: 'created_at_asc' } }
- it "returns reports sorted by #{described_class::DEFAULT_SORT}" do
- expect(subject).to eq [abuse_report_2, abuse_report_1]
+ it 'returns reports sorted by the specified sort attribute' do
+ expect(subject).to eq([abuse_report_2, abuse_report_5])
+ end
+ end
+
+ context 'when params[:sort] is invalid' do
+ let(:params) { { status: 'closed', sort: 'invalid' } }
+
+ it 'sorts reports by created_at in descending order' do
+ expect(finder).to eq([abuse_report_5, abuse_report_2])
+ end
+ end
end
end
- context 'when abuse_reports_list feature flag is disabled' do
- let_it_be(:abuse_report_3) { create(:abuse_report, id: 10) }
-
+ context 'when legacy view is enabled' do
before do
stub_feature_flags(abuse_reports_list: false)
end
- it 'returns reports sorted by id in descending order' do
- expect(subject).to eq [abuse_report_2, abuse_report_1, abuse_report_3]
+ context 'when params is empty' do
+ it 'returns all abuse reports' do
+ expect(subject).to match_array([abuse_report_1, abuse_report_2])
+ end
+ end
+
+ context 'when params[:user_id] is present' do
+ let(:params) { { user_id: user_1 } }
+
+ it 'returns abuse reports for the specified user' do
+ expect(subject).to match_array([abuse_report_1])
+ end
+ end
+
+ context 'when sorting' do
+ it 'returns reports sorted by id in descending order' do
+ expect(subject).to match_array([abuse_report_2, abuse_report_1])
+ end
+ end
+
+ context 'when any of the new filters are present such as params[:status]' do
+ let(:params) { { status: 'open' } }
+
+ it 'returns all abuse reports' do
+ expect(subject).to match_array([abuse_report_1, abuse_report_2])
+ end
end
end
end
diff --git a/spec/frontend/access_tokens/components/new_access_token_app_spec.js b/spec/frontend/access_tokens/components/new_access_token_app_spec.js
index fb92cc34ce9..70f77932ccf 100644
--- a/spec/frontend/access_tokens/components/new_access_token_app_spec.js
+++ b/spec/frontend/access_tokens/components/new_access_token_app_spec.js
@@ -69,6 +69,7 @@ describe('~/access_tokens/components/new_access_token_app', () => {
const InputCopyToggleVisibilityComponent = wrapper.findComponent(InputCopyToggleVisibility);
expect(InputCopyToggleVisibilityComponent.props('value')).toBe(newToken);
+ expect(InputCopyToggleVisibilityComponent.props('readonly')).toBe(true);
expect(InputCopyToggleVisibilityComponent.props('copyButtonTitle')).toBe(
sprintf(__('Copy %{accessTokenType}'), { accessTokenType }),
);
diff --git a/spec/frontend/access_tokens/components/token_spec.js b/spec/frontend/access_tokens/components/token_spec.js
index f62f7d72e3b..ad92366c3b6 100644
--- a/spec/frontend/access_tokens/components/token_spec.js
+++ b/spec/frontend/access_tokens/components/token_spec.js
@@ -50,6 +50,7 @@ describe('Token', () => {
formInputGroupProps: {
id: defaultPropsData.inputId,
},
+ readonly: true,
value: defaultPropsData.token,
copyButtonTitle: defaultPropsData.copyButtonTitle,
});
diff --git a/spec/frontend/admin/abuse_reports/components/abuse_report_row_spec.js b/spec/frontend/admin/abuse_reports/components/abuse_report_row_spec.js
index 03bf510f3ad..8482faccca0 100644
--- a/spec/frontend/admin/abuse_reports/components/abuse_report_row_spec.js
+++ b/spec/frontend/admin/abuse_reports/components/abuse_report_row_spec.js
@@ -94,4 +94,19 @@ describe('AbuseReportRow', () => {
it('renders abuse category', () => {
expect(findAbuseCategory().exists()).toBe(true);
});
+
+ describe('aggregated report', () => {
+ const mockAggregatedAbuseReport = mockAbuseReports[1];
+ const { reportedUser, category, count } = mockAggregatedAbuseReport;
+
+ beforeEach(() => {
+ createComponent({ report: mockAggregatedAbuseReport });
+ });
+
+ it('displays title with number of aggregated reports', () => {
+ expect(findAbuseReportTitle().text()).toMatchInterpolatedText(
+ `${reportedUser.name} reported for ${category} by ${count} users`,
+ );
+ });
+ });
});
diff --git a/spec/frontend/admin/abuse_reports/components/abuse_reports_filtered_search_bar_spec.js b/spec/frontend/admin/abuse_reports/components/abuse_reports_filtered_search_bar_spec.js
index 1f3f2caa995..dda9263d094 100644
--- a/spec/frontend/admin/abuse_reports/components/abuse_reports_filtered_search_bar_spec.js
+++ b/spec/frontend/admin/abuse_reports/components/abuse_reports_filtered_search_bar_spec.js
@@ -8,8 +8,10 @@ import {
FILTERED_SEARCH_TOKEN_REPORTER,
FILTERED_SEARCH_TOKEN_STATUS,
FILTERED_SEARCH_TOKEN_CATEGORY,
- DEFAULT_SORT,
- SORT_OPTIONS,
+ DEFAULT_SORT_STATUS_OPEN,
+ DEFAULT_SORT_STATUS_CLOSED,
+ SORT_OPTIONS_STATUS_OPEN,
+ SORT_OPTIONS_STATUS_CLOSED,
} from '~/admin/abuse_reports/constants';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
@@ -53,8 +55,8 @@ describe('AbuseReportsFilteredSearchBar', () => {
recentSearchesStorageKey: 'abuse_reports',
searchInputPlaceholder: 'Filter reports',
tokens: [...FILTERED_SEARCH_TOKENS, categoryToken],
- initialSortBy: DEFAULT_SORT,
- sortOptions: SORT_OPTIONS,
+ initialSortBy: DEFAULT_SORT_STATUS_OPEN,
+ sortOptions: SORT_OPTIONS_STATUS_OPEN,
});
});
@@ -88,6 +90,10 @@ describe('AbuseReportsFilteredSearchBar', () => {
expect(findFilteredSearchBar().props('initialFilterValue')).toMatchObject([
{
+ type: FILTERED_SEARCH_TOKEN_STATUS.type,
+ value: { data: 'closed', operator: '=' },
+ },
+ {
type: FILTERED_SEARCH_TOKEN_USER.type,
value: { data: 'mr_abuser', operator: '=' },
},
@@ -95,16 +101,12 @@ describe('AbuseReportsFilteredSearchBar', () => {
type: FILTERED_SEARCH_TOKEN_REPORTER.type,
value: { data: 'ms_nitch', operator: '=' },
},
- {
- type: FILTERED_SEARCH_TOKEN_STATUS.type,
- value: { data: 'closed', operator: '=' },
- },
]);
});
describe('initial sort', () => {
it.each(
- SORT_OPTIONS.flatMap(({ sortDirection: { descending, ascending } }) => [
+ SORT_OPTIONS_STATUS_OPEN.flatMap(({ sortDirection: { descending, ascending } }) => [
descending,
ascending,
]),
@@ -115,16 +117,20 @@ describe('AbuseReportsFilteredSearchBar', () => {
createComponent();
- expect(findFilteredSearchBar().props('initialSortBy')).toEqual(sortBy);
+ if (sortBy) {
+ expect(findFilteredSearchBar().props('initialSortBy')).toEqual(sortBy);
+ } else {
+ expect(findFilteredSearchBar().props('initialSortBy')).toEqual(DEFAULT_SORT_STATUS_OPEN);
+ }
},
);
- it(`uses ${DEFAULT_SORT} as initialSortBy when sort query param is invalid`, () => {
+ it(`uses ${DEFAULT_SORT_STATUS_OPEN} as initialSortBy when sort query param is invalid`, () => {
setWindowLocation(`?sort=unknown`);
createComponent();
- expect(findFilteredSearchBar().props('initialSortBy')).toEqual(DEFAULT_SORT);
+ expect(findFilteredSearchBar().props('initialSortBy')).toEqual(DEFAULT_SORT_STATUS_OPEN);
});
});
@@ -161,26 +167,39 @@ describe('AbuseReportsFilteredSearchBar', () => {
(filterToken) => {
createComponentAndFilter([filterToken]);
const { type, value } = filterToken;
- expect(redirectTo).toHaveBeenCalledWith(`https://localhost/?${type}=${value.data}`); // eslint-disable-line import/no-deprecated
+
+ // eslint-disable-next-line import/no-deprecated
+ expect(redirectTo).toHaveBeenCalledWith(
+ `https://localhost/?${type}=${value.data}&sort=${DEFAULT_SORT_STATUS_OPEN}`,
+ );
},
);
it('ignores search query param', () => {
const searchFilterToken = { type: FILTERED_SEARCH_TERM, value: { data: 'ignored' } };
createComponentAndFilter([USER_FILTER_TOKEN, searchFilterToken]);
- expect(redirectTo).toHaveBeenCalledWith('https://localhost/?user=mr_abuser'); // eslint-disable-line import/no-deprecated
+
+ // eslint-disable-next-line import/no-deprecated
+ expect(redirectTo).toHaveBeenCalledWith(
+ `https://localhost/?user=mr_abuser&sort=${DEFAULT_SORT_STATUS_OPEN}`,
+ );
});
it('redirects without page query param', () => {
createComponentAndFilter([USER_FILTER_TOKEN], '?page=2');
- expect(redirectTo).toHaveBeenCalledWith('https://localhost/?user=mr_abuser'); // eslint-disable-line import/no-deprecated
+
+ // eslint-disable-next-line import/no-deprecated
+ expect(redirectTo).toHaveBeenCalledWith(
+ `https://localhost/?user=mr_abuser&sort=${DEFAULT_SORT_STATUS_OPEN}`,
+ );
});
it('redirects with existing sort query param', () => {
- createComponentAndFilter([USER_FILTER_TOKEN], `?sort=${DEFAULT_SORT}`);
+ createComponentAndFilter([USER_FILTER_TOKEN], `?sort=${DEFAULT_SORT_STATUS_OPEN}`);
+
// eslint-disable-next-line import/no-deprecated
expect(redirectTo).toHaveBeenCalledWith(
- `https://localhost/?user=mr_abuser&sort=${DEFAULT_SORT}`,
+ `https://localhost/?user=mr_abuser&sort=${DEFAULT_SORT_STATUS_OPEN}`,
);
});
});
@@ -222,4 +241,42 @@ describe('AbuseReportsFilteredSearchBar', () => {
);
});
});
+
+ describe('sortOptions', () => {
+ describe('when status is closed', () => {
+ beforeEach(() => {
+ setWindowLocation('?status=closed');
+
+ createComponent();
+ });
+
+ it('only shows created_at & updated_at as sorting options', () => {
+ expect(findFilteredSearchBar().props('sortOptions')).toMatchObject(
+ SORT_OPTIONS_STATUS_CLOSED,
+ );
+ });
+
+ it('initially sorts by created_at_desc', () => {
+ expect(findFilteredSearchBar().props('initialSortBy')).toEqual(DEFAULT_SORT_STATUS_CLOSED);
+ });
+ });
+
+ describe('when status is open', () => {
+ beforeEach(() => {
+ setWindowLocation('?status=open');
+
+ createComponent();
+ });
+
+ it('shows number of reports as an additional sorting option', () => {
+ expect(findFilteredSearchBar().props('sortOptions')).toMatchObject(
+ SORT_OPTIONS_STATUS_OPEN,
+ );
+ });
+
+ it('initially sorts by number_of_reports_desc', () => {
+ expect(findFilteredSearchBar().props('initialSortBy')).toEqual(DEFAULT_SORT_STATUS_OPEN);
+ });
+ });
+ });
});
diff --git a/spec/frontend/admin/abuse_reports/mock_data.js b/spec/frontend/admin/abuse_reports/mock_data.js
index 1ea6ea7d131..33a28a21cca 100644
--- a/spec/frontend/admin/abuse_reports/mock_data.js
+++ b/spec/frontend/admin/abuse_reports/mock_data.js
@@ -6,6 +6,7 @@ export const mockAbuseReports = [
reporter: { name: 'Ms. Admin' },
reportedUser: { name: 'Mr. Abuser' },
reportPath: '/admin/abuse_reports/1',
+ count: 1,
},
{
category: 'phishing',
@@ -14,5 +15,6 @@ export const mockAbuseReports = [
reporter: { name: 'Ms. Reporter' },
reportedUser: { name: 'Mr. Phisher' },
reportPath: '/admin/abuse_reports/2',
+ count: 2,
},
];
diff --git a/spec/frontend/authentication/two_factor_auth/components/recovery_codes_spec.js b/spec/frontend/authentication/two_factor_auth/components/recovery_codes_spec.js
index 0d9196b88ed..aef06a74fdd 100644
--- a/spec/frontend/authentication/two_factor_auth/components/recovery_codes_spec.js
+++ b/spec/frontend/authentication/two_factor_auth/components/recovery_codes_spec.js
@@ -6,12 +6,10 @@ import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import RecoveryCodes, {
i18n,
} from '~/authentication/two_factor_auth/components/recovery_codes.vue';
-import {
- RECOVERY_CODE_DOWNLOAD_FILENAME,
- COPY_KEYBOARD_SHORTCUT,
-} from '~/authentication/two_factor_auth/constants';
+import { RECOVERY_CODE_DOWNLOAD_FILENAME } from '~/authentication/two_factor_auth/constants';
import Tracking from '~/tracking';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import { MOUSETRAP_COPY_KEYBOARD_SHORTCUT } from '~/lib/mousetrap';
import { codes, codesFormattedString, codesDownloadHref, profileAccountPath } from '../mock_data';
describe('RecoveryCodes', () => {
@@ -42,7 +40,7 @@ describe('RecoveryCodes', () => {
const findPrintButton = () => findButtonByText('Print codes');
const findProceedButton = () => findButtonByText('Proceed');
const manuallyCopyRecoveryCodes = () =>
- wrapper.vm.$options.mousetrap.trigger(COPY_KEYBOARD_SHORTCUT);
+ wrapper.vm.$options.mousetrap.trigger(MOUSETRAP_COPY_KEYBOARD_SHORTCUT);
beforeEach(() => {
jest.spyOn(Tracking, 'event');
diff --git a/spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js b/spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js
index e4373d1c198..3fb845b186a 100644
--- a/spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js
+++ b/spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js
@@ -168,9 +168,8 @@ describe('RegistrationDropdown', () => {
expect(findTokenDropdownItem().exists()).toBe(true);
});
- it('Displays masked value by default', () => {
+ it('Displays masked value as password input by default', () => {
const mockToken = '0123456789';
- const maskToken = '**********';
createComponent(
{
@@ -179,7 +178,7 @@ describe('RegistrationDropdown', () => {
mountExtended,
);
- expect(findRegistrationTokenInput().element.value).toBe(maskToken);
+ expect(findRegistrationTokenInput().element.type).toBe('password');
});
});
diff --git a/spec/frontend/ci/runner/components/registration/registration_token_spec.js b/spec/frontend/ci/runner/components/registration/registration_token_spec.js
index fd3896d5500..eccfe43b47f 100644
--- a/spec/frontend/ci/runner/components/registration/registration_token_spec.js
+++ b/spec/frontend/ci/runner/components/registration/registration_token_spec.js
@@ -38,10 +38,15 @@ describe('RegistrationToken', () => {
);
});
+ it('Renders readonly input', () => {
+ createComponent();
+
+ expect(findInputCopyToggleVisibility().props('readonly')).toBe(true);
+ });
+
// Component integration test to ensure secure masking
- it('Displays masked value by default', () => {
+ it('Displays masked value as password input by default', () => {
const mockToken = '0123456789';
- const maskToken = '**********';
createComponent({
props: {
@@ -50,7 +55,7 @@ describe('RegistrationToken', () => {
mountFn: mountExtended,
});
- expect(wrapper.find('input').element.value).toBe(maskToken);
+ expect(wrapper.find('input').element.type).toBe('password');
});
describe('When the copy to clipboard button is clicked', () => {
diff --git a/spec/frontend/diffs/components/app_spec.js b/spec/frontend/diffs/components/app_spec.js
index fb5cf4dfd0a..2ad04a7c371 100644
--- a/spec/frontend/diffs/components/app_spec.js
+++ b/spec/frontend/diffs/components/app_spec.js
@@ -11,7 +11,7 @@ import CompareVersions from '~/diffs/components/compare_versions.vue';
import DiffFile from '~/diffs/components/diff_file.vue';
import NoChanges from '~/diffs/components/no_changes.vue';
import findingsDrawer from '~/diffs/components/shared/findings_drawer.vue';
-import TreeList from '~/diffs/components/tree_list.vue';
+import DiffsFileTree from '~/diffs/components/diffs_file_tree.vue';
import CollapsedFilesWarning from '~/diffs/components/collapsed_files_warning.vue';
import HiddenFilesWarning from '~/diffs/components/hidden_files_warning.vue';
@@ -252,34 +252,6 @@ describe('diffs/components/app', () => {
});
});
- describe('resizable', () => {
- afterEach(() => {
- localStorage.removeItem('mr_tree_list_width');
- });
-
- it('sets initial width when no localStorage has been set', () => {
- createComponent();
-
- expect(wrapper.vm.treeWidth).toEqual(320);
- });
-
- it('sets initial width to localStorage size', () => {
- localStorage.setItem('mr_tree_list_width', '200');
-
- createComponent();
-
- expect(wrapper.vm.treeWidth).toEqual(200);
- });
-
- it('sets width of tree list', () => {
- createComponent({}, ({ state }) => {
- state.diffs.treeEntries = { 111: { type: 'blob', fileHash: '111', path: '111.js' } };
- });
-
- expect(wrapper.find('.js-diff-tree-list').element.style.width).toEqual('320px');
- });
- });
-
it('marks current diff file based on currently highlighted row', async () => {
window.location.hash = 'ABC_123';
@@ -596,18 +568,21 @@ describe('diffs/components/app', () => {
);
});
- it("doesn't render tree list when no changes exist", () => {
+ it('should always render diffs file tree', () => {
createComponent();
-
- expect(wrapper.findComponent(TreeList).exists()).toBe(false);
+ expect(wrapper.findComponent(DiffsFileTree).exists()).toBe(true);
});
- it('should render tree list', () => {
+ it('should pass renderDiffFiles to file tree as true when files are present', () => {
createComponent({}, ({ state }) => {
state.diffs.treeEntries = { 111: { type: 'blob', fileHash: '111', path: '111.js' } };
});
+ expect(wrapper.findComponent(DiffsFileTree).props('renderDiffFiles')).toBe(true);
+ });
- expect(wrapper.findComponent(TreeList).exists()).toBe(true);
+ it('should pass renderDiffFiles to file tree as false without files', () => {
+ createComponent();
+ expect(wrapper.findComponent(DiffsFileTree).props('renderDiffFiles')).toBe(false);
});
});
diff --git a/spec/frontend/diffs/components/compare_versions_spec.js b/spec/frontend/diffs/components/compare_versions_spec.js
index cbbfd88260b..3601f0cc7b0 100644
--- a/spec/frontend/diffs/components/compare_versions_spec.js
+++ b/spec/frontend/diffs/components/compare_versions_spec.js
@@ -84,7 +84,7 @@ describe('CompareVersions', () => {
const treeListBtn = wrapper.find('.js-toggle-tree-list');
expect(treeListBtn.exists()).toBe(true);
- expect(treeListBtn.attributes('title')).toBe('Hide file browser');
+ expect(treeListBtn.attributes('title')).toBe('Hide file browser (or press F)');
expect(treeListBtn.props('icon')).toBe('file-tree');
});
diff --git a/spec/frontend/diffs/components/diffs_file_tree_spec.js b/spec/frontend/diffs/components/diffs_file_tree_spec.js
new file mode 100644
index 00000000000..a79023a07cb
--- /dev/null
+++ b/spec/frontend/diffs/components/diffs_file_tree_spec.js
@@ -0,0 +1,116 @@
+import { nextTick } from 'vue';
+import { shallowMount } from '@vue/test-utils';
+import { Mousetrap } from '~/lib/mousetrap';
+import DiffsFileTree from '~/diffs/components/diffs_file_tree.vue';
+import TreeList from '~/diffs/components/tree_list.vue';
+import PanelResizer from '~/vue_shared/components/panel_resizer.vue';
+import { SET_SHOW_TREE_LIST } from '~/diffs/store/mutation_types';
+import createDiffsStore from '../create_diffs_store';
+
+describe('DiffsFileTree', () => {
+ let wrapper;
+ let store;
+
+ const createComponent = ({ renderDiffFiles = true, showTreeList = true } = {}) => {
+ store = createDiffsStore();
+ store.commit(`diffs/${SET_SHOW_TREE_LIST}`, showTreeList);
+ wrapper = shallowMount(DiffsFileTree, {
+ store,
+ propsData: {
+ renderDiffFiles,
+ },
+ });
+ };
+
+ describe('visibility', () => {
+ describe('when renderDiffFiles and showTreeList are true', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('tree list is visible', () => {
+ expect(wrapper.findComponent(TreeList).exists()).toBe(true);
+ });
+ });
+
+ describe('when renderDiffFiles and showTreeList are false', () => {
+ beforeEach(() => {
+ createComponent({ renderDiffFiles: false, showTreeList: false });
+ });
+
+ it('tree list is hidden', () => {
+ expect(wrapper.findComponent(TreeList).exists()).toBe(false);
+ });
+ });
+ });
+
+ it('emits toggled event', async () => {
+ createComponent();
+ store.commit(`diffs/${SET_SHOW_TREE_LIST}`, false);
+ await nextTick();
+ expect(wrapper.emitted('toggled')).toStrictEqual([[]]);
+ });
+
+ it('toggles when "f" hotkey is pressed', async () => {
+ createComponent();
+ Mousetrap.trigger('f');
+ await nextTick();
+ expect(wrapper.findComponent(TreeList).exists()).toBe(false);
+ });
+
+ describe('size', () => {
+ const checkWidth = (width) => {
+ expect(wrapper.element.style.width).toEqual(`${width}px`);
+ expect(wrapper.findComponent(PanelResizer).props('startSize')).toEqual(width);
+ };
+
+ afterEach(() => {
+ localStorage.removeItem('mr_tree_list_width');
+ });
+
+ describe('when no localStorage record is set', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('sets initial width when no localStorage has been set', () => {
+ checkWidth(320);
+ });
+ });
+
+ it('sets initial width to localStorage size', () => {
+ localStorage.setItem('mr_tree_list_width', '200');
+ createComponent();
+ checkWidth(200);
+ });
+
+ it('sets width of tree list', () => {
+ createComponent({}, ({ state }) => {
+ state.diffs.treeEntries = { 111: { type: 'blob', fileHash: '111', path: '111.js' } };
+ });
+ checkWidth(320);
+ });
+
+ it('updates width', async () => {
+ const WIDTH = 500;
+ createComponent();
+ wrapper.findComponent(PanelResizer).vm.$emit('update:size', WIDTH);
+ await nextTick();
+ checkWidth(WIDTH);
+ });
+
+ it('passes down hideFileStats as true when width is less than 260', async () => {
+ createComponent();
+ wrapper.findComponent(PanelResizer).vm.$emit('update:size', 200);
+ await nextTick();
+ expect(wrapper.findComponent(TreeList).props('hideFileStats')).toBe(true);
+ });
+
+ it('passes down hideFileStats as false when width is bigger than 260', async () => {
+ createComponent();
+ wrapper.findComponent(PanelResizer).vm.$emit('update:size', 300);
+ await nextTick();
+ expect(wrapper.findComponent(TreeList).props('hideFileStats')).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/oauth_application/components/oauth_secret_spec.js b/spec/frontend/oauth_application/components/oauth_secret_spec.js
index c38bd066da8..5ad55c1e81b 100644
--- a/spec/frontend/oauth_application/components/oauth_secret_spec.js
+++ b/spec/frontend/oauth_application/components/oauth_secret_spec.js
@@ -47,6 +47,10 @@ describe('OAuthSecret', () => {
it('shows the renew secret button', () => {
expect(findRenewSecretButton().exists()).toBe(true);
});
+
+ it('renders secret in readonly input', () => {
+ expect(findInputCopyToggleVisibility().props('readonly')).toBe(true);
+ });
});
describe('when secret is not provided', () => {
diff --git a/spec/frontend/super_sidebar/components/flyout_menu_spec.js b/spec/frontend/super_sidebar/components/flyout_menu_spec.js
new file mode 100644
index 00000000000..b894d29c875
--- /dev/null
+++ b/spec/frontend/super_sidebar/components/flyout_menu_spec.js
@@ -0,0 +1,25 @@
+import { shallowMount } from '@vue/test-utils';
+import FlyoutMenu from '~/super_sidebar/components/flyout_menu.vue';
+
+jest.mock('@floating-ui/dom');
+
+describe('FlyoutMenu', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = shallowMount(FlyoutMenu, {
+ propsData: {
+ targetId: 'section-1',
+ items: [],
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders the component', () => {
+ expect(wrapper.exists()).toBe(true);
+ });
+});
diff --git a/spec/frontend/super_sidebar/components/menu_section_spec.js b/spec/frontend/super_sidebar/components/menu_section_spec.js
index 556e07a2e31..dd729d8fd6a 100644
--- a/spec/frontend/super_sidebar/components/menu_section_spec.js
+++ b/spec/frontend/super_sidebar/components/menu_section_spec.js
@@ -2,6 +2,7 @@ import { GlCollapse } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import MenuSection from '~/super_sidebar/components/menu_section.vue';
import NavItem from '~/super_sidebar/components/nav_item.vue';
+import FlyoutMenu from '~/super_sidebar/components/flyout_menu.vue';
import { stubComponent } from 'helpers/stub_component';
describe('MenuSection component', () => {
@@ -9,6 +10,7 @@ describe('MenuSection component', () => {
const findButton = () => wrapper.find('button');
const findCollapse = () => wrapper.getComponent(GlCollapse);
+ const findFlyout = () => wrapper.findComponent(FlyoutMenu);
const findNavItems = () => wrapper.findAllComponents(NavItem);
const createWrapper = (item, otherProps) => {
wrapper = shallowMountExtended(MenuSection, {
@@ -68,6 +70,40 @@ describe('MenuSection component', () => {
});
});
+ describe('flyout behavior', () => {
+ describe('when hasFlyout is false', () => {
+ it('is not rendered', () => {
+ createWrapper({ title: 'Asdf' }, { 'has-flyout': false });
+ expect(findFlyout().exists()).toBe(false);
+ });
+ });
+
+ describe('when hasFlyout is true', () => {
+ it('is rendered', () => {
+ createWrapper({ title: 'Asdf' }, { 'has-flyout': true });
+ expect(findFlyout().exists()).toBe(true);
+ });
+
+ describe('on mouse hover', () => {
+ describe('when section is expanded', () => {
+ it('is not shown', async () => {
+ createWrapper({ title: 'Asdf' }, { 'has-flyout': true, expanded: true });
+ await findButton().trigger('pointerover', { pointerType: 'mouse' });
+ expect(findFlyout().isVisible()).toBe(false);
+ });
+ });
+
+ describe('when section is not expanded', () => {
+ it('is shown', async () => {
+ createWrapper({ title: 'Asdf' }, { 'has-flyout': true, expanded: false });
+ await findButton().trigger('pointerover', { pointerType: 'mouse' });
+ expect(findFlyout().isVisible()).toBe(true);
+ });
+ });
+ });
+ });
+ });
+
describe('`separated` prop', () => {
describe('by default (false)', () => {
it('does not render a separator', () => {
diff --git a/spec/frontend/super_sidebar/components/pinned_section_spec.js b/spec/frontend/super_sidebar/components/pinned_section_spec.js
index fd6e2b7343e..00cc7cf29c9 100644
--- a/spec/frontend/super_sidebar/components/pinned_section_spec.js
+++ b/spec/frontend/super_sidebar/components/pinned_section_spec.js
@@ -2,10 +2,12 @@ import { nextTick } from 'vue';
import Cookies from '~/lib/utils/cookies';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import PinnedSection from '~/super_sidebar/components/pinned_section.vue';
+import MenuSection from '~/super_sidebar/components/menu_section.vue';
import NavItem from '~/super_sidebar/components/nav_item.vue';
import { SIDEBAR_PINS_EXPANDED_COOKIE, SIDEBAR_COOKIE_EXPIRATION } from '~/super_sidebar/constants';
import { setCookie } from '~/lib/utils/common_utils';
+jest.mock('@floating-ui/dom');
jest.mock('~/lib/utils/common_utils', () => ({
getCookie: jest.requireActual('~/lib/utils/common_utils').getCookie,
setCookie: jest.fn(),
@@ -16,10 +18,11 @@ describe('PinnedSection component', () => {
const findToggle = () => wrapper.find('button');
- const createWrapper = () => {
+ const createWrapper = (props = {}) => {
wrapper = mountExtended(PinnedSection, {
propsData: {
items: [{ title: 'Pin 1', href: '/page1' }],
+ ...props,
},
});
};
@@ -72,4 +75,16 @@ describe('PinnedSection component', () => {
});
});
});
+
+ describe('hasFlyout prop', () => {
+ describe.each([true, false])(`when %s`, (hasFlyout) => {
+ beforeEach(() => {
+ createWrapper({ hasFlyout });
+ });
+
+ it(`passes ${hasFlyout} to the section's hasFlyout prop`, () => {
+ expect(wrapper.findComponent(MenuSection).props('hasFlyout')).toBe(hasFlyout);
+ });
+ });
+ });
});
diff --git a/spec/frontend/super_sidebar/components/sidebar_menu_spec.js b/spec/frontend/super_sidebar/components/sidebar_menu_spec.js
index 21e5220edd9..ac94f3f8f82 100644
--- a/spec/frontend/super_sidebar/components/sidebar_menu_spec.js
+++ b/spec/frontend/super_sidebar/components/sidebar_menu_spec.js
@@ -1,3 +1,4 @@
+import { GlBreakpointInstance } from '@gitlab/ui/dist/utils';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import SidebarMenu from '~/super_sidebar/components/sidebar_menu.vue';
import PinnedSection from '~/super_sidebar/components/pinned_section.vue';
@@ -15,9 +16,13 @@ const menuItems = [
describe('Sidebar Menu', () => {
let wrapper;
+ let flyoutFlag = false;
const createWrapper = (extraProps = {}) => {
wrapper = shallowMountExtended(SidebarMenu, {
+ provide: {
+ glFeatures: { superSidebarFlyoutMenus: flyoutFlag },
+ },
propsData: {
items: sidebarData.current_menu_items,
pinnedItemIds: sidebarData.pinned_items,
@@ -117,6 +122,65 @@ describe('Sidebar Menu', () => {
);
});
});
+
+ describe('flyout menus', () => {
+ describe('when feature is disabled', () => {
+ beforeEach(() => {
+ createWrapper({
+ items: menuItems,
+ });
+ });
+
+ it('does not add flyout menus to sections', () => {
+ expect(findNonStaticSectionItems().wrappers.map((w) => w.props('hasFlyout'))).toEqual([
+ false,
+ false,
+ ]);
+ });
+ });
+
+ describe('when feature is enabled', () => {
+ beforeEach(() => {
+ flyoutFlag = true;
+ });
+
+ describe('when screen width is smaller than "md" breakpoint', () => {
+ beforeEach(() => {
+ jest.spyOn(GlBreakpointInstance, 'windowWidth').mockImplementation(() => {
+ return 767;
+ });
+ createWrapper({
+ items: menuItems,
+ });
+ });
+
+ it('does not add flyout menus to sections', () => {
+ expect(findNonStaticSectionItems().wrappers.map((w) => w.props('hasFlyout'))).toEqual([
+ false,
+ false,
+ ]);
+ });
+ });
+
+ describe('when screen width is equal or larger than "md" breakpoint', () => {
+ beforeEach(() => {
+ jest.spyOn(GlBreakpointInstance, 'windowWidth').mockImplementation(() => {
+ return 768;
+ });
+ createWrapper({
+ items: menuItems,
+ });
+ });
+
+ it('adds flyout menus to sections', () => {
+ expect(findNonStaticSectionItems().wrappers.map((w) => w.props('hasFlyout'))).toEqual([
+ true,
+ true,
+ ]);
+ });
+ });
+ });
+ });
});
describe('Separators', () => {
diff --git a/spec/frontend/vue_shared/components/form/input_copy_toggle_visibility_spec.js b/spec/frontend/vue_shared/components/form/input_copy_toggle_visibility_spec.js
index 4f1603f93ba..7afa8e9b8dc 100644
--- a/spec/frontend/vue_shared/components/form/input_copy_toggle_visibility_spec.js
+++ b/spec/frontend/vue_shared/components/form/input_copy_toggle_visibility_spec.js
@@ -1,11 +1,12 @@
+import { nextTick } from 'vue';
import { merge } from 'lodash';
import { GlFormInputGroup } from '@gitlab/ui';
import InputCopyToggleVisibility from '~/vue_shared/components/form/input_copy_toggle_visibility.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
-
import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { MOUSETRAP_COPY_KEYBOARD_SHORTCUT } from '~/lib/mousetrap';
describe('InputCopyToggleVisibility', () => {
let wrapper;
@@ -40,6 +41,18 @@ describe('InputCopyToggleVisibility', () => {
return event;
};
+ const triggerCopyShortcut = () => {
+ wrapper.vm.$options.mousetrap.trigger(MOUSETRAP_COPY_KEYBOARD_SHORTCUT);
+ };
+
+ function expectInputToBeMasked() {
+ expect(findFormInput().element.type).toBe('password');
+ }
+
+ function expectInputToBeRevealed() {
+ expect(findFormInput().element.type).toBe('text');
+ expect(findFormInput().element.value).toBe(valueProp);
+ }
const itDoesNotModifyCopyEvent = () => {
it('does not modify copy event', () => {
@@ -61,29 +74,55 @@ describe('InputCopyToggleVisibility', () => {
});
});
- it('displays value as hidden', () => {
- expect(findFormInput().element.value).toBe('********************');
+ it('hides the value with a password input', () => {
+ expectInputToBeMasked();
});
- it('saves actual value to clipboard when manually copied', () => {
- const event = createCopyEvent();
- findFormInput().element.dispatchEvent(event);
+ it('emits `copy` event and sets clipboard when copying token via keyboard shortcut', async () => {
+ const writeTextSpy = jest.spyOn(global.navigator.clipboard, 'writeText');
- expect(event.clipboardData.setData).toHaveBeenCalledWith('text/plain', valueProp);
- expect(event.preventDefault).toHaveBeenCalled();
- });
-
- it('emits `copy` event when manually copied the token', () => {
expect(wrapper.emitted('copy')).toBeUndefined();
- findFormInput().element.dispatchEvent(createCopyEvent());
+ triggerCopyShortcut();
+ await nextTick();
- expect(wrapper.emitted()).toHaveProperty('copy');
- expect(wrapper.emitted('copy')).toHaveLength(1);
expect(wrapper.emitted('copy')[0]).toEqual([]);
+ expect(writeTextSpy).toHaveBeenCalledWith(valueProp);
});
+ describe('copy button', () => {
+ it('renders button with correct props passed', () => {
+ expect(findCopyButton().props()).toMatchObject({
+ text: valueProp,
+ title: 'Copy',
+ });
+ });
+
+ describe('when clicked', () => {
+ beforeEach(async () => {
+ await findCopyButton().trigger('click');
+ });
+
+ it('emits `copy` event', () => {
+ expect(wrapper.emitted()).toHaveProperty('copy');
+ expect(wrapper.emitted('copy')).toHaveLength(1);
+ expect(wrapper.emitted('copy')[0]).toEqual([]);
+ });
+ });
+ });
+ });
+
+ describe('when input is readonly', () => {
describe('visibility toggle button', () => {
+ beforeEach(() => {
+ createComponent({
+ propsData: {
+ value: valueProp,
+ readonly: true,
+ },
+ });
+ });
+
it('renders a reveal button', () => {
const revealButton = findRevealButton();
@@ -103,7 +142,7 @@ describe('InputCopyToggleVisibility', () => {
});
it('displays value', () => {
- expect(findFormInput().element.value).toBe(valueProp);
+ expectInputToBeRevealed();
});
it('renders a hide button', () => {
@@ -127,78 +166,161 @@ describe('InputCopyToggleVisibility', () => {
});
});
- describe('copy button', () => {
- it('renders button with correct props passed', () => {
- expect(findCopyButton().props()).toMatchObject({
- text: valueProp,
- title: 'Copy',
+ describe('when `initialVisibility` prop is `true`', () => {
+ const label = 'My label';
+ beforeEach(() => {
+ createComponent({
+ propsData: {
+ value: valueProp,
+ initialVisibility: true,
+ readonly: true,
+ label,
+ 'label-for': 'my-input',
+ formInputGroupProps: {
+ id: 'my-input',
+ },
+ },
});
});
- describe('when clicked', () => {
- beforeEach(async () => {
- await findCopyButton().trigger('click');
+ it('displays value', () => {
+ expectInputToBeRevealed();
+ });
+
+ itDoesNotModifyCopyEvent();
+
+ describe('when input is clicked', () => {
+ it('selects input value', async () => {
+ const mockSelect = jest.fn();
+ wrapper.vm.$refs.input.$el.select = mockSelect;
+ await findFormInput().trigger('click');
+
+ expect(mockSelect).toHaveBeenCalled();
});
+ });
- it('emits `copy` event', () => {
- expect(wrapper.emitted()).toHaveProperty('copy');
- expect(wrapper.emitted('copy')).toHaveLength(1);
- expect(wrapper.emitted('copy')[0]).toEqual([]);
+ describe('when label is clicked', () => {
+ it('selects input value', async () => {
+ const mockSelect = jest.fn();
+ wrapper.vm.$refs.input.$el.select = mockSelect;
+ await wrapper.find('label').trigger('click');
+
+ expect(mockSelect).toHaveBeenCalled();
});
});
});
});
- describe('when `value` prop is not passed', () => {
- beforeEach(() => {
- createComponent();
- });
+ describe('when input is editable', () => {
+ describe('and no `value` prop is passed', () => {
+ beforeEach(() => {
+ createComponent({
+ propsData: {
+ value: '',
+ readonly: false,
+ },
+ });
+ });
- it('displays value as hidden with 20 asterisks', () => {
- expect(findFormInput().element.value).toBe('********************');
- });
- });
+ it('displays value', () => {
+ expect(findRevealButton().exists()).toBe(false);
+ expect(findHideButton().exists()).toBe(true);
- describe('when `initialVisibility` prop is `true`', () => {
- const label = 'My label';
+ const input = findFormInput();
+ input.element.value = valueProp;
+ input.trigger('input');
- beforeEach(() => {
- createComponent({
- propsData: {
- value: valueProp,
- initialVisibility: true,
- label,
- 'label-for': 'my-input',
- formInputGroupProps: {
- id: 'my-input',
- },
- },
+ expectInputToBeRevealed();
});
});
- it('displays value', () => {
- expect(findFormInput().element.value).toBe(valueProp);
- });
+ describe('and `value` prop is passed', () => {
+ beforeEach(() => {
+ createComponent({
+ propsData: {
+ value: valueProp,
+ readonly: false,
+ },
+ });
+ });
- itDoesNotModifyCopyEvent();
+ it('renders a reveal button', () => {
+ const revealButton = findRevealButton();
- describe('when input is clicked', () => {
- it('selects input value', async () => {
- const mockSelect = jest.fn();
- wrapper.vm.$refs.input.$el.select = mockSelect;
- await wrapper.findByLabelText(label).trigger('click');
+ expect(revealButton.exists()).toBe(true);
+
+ const tooltip = getBinding(revealButton.element, 'gl-tooltip');
- expect(mockSelect).toHaveBeenCalled();
+ expect(tooltip.value).toBe(InputCopyToggleVisibility.i18n.toggleVisibilityLabelReveal);
});
- });
- describe('when label is clicked', () => {
- it('selects input value', async () => {
- const mockSelect = jest.fn();
- wrapper.vm.$refs.input.$el.select = mockSelect;
- await wrapper.find('label').trigger('click');
+ it('renders a hide button once revealed', async () => {
+ const revealButton = findRevealButton();
+ await revealButton.trigger('click');
+ await nextTick();
+
+ const hideButton = findHideButton();
+ expect(hideButton.exists()).toBe(true);
+
+ const tooltip = getBinding(hideButton.element, 'gl-tooltip');
+
+ expect(tooltip.value).toBe(InputCopyToggleVisibility.i18n.toggleVisibilityLabelHide);
+ });
+
+ it('emits `input` event when editing', () => {
+ expect(wrapper.emitted('input')).toBeUndefined();
+ const newVal = 'ding!';
+
+ const input = findFormInput();
+ input.element.value = newVal;
+ input.trigger('input');
+
+ expect(wrapper.emitted()).toHaveProperty('input');
+ expect(wrapper.emitted('input')).toHaveLength(1);
+ expect(wrapper.emitted('input')[0][0]).toBe(newVal);
+ });
+
+ it('copies updated value to clipboard after editing', async () => {
+ const writeTextSpy = jest.spyOn(global.navigator.clipboard, 'writeText');
+
+ triggerCopyShortcut();
+ await nextTick();
- expect(mockSelect).toHaveBeenCalled();
+ expect(wrapper.emitted('copy')).toHaveLength(1);
+ expect(writeTextSpy).toHaveBeenCalledWith(valueProp);
+
+ const updatedValue = 'wow amazing';
+ wrapper.setProps({ value: updatedValue });
+ await nextTick();
+
+ triggerCopyShortcut();
+ await nextTick();
+
+ expect(wrapper.emitted('copy')).toHaveLength(2);
+ expect(writeTextSpy).toHaveBeenCalledWith(updatedValue);
+ });
+
+ describe('when input is clicked', () => {
+ it('shows the actual value', async () => {
+ const input = findFormInput();
+
+ expectInputToBeMasked();
+ await findFormInput().trigger('click');
+
+ expect(input.element.value).toBe(valueProp);
+ });
+
+ it('ensures the selection start/end are in the correct position once the actual value has been revealed', async () => {
+ const input = findFormInput();
+ const selectionStart = 2;
+ const selectionEnd = 4;
+
+ input.element.setSelectionRange(selectionStart, selectionEnd);
+ await input.trigger('click');
+
+ expect(input.element.selectionStart).toBe(selectionStart);
+ expect(input.element.selectionEnd).toBe(selectionEnd);
+ });
});
});
});
@@ -219,7 +341,7 @@ describe('InputCopyToggleVisibility', () => {
});
it('displays value', () => {
- expect(findFormInput().element.value).toBe(valueProp);
+ expectInputToBeRevealed();
});
itDoesNotModifyCopyEvent();
diff --git a/spec/lib/gitlab/regex_spec.rb b/spec/lib/gitlab/regex_spec.rb
index afabcc49017..092d3c07716 100644
--- a/spec/lib/gitlab/regex_spec.rb
+++ b/spec/lib/gitlab/regex_spec.rb
@@ -74,6 +74,18 @@ RSpec.describe Gitlab::Regex, feature_category: :tooling do
it { is_expected.to eq("can contain only letters, digits, emoji, '_', '.', dash, space, parenthesis. It must start with letter, digit, emoji or '_'.") }
end
+ describe '.slack_link_regex' do
+ subject { described_class.slack_link_regex }
+
+ it { is_expected.not_to match('http://custom-url.com|click here') }
+ it { is_expected.not_to match('custom-url.com|any-Charact3r$') }
+ it { is_expected.not_to match("&lt;custom-url.com|any-Charact3r$&gt;") }
+
+ it { is_expected.to match('<http://custom-url.com|click here>') }
+ it { is_expected.to match('<custom-url.com|any-Charact3r$>') }
+ it { is_expected.to match('<any-Charact3r$|any-Charact3r$>') }
+ end
+
describe '.bulk_import_destination_namespace_path_regex_message' do
subject { described_class.bulk_import_destination_namespace_path_regex_message }
diff --git a/spec/lib/gitlab/usage/metrics/instrumentations/work_items_activity_aggregated_metric_spec.rb b/spec/lib/gitlab/usage/metrics/instrumentations/work_items_activity_aggregated_metric_spec.rb
deleted file mode 100644
index 35e5d7f2796..00000000000
--- a/spec/lib/gitlab/usage/metrics/instrumentations/work_items_activity_aggregated_metric_spec.rb
+++ /dev/null
@@ -1,72 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::Usage::Metrics::Instrumentations::WorkItemsActivityAggregatedMetric do
- let(:metric_definition) do
- {
- data_source: 'redis_hll',
- time_frame: time_frame,
- options: {
- aggregate: {
- operator: 'OR'
- },
- events: %w[
- users_creating_work_items
- users_updating_work_item_title
- users_updating_work_item_dates
- users_updating_work_item_labels
- users_updating_work_item_milestone
- users_updating_work_item_iteration
- ]
- }
- }
- end
-
- around do |example|
- freeze_time { example.run }
- end
-
- where(:time_frame) { [['28d'], ['7d']] }
-
- with_them do
- describe '#available?' do
- it 'returns false without track_work_items_activity feature' do
- stub_feature_flags(track_work_items_activity: false)
-
- expect(described_class.new(metric_definition).available?).to eq(false)
- end
-
- it 'returns true with track_work_items_activity feature' do
- stub_feature_flags(track_work_items_activity: true)
-
- expect(described_class.new(metric_definition).available?).to eq(true)
- end
- end
-
- describe '#value', :clean_gitlab_redis_shared_state do
- let(:counter) { Gitlab::UsageDataCounters::HLLRedisCounter }
- let(:author1_id) { 1 }
- let(:author2_id) { 2 }
- let(:event_time) { 1.week.ago }
-
- before do
- counter.track_event(:users_creating_work_items, values: author1_id, time: event_time)
- end
-
- it 'has correct value after events are tracked', :aggregate_failures do
- expect do
- counter.track_event(:users_updating_work_item_title, values: author1_id, time: event_time)
- counter.track_event(:users_updating_work_item_dates, values: author1_id, time: event_time)
- counter.track_event(:users_updating_work_item_labels, values: author1_id, time: event_time)
- counter.track_event(:users_updating_work_item_milestone, values: author1_id, time: event_time)
- end.to not_change { described_class.new(metric_definition).value }
-
- expect do
- counter.track_event(:users_updating_work_item_iteration, values: author2_id, time: event_time)
- counter.track_event(:users_updating_weight_estimate, values: author1_id, time: event_time)
- end.to change { described_class.new(metric_definition).value }.from(1).to(2)
- end
- end
- end
-end
diff --git a/spec/lib/slack_markdown_sanitizer_spec.rb b/spec/lib/slack_markdown_sanitizer_spec.rb
index f4042439213..d9552542465 100644
--- a/spec/lib/slack_markdown_sanitizer_spec.rb
+++ b/spec/lib/slack_markdown_sanitizer_spec.rb
@@ -20,4 +20,21 @@ RSpec.describe SlackMarkdownSanitizer, feature_category: :integrations do
end
end
end
+
+ describe '.sanitize_slack_link' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:input, :output) do
+ '' | ''
+ '[label](url)' | '[label](url)'
+ '<url|label>' | '&lt;url|label&gt;'
+ '<a href="url">label</a>' | '<a href="url">label</a>'
+ end
+
+ with_them do
+ it 'returns the expected output' do
+ expect(described_class.sanitize_slack_link(input)).to eq(output)
+ end
+ end
+ end
end
diff --git a/spec/models/abuse_report_spec.rb b/spec/models/abuse_report_spec.rb
index 6192a271028..584f9b010ad 100644
--- a/spec/models/abuse_report_spec.rb
+++ b/spec/models/abuse_report_spec.rb
@@ -164,6 +164,34 @@ RSpec.describe AbuseReport, feature_category: :insider_threat do
expect(described_class.by_category('phishing')).to match_array([report2])
end
end
+
+ describe '.aggregated_by_user_and_category' do
+ let_it_be(:report3) { create(:abuse_report, category: report1.category, user: report1.user) }
+ let_it_be(:report4) { create(:abuse_report, category: 'phishing', user: report1.user) }
+ let_it_be(:report5) { create(:abuse_report, category: report1.category, user: build(:user)) }
+
+ let_it_be(:sort_by_count) { true }
+
+ subject(:aggregated) { described_class.aggregated_by_user_and_category(sort_by_count) }
+
+ context 'when sort_by_count = true' do
+ it 'sorts by aggregated_count in descending order and created_at in descending order' do
+ expect(aggregated).to eq([report1, report5, report4, report])
+ end
+
+ it 'returns count with aggregated reports' do
+ expect(aggregated[0].count).to eq(2)
+ end
+ end
+
+ context 'when sort_by_count = false' do
+ let_it_be(:sort_by_count) { false }
+
+ it 'does not sort using a specific order' do
+ expect(aggregated).to match_array([report, report1, report4, report5])
+ end
+ end
+ end
end
describe 'before_validation' do
diff --git a/spec/models/integrations/chat_message/issue_message_spec.rb b/spec/models/integrations/chat_message/issue_message_spec.rb
index cd40e4c361e..14451427a5a 100644
--- a/spec/models/integrations/chat_message/issue_message_spec.rb
+++ b/spec/models/integrations/chat_message/issue_message_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Integrations::ChatMessage::IssueMessage do
+RSpec.describe Integrations::ChatMessage::IssueMessage, feature_category: :integrations do
subject { described_class.new(args) }
let(:args) do
@@ -24,7 +24,7 @@ RSpec.describe Integrations::ChatMessage::IssueMessage do
url: 'http://url.com',
action: 'open',
state: 'opened',
- description: 'issue description'
+ description: 'issue description <http://custom-url.com|CLICK HERE>'
}
}
end
@@ -45,7 +45,7 @@ RSpec.describe Integrations::ChatMessage::IssueMessage do
end
context 'open' do
- it 'returns a message regarding opening of issues' do
+ it 'returns a slack-link sanitized message regarding opening of issues' do
expect(subject.pretext).to eq(
'[<http://somewhere.com|project_name>] Issue <http://url.com|#100 Issue title> opened by Test User (test.user)')
expect(subject.attachments).to eq(
@@ -53,7 +53,7 @@ RSpec.describe Integrations::ChatMessage::IssueMessage do
{
title: "#100 Issue title",
title_link: "http://url.com",
- text: "issue description",
+ text: "issue description &lt;http://custom-url.com|CLICK HERE&gt;",
color: color
}
])
@@ -96,7 +96,7 @@ RSpec.describe Integrations::ChatMessage::IssueMessage do
it 'returns a message regarding opening of issues' do
expect(subject.pretext).to eq(
'[[project_name](http://somewhere.com)] Issue [#100 Issue title](http://url.com) opened by Test User (test.user)')
- expect(subject.attachments).to eq('issue description')
+ expect(subject.attachments).to eq('issue description &lt;http://custom-url.com|CLICK HERE&gt;')
expect(subject.activity).to eq({
title: 'Issue opened by Test User (test.user)',
subtitle: 'in [project_name](http://somewhere.com)',
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index e99d77dc0a0..d56bc210d82 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -795,6 +795,24 @@ RSpec.describe Note, feature_category: :team_planning do
expect(note.system_note_visible_for?(nil)).to be_truthy
end
end
+
+ context 'when referenced resource is not present' do
+ let(:note) do
+ create :note, noteable: ext_issue, project: ext_proj, note: "mentioned in merge request !1", system: true
+ end
+
+ it "returns true for other users" do
+ expect(note.system_note_visible_for?(private_user)).to be_truthy
+ end
+
+ it "returns true if user visible reference count set" do
+ note.user_visible_reference_count = 0
+ note.total_reference_count = 0
+
+ expect(note).not_to receive(:reference_mentionables)
+ expect(note.system_note_visible_for?(ext_issue.author)).to be_truthy
+ end
+ end
end
describe '#system_note_with_references?' do
diff --git a/spec/serializers/admin/abuse_report_entity_spec.rb b/spec/serializers/admin/abuse_report_entity_spec.rb
index 003d76a172f..c7f57258f40 100644
--- a/spec/serializers/admin/abuse_report_entity_spec.rb
+++ b/spec/serializers/admin/abuse_report_entity_spec.rb
@@ -19,6 +19,7 @@ RSpec.describe Admin::AbuseReportEntity, feature_category: :insider_threat do
:category,
:created_at,
:updated_at,
+ :count,
:reported_user,
:reporter,
:report_path
diff --git a/spec/support/shared_examples/features/runners_shared_examples.rb b/spec/support/shared_examples/features/runners_shared_examples.rb
index 54a4db0e81d..0c043f48c5f 100644
--- a/spec/support/shared_examples/features/runners_shared_examples.rb
+++ b/spec/support/shared_examples/features/runners_shared_examples.rb
@@ -57,7 +57,7 @@ RSpec.shared_examples 'shows and resets runner registration token' do
click_on dropdown_text
click_on 'Click to reveal'
- expect(old_registration_token).not_to eq registration_token
+ expect(find_field('token-value').value).not_to eq old_registration_token
end
end
end
diff --git a/spec/support/shared_examples/usage_data_counters/work_item_activity_unique_counter_shared_examples.rb b/spec/support/shared_examples/usage_data_counters/work_item_activity_unique_counter_shared_examples.rb
index 4655585a092..83119046377 100644
--- a/spec/support/shared_examples/usage_data_counters/work_item_activity_unique_counter_shared_examples.rb
+++ b/spec/support/shared_examples/usage_data_counters/work_item_activity_unique_counter_shared_examples.rb
@@ -1,41 +1,27 @@
# frozen_string_literal: true
-RSpec.shared_examples 'counter that does not track the event' do
- it 'does not track the event' do
- expect { 3.times { track_event } }.to not_change {
+RSpec.shared_examples 'work item unique counter' do
+ it 'tracks a unique event only once' do
+ expect { 3.times { track_event } }.to change {
Gitlab::UsageDataCounters::HLLRedisCounter.unique_events(
event_names: event_name,
start_date: 2.weeks.ago,
end_date: 2.weeks.from_now
)
- }
+ }.by(1)
end
-end
-RSpec.shared_examples 'work item unique counter' do
- context 'when track_work_items_activity FF is enabled' do
- it 'tracks a unique event only once' do
- expect { 3.times { track_event } }.to change {
+ context 'when author is nil' do
+ let(:user) { nil }
+
+ it 'does not track the event' do
+ expect { 3.times { track_event } }.to not_change {
Gitlab::UsageDataCounters::HLLRedisCounter.unique_events(
event_names: event_name,
start_date: 2.weeks.ago,
end_date: 2.weeks.from_now
)
- }.by(1)
+ }
end
-
- context 'when author is nil' do
- let(:user) { nil }
-
- it_behaves_like 'counter that does not track the event'
- end
- end
-
- context 'when track_work_items_activity FF is disabled' do
- before do
- stub_feature_flags(track_work_items_activity: false)
- end
-
- it_behaves_like 'counter that does not track the event'
end
end