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-11-30 15:23:27 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-11-30 15:23:27 +0300
commit3bba41a8c5dfcca0d086eaef10ef36a705dd4f7a (patch)
tree81954681947aaa85592fa7f3c9beed23a7b6bb01 /spec
parent1aa447601c6be1e964acbb674887649dab23b804 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
-rw-r--r--spec/components/projects/ml/show_ml_model_component_spec.rb5
-rw-r--r--spec/components/projects/ml/show_ml_model_version_component_spec.rb5
-rw-r--r--spec/factories/ml/model_versions.rb1
-rw-r--r--spec/features/frequently_visited_projects_and_groups_spec.rb2
-rw-r--r--spec/features/projects/new_project_spec.rb2
-rw-r--r--spec/features/projects/show/user_sees_git_instructions_spec.rb1
-rw-r--r--spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb3
-rw-r--r--spec/features/projects_spec.rb8
-rw-r--r--spec/features/uploads/user_uploads_avatar_to_profile_spec.rb6
-rw-r--r--spec/frontend/environments/kubernetes_overview_spec.js19
-rw-r--r--spec/frontend/environments/kubernetes_pods_spec.js10
-rw-r--r--spec/frontend/environments/kubernetes_summary_spec.js4
-rw-r--r--spec/frontend/environments/kubernetes_tabs_spec.js7
-rw-r--r--spec/frontend/ml/model_registry/apps/show_ml_model_spec.js12
-rw-r--r--spec/frontend/ml/model_registry/apps/show_ml_model_version_spec.js16
-rw-r--r--spec/frontend/ml/model_registry/components/model_version_detail_spec.js50
-rw-r--r--spec/frontend/ml/model_registry/mock_data.js10
-rw-r--r--spec/frontend/nav/components/responsive_app_spec.js122
-rw-r--r--spec/frontend/nav/components/responsive_header_spec.js63
-rw-r--r--spec/frontend/nav/components/responsive_home_spec.js133
-rw-r--r--spec/frontend/nav/components/top_nav_app_spec.js68
-rw-r--r--spec/frontend/nav/components/top_nav_container_view_spec.js120
-rw-r--r--spec/frontend/nav/components/top_nav_dropdown_menu_spec.js146
-rw-r--r--spec/frontend/nav/components/top_nav_menu_item_spec.js145
-rw-r--r--spec/frontend/nav/components/top_nav_menu_sections_spec.js138
-rw-r--r--spec/frontend/nav/components/top_nav_new_dropdown_spec.js142
-rw-r--r--spec/frontend/nav/mock_data.js39
-rw-r--r--spec/frontend/observability/client_spec.js74
-rw-r--r--spec/frontend/profile/edit/components/profile_edit_app_spec.js6
-rw-r--r--spec/frontend/read_more_spec.js8
-rw-r--r--spec/frontend/super_sidebar/components/user_menu_spec.js14
-rw-r--r--spec/frontend/super_sidebar/utils_spec.js26
-rw-r--r--spec/frontend/vue_merge_request_widget/components/merge_checks_spec.js2
-rw-r--r--spec/helpers/stat_anchors_helper_spec.rb4
-rw-r--r--spec/lib/gitlab/database/background_migration/batched_migration_spec.rb14
-rw-r--r--spec/migrations/fix_broken_user_achievements_awarded_spec.rb46
-rw-r--r--spec/migrations/fix_broken_user_achievements_revoked_spec.rb44
-rw-r--r--spec/models/members/project_member_spec.rb25
-rw-r--r--spec/presenters/project_presenter_spec.rb5
-rw-r--r--spec/services/users/migrate_records_to_ghost_user_service_spec.rb17
-rw-r--r--spec/support/helpers/features/top_nav_spec_helpers.rb33
-rw-r--r--spec/views/projects/_files.html.haml_spec.rb6
-rw-r--r--spec/views/projects/_home_panel.html.haml_spec.rb2
43 files changed, 425 insertions, 1178 deletions
diff --git a/spec/components/projects/ml/show_ml_model_component_spec.rb b/spec/components/projects/ml/show_ml_model_component_spec.rb
index ec125851d3d..02fad55e0be 100644
--- a/spec/components/projects/ml/show_ml_model_component_spec.rb
+++ b/spec/components/projects/ml/show_ml_model_component_spec.rb
@@ -25,7 +25,10 @@ RSpec.describe Projects::Ml::ShowMlModelComponent, type: :component, feature_cat
'path' => "/#{project.full_path}/-/ml/models/#{model1.id}",
'description' => 'This is a placeholder for the short description',
'latestVersion' => {
- 'version' => model1.latest_version.version
+ 'version' => model1.latest_version.version,
+ 'description' => model1.latest_version.description,
+ 'projectPath' => "/#{project.full_path}",
+ 'packageId' => model1.latest_version.package_id
},
'versionCount' => 1
}
diff --git a/spec/components/projects/ml/show_ml_model_version_component_spec.rb b/spec/components/projects/ml/show_ml_model_version_component_spec.rb
index 973d8123c45..a7dad5e4b2b 100644
--- a/spec/components/projects/ml/show_ml_model_version_component_spec.rb
+++ b/spec/components/projects/ml/show_ml_model_version_component_spec.rb
@@ -5,7 +5,7 @@ require "spec_helper"
RSpec.describe Projects::Ml::ShowMlModelVersionComponent, type: :component, feature_category: :mlops do
let_it_be(:project) { build_stubbed(:project) }
let_it_be(:model) { build_stubbed(:ml_models, project: project) }
- let_it_be(:version) { build_stubbed(:ml_model_versions, model: model) }
+ let_it_be(:version) { build_stubbed(:ml_model_versions, :with_package, model: model, description: 'abc') }
subject(:component) do
described_class.new(model_version: version)
@@ -23,7 +23,10 @@ RSpec.describe Projects::Ml::ShowMlModelVersionComponent, type: :component, feat
'modelVersion' => {
'id' => version.id,
'version' => version.version,
+ 'description' => 'abc',
+ 'projectPath' => "/#{project.full_path}",
'path' => "/#{project.full_path}/-/ml/models/#{model.id}/versions/#{version.id}",
+ 'packageId' => version.package_id,
'model' => {
'name' => model.name,
'path' => "/#{project.full_path}/-/ml/models/#{model.id}"
diff --git a/spec/factories/ml/model_versions.rb b/spec/factories/ml/model_versions.rb
index 456d1b1e913..a097640b134 100644
--- a/spec/factories/ml/model_versions.rb
+++ b/spec/factories/ml/model_versions.rb
@@ -6,6 +6,7 @@ FactoryBot.define do
model { association :ml_models }
project { model.project }
+ description { 'Some description' }
trait :with_package do
package do
diff --git a/spec/features/frequently_visited_projects_and_groups_spec.rb b/spec/features/frequently_visited_projects_and_groups_spec.rb
index 514b642a2d4..764e88882a8 100644
--- a/spec/features/frequently_visited_projects_and_groups_spec.rb
+++ b/spec/features/frequently_visited_projects_and_groups_spec.rb
@@ -3,8 +3,6 @@
require 'spec_helper'
RSpec.describe 'Frequently visited items', :js, feature_category: :shared do
- include Features::TopNavSpecHelpers
-
let_it_be(:user) { create(:user) }
before do
diff --git a/spec/features/projects/new_project_spec.rb b/spec/features/projects/new_project_spec.rb
index a3cbb86da2c..1c25fce5270 100644
--- a/spec/features/projects/new_project_spec.rb
+++ b/spec/features/projects/new_project_spec.rb
@@ -3,8 +3,6 @@
require 'spec_helper'
RSpec.describe 'New project', :js, feature_category: :groups_and_projects do
- include Features::TopNavSpecHelpers
-
before do
stub_application_setting(import_sources: Gitlab::ImportSources.values)
end
diff --git a/spec/features/projects/show/user_sees_git_instructions_spec.rb b/spec/features/projects/show/user_sees_git_instructions_spec.rb
index 5e6857843a6..4933b3f239c 100644
--- a/spec/features/projects/show/user_sees_git_instructions_spec.rb
+++ b/spec/features/projects/show/user_sees_git_instructions_spec.rb
@@ -10,6 +10,7 @@ RSpec.describe 'Projects > Show > User sees Git instructions', feature_category:
# validation failure on NotificationSetting.
# See https://gitlab.com/gitlab-org/gitlab/-/issues/299822#note_492817174
user.notification_settings.reset
+ stub_feature_flags(project_overview_reorg: false)
end
shared_examples_for 'redirects to the sign in page' do
diff --git a/spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb b/spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb
index 41eab966895..674c7db83f1 100644
--- a/spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb
+++ b/spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb
@@ -17,8 +17,8 @@ RSpec.describe 'Projects > Show > User sees setup shortcut buttons', feature_cat
describe 'as a normal user' do
before do
+ stub_feature_flags(project_overview_reorg: false)
sign_in(user)
-
visit project_path(project)
end
@@ -40,6 +40,7 @@ RSpec.describe 'Projects > Show > User sees setup shortcut buttons', feature_cat
describe 'as a maintainer' do
before do
+ stub_feature_flags(project_overview_reorg: false)
project.add_maintainer(user)
sign_in(user)
diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb
index c6966e47f0a..77ba5c53a35 100644
--- a/spec/features/projects_spec.rb
+++ b/spec/features/projects_spec.rb
@@ -12,6 +12,7 @@ RSpec.describe 'Project', feature_category: :groups_and_projects do
before do
sign_in user
visit new_project_path
+ stub_feature_flags(project_overview_reorg: false)
end
shared_examples 'creates from template' do |template, sub_template_tab = nil|
@@ -99,6 +100,7 @@ RSpec.describe 'Project', feature_category: :groups_and_projects do
before do
sign_in(project.first_owner)
+ stub_feature_flags(project_overview_reorg: false)
end
it 'parses Markdown' do
@@ -164,6 +166,7 @@ RSpec.describe 'Project', feature_category: :groups_and_projects do
before do
sign_in(project.first_owner)
visit path
+ stub_feature_flags(project_overview_reorg: false)
end
it 'shows project topics' do
@@ -195,6 +198,7 @@ RSpec.describe 'Project', feature_category: :groups_and_projects do
before do
sign_in(project.first_owner)
visit path
+ stub_feature_flags(project_overview_reorg: false)
end
context 'desktop component' do
@@ -427,6 +431,10 @@ RSpec.describe 'Project', feature_category: :groups_and_projects do
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
+ before do
+ stub_feature_flags(project_overview_reorg: false)
+ end
+
it 'does not contain default branch information in its content', :js do
default_branch = 'merge-commit-analyze-side-branch'
diff --git a/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb b/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb
index 83eb7cb989e..5d121d9eeba 100644
--- a/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb
+++ b/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb
@@ -18,8 +18,10 @@ RSpec.describe 'User uploads avatar to profile', feature_category: :user_profile
wait_for_all_requests
- data_uri = find('.avatar-image .gl-avatar')['src']
- within_testid('user-dropdown') { expect(find('.gl-avatar')['src']).to eq data_uri }
+ within_testid('user-dropdown') do
+ # We are setting a blob URL
+ expect(find('.gl-avatar')['src']).to start_with 'blob:'
+ end
visit profile_path
diff --git a/spec/frontend/environments/kubernetes_overview_spec.js b/spec/frontend/environments/kubernetes_overview_spec.js
index 12689df586f..9f4a7518c47 100644
--- a/spec/frontend/environments/kubernetes_overview_spec.js
+++ b/spec/frontend/environments/kubernetes_overview_spec.js
@@ -149,14 +149,14 @@ describe('~/environments/components/kubernetes_overview.vue', () => {
});
it('sets `clusterHealthStatus` as error when pods emitted a failure', async () => {
- findKubernetesPods().vm.$emit('failed');
+ findKubernetesPods().vm.$emit('update-failed-state', { pods: true });
await nextTick();
expect(findKubernetesStatusBar().props('clusterHealthStatus')).toBe('error');
});
it('sets `clusterHealthStatus` as error when workload types emitted a failure', async () => {
- findKubernetesTabs().vm.$emit('failed');
+ findKubernetesTabs().vm.$emit('update-failed-state', { summary: true });
await nextTick();
expect(findKubernetesStatusBar().props('clusterHealthStatus')).toBe('error');
@@ -165,6 +165,21 @@ describe('~/environments/components/kubernetes_overview.vue', () => {
it('sets `clusterHealthStatus` as success when data is loaded and no failures where emitted', () => {
expect(findKubernetesStatusBar().props('clusterHealthStatus')).toBe('success');
});
+
+ it('sets `clusterHealthStatus` as success after state update if there are no failures', async () => {
+ findKubernetesTabs().vm.$emit('update-failed-state', { summary: true });
+ findKubernetesTabs().vm.$emit('update-failed-state', { pods: true });
+ await nextTick();
+ expect(findKubernetesStatusBar().props('clusterHealthStatus')).toBe('error');
+
+ findKubernetesTabs().vm.$emit('update-failed-state', { summary: false });
+ await nextTick();
+ expect(findKubernetesStatusBar().props('clusterHealthStatus')).toBe('error');
+
+ findKubernetesTabs().vm.$emit('update-failed-state', { pods: false });
+ await nextTick();
+ expect(findKubernetesStatusBar().props('clusterHealthStatus')).toBe('success');
+ });
});
describe('on cluster error', () => {
diff --git a/spec/frontend/environments/kubernetes_pods_spec.js b/spec/frontend/environments/kubernetes_pods_spec.js
index 2952e0c68c2..6c3e49e4d8a 100644
--- a/spec/frontend/environments/kubernetes_pods_spec.js
+++ b/spec/frontend/environments/kubernetes_pods_spec.js
@@ -90,11 +90,17 @@ describe('~/environments/components/kubernetes_pods.vue', () => {
]);
});
- it('emits a failed event when there are failed pods', async () => {
+ it('emits a update-failed-state event for each pod', async () => {
createWrapper();
await waitForPromises();
- expect(wrapper.emitted('failed')).toHaveLength(1);
+ expect(wrapper.emitted('update-failed-state')).toHaveLength(4);
+ expect(wrapper.emitted('update-failed-state')).toEqual([
+ [{ pods: false }],
+ [{ pods: false }],
+ [{ pods: false }],
+ [{ pods: true }],
+ ]);
});
});
diff --git a/spec/frontend/environments/kubernetes_summary_spec.js b/spec/frontend/environments/kubernetes_summary_spec.js
index efabd766001..0d448d0b6af 100644
--- a/spec/frontend/environments/kubernetes_summary_spec.js
+++ b/spec/frontend/environments/kubernetes_summary_spec.js
@@ -107,8 +107,8 @@ describe('~/environments/components/kubernetes_summary.vue', () => {
);
});
- it('emits a failed event when there are failed workload types', () => {
- expect(wrapper.emitted('failed')).toHaveLength(1);
+ it('emits a update-failed-state event when there are failed workload types', () => {
+ expect(wrapper.emitted('update-failed-state')).toEqual([[{ summary: true }]]);
});
it('emits an error message when gets an error from the cluster_client API', async () => {
diff --git a/spec/frontend/environments/kubernetes_tabs_spec.js b/spec/frontend/environments/kubernetes_tabs_spec.js
index fecd6d2a8ee..bf029ad6a81 100644
--- a/spec/frontend/environments/kubernetes_tabs_spec.js
+++ b/spec/frontend/environments/kubernetes_tabs_spec.js
@@ -179,9 +179,10 @@ describe('~/environments/components/kubernetes_tabs.vue', () => {
expect(wrapper.emitted('loading')[1]).toEqual([false]);
});
- it('emits a failed event when gets it from the component', () => {
- findKubernetesSummary().vm.$emit('failed');
- expect(wrapper.emitted('failed')).toHaveLength(1);
+ it('emits a state update event when gets it from the component', () => {
+ const eventData = { summary: true };
+ findKubernetesSummary().vm.$emit('update-failed-state', eventData);
+ expect(wrapper.emitted('update-failed-state')).toEqual([[eventData]]);
});
});
});
diff --git a/spec/frontend/ml/model_registry/apps/show_ml_model_spec.js b/spec/frontend/ml/model_registry/apps/show_ml_model_spec.js
index 3bdbb5e39ae..d58fc70af64 100644
--- a/spec/frontend/ml/model_registry/apps/show_ml_model_spec.js
+++ b/spec/frontend/ml/model_registry/apps/show_ml_model_spec.js
@@ -2,6 +2,7 @@ import { GlBadge, GlTab } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { ShowMlModel } from '~/ml/model_registry/apps';
import ModelVersionList from '~/ml/model_registry/components/model_version_list.vue';
+import ModelVersionDetail from '~/ml/model_registry/components/model_version_detail.vue';
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue';
import { NO_VERSIONS_LABEL } from '~/ml/model_registry/translations';
@@ -16,6 +17,7 @@ const findDetailTab = () => wrapper.findAllComponents(GlTab).at(0);
const findVersionsTab = () => wrapper.findAllComponents(GlTab).at(1);
const findVersionsCountBadge = () => findVersionsTab().findComponent(GlBadge);
const findModelVersionList = () => findVersionsTab().findComponent(ModelVersionList);
+const findModelVersionDetail = () => findDetailTab().findComponent(ModelVersionDetail);
const findCandidateTab = () => wrapper.findAllComponents(GlTab).at(2);
const findCandidatesCountBadge = () => findCandidateTab().findComponent(GlBadge);
const findTitleArea = () => wrapper.findComponent(TitleArea);
@@ -47,7 +49,11 @@ describe('ShowMlModel', () => {
describe('when it has latest version', () => {
it('displays the version', () => {
- expect(findDetailTab().text()).toContain(MODEL.latestVersion.version);
+ expect(findModelVersionDetail().props('modelVersion')).toBe(MODEL.latestVersion);
+ });
+
+ it('displays the title', () => {
+ expect(findDetailTab().text()).toContain('Latest version: 1.2.3');
});
});
@@ -59,6 +65,10 @@ describe('ShowMlModel', () => {
it('shows no version message', () => {
expect(findDetailTab().text()).toContain(NO_VERSIONS_LABEL);
});
+
+ it('does not render model version detail', () => {
+ expect(findModelVersionDetail().exists()).toBe(false);
+ });
});
});
diff --git a/spec/frontend/ml/model_registry/apps/show_ml_model_version_spec.js b/spec/frontend/ml/model_registry/apps/show_ml_model_version_spec.js
index 77fca53c00e..2605a75d961 100644
--- a/spec/frontend/ml/model_registry/apps/show_ml_model_version_spec.js
+++ b/spec/frontend/ml/model_registry/apps/show_ml_model_version_spec.js
@@ -1,5 +1,7 @@
import { shallowMount } from '@vue/test-utils';
import { ShowMlModelVersion } from '~/ml/model_registry/apps';
+import ModelVersionDetail from '~/ml/model_registry/components/model_version_detail.vue';
+import TitleArea from '~/vue_shared/components/registry/title_area.vue';
import { MODEL_VERSION } from '../mock_data';
let wrapper;
@@ -7,9 +9,17 @@ const createWrapper = () => {
wrapper = shallowMount(ShowMlModelVersion, { propsData: { modelVersion: MODEL_VERSION } });
};
-describe('ShowMlModelVersion', () => {
+const findTitleArea = () => wrapper.findComponent(TitleArea);
+const findModelVersionDetail = () => wrapper.findComponent(ModelVersionDetail);
+
+describe('ml/model_registry/apps/show_model_version.vue', () => {
beforeEach(() => createWrapper());
- it('renders the app', () => {
- expect(wrapper.text()).toContain(`${MODEL_VERSION.model.name} - ${MODEL_VERSION.version}`);
+
+ it('renders the title', () => {
+ expect(findTitleArea().props('title')).toBe('blah / 1.2.3');
+ });
+
+ it('renders the model version detail', () => {
+ expect(findModelVersionDetail().props('modelVersion')).toBe(MODEL_VERSION);
});
});
diff --git a/spec/frontend/ml/model_registry/components/model_version_detail_spec.js b/spec/frontend/ml/model_registry/components/model_version_detail_spec.js
new file mode 100644
index 00000000000..aeb9d13ad97
--- /dev/null
+++ b/spec/frontend/ml/model_registry/components/model_version_detail_spec.js
@@ -0,0 +1,50 @@
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import ModelVersionDetail from '~/ml/model_registry/components/model_version_detail.vue';
+import PackageFiles from '~/packages_and_registries/package_registry/components/details/package_files.vue';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { makeModelVersion, MODEL_VERSION } from '../mock_data';
+
+Vue.use(VueApollo);
+
+let wrapper;
+const createWrapper = (modelVersion = MODEL_VERSION) => {
+ const apolloProvider = createMockApollo([]);
+ wrapper = shallowMount(ModelVersionDetail, { apolloProvider, propsData: { modelVersion } });
+};
+
+const findPackageFiles = () => wrapper.findComponent(PackageFiles);
+
+describe('ml/model_registry/components/model_version_detail.vue', () => {
+ describe('description', () => {
+ beforeEach(() => createWrapper());
+
+ it('shows the description', () => {
+ expect(wrapper.text()).toContain(MODEL_VERSION.description);
+ });
+ });
+
+ describe('package files', () => {
+ describe('if package exists', () => {
+ beforeEach(() => createWrapper());
+
+ it('renders files', () => {
+ expect(findPackageFiles().props()).toEqual({
+ packageId: 'gid://gitlab/Packages::Package/12',
+ projectPath: MODEL_VERSION.projectPath,
+ packageType: 'ml_model',
+ canDelete: false,
+ });
+ });
+ });
+
+ describe('if package does not exist', () => {
+ beforeEach(() => createWrapper(makeModelVersion({ packageId: 0 })));
+
+ it('does not render files', () => {
+ expect(findPackageFiles().exists()).toBe(false);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ml/model_registry/mock_data.js b/spec/frontend/ml/model_registry/mock_data.js
index cbf915cb5ed..7963e8aaa7b 100644
--- a/spec/frontend/ml/model_registry/mock_data.js
+++ b/spec/frontend/ml/model_registry/mock_data.js
@@ -14,7 +14,15 @@ export const makeModel = ({ latestVersion } = { latestVersion: LATEST_VERSION })
export const MODEL = makeModel();
-export const MODEL_VERSION = { version: '1.2.3', model: MODEL };
+export const makeModelVersion = ({ version = '1.2.3', model = MODEL, packageId = 12 } = {}) => ({
+ version,
+ model,
+ packageId,
+ description: 'Model version description',
+ projectPath: 'path/to/project',
+});
+
+export const MODEL_VERSION = makeModelVersion();
export const mockModels = [
{
diff --git a/spec/frontend/nav/components/responsive_app_spec.js b/spec/frontend/nav/components/responsive_app_spec.js
deleted file mode 100644
index 9d3b43520ec..00000000000
--- a/spec/frontend/nav/components/responsive_app_spec.js
+++ /dev/null
@@ -1,122 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
-import ResponsiveApp from '~/nav/components/responsive_app.vue';
-import ResponsiveHeader from '~/nav/components/responsive_header.vue';
-import ResponsiveHome from '~/nav/components/responsive_home.vue';
-import TopNavContainerView from '~/nav/components/top_nav_container_view.vue';
-import { resetMenuItemsActive } from '~/nav/utils/reset_menu_items_active';
-import KeepAliveSlots from '~/vue_shared/components/keep_alive_slots.vue';
-import { TEST_NAV_DATA } from '../mock_data';
-
-describe('~/nav/components/responsive_app.vue', () => {
- let wrapper;
-
- const createComponent = () => {
- wrapper = shallowMount(ResponsiveApp, {
- propsData: {
- navData: TEST_NAV_DATA,
- },
- stubs: {
- KeepAliveSlots,
- },
- });
- };
- const findHome = () => wrapper.findComponent(ResponsiveHome);
- const findMobileOverlay = () => wrapper.find('[data-testid="mobile-overlay"]');
- const findSubviewHeader = () => wrapper.findComponent(ResponsiveHeader);
- const findSubviewContainer = () => wrapper.findComponent(TopNavContainerView);
- const hasMobileOverlayVisible = () => findMobileOverlay().classes('mobile-nav-open');
-
- beforeEach(() => {
- document.body.innerHTML = '';
- // Add test class to reset state + assert that we're adding classes correctly
- document.body.className = 'test-class';
- });
-
- describe('default', () => {
- beforeEach(() => {
- createComponent();
- });
-
- it('shows home by default', () => {
- expect(findHome().isVisible()).toBe(true);
- expect(findHome().props()).toEqual({
- navData: resetMenuItemsActive(TEST_NAV_DATA),
- });
- });
-
- it.each`
- events | expectation
- ${[]} | ${false}
- ${['bv::dropdown::show']} | ${true}
- ${['bv::dropdown::show', 'bv::dropdown::hide']} | ${false}
- `(
- 'with root events $events, movile overlay visible = $expectation',
- async ({ events, expectation }) => {
- // `await...reduce(async` is like doing an `forEach(async (...))` excpet it works
- await events.reduce(async (acc, evt) => {
- await acc;
-
- wrapper.vm.$root.$emit(evt);
-
- await nextTick();
- }, Promise.resolve());
-
- expect(hasMobileOverlayVisible()).toBe(expectation);
- },
- );
- });
-
- const projectsContainerProps = {
- containerClass: 'gl-px-3',
- frequentItemsDropdownType: ResponsiveApp.FREQUENT_ITEMS_PROJECTS.namespace,
- frequentItemsVuexModule: ResponsiveApp.FREQUENT_ITEMS_PROJECTS.vuexModule,
- currentItem: {},
- linksPrimary: TEST_NAV_DATA.views.projects.linksPrimary,
- linksSecondary: TEST_NAV_DATA.views.projects.linksSecondary,
- };
- const groupsContainerProps = {
- containerClass: 'gl-px-3',
- frequentItemsDropdownType: ResponsiveApp.FREQUENT_ITEMS_GROUPS.namespace,
- frequentItemsVuexModule: ResponsiveApp.FREQUENT_ITEMS_GROUPS.vuexModule,
- currentItem: {},
- linksPrimary: TEST_NAV_DATA.views.groups.linksPrimary,
- linksSecondary: TEST_NAV_DATA.views.groups.linksSecondary,
- };
-
- describe.each`
- view | header | containerProps
- ${'projects'} | ${'Projects'} | ${projectsContainerProps}
- ${'groups'} | ${'Groups'} | ${groupsContainerProps}
- `('when menu item with $view is clicked', ({ view, header, containerProps }) => {
- beforeEach(async () => {
- createComponent();
-
- findHome().vm.$emit('menu-item-click', { view });
-
- await nextTick();
- });
-
- it('shows header', () => {
- expect(findSubviewHeader().text()).toBe(header);
- });
-
- it('shows container subview', () => {
- expect(findSubviewContainer().props()).toEqual(containerProps);
- });
-
- it('hides home', () => {
- expect(findHome().isVisible()).toBe(false);
- });
-
- describe('when header back button is clicked', () => {
- beforeEach(() => {
- findSubviewHeader().vm.$emit('menu-item-click', { view: 'home' });
- });
-
- it('shows home', () => {
- expect(findHome().isVisible()).toBe(true);
- });
- });
- });
-});
diff --git a/spec/frontend/nav/components/responsive_header_spec.js b/spec/frontend/nav/components/responsive_header_spec.js
deleted file mode 100644
index 2514035270a..00000000000
--- a/spec/frontend/nav/components/responsive_header_spec.js
+++ /dev/null
@@ -1,63 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
-import ResponsiveHeader from '~/nav/components/responsive_header.vue';
-import TopNavMenuItem from '~/nav/components/top_nav_menu_item.vue';
-
-const TEST_SLOT_CONTENT = 'Test slot content';
-
-describe('~/nav/components/top_nav_menu_sections.vue', () => {
- let wrapper;
-
- const createComponent = () => {
- wrapper = shallowMount(ResponsiveHeader, {
- slots: {
- default: TEST_SLOT_CONTENT,
- },
- directives: {
- GlTooltip: createMockDirective('gl-tooltip'),
- },
- });
- };
-
- const findMenuItem = () => wrapper.findComponent(TopNavMenuItem);
-
- beforeEach(() => {
- createComponent();
- });
-
- it('renders slot', () => {
- expect(wrapper.text()).toBe(TEST_SLOT_CONTENT);
- });
-
- it('renders back button', () => {
- const button = findMenuItem();
-
- const tooltip = getBinding(button.element, 'gl-tooltip').value.title;
-
- expect(tooltip).toBe('Go back');
- expect(button.props()).toEqual({
- menuItem: {
- id: 'home',
- view: 'home',
- icon: 'chevron-lg-left',
- },
- iconOnly: true,
- });
- });
-
- it('emits nothing', () => {
- expect(wrapper.emitted()).toEqual({});
- });
-
- describe('when back button is clicked', () => {
- beforeEach(() => {
- findMenuItem().vm.$emit('click');
- });
-
- it('emits menu-item-click', () => {
- expect(wrapper.emitted()).toEqual({
- 'menu-item-click': [[{ id: 'home', view: 'home', icon: 'chevron-lg-left' }]],
- });
- });
- });
-});
diff --git a/spec/frontend/nav/components/responsive_home_spec.js b/spec/frontend/nav/components/responsive_home_spec.js
deleted file mode 100644
index 5a5cfc93607..00000000000
--- a/spec/frontend/nav/components/responsive_home_spec.js
+++ /dev/null
@@ -1,133 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
-import ResponsiveHome from '~/nav/components/responsive_home.vue';
-import TopNavMenuItem from '~/nav/components/top_nav_menu_item.vue';
-import TopNavMenuSections from '~/nav/components/top_nav_menu_sections.vue';
-import TopNavNewDropdown from '~/nav/components/top_nav_new_dropdown.vue';
-import { TEST_NAV_DATA } from '../mock_data';
-
-const TEST_SEARCH_MENU_ITEM = {
- id: 'search',
- title: 'search',
- icon: 'search',
- href: '/search',
-};
-
-const TEST_NEW_DROPDOWN_VIEW_MODEL = {
- title: 'new',
- menu_sections: [],
-};
-
-describe('~/nav/components/responsive_home.vue', () => {
- let wrapper;
- let menuItemClickListener;
-
- const createComponent = (props = {}) => {
- wrapper = shallowMount(ResponsiveHome, {
- propsData: {
- navData: TEST_NAV_DATA,
- ...props,
- },
- directives: {
- GlTooltip: createMockDirective('gl-tooltip'),
- },
- listeners: {
- 'menu-item-click': menuItemClickListener,
- },
- });
- };
-
- const findSearchMenuItem = () => wrapper.findComponent(TopNavMenuItem);
- const findNewDropdown = () => wrapper.findComponent(TopNavNewDropdown);
- const findMenuSections = () => wrapper.findComponent(TopNavMenuSections);
-
- beforeEach(() => {
- menuItemClickListener = jest.fn();
- });
-
- describe('default', () => {
- beforeEach(() => {
- createComponent();
- });
-
- it.each`
- desc | fn
- ${'does not show search menu item'} | ${findSearchMenuItem}
- ${'does not show new dropdown'} | ${findNewDropdown}
- `('$desc', ({ fn }) => {
- expect(fn().exists()).toBe(false);
- });
-
- it('shows menu sections', () => {
- expect(findMenuSections().props('sections')).toEqual([
- { id: 'primary', menuItems: TEST_NAV_DATA.primary },
- { id: 'secondary', menuItems: TEST_NAV_DATA.secondary },
- ]);
- });
-
- it('emits when menu sections emits', () => {
- expect(menuItemClickListener).not.toHaveBeenCalled();
-
- findMenuSections().vm.$emit('menu-item-click', TEST_NAV_DATA.primary[0]);
-
- expect(menuItemClickListener).toHaveBeenCalledWith(TEST_NAV_DATA.primary[0]);
- });
- });
-
- describe('without secondary', () => {
- beforeEach(() => {
- createComponent({ navData: { ...TEST_NAV_DATA, secondary: null } });
- });
-
- it('shows menu sections', () => {
- expect(findMenuSections().props('sections')).toEqual([
- { id: 'primary', menuItems: TEST_NAV_DATA.primary },
- ]);
- });
- });
-
- describe('with search view', () => {
- beforeEach(() => {
- createComponent({
- navData: {
- ...TEST_NAV_DATA,
- views: { search: TEST_SEARCH_MENU_ITEM },
- },
- });
- });
-
- it('shows search menu item', () => {
- expect(findSearchMenuItem().props()).toEqual({
- menuItem: TEST_SEARCH_MENU_ITEM,
- iconOnly: true,
- });
- });
-
- it('shows tooltip for search', () => {
- const tooltip = getBinding(findSearchMenuItem().element, 'gl-tooltip');
- expect(tooltip.value).toEqual({ title: TEST_SEARCH_MENU_ITEM.title });
- });
- });
-
- describe('with new view', () => {
- beforeEach(() => {
- createComponent({
- navData: {
- ...TEST_NAV_DATA,
- views: { new: TEST_NEW_DROPDOWN_VIEW_MODEL },
- },
- });
- });
-
- it('shows new dropdown', () => {
- expect(findNewDropdown().props()).toEqual({
- viewModel: TEST_NEW_DROPDOWN_VIEW_MODEL,
- });
- });
-
- it('shows tooltip for new dropdown', () => {
- const tooltip = getBinding(findNewDropdown().element, 'gl-tooltip');
- expect(tooltip.value).toEqual({ title: TEST_NEW_DROPDOWN_VIEW_MODEL.title });
- });
- });
-});
diff --git a/spec/frontend/nav/components/top_nav_app_spec.js b/spec/frontend/nav/components/top_nav_app_spec.js
deleted file mode 100644
index 7f39552eb42..00000000000
--- a/spec/frontend/nav/components/top_nav_app_spec.js
+++ /dev/null
@@ -1,68 +0,0 @@
-import { GlNavItemDropdown } from '@gitlab/ui';
-import { mount, shallowMount } from '@vue/test-utils';
-import { mockTracking } from 'helpers/tracking_helper';
-import TopNavApp from '~/nav/components/top_nav_app.vue';
-import TopNavDropdownMenu from '~/nav/components/top_nav_dropdown_menu.vue';
-import { TEST_NAV_DATA } from '../mock_data';
-
-describe('~/nav/components/top_nav_app.vue', () => {
- let wrapper;
-
- const createComponent = () => {
- wrapper = mount(TopNavApp, {
- propsData: {
- navData: TEST_NAV_DATA,
- },
- });
- };
-
- const createComponentShallow = () => {
- wrapper = shallowMount(TopNavApp, {
- propsData: {
- navData: TEST_NAV_DATA,
- },
- });
- };
-
- const findNavItemDropdown = () => wrapper.findComponent(GlNavItemDropdown);
- const findNavItemDropdowToggle = () => findNavItemDropdown().find('.js-top-nav-dropdown-toggle');
- const findMenu = () => wrapper.findComponent(TopNavDropdownMenu);
-
- describe('default', () => {
- beforeEach(() => {
- createComponentShallow();
- });
-
- it('renders nav item dropdown', () => {
- expect(findNavItemDropdown().attributes('href')).toBeUndefined();
- expect(findNavItemDropdown().attributes()).toMatchObject({
- icon: '',
- text: '',
- 'no-flip': '',
- 'no-caret': '',
- });
- });
-
- it('renders top nav dropdown menu', () => {
- expect(findMenu().props()).toStrictEqual({
- primary: TEST_NAV_DATA.primary,
- secondary: TEST_NAV_DATA.secondary,
- views: TEST_NAV_DATA.views,
- });
- });
- });
-
- describe('tracking', () => {
- it('emits a tracking event when the toggle is clicked', () => {
- const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
- createComponent();
-
- findNavItemDropdowToggle().trigger('click');
-
- expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_nav', {
- label: 'hamburger_menu',
- property: 'navigation_top',
- });
- });
- });
-});
diff --git a/spec/frontend/nav/components/top_nav_container_view_spec.js b/spec/frontend/nav/components/top_nav_container_view_spec.js
deleted file mode 100644
index 388ac243648..00000000000
--- a/spec/frontend/nav/components/top_nav_container_view_spec.js
+++ /dev/null
@@ -1,120 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import { merge } from 'lodash';
-import { nextTick } from 'vue';
-import FrequentItemsApp from '~/frequent_items/components/app.vue';
-import { FREQUENT_ITEMS_PROJECTS } from '~/frequent_items/constants';
-import eventHub from '~/frequent_items/event_hub';
-import TopNavContainerView from '~/nav/components/top_nav_container_view.vue';
-import TopNavMenuSections from '~/nav/components/top_nav_menu_sections.vue';
-import VuexModuleProvider from '~/vue_shared/components/vuex_module_provider.vue';
-import { TEST_NAV_DATA } from '../mock_data';
-
-const DEFAULT_PROPS = {
- frequentItemsDropdownType: FREQUENT_ITEMS_PROJECTS.namespace,
- frequentItemsVuexModule: FREQUENT_ITEMS_PROJECTS.vuexModule,
- linksPrimary: TEST_NAV_DATA.primary,
- linksSecondary: TEST_NAV_DATA.secondary,
- containerClass: 'test-frequent-items-container-class',
-};
-const TEST_OTHER_PROPS = {
- namespace: 'projects',
- currentUserName: 'test-user',
- currentItem: { id: 'test' },
-};
-
-describe('~/nav/components/top_nav_container_view.vue', () => {
- let wrapper;
-
- const createComponent = (props = {}, options = {}) => {
- wrapper = shallowMount(TopNavContainerView, {
- propsData: {
- ...DEFAULT_PROPS,
- ...TEST_OTHER_PROPS,
- ...props,
- },
- ...options,
- });
- };
-
- const findMenuSections = () => wrapper.findComponent(TopNavMenuSections);
- const findFrequentItemsApp = () => {
- const parent = wrapper.findComponent(VuexModuleProvider);
-
- return {
- vuexModule: parent.props('vuexModule'),
- props: parent.findComponent(FrequentItemsApp).props(),
- attributes: parent.findComponent(FrequentItemsApp).attributes(),
- };
- };
- const findFrequentItemsContainer = () => wrapper.find('[data-testid="frequent-items-container"]');
-
- it.each(['projects', 'groups'])(
- 'emits frequent items event to event hub (%s)',
- async (frequentItemsDropdownType) => {
- const listener = jest.fn();
- eventHub.$on(`${frequentItemsDropdownType}-dropdownOpen`, listener);
- createComponent({ frequentItemsDropdownType });
-
- expect(listener).not.toHaveBeenCalled();
-
- await nextTick();
-
- expect(listener).toHaveBeenCalled();
- },
- );
-
- describe('default', () => {
- const EXTRA_ATTRS = { 'data-test-attribute': 'foo' };
-
- beforeEach(() => {
- createComponent({}, { attrs: EXTRA_ATTRS });
- });
-
- it('does not inherit extra attrs', () => {
- expect(wrapper.attributes()).toEqual({
- class: expect.any(String),
- });
- });
-
- it('renders frequent items app', () => {
- expect(findFrequentItemsApp()).toEqual({
- vuexModule: DEFAULT_PROPS.frequentItemsVuexModule,
- props: expect.objectContaining(
- merge({ currentItem: { lastAccessedOn: Date.now() } }, TEST_OTHER_PROPS),
- ),
- attributes: expect.objectContaining(EXTRA_ATTRS),
- });
- });
-
- it('renders given container class', () => {
- expect(findFrequentItemsContainer().classes(DEFAULT_PROPS.containerClass)).toBe(true);
- });
-
- it('renders menu sections', () => {
- const sections = [
- { id: 'primary', menuItems: TEST_NAV_DATA.primary },
- { id: 'secondary', menuItems: TEST_NAV_DATA.secondary },
- ];
-
- expect(findMenuSections().props()).toEqual({
- sections,
- withTopBorder: true,
- isPrimarySection: false,
- });
- });
- });
-
- describe('without secondary links', () => {
- beforeEach(() => {
- createComponent({
- linksSecondary: [],
- });
- });
-
- it('renders one menu item group', () => {
- expect(findMenuSections().props('sections')).toEqual([
- { id: 'primary', menuItems: TEST_NAV_DATA.primary },
- ]);
- });
- });
-});
diff --git a/spec/frontend/nav/components/top_nav_dropdown_menu_spec.js b/spec/frontend/nav/components/top_nav_dropdown_menu_spec.js
deleted file mode 100644
index 1d516240306..00000000000
--- a/spec/frontend/nav/components/top_nav_dropdown_menu_spec.js
+++ /dev/null
@@ -1,146 +0,0 @@
-import { shallowMount, mount } from '@vue/test-utils';
-import { nextTick } from 'vue';
-import TopNavDropdownMenu from '~/nav/components/top_nav_dropdown_menu.vue';
-import TopNavMenuItem from '~/nav/components/top_nav_menu_item.vue';
-import TopNavMenuSections from '~/nav/components/top_nav_menu_sections.vue';
-import KeepAliveSlots from '~/vue_shared/components/keep_alive_slots.vue';
-import { TEST_NAV_DATA } from '../mock_data';
-import { stubComponent } from '../../__helpers__/stub_component';
-
-describe('~/nav/components/top_nav_dropdown_menu.vue', () => {
- let wrapper;
-
- const createComponent = (props = {}, mountFn = shallowMount) => {
- wrapper = mountFn(TopNavDropdownMenu, {
- propsData: {
- primary: TEST_NAV_DATA.primary,
- secondary: TEST_NAV_DATA.secondary,
- views: TEST_NAV_DATA.views,
- ...props,
- },
- stubs: {
- // Stub the keep-alive-slots so we don't render frequent items which uses a store
- KeepAliveSlots: stubComponent(KeepAliveSlots),
- },
- });
- };
-
- const findMenuItems = () => wrapper.findAllComponents(TopNavMenuItem);
- const findMenuSections = () => wrapper.findComponent(TopNavMenuSections);
- const findMenuSidebar = () => wrapper.find('[data-testid="menu-sidebar"]');
- const findMenuSubview = () => wrapper.findComponent(KeepAliveSlots);
- const hasFullWidthMenuSidebar = () => findMenuSidebar().classes('gl-w-full');
-
- const withActiveIndex = (menuItems, activeIndex) =>
- menuItems.map((x, idx) => ({
- ...x,
- active: idx === activeIndex,
- }));
-
- beforeEach(() => {
- jest.spyOn(console, 'error').mockImplementation();
- });
-
- describe('default', () => {
- beforeEach(() => {
- createComponent();
- });
-
- it('renders menu sections', () => {
- expect(findMenuSections().props()).toEqual({
- sections: [
- { id: 'primary', menuItems: TEST_NAV_DATA.primary },
- { id: 'secondary', menuItems: TEST_NAV_DATA.secondary },
- ],
- withTopBorder: false,
- isPrimarySection: true,
- });
- });
-
- it('has full width menu sidebar', () => {
- expect(hasFullWidthMenuSidebar()).toBe(true);
- });
-
- it('renders hidden subview with no slot key', () => {
- const subview = findMenuSubview();
-
- expect(subview.isVisible()).toBe(false);
- expect(subview.props()).toEqual({ slotKey: '' });
- });
- });
-
- describe('with pre-initialized active view', () => {
- beforeEach(() => {
- // We opt for a small integration test, to make sure the event is handled correctly
- // as it would in prod.
- createComponent(
- {
- primary: withActiveIndex(TEST_NAV_DATA.primary, 1),
- },
- mount,
- );
- });
-
- it('renders menu sections', () => {
- expect(findMenuSections().props('sections')).toStrictEqual([
- { id: 'primary', menuItems: withActiveIndex(TEST_NAV_DATA.primary, 1) },
- { id: 'secondary', menuItems: TEST_NAV_DATA.secondary },
- ]);
- });
-
- it('does not have full width menu sidebar', () => {
- expect(hasFullWidthMenuSidebar()).toBe(false);
- });
-
- it('renders visible subview with slot key', () => {
- const subview = findMenuSubview();
-
- expect(subview.isVisible()).toBe(true);
- expect(subview.props('slotKey')).toBe(TEST_NAV_DATA.primary[1].view);
- });
-
- it('does not change view if non-view menu item is clicked', async () => {
- const secondaryLink = findMenuItems().at(TEST_NAV_DATA.primary.length);
-
- // Ensure this doesn't have a view
- expect(secondaryLink.props('menuItem').view).toBeUndefined();
-
- secondaryLink.vm.$emit('click');
-
- await nextTick();
-
- expect(findMenuSubview().props('slotKey')).toBe(TEST_NAV_DATA.primary[1].view);
- });
-
- describe('when menu item is clicked', () => {
- let primaryLink;
-
- beforeEach(async () => {
- primaryLink = findMenuItems().at(0);
- primaryLink.vm.$emit('click');
- await nextTick();
- });
-
- it('clicked on link with view', () => {
- expect(primaryLink.props('menuItem').view).toBe(TEST_NAV_DATA.views.projects.namespace);
- });
-
- it('changes active view', () => {
- expect(findMenuSubview().props('slotKey')).toBe(TEST_NAV_DATA.primary[0].view);
- });
-
- it('changes active status on menu item', () => {
- expect(findMenuSections().props('sections')).toStrictEqual([
- {
- id: 'primary',
- menuItems: withActiveIndex(TEST_NAV_DATA.primary, 0),
- },
- {
- id: 'secondary',
- menuItems: withActiveIndex(TEST_NAV_DATA.secondary, -1),
- },
- ]);
- });
- });
- });
-});
diff --git a/spec/frontend/nav/components/top_nav_menu_item_spec.js b/spec/frontend/nav/components/top_nav_menu_item_spec.js
deleted file mode 100644
index b9cf39b8c1d..00000000000
--- a/spec/frontend/nav/components/top_nav_menu_item_spec.js
+++ /dev/null
@@ -1,145 +0,0 @@
-import { GlButton, GlIcon } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import TopNavMenuItem from '~/nav/components/top_nav_menu_item.vue';
-
-const TEST_MENU_ITEM = {
- title: 'Cheeseburger',
- icon: 'search',
- href: '/pretty/good/burger',
- view: 'burger-view',
- data: { qa_selector: 'not-a-real-selector', method: 'post', testFoo: 'test' },
-};
-
-describe('~/nav/components/top_nav_menu_item.vue', () => {
- let listener;
- let wrapper;
-
- const createComponent = (props = {}) => {
- wrapper = shallowMount(TopNavMenuItem, {
- propsData: {
- menuItem: TEST_MENU_ITEM,
- ...props,
- },
- listeners: {
- click: listener,
- },
- });
- };
-
- const findButton = () => wrapper.findComponent(GlButton);
- const findButtonIcons = () =>
- findButton()
- .findAllComponents(GlIcon)
- .wrappers.map((x) => ({
- name: x.props('name'),
- classes: x.classes(),
- }));
-
- beforeEach(() => {
- listener = jest.fn();
- });
-
- describe('default', () => {
- beforeEach(() => {
- createComponent();
- });
-
- it('renders button href and text', () => {
- const button = findButton();
-
- expect(button.attributes('href')).toBe(TEST_MENU_ITEM.href);
- expect(button.text()).toBe(TEST_MENU_ITEM.title);
- });
-
- it('renders button data attributes', () => {
- const button = findButton();
-
- expect(button.attributes()).toMatchObject({
- 'data-qa-selector': TEST_MENU_ITEM.data.qa_selector,
- 'data-method': TEST_MENU_ITEM.data.method,
- 'data-test-foo': TEST_MENU_ITEM.data.testFoo,
- });
- });
-
- it('passes listeners to button', () => {
- expect(listener).not.toHaveBeenCalled();
-
- findButton().vm.$emit('click', 'TEST');
-
- expect(listener).toHaveBeenCalledWith('TEST');
- });
-
- it('renders expected icons', () => {
- expect(findButtonIcons()).toEqual([
- {
- name: TEST_MENU_ITEM.icon,
- classes: ['gl-mr-3!'],
- },
- {
- name: 'chevron-right',
- classes: ['gl-ml-auto'],
- },
- ]);
- });
- });
-
- describe('with icon-only', () => {
- beforeEach(() => {
- createComponent({ iconOnly: true });
- });
-
- it('does not render title or view icon', () => {
- expect(wrapper.text()).toBe('');
- });
-
- it('only renders menuItem icon', () => {
- expect(findButtonIcons()).toEqual([
- {
- name: TEST_MENU_ITEM.icon,
- classes: [],
- },
- ]);
- });
- });
-
- describe.each`
- desc | menuItem | expectedIcons
- ${'with no icon'} | ${{ ...TEST_MENU_ITEM, icon: null }} | ${['chevron-right']}
- ${'with no view'} | ${{ ...TEST_MENU_ITEM, view: null }} | ${[TEST_MENU_ITEM.icon]}
- ${'with no icon or view'} | ${{ ...TEST_MENU_ITEM, view: null, icon: null }} | ${[]}
- `('$desc', ({ menuItem, expectedIcons }) => {
- beforeEach(() => {
- createComponent({ menuItem });
- });
-
- it(`renders expected icons ${JSON.stringify(expectedIcons)}`, () => {
- expect(findButtonIcons().map((x) => x.name)).toEqual(expectedIcons);
- });
- });
-
- describe.each`
- desc | active | cssClass | expectedClasses
- ${'default'} | ${false} | ${''} | ${[]}
- ${'with css class'} | ${false} | ${'test-css-class testing-123'} | ${['test-css-class', 'testing-123']}
- ${'with css class & active'} | ${true} | ${'test-css-class'} | ${['test-css-class', ...TopNavMenuItem.ACTIVE_CLASS.split(' ')]}
- `('$desc', ({ active, cssClass, expectedClasses }) => {
- beforeEach(() => {
- createComponent({
- menuItem: {
- ...TEST_MENU_ITEM,
- active,
- css_class: cssClass,
- },
- });
- });
-
- it('renders expected classes', () => {
- expect(wrapper.classes()).toStrictEqual([
- 'top-nav-menu-item',
- 'gl-display-block',
- 'gl-pr-3!',
- ...expectedClasses,
- ]);
- });
- });
-});
diff --git a/spec/frontend/nav/components/top_nav_menu_sections_spec.js b/spec/frontend/nav/components/top_nav_menu_sections_spec.js
deleted file mode 100644
index 7a3e58fd964..00000000000
--- a/spec/frontend/nav/components/top_nav_menu_sections_spec.js
+++ /dev/null
@@ -1,138 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import TopNavMenuSections from '~/nav/components/top_nav_menu_sections.vue';
-
-const TEST_SECTIONS = [
- {
- id: 'primary',
- menuItems: [
- { type: 'header', title: 'Heading' },
- { type: 'item', id: 'test', href: '/test/href' },
- { type: 'header', title: 'Another Heading' },
- { type: 'item', id: 'foo' },
- { type: 'item', id: 'bar' },
- ],
- },
- {
- id: 'secondary',
- menuItems: [
- { type: 'item', id: 'lorem' },
- { type: 'item', id: 'ipsum' },
- ],
- },
-];
-
-describe('~/nav/components/top_nav_menu_sections.vue', () => {
- let wrapper;
-
- const createComponent = (props = {}) => {
- wrapper = shallowMount(TopNavMenuSections, {
- propsData: {
- sections: TEST_SECTIONS,
- ...props,
- },
- });
- };
-
- const findMenuItemModels = (parent) =>
- parent.findAll('[data-testid="menu-header"],[data-testid="menu-item"]').wrappers.map((x) => {
- return {
- menuItem: x.vm
- ? {
- type: 'item',
- ...x.props('menuItem'),
- }
- : {
- type: 'header',
- title: x.text(),
- },
- classes: x.classes(),
- };
- });
- const findSectionModels = () =>
- wrapper.findAll('[data-testid="menu-section"]').wrappers.map((x) => ({
- classes: x.classes(),
- menuItems: findMenuItemModels(x),
- }));
-
- describe('default', () => {
- beforeEach(() => {
- createComponent();
- });
-
- it('renders sections with menu items', () => {
- const headerClasses = ['gl-px-4', 'gl-py-2', 'gl-text-gray-900', 'gl-display-block'];
- const itemClasses = ['gl-w-full'];
-
- expect(findSectionModels()).toEqual([
- {
- classes: [],
- menuItems: TEST_SECTIONS[0].menuItems.map((menuItem, index) => {
- const classes = menuItem.type === 'header' ? [...headerClasses] : [...itemClasses];
- if (index > 0) classes.push(menuItem.type === 'header' ? 'gl-pt-3!' : 'gl-mt-1');
- return {
- menuItem,
- classes,
- };
- }),
- },
- {
- classes: [
- ...TopNavMenuSections.BORDER_CLASSES.split(' '),
- 'gl-border-gray-50',
- 'gl-mt-3',
- ],
- menuItems: TEST_SECTIONS[1].menuItems.map((menuItem, index) => {
- const classes = menuItem.type === 'header' ? [...headerClasses] : [...itemClasses];
- if (index > 0) classes.push(menuItem.type === 'header' ? 'gl-pt-3!' : 'gl-mt-1');
- return {
- menuItem,
- classes,
- };
- }),
- },
- ]);
- });
-
- it('when clicked menu item with href, does nothing', () => {
- const menuItem = wrapper.findAll('[data-testid="menu-item"]').at(0);
-
- menuItem.vm.$emit('click');
-
- expect(wrapper.emitted()).toEqual({});
- });
-
- it('when clicked menu item without href, emits "menu-item-click"', () => {
- const menuItem = wrapper.findAll('[data-testid="menu-item"]').at(1);
-
- menuItem.vm.$emit('click');
-
- expect(wrapper.emitted('menu-item-click')).toEqual([[TEST_SECTIONS[0].menuItems[3]]]);
- });
- });
-
- describe('with withTopBorder=true', () => {
- beforeEach(() => {
- createComponent({ withTopBorder: true });
- });
-
- it('renders border classes for top section', () => {
- expect(findSectionModels().map((x) => x.classes)).toEqual([
- [...TopNavMenuSections.BORDER_CLASSES.split(' '), 'gl-border-gray-50'],
- [...TopNavMenuSections.BORDER_CLASSES.split(' '), 'gl-border-gray-50', 'gl-mt-3'],
- ]);
- });
- });
-
- describe('with isPrimarySection=true', () => {
- beforeEach(() => {
- createComponent({ isPrimarySection: true });
- });
-
- it('renders border classes for top section', () => {
- expect(findSectionModels().map((x) => x.classes)).toEqual([
- [],
- [...TopNavMenuSections.BORDER_CLASSES.split(' '), 'gl-border-gray-100', 'gl-mt-3'],
- ]);
- });
- });
-});
diff --git a/spec/frontend/nav/components/top_nav_new_dropdown_spec.js b/spec/frontend/nav/components/top_nav_new_dropdown_spec.js
deleted file mode 100644
index 432ee5e9ecd..00000000000
--- a/spec/frontend/nav/components/top_nav_new_dropdown_spec.js
+++ /dev/null
@@ -1,142 +0,0 @@
-import { GlDropdown } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import TopNavNewDropdown from '~/nav/components/top_nav_new_dropdown.vue';
-import InviteMembersTrigger from '~/invite_members/components/invite_members_trigger.vue';
-import { TOP_NAV_INVITE_MEMBERS_COMPONENT } from '~/invite_members/constants';
-
-const TEST_VIEW_MODEL = {
- title: 'Dropdown',
- menu_sections: [
- {
- title: 'Section 1',
- menu_items: [
- { id: 'foo-1', title: 'Foo 1', href: '/foo/1' },
- { id: 'foo-2', title: 'Foo 2', href: '/foo/2' },
- { id: 'foo-3', title: 'Foo 3', href: '/foo/3' },
- ],
- },
- {
- title: 'Section 2',
- menu_items: [
- { id: 'bar-1', title: 'Bar 1', href: '/bar/1' },
- { id: 'bar-2', title: 'Bar 2', href: '/bar/2' },
- {
- id: 'invite',
- title: '_invite members title_',
- component: TOP_NAV_INVITE_MEMBERS_COMPONENT,
- icon: '_icon_',
- data: {
- trigger_element: '_trigger_element_',
- trigger_source: '_trigger_source_',
- },
- },
- ],
- },
- ],
-};
-
-describe('~/nav/components/top_nav_menu_sections.vue', () => {
- let wrapper;
-
- const createComponent = (props = {}) => {
- wrapper = shallowMount(TopNavNewDropdown, {
- propsData: {
- viewModel: TEST_VIEW_MODEL,
- ...props,
- },
- });
- };
-
- const findDropdown = () => wrapper.findComponent(GlDropdown);
- const findInviteMembersTrigger = () => wrapper.findComponent(InviteMembersTrigger);
- const findDropdownContents = () =>
- findDropdown()
- .findAll('[data-testid]')
- .wrappers.map((child) => {
- const type = child.attributes('data-testid');
-
- if (type === 'divider') {
- return { type };
- }
- if (type === 'header') {
- return { type, text: child.text() };
- }
-
- return {
- type,
- text: child.text(),
- href: child.attributes('href'),
- };
- });
-
- describe('default', () => {
- beforeEach(() => {
- createComponent();
- });
-
- it('renders dropdown parent', () => {
- expect(findDropdown().props()).toMatchObject({
- text: TEST_VIEW_MODEL.title,
- textSrOnly: true,
- icon: 'plus',
- });
- });
-
- it('renders dropdown content', () => {
- const hrefItems = TEST_VIEW_MODEL.menu_sections[1].menu_items.filter((item) =>
- Boolean(item.href),
- );
-
- expect(findDropdownContents()).toEqual([
- {
- type: 'header',
- text: TEST_VIEW_MODEL.menu_sections[0].title,
- },
- ...TEST_VIEW_MODEL.menu_sections[0].menu_items.map(({ title, href }) => ({
- type: 'item',
- href,
- text: title,
- })),
- {
- type: 'divider',
- },
- {
- type: 'header',
- text: TEST_VIEW_MODEL.menu_sections[1].title,
- },
- ...hrefItems.map(({ title, href }) => ({
- type: 'item',
- href,
- text: title,
- })),
- ]);
- expect(findInviteMembersTrigger().props()).toMatchObject({
- displayText: '_invite members title_',
- icon: '_icon_',
- triggerElement: 'dropdown-_trigger_element_',
- triggerSource: '_trigger_source_',
- });
- });
- });
-
- describe('with only 1 section', () => {
- beforeEach(() => {
- createComponent({
- viewModel: {
- ...TEST_VIEW_MODEL,
- menu_sections: TEST_VIEW_MODEL.menu_sections.slice(0, 1),
- },
- });
- });
-
- it('renders dropdown content without headers and dividers', () => {
- expect(findDropdownContents()).toEqual(
- TEST_VIEW_MODEL.menu_sections[0].menu_items.map(({ title, href }) => ({
- type: 'item',
- href,
- text: title,
- })),
- );
- });
- });
-});
diff --git a/spec/frontend/nav/mock_data.js b/spec/frontend/nav/mock_data.js
deleted file mode 100644
index 2052acfe001..00000000000
--- a/spec/frontend/nav/mock_data.js
+++ /dev/null
@@ -1,39 +0,0 @@
-import { range } from 'lodash';
-
-export const TEST_NAV_DATA = {
- menuTitle: 'Test Menu Title',
- primary: [
- ...['projects', 'groups'].map((view) => ({
- id: view,
- href: null,
- title: view,
- view,
- })),
- ...range(0, 2).map((idx) => ({
- id: `primary-link-${idx}`,
- href: `/path/to/primary/${idx}`,
- title: `Title ${idx}`,
- })),
- ],
- secondary: range(0, 2).map((idx) => ({
- id: `secondary-link-${idx}`,
- href: `/path/to/secondary/${idx}`,
- title: `SecTitle ${idx}`,
- })),
- views: {
- projects: {
- namespace: 'projects',
- currentUserName: '',
- currentItem: {},
- linksPrimary: [{ id: 'project-link', href: '/path/to/projects', title: 'Project Link' }],
- linksSecondary: [],
- },
- groups: {
- namespace: 'groups',
- currentUserName: '',
- currentItem: {},
- linksPrimary: [],
- linksSecondary: [{ id: 'group-link', href: '/path/to/groups', title: 'Group Link' }],
- },
- },
-};
diff --git a/spec/frontend/observability/client_spec.js b/spec/frontend/observability/client_spec.js
index 57259fe61c2..9b8f100785b 100644
--- a/spec/frontend/observability/client_spec.js
+++ b/spec/frontend/observability/client_spec.js
@@ -439,10 +439,84 @@ describe('buildClient', () => {
expect(axios.get).toHaveBeenCalledTimes(1);
expect(axios.get).toHaveBeenCalledWith(metricsUrl, {
withCredentials: true,
+ params: expect.any(URLSearchParams),
});
expect(result).toEqual(mockResponse);
});
+ describe('query filter', () => {
+ beforeEach(() => {
+ axiosMock.onGet(metricsUrl).reply(200, {
+ metrics: [],
+ });
+ });
+
+ it('does not set any query param without filters', async () => {
+ await client.fetchMetrics();
+
+ expect(getQueryParam()).toBe('');
+ });
+
+ it('sets the start_with query param based on the search filter', async () => {
+ await client.fetchMetrics({
+ filters: { search: [{ value: 'foo' }, { value: 'bar' }, { value: ' ' }] },
+ });
+ expect(getQueryParam()).toBe('starts_with=foo+bar');
+ });
+
+ it('ignores empty search', async () => {
+ await client.fetchMetrics({
+ filters: {
+ search: [{ value: ' ' }, { value: '' }, { value: null }, { value: undefined }],
+ },
+ });
+ expect(getQueryParam()).toBe('');
+ });
+
+ it('ignores unsupported filters', async () => {
+ await client.fetchMetrics({
+ filters: {
+ unsupportedFilter: [{ operator: '=', value: 'foo' }],
+ },
+ });
+
+ expect(getQueryParam()).toBe('');
+ });
+
+ it('ignores non-array search filters', async () => {
+ await client.fetchMetrics({
+ filters: {
+ search: { value: 'foo' },
+ },
+ });
+
+ expect(getQueryParam()).toBe('');
+ });
+
+ it('adds the search limit param if specified with the search filter', async () => {
+ await client.fetchMetrics({
+ filters: { search: [{ value: 'foo' }] },
+ limit: 50,
+ });
+ expect(getQueryParam()).toBe('starts_with=foo&limit=50');
+ });
+
+ it('does not add the search limit param if the search filter is missing', async () => {
+ await client.fetchMetrics({
+ limit: 50,
+ });
+ expect(getQueryParam()).toBe('');
+ });
+
+ it('does not add the search limit param if the search filter is empty', async () => {
+ await client.fetchMetrics({
+ limit: 50,
+ search: [{ value: ' ' }, { value: '' }, { value: null }, { value: undefined }],
+ });
+ expect(getQueryParam()).toBe('');
+ });
+ });
+
it('rejects if metrics are missing', async () => {
axiosMock.onGet(metricsUrl).reply(200, {});
diff --git a/spec/frontend/profile/edit/components/profile_edit_app_spec.js b/spec/frontend/profile/edit/components/profile_edit_app_spec.js
index 31a368aefa9..39bf597352b 100644
--- a/spec/frontend/profile/edit/components/profile_edit_app_spec.js
+++ b/spec/frontend/profile/edit/components/profile_edit_app_spec.js
@@ -3,7 +3,6 @@ import MockAdapter from 'axios-mock-adapter';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { readFileAsDataURL } from '~/lib/utils/file_utility';
import axios from '~/lib/utils/axios_utils';
import ProfileEditApp from '~/profile/edit/components/profile_edit_app.vue';
import UserAvatar from '~/profile/edit/components/user_avatar.vue';
@@ -103,6 +102,8 @@ describe('Profile Edit App', () => {
});
it('syncs header avatars', async () => {
+ jest.spyOn(document, 'dispatchEvent');
+ jest.spyOn(URL, 'createObjectURL');
mockAxios.onPut(stubbedProfilePath).reply(200, {
message: successMessage,
});
@@ -112,7 +113,8 @@ describe('Profile Edit App', () => {
await waitForPromises();
- expect(readFileAsDataURL).toHaveBeenCalledWith(mockAvatarFile);
+ expect(URL.createObjectURL).toHaveBeenCalledWith(mockAvatarFile);
+ expect(document.dispatchEvent).toHaveBeenCalledWith(new CustomEvent('userAvatar:update'));
});
it('contains changes from the status form', async () => {
diff --git a/spec/frontend/read_more_spec.js b/spec/frontend/read_more_spec.js
index 5f7bd32e231..9b25c56f193 100644
--- a/spec/frontend/read_more_spec.js
+++ b/spec/frontend/read_more_spec.js
@@ -1,4 +1,3 @@
-import htmlProjectsOverview from 'test_fixtures/projects/overview.html';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import initReadMore from '~/read_more';
@@ -11,7 +10,12 @@ describe('Read more click-to-expand functionality', () => {
describe('expands target element', () => {
beforeEach(() => {
- setHTMLFixture(htmlProjectsOverview);
+ setHTMLFixture(`
+ <p class="read-more-container">Target</p>
+ <button type="button" class="js-read-more-trigger">
+ <span>Button text</span>
+ </button>
+ `);
});
it('adds "is-expanded" class to target element', () => {
diff --git a/spec/frontend/super_sidebar/components/user_menu_spec.js b/spec/frontend/super_sidebar/components/user_menu_spec.js
index ba675c8b3f5..4af3247693b 100644
--- a/spec/frontend/super_sidebar/components/user_menu_spec.js
+++ b/spec/frontend/super_sidebar/components/user_menu_spec.js
@@ -78,6 +78,20 @@ describe('UserMenu component', () => {
});
});
+ it('updates avatar url on custom avatar update event', async () => {
+ const url = `${userMenuMockData.avatar_url}-new-avatar`;
+
+ document.dispatchEvent(new CustomEvent('userAvatar:update', { detail: { url } }));
+ await nextTick();
+
+ const avatar = toggle.findComponent(GlAvatar);
+ expect(avatar.exists()).toBe(true);
+ expect(avatar.props()).toMatchObject({
+ entityName: userMenuMockData.name,
+ src: url,
+ });
+ });
+
it('renders screen reader text', () => {
expect(toggle.find('.gl-sr-only').text()).toBe(`${userMenuMockData.name} user’s menu`);
});
diff --git a/spec/frontend/super_sidebar/utils_spec.js b/spec/frontend/super_sidebar/utils_spec.js
index 690d34a966b..3e99099b663 100644
--- a/spec/frontend/super_sidebar/utils_spec.js
+++ b/spec/frontend/super_sidebar/utils_spec.js
@@ -1,18 +1,42 @@
import MockAdapter from 'axios-mock-adapter';
import * as Sentry from '~/sentry/sentry_browser_wrapper';
-import { trackContextAccess, ariaCurrent } from '~/super_sidebar/utils';
+import { getTopFrequentItems, trackContextAccess, ariaCurrent } from '~/super_sidebar/utils';
import axios from '~/lib/utils/axios_utils';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import AccessorUtilities from '~/lib/utils/accessor';
import { FREQUENT_ITEMS, FIFTEEN_MINUTES_IN_MS } from '~/frequent_items/constants';
import { HTTP_STATUS_OK, HTTP_STATUS_INTERNAL_SERVER_ERROR } from '~/lib/utils/http_status';
import waitForPromises from 'helpers/wait_for_promises';
+import { unsortedFrequentItems, sortedFrequentItems } from '../frequent_items/mock_data';
jest.mock('~/sentry/sentry_browser_wrapper');
useLocalStorageSpy();
describe('Super sidebar utils spec', () => {
+ describe('getTopFrequentItems', () => {
+ const maxItems = 3;
+
+ it.each([undefined, null, []])('returns empty array if `items` is %s', (items) => {
+ const result = getTopFrequentItems(items);
+
+ expect(result.length).toBe(0);
+ });
+
+ it('returns the requested amount of items', () => {
+ const result = getTopFrequentItems(unsortedFrequentItems, maxItems);
+
+ expect(result.length).toBe(maxItems);
+ });
+
+ it('sorts frequent items in order of frequency and lastAccessedOn', () => {
+ const result = getTopFrequentItems(unsortedFrequentItems, maxItems);
+ const expectedResult = sortedFrequentItems.slice(0, maxItems);
+
+ expect(result).toEqual(expectedResult);
+ });
+ });
+
describe('trackContextAccess', () => {
useLocalStorageSpy();
diff --git a/spec/frontend/vue_merge_request_widget/components/merge_checks_spec.js b/spec/frontend/vue_merge_request_widget/components/merge_checks_spec.js
index d39098b27c2..b19095cc686 100644
--- a/spec/frontend/vue_merge_request_widget/components/merge_checks_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/merge_checks_spec.js
@@ -138,7 +138,7 @@ describe('Merge request merge checks component', () => {
it.each`
identifier
${'conflict'}
- ${'unresolved_discussions'}
+ ${'discussions_not_resolved'}
${'need_rebase'}
${'default'}
`('renders $identifier merge check', async ({ identifier }) => {
diff --git a/spec/helpers/stat_anchors_helper_spec.rb b/spec/helpers/stat_anchors_helper_spec.rb
index f3830bf4172..41ac7509c39 100644
--- a/spec/helpers/stat_anchors_helper_spec.rb
+++ b/spec/helpers/stat_anchors_helper_spec.rb
@@ -8,6 +8,10 @@ RSpec.describe StatAnchorsHelper do
describe '#stat_anchor_attrs' do
subject { helper.stat_anchor_attrs(anchor) }
+ before do
+ stub_feature_flags(project_overview_reorg: false)
+ end
+
context 'when anchor is a link' do
let(:anchor) { anchor_klass.new(true) }
diff --git a/spec/lib/gitlab/database/background_migration/batched_migration_spec.rb b/spec/lib/gitlab/database/background_migration/batched_migration_spec.rb
index f70b38377d8..ffede2b6759 100644
--- a/spec/lib/gitlab/database/background_migration/batched_migration_spec.rb
+++ b/spec/lib/gitlab/database/background_migration/batched_migration_spec.rb
@@ -911,4 +911,18 @@ RSpec.describe Gitlab::Database::BackgroundMigration::BatchedMigration, type: :m
expect(actual).to contain_exactly(migration)
end
end
+
+ describe '#finalize_command' do
+ let_it_be(:migration) do
+ create(
+ :batched_background_migration,
+ gitlab_schema: :gitlab_main,
+ job_arguments: [['column_1'], ['column_1_convert_to_bigint']]
+ )
+ end
+
+ it 'generates the correct finalize command' do
+ expect(migration.finalize_command).to eq("sudo gitlab-rake gitlab:background_migrations:finalize[CopyColumnUsingBackgroundMigrationJob,events,id,'[[\"column_1\"]\\,[\"column_1_convert_to_bigint\"]]']")
+ end
+ end
end
diff --git a/spec/migrations/fix_broken_user_achievements_awarded_spec.rb b/spec/migrations/fix_broken_user_achievements_awarded_spec.rb
new file mode 100644
index 00000000000..cb31ca44a9f
--- /dev/null
+++ b/spec/migrations/fix_broken_user_achievements_awarded_spec.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe FixBrokenUserAchievementsAwarded, migration: :gitlab_main, feature_category: :user_profile do
+ let(:migration) { described_class.new }
+
+ let(:users_table) { table(:users) }
+ let(:namespaces_table) { table(:namespaces) }
+ let(:achievements_table) { table(:achievements) }
+ let(:user_achievements_table) { table(:user_achievements) }
+ let(:namespace) { namespaces_table.create!(name: 'something', path: generate(:username)) }
+ let(:achievement) { achievements_table.create!(name: 'something', namespace_id: namespace.id) }
+ let(:user) { users_table.create!(username: generate(:username), projects_limit: 0) }
+ let(:awarding_user) do
+ users_table.create!(username: generate(:username), email: generate(:email), projects_limit: 0)
+ end
+
+ let!(:user_achievement_invalid) do
+ user_achievements_table.create!(user_id: user.id, achievement_id: achievement.id,
+ awarded_by_user_id: awarding_user.id)
+ end
+
+ let!(:user_achievement_valid) do
+ user_achievements_table.create!(user_id: user.id, achievement_id: achievement.id,
+ awarded_by_user_id: user.id)
+ end
+
+ describe '#up' do
+ before do
+ awarding_user.delete
+ end
+
+ it 'migrates the invalid user achievement' do
+ expect { migrate! }
+ .to change { user_achievement_invalid.reload.awarded_by_user_id }
+ .from(nil).to(Users::Internal.ghost.id)
+ end
+
+ it 'does not migrate the valid user achievement' do
+ expect { migrate! }
+ .not_to change { user_achievement_valid.reload.awarded_by_user_id }
+ end
+ end
+end
diff --git a/spec/migrations/fix_broken_user_achievements_revoked_spec.rb b/spec/migrations/fix_broken_user_achievements_revoked_spec.rb
new file mode 100644
index 00000000000..34517ae67b4
--- /dev/null
+++ b/spec/migrations/fix_broken_user_achievements_revoked_spec.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe FixBrokenUserAchievementsRevoked, migration: :gitlab_main, feature_category: :user_profile do
+ let(:migration) { described_class.new }
+
+ let(:users_table) { table(:users) }
+ let(:namespaces_table) { table(:namespaces) }
+ let(:achievements_table) { table(:achievements) }
+ let(:user_achievements_table) { table(:user_achievements) }
+ let(:namespace) { namespaces_table.create!(name: 'something', path: generate(:username)) }
+ let(:achievement) { achievements_table.create!(name: 'something', namespace_id: namespace.id) }
+ let(:user) { users_table.create!(username: generate(:username), projects_limit: 0) }
+ let(:revoked_invalid) do
+ user_achievements_table.create!(user_id: user.id, achievement_id: achievement.id, revoked_at: Time.current)
+ end
+
+ let(:revoked_valid) do
+ user_achievements_table.create!(user_id: user.id, achievement_id: achievement.id, revoked_at: Time.current,
+ revoked_by_user_id: user.id)
+ end
+
+ let(:not_revoked) { user_achievements_table.create!(user_id: user.id, achievement_id: achievement.id) }
+
+ describe '#up' do
+ it 'migrates the invalid user achievement' do
+ expect { migrate! }
+ .to change { revoked_invalid.reload.revoked_by_user_id }
+ .from(nil).to(Users::Internal.ghost.id)
+ end
+
+ it 'does not migrate valid revoked user achievement' do
+ expect { migrate! }
+ .not_to change { revoked_valid.reload.revoked_by_user_id }
+ end
+
+ it 'does not migrate the not revoked user achievement' do
+ expect { migrate! }
+ .not_to change { not_revoked.reload.revoked_by_user_id }
+ end
+ end
+end
diff --git a/spec/models/members/project_member_spec.rb b/spec/models/members/project_member_spec.rb
index a9725a796bf..70f843be0e1 100644
--- a/spec/models/members/project_member_spec.rb
+++ b/spec/models/members/project_member_spec.rb
@@ -57,6 +57,7 @@ RSpec.describe ProjectMember, feature_category: :groups_and_projects do
let_it_be(:developer) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
+ let_it_be(:admin) { create(:admin) }
before do
project.add_owner(owner)
@@ -74,6 +75,30 @@ RSpec.describe ProjectMember, feature_category: :groups_and_projects do
end
end
+ context 'when member can manage owners via admin' do
+ let(:user) { admin }
+
+ context 'with admin mode', :enable_admin_mode do
+ it 'returns Gitlab::Access.options_with_owner' do
+ expect(access_levels).to eq(Gitlab::Access.options_with_owner)
+ end
+ end
+
+ context 'without admin mode' do
+ it 'returns empty hash' do
+ expect(access_levels).to eq({})
+ end
+ end
+ end
+
+ context 'when user is not a project member' do
+ let(:user) { create(:user) }
+
+ it 'return an empty hash' do
+ expect(access_levels).to eq({})
+ end
+ end
+
context 'when member cannot manage owners' do
let(:user) { maintainer }
diff --git a/spec/presenters/project_presenter_spec.rb b/spec/presenters/project_presenter_spec.rb
index 48db41ea8e3..d7738023805 100644
--- a/spec/presenters/project_presenter_spec.rb
+++ b/spec/presenters/project_presenter_spec.rb
@@ -428,6 +428,10 @@ RSpec.describe ProjectPresenter do
end
describe '#new_file_anchor_data' do
+ before do
+ stub_feature_flags(project_overview_reorg: false)
+ end
+
it 'returns new file data if user can push' do
project.add_developer(user)
@@ -751,6 +755,7 @@ RSpec.describe ProjectPresenter do
subject(:empty_repo_statistics_buttons) { presenter.empty_repo_statistics_buttons }
before do
+ stub_feature_flags(project_overview_reorg: false)
allow(project).to receive(:auto_devops_enabled?).and_return(false)
end
diff --git a/spec/services/users/migrate_records_to_ghost_user_service_spec.rb b/spec/services/users/migrate_records_to_ghost_user_service_spec.rb
index d6fb7a2954d..57378c07dd7 100644
--- a/spec/services/users/migrate_records_to_ghost_user_service_spec.rb
+++ b/spec/services/users/migrate_records_to_ghost_user_service_spec.rb
@@ -143,6 +143,13 @@ RSpec.describe Users::MigrateRecordsToGhostUserService, feature_category: :user_
let(:created_record) { create(:release, author: user) }
end
end
+
+ context 'for user achievements' do
+ include_examples 'migrating records to the ghost user', Achievements::UserAchievement,
+ [:awarded_by_user, :revoked_by_user] do
+ let(:created_record) { create(:user_achievement, awarded_by_user: user, revoked_by_user: user) }
+ end
+ end
end
context 'on post-migrate cleanups' do
@@ -358,6 +365,16 @@ RSpec.describe Users::MigrateRecordsToGhostUserService, feature_category: :user_
expect(Issue).not_to exist(issue.id)
end
+
+ it 'migrates awarded and revoked fields of user achievements' do
+ user_achievement = create(:user_achievement, awarded_by_user: user, revoked_by_user: user)
+
+ service.execute(hard_delete: true)
+ user_achievement.reload
+
+ expect(user_achievement.revoked_by_user).to eq(Users::Internal.ghost)
+ expect(user_achievement.awarded_by_user).to eq(Users::Internal.ghost)
+ end
end
end
end
diff --git a/spec/support/helpers/features/top_nav_spec_helpers.rb b/spec/support/helpers/features/top_nav_spec_helpers.rb
deleted file mode 100644
index ecc05189fb4..00000000000
--- a/spec/support/helpers/features/top_nav_spec_helpers.rb
+++ /dev/null
@@ -1,33 +0,0 @@
-# frozen_string_literal: true
-
-# These helpers help you interact within the Source Editor (single-file editor, snippets, etc.).
-#
-module Features
- module TopNavSpecHelpers
- def open_top_nav
- find('.js-top-nav-dropdown-toggle').click
- end
-
- def within_top_nav
- within('.js-top-nav-dropdown-menu') do
- yield
- end
- end
-
- def open_top_nav_projects
- open_top_nav
-
- within_top_nav do
- click_button('Projects')
- end
- end
-
- def open_top_nav_groups
- open_top_nav
-
- within_top_nav do
- click_button('Groups')
- end
- end
- end
-end
diff --git a/spec/views/projects/_files.html.haml_spec.rb b/spec/views/projects/_files.html.haml_spec.rb
index 96c6c2bdfab..870d436ca88 100644
--- a/spec/views/projects/_files.html.haml_spec.rb
+++ b/spec/views/projects/_files.html.haml_spec.rb
@@ -35,6 +35,8 @@ RSpec.describe 'projects/_files', feature_category: :groups_and_projects do
before do
allow(view).to receive(:current_user).and_return(user)
allow(user).to receive(:project_shortcut_buttons).and_return(true)
+
+ stub_feature_flags(project_overview_reorg: false)
end
it 'renders buttons' do
@@ -45,6 +47,10 @@ RSpec.describe 'projects/_files', feature_category: :groups_and_projects do
end
context 'when rendered in the project overview page and there is no current user' do
+ before do
+ stub_feature_flags(project_overview_reorg: false)
+ end
+
it 'renders buttons' do
render(template, is_project_overview: true)
diff --git a/spec/views/projects/_home_panel.html.haml_spec.rb b/spec/views/projects/_home_panel.html.haml_spec.rb
index e5081df4c22..a35c87cb4b3 100644
--- a/spec/views/projects/_home_panel.html.haml_spec.rb
+++ b/spec/views/projects/_home_panel.html.haml_spec.rb
@@ -100,6 +100,8 @@ RSpec.describe 'projects/_home_panel' do
allow(view).to receive(:current_user).and_return(user)
allow(view).to receive(:can?).with(user, :read_project, project).and_return(false)
allow(project).to receive(:license_anchor_data).and_return(false)
+
+ stub_feature_flags(project_overview_reorg: false)
end
context 'has no badges' do