diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-01-17 18:16:12 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-01-17 18:16:12 +0300 |
commit | 8432be20de0a29f4dde4980efe37d013c9e90034 (patch) | |
tree | af163db5a7c8ac17ca4da59d505c75552452956c /spec | |
parent | dd33e917374b611cd5a596c7fa51b47af6e153f6 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
17 files changed, 1694 insertions, 79 deletions
diff --git a/spec/controllers/concerns/work_items_hierarchy_spec.rb b/spec/controllers/concerns/work_items_hierarchy_spec.rb new file mode 100644 index 00000000000..270e30cb5f9 --- /dev/null +++ b/spec/controllers/concerns/work_items_hierarchy_spec.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe WorkItemsHierarchy do + controller(ApplicationController) do + include WorkItemsHierarchy + end + + let_it_be(:user) { create(:user) } + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project, group: group) } + + render_views + + before do + sign_in user + routes.draw { get :planning_hierarchy, to: "anonymous#planning_hierarchy" } + controller.instance_variable_set(:@project, project) + end + + it 'renders hierarchy' do + stub_feature_flags(work_items_hierarchy: true) + + get :planning_hierarchy + + expect(response).to have_gitlab_http_status(:ok) + expect(response.body).to match(/id="js-work-items-hierarchy"/) + end + + it 'renders 404' do + stub_feature_flags(work_items_hierarchy: false) + + get :planning_hierarchy + + expect(response).to have_gitlab_http_status(:not_found) + expect(response.body).not_to match(/id="js-work-items-hierarchy"/) + end +end diff --git a/spec/features/profile_spec.rb b/spec/features/profile_spec.rb index eeb9bf476c3..34eb07d78f1 100644 --- a/spec/features/profile_spec.rb +++ b/spec/features/profile_spec.rb @@ -62,66 +62,33 @@ RSpec.describe 'Profile account page', :js do end end - describe 'when I reset feed token' do - it 'resets feed token with `hide_access_tokens` feature flag enabled' do - visit profile_personal_access_tokens_path + it 'allows resetting of feed token' do + visit profile_personal_access_tokens_path - within('[data-testid="feed-token-container"]') do - previous_token = find_field('Feed token').value + within('[data-testid="feed-token-container"]') do + previous_token = find_field('Feed token').value - accept_confirm { click_link('reset this token') } + accept_confirm { click_link('reset this token') } - click_button('Click to reveal') + click_button('Click to reveal') - expect(find_field('Feed token').value).not_to eq(previous_token) - end - end - - it 'resets feed token with `hide_access_tokens` feature flag disabled' do - stub_feature_flags(hide_access_tokens: false) - visit profile_personal_access_tokens_path - - within('.feed-token-reset') do - previous_token = find("#feed_token").value - - accept_confirm { find('[data-testid="reset_feed_token_link"]').click } - - expect(find('#feed_token').value).not_to eq(previous_token) - end + expect(find_field('Feed token').value).not_to eq(previous_token) end end - describe 'when I reset incoming email token' do - before do - allow(Gitlab.config.incoming_email).to receive(:enabled).and_return(true) - stub_feature_flags(bootstrap_confirmation_modals: false) - end - - it 'resets incoming email token with `hide_access_tokens` feature flag enabled' do - visit profile_personal_access_tokens_path - - within('[data-testid="incoming-email-token-container"]') do - previous_token = find_field('Incoming email token').value - - accept_confirm { click_link('reset this token') } + it 'allows resetting of incoming email token' do + allow(Gitlab.config.incoming_email).to receive(:enabled).and_return(true) - click_button('Click to reveal') + visit profile_personal_access_tokens_path - expect(find_field('Incoming email token').value).not_to eq(previous_token) - end - end + within('[data-testid="incoming-email-token-container"]') do + previous_token = find_field('Incoming email token').value - it 'resets incoming email token with `hide_access_tokens` feature flag disabled' do - stub_feature_flags(hide_access_tokens: false) - visit profile_personal_access_tokens_path + accept_confirm { click_link('reset this token') } - within('.incoming-email-token-reset') do - previous_token = find('#incoming_email_token').value + click_button('Click to reveal') - accept_confirm { find('[data-testid="reset_email_token_link"]').click } - - expect(find('#incoming_email_token').value).not_to eq(previous_token) - end + expect(find_field('Incoming email token').value).not_to eq(previous_token) end end diff --git a/spec/features/profiles/personal_access_tokens_spec.rb b/spec/features/profiles/personal_access_tokens_spec.rb index 135a940807e..f1e5658cd7b 100644 --- a/spec/features/profiles/personal_access_tokens_spec.rb +++ b/spec/features/profiles/personal_access_tokens_spec.rb @@ -132,7 +132,7 @@ RSpec.describe 'Profile > Personal Access Tokens', :js do describe "feed token" do context "when enabled" do - it "displays feed token with `hide_access_tokens` feature flag enabled" do + it "displays feed token" do allow(Gitlab::CurrentSettings).to receive(:disable_feed_token).and_return(false) visit profile_personal_access_tokens_path @@ -143,15 +143,6 @@ RSpec.describe 'Profile > Personal Access Tokens', :js do expect(page).to have_content(feed_token_description) end end - - it "displays feed token with `hide_access_tokens` feature flag disabled" do - stub_feature_flags(hide_access_tokens: false) - allow(Gitlab::CurrentSettings).to receive(:disable_feed_token).and_return(false) - visit profile_personal_access_tokens_path - - expect(page).to have_field('Feed token', with: user.feed_token) - expect(page).to have_content(feed_token_description) - end end context "when disabled" do diff --git a/spec/features/projects/features_visibility_spec.rb b/spec/features/projects/features_visibility_spec.rb index a7e773dda2d..23fcc1fe444 100644 --- a/spec/features/projects/features_visibility_spec.rb +++ b/spec/features/projects/features_visibility_spec.rb @@ -24,7 +24,7 @@ RSpec.describe 'Edit Project Settings' do # disable by clicking toggle toggle_feature_off("project[project_feature_attributes][#{tool_name}_access_level]") page.within('.sharing-permissions') do - find('input[value="Save changes"]').click + find('[data-testid="project-features-save-button"]').click end wait_for_requests expect(page).not_to have_selector(".shortcuts-#{shortcut_name}") @@ -32,7 +32,7 @@ RSpec.describe 'Edit Project Settings' do # re-enable by clicking toggle again toggle_feature_on("project[project_feature_attributes][#{tool_name}_access_level]") page.within('.sharing-permissions') do - find('input[value="Save changes"]').click + find('[data-testid="project-features-save-button"]').click end wait_for_requests expect(page).to have_selector(".shortcuts-#{shortcut_name}") diff --git a/spec/features/projects/settings/project_settings_spec.rb b/spec/features/projects/settings/project_settings_spec.rb index 71b319d192c..b67caa5a5f9 100644 --- a/spec/features/projects/settings/project_settings_spec.rb +++ b/spec/features/projects/settings/project_settings_spec.rb @@ -47,7 +47,7 @@ RSpec.describe 'Projects settings' do # disable by clicking toggle forking_enabled_button.click page.within('.sharing-permissions') do - find('input[value="Save changes"]').click + find('[data-testid="project-features-save-button"]').click end wait_for_requests @@ -77,7 +77,7 @@ RSpec.describe 'Projects settings' do expect(default_award_emojis_input.value).to eq('false') page.within('.sharing-permissions') do - find('input[value="Save changes"]').click + find('[data-testid="project-features-save-button"]').click end wait_for_requests diff --git a/spec/features/projects/settings/user_manages_merge_requests_settings_spec.rb b/spec/features/projects/settings/user_manages_merge_requests_settings_spec.rb index 862bae45fc6..77be351f3d8 100644 --- a/spec/features/projects/settings/user_manages_merge_requests_settings_spec.rb +++ b/spec/features/projects/settings/user_manages_merge_requests_settings_spec.rb @@ -54,7 +54,7 @@ RSpec.describe 'Projects > Settings > User manages merge request settings' do within('.sharing-permissions-form') do find('.project-feature-controls[data-for="project[project_feature_attributes][merge_requests_access_level]"] .gl-toggle').click - find('input[value="Save changes"]').send_keys(:return) + find('[data-testid="project-features-save-button"]').send_keys(:return) end expect(page).not_to have_content 'Pipelines must succeed' @@ -74,7 +74,7 @@ RSpec.describe 'Projects > Settings > User manages merge request settings' do within('.sharing-permissions-form') do find('.project-feature-controls[data-for="project[project_feature_attributes][builds_access_level]"] .gl-toggle').click - find('input[value="Save changes"]').send_keys(:return) + find('[data-testid="project-features-save-button"]').send_keys(:return) end expect(page).to have_content 'Pipelines must succeed' @@ -95,7 +95,7 @@ RSpec.describe 'Projects > Settings > User manages merge request settings' do within('.sharing-permissions-form') do find('.project-feature-controls[data-for="project[project_feature_attributes][merge_requests_access_level]"] .gl-toggle').click - find('input[value="Save changes"]').send_keys(:return) + find('[data-testid="project-features-save-button"]').send_keys(:return) end expect(page).to have_content 'Pipelines must succeed' diff --git a/spec/features/projects/user_changes_project_visibility_spec.rb b/spec/features/projects/user_changes_project_visibility_spec.rb index 10b9cf84256..68fed9b8a74 100644 --- a/spec/features/projects/user_changes_project_visibility_spec.rb +++ b/spec/features/projects/user_changes_project_visibility_spec.rb @@ -5,14 +5,6 @@ require 'spec_helper' RSpec.describe 'User changes public project visibility', :js do include ProjectForksHelper - before do - fork_project(project, project.owner) - - sign_in(project.owner) - - visit edit_project_path(project) - end - shared_examples 'changing visibility to private' do it 'requires confirmation' do visibility_select = first('.project-feature-controls .select-control') @@ -34,15 +26,85 @@ RSpec.describe 'User changes public project visibility', :js do end end - context 'when a project is public' do + shared_examples 'does not require confirmation' do + it 'saves without confirmation' do + visibility_select = first('.project-feature-controls .select-control') + visibility_select.select('Private') + + page.within('#js-shared-permissions') do + click_button 'Save changes' + end + + wait_for_requests + + expect(project.reload).to be_private + end + end + + context 'when the project has forks' do + before do + fork_project(project, project.owner) + + sign_in(project.owner) + + visit edit_project_path(project) + end + + context 'when a project is public' do + let(:project) { create(:project, :empty_repo, :public) } + + it_behaves_like 'changing visibility to private' + end + + context 'when the project is internal' do + let(:project) { create(:project, :empty_repo, :internal) } + + it_behaves_like 'changing visibility to private' + end + + context 'when the visibility level is untouched' do + let(:project) { create(:project, :empty_repo, :public) } + + it 'saves without confirmation' do + expect(page).to have_selector('.js-emails-disabled', visible: true) + find('.js-emails-disabled input[type="checkbox"]').click + + page.within('#js-shared-permissions') do + click_button 'Save changes' + end + + wait_for_requests + + expect(project.reload).to be_public + end + end + end + + context 'when the project is not forked' do let(:project) { create(:project, :empty_repo, :public) } - it_behaves_like 'changing visibility to private' + before do + sign_in(project.owner) + + visit edit_project_path(project) + end + + it_behaves_like 'does not require confirmation' end - context 'when the project is internal' do - let(:project) { create(:project, :empty_repo, :internal) } + context 'with unlink_fork_network_upon_visibility_decrease = false' do + let(:project) { create(:project, :empty_repo, :public) } + + before do + stub_feature_flags(unlink_fork_network_upon_visibility_decrease: false) + + fork_project(project, project.owner) + + sign_in(project.owner) + + visit edit_project_path(project) + end - it_behaves_like 'changing visibility to private' + it_behaves_like 'does not require confirmation' end end diff --git a/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js b/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js index 0020269e4e7..8a9bb025d55 100644 --- a/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js +++ b/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js @@ -7,6 +7,7 @@ import { visibilityLevelDescriptions, visibilityOptions, } from '~/pages/projects/shared/permissions/constants'; +import ConfirmDanger from '~/vue_shared/components/confirm_danger/confirm_danger.vue'; const defaultProps = { currentSettings: { @@ -47,6 +48,8 @@ const defaultProps = { packagesAvailable: false, packagesHelpPath: '/help/user/packages/index', requestCveAvailable: true, + confirmationPhrase: 'my-fake-project', + showVisibilityConfirmModal: false, }; describe('Settings Panel', () => { @@ -104,6 +107,7 @@ describe('Settings Panel', () => { ); const findMetricsVisibilitySettings = () => wrapper.find({ ref: 'metrics-visibility-settings' }); const findOperationsSettings = () => wrapper.find({ ref: 'operations-settings' }); + const findConfirmDangerButton = () => wrapper.findComponent(ConfirmDanger); afterEach(() => { wrapper.destroy(); @@ -177,6 +181,44 @@ describe('Settings Panel', () => { expect(findRequestAccessEnabledInput().exists()).toBe(false); }); + + it('does not require confirmation if the visibility is reduced', async () => { + wrapper = mountComponent({ + currentSettings: { visibilityLevel: visibilityOptions.INTERNAL }, + }); + + expect(findConfirmDangerButton().exists()).toBe(false); + + await findProjectVisibilityLevelInput().setValue(visibilityOptions.PRIVATE); + + expect(findConfirmDangerButton().exists()).toBe(false); + }); + + describe('showVisibilityConfirmModal=true', () => { + beforeEach(() => { + wrapper = mountComponent({ + currentSettings: { visibilityLevel: visibilityOptions.INTERNAL }, + showVisibilityConfirmModal: true, + }); + }); + + it('will render the confirmation dialog if the visibility is reduced', async () => { + expect(findConfirmDangerButton().exists()).toBe(false); + + await findProjectVisibilityLevelInput().setValue(visibilityOptions.PRIVATE); + + expect(findConfirmDangerButton().exists()).toBe(true); + }); + + it('emits the `confirm` event when the reduce visibility warning is confirmed', async () => { + expect(wrapper.emitted('confirm')).toBeUndefined(); + + await findProjectVisibilityLevelInput().setValue(visibilityOptions.PRIVATE); + await findConfirmDangerButton().vm.$emit('confirm'); + + expect(wrapper.emitted('confirm')).toHaveLength(1); + }); + }); }); describe('Issues settings', () => { diff --git a/spec/frontend/work_items_hierarchy/components/__snapshots__/app_spec.js.snap b/spec/frontend/work_items_hierarchy/components/__snapshots__/app_spec.js.snap new file mode 100644 index 00000000000..f4979e4e707 --- /dev/null +++ b/spec/frontend/work_items_hierarchy/components/__snapshots__/app_spec.js.snap @@ -0,0 +1,1197 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`WorkItemsHierarchy App when licensePlan is free matches the snapshot 1`] = ` +<div> + <div + class="gl-card gl-px-8 gl-py-6 gl-line-height-20 gl-mt-4 gl-px-5!" + > + <!----> + + <div + class="gl-card-body gl-display-flex gl-p-0!" + > + <div + class="gl-banner-illustration" + > + <img + alt="" + role="presentation" + src="/foo.svg" + /> + </div> + + <div + class="gl-banner-content" + > + <h1 + class="gl-banner-title" + > + Help us improve work items in GitLab! + </h1> + + <p> + + Is there a framework or type of work item you wish you had access to in GitLab? Give us your feedback and help us build the experiences valuable to you. + + </p> + + <a + class="btn btn-confirm btn-md gl-button" + data-testid="gl-banner-primary-button" + href="https://forms.gle/u1BmRp8rTbwj52iq5" + > + <!----> + + <!----> + + <span + class="gl-button-text" + > + Take the work items survey + </span> + </a> + + </div> + + <button + aria-label="Close banner" + class="btn gl-banner-close btn-default btn-sm gl-button btn-default-tertiary btn-icon" + type="button" + > + <!----> + + <svg + aria-hidden="true" + class="gl-button-icon gl-icon s16" + data-testid="close-icon" + role="img" + > + <use + href="#close" + /> + </svg> + + <!----> + </button> + </div> + + <!----> + </div> + + <h3 + class="gl-mt-5!" + > + Planning hierarchy + </h3> + + <p> + + Deliver value more efficiently by breaking down necessary work into a hierarchical structure. This structure helps teams understand scope, priorities, and how work cascades up toward larger goals. + + </p> + + <div + class="gl-font-weight-bold gl-mb-2" + > + Current structure + </div> + + <p + class="gl-mb-3!" + > + You can start using these items now. + </p> + + <div> + <div + class="gl-mb-3" + > + <span + class="gl-border-gray-100 gl-border-1 gl-border-solid gl-rounded-base gl-pl-2 gl-pt-2 gl-pb-2 gl-pr-3 gl-display-inline-flex gl-align-items-center gl-justify-content-center gl-line-height-normal" + data-testid="work-item-wrapper" + > + <span + class="gl-rounded-base gl-mr-2 gl-display-inline-flex justify-content-center align-items-center hierarchy-icon-wrapper" + style="background-color: rgb(203, 226, 249); color: rgb(16, 104, 191);" + > + <svg + aria-hidden="true" + class="gl-icon s12" + data-testid="issues-icon" + role="img" + > + <use + href="#issues" + /> + </svg> + </span> + + + Issue + + </span> + + <!----> + + <!----> + </div> + <div + class="gl-mb-3" + > + <span + class="gl-border-gray-100 gl-border-1 gl-border-solid gl-rounded-base gl-pl-2 gl-pt-2 gl-pb-2 gl-pr-3 gl-display-inline-flex gl-align-items-center gl-justify-content-center gl-line-height-normal" + data-testid="work-item-wrapper" + > + <span + class="gl-rounded-base gl-mr-2 gl-display-inline-flex justify-content-center align-items-center hierarchy-icon-wrapper" + style="background-color: rgb(195, 230, 205); color: rgb(33, 118, 69);" + > + <svg + aria-hidden="true" + class="gl-icon s12" + data-testid="task-done-icon" + role="img" + > + <use + href="#task-done" + /> + </svg> + </span> + + + Task + + </span> + + <!----> + + <!----> + </div> + <div + class="gl-mb-3" + > + <span + class="gl-border-gray-100 gl-border-1 gl-border-solid gl-rounded-base gl-pl-2 gl-pt-2 gl-pb-2 gl-pr-3 gl-display-inline-flex gl-align-items-center gl-justify-content-center gl-line-height-normal" + data-testid="work-item-wrapper" + > + <span + class="gl-rounded-base gl-mr-2 gl-display-inline-flex justify-content-center align-items-center hierarchy-icon-wrapper" + style="background-color: rgb(219, 42, 15); color: rgb(253, 212, 205);" + > + <svg + aria-hidden="true" + class="gl-icon s16" + data-testid="issue-type-incident-icon" + role="img" + > + <use + href="#issue-type-incident" + /> + </svg> + </span> + + + Incident + + </span> + + <!----> + + <!----> + </div> + </div> + + <div + class="gl-font-weight-bold gl-mt-5 gl-mb-2" + data-testid="unavailable-structure" + > + + Unavailable structure + + </div> + + <p + class="gl-mb-3!" + > + + These items are unavailable in the current structure. + + </p> + + <div> + <div + class="gl-mb-3 flex" + > + <span + class="gl-border-gray-100 gl-border-1 gl-border-solid gl-rounded-base gl-pl-2 gl-pt-2 gl-pb-2 gl-pr-3 gl-display-inline-flex gl-align-items-center gl-justify-content-center gl-line-height-normal" + data-testid="work-item-wrapper" + > + <span + class="gl-rounded-base gl-mr-2 gl-display-inline-flex justify-content-center align-items-center hierarchy-icon-wrapper" + style="background-color: rgb(225, 216, 249); color: rgb(105, 76, 192);" + > + <svg + aria-hidden="true" + class="gl-icon s12" + data-testid="epic-icon" + role="img" + > + <use + href="#epic" + /> + </svg> + </span> + + + Epic + + </span> + + <span + class="badge gl-ml-3 gl-align-self-center badge-info badge-pill gl-badge sm" + > + <svg + aria-hidden="true" + class="gl-badge-icon gl-icon s16 gl-mr-2" + data-testid="license-icon" + role="img" + > + <use + href="#license" + /> + </svg> + Premium + </span> + + <!----> + </div> + <div + class="gl-mb-3 flex" + > + <span + class="gl-border-gray-100 gl-border-1 gl-border-solid gl-rounded-base gl-pl-2 gl-pt-2 gl-pb-2 gl-pr-3 gl-display-inline-flex gl-align-items-center gl-justify-content-center gl-line-height-normal" + data-testid="work-item-wrapper" + > + <span + class="gl-rounded-base gl-mr-2 gl-display-inline-flex justify-content-center align-items-center hierarchy-icon-wrapper" + style="background-color: rgb(245, 217, 168); color: rgb(171, 97, 0);" + > + <svg + aria-hidden="true" + class="gl-icon s12" + data-testid="epic-icon" + role="img" + > + <use + href="#epic" + /> + </svg> + </span> + + + Child epic + + </span> + + <span + class="badge gl-ml-3 gl-align-self-center badge-info badge-pill gl-badge sm" + > + <svg + aria-hidden="true" + class="gl-badge-icon gl-icon s16 gl-mr-2" + data-testid="license-icon" + role="img" + > + <use + href="#license" + /> + </svg> + Ultimate + </span> + + <!----> + </div> + <div + class="gl-mb-3 flex" + > + <span + class="gl-border-gray-100 gl-border-1 gl-border-solid gl-rounded-base gl-pl-2 gl-pt-2 gl-pb-2 gl-pr-3 gl-display-inline-flex gl-align-items-center gl-justify-content-center gl-line-height-normal" + data-testid="work-item-wrapper" + > + <span + class="gl-rounded-base gl-mr-2 gl-display-inline-flex justify-content-center align-items-center hierarchy-icon-wrapper" + style="background-color: rgb(197, 227, 251); color: rgb(0, 104, 197);" + > + <svg + aria-hidden="true" + class="gl-icon s12" + data-testid="requirements-icon" + role="img" + > + <use + href="#requirements" + /> + </svg> + </span> + + + Requirement + + </span> + + <span + class="badge gl-ml-3 gl-align-self-center badge-info badge-pill gl-badge sm" + > + <svg + aria-hidden="true" + class="gl-badge-icon gl-icon s16 gl-mr-2" + data-testid="license-icon" + role="img" + > + <use + href="#license" + /> + </svg> + Ultimate + </span> + + <!----> + </div> + <div + class="gl-mb-3 flex" + > + <span + class="gl-border-gray-100 gl-border-1 gl-border-solid gl-rounded-base gl-pl-2 gl-pt-2 gl-pb-2 gl-pr-3 gl-display-inline-flex gl-align-items-center gl-justify-content-center gl-line-height-normal" + data-testid="work-item-wrapper" + > + <span + class="gl-rounded-base gl-mr-2 gl-display-inline-flex justify-content-center align-items-center hierarchy-icon-wrapper" + style="background-color: rgb(0, 122, 63); color: rgb(186, 232, 203);" + > + <svg + aria-hidden="true" + class="gl-icon s16" + data-testid="issue-type-test-case-icon" + role="img" + > + <use + href="#issue-type-test-case" + /> + </svg> + </span> + + + Test case + + </span> + + <span + class="badge gl-ml-3 gl-align-self-center badge-info badge-pill gl-badge sm" + > + <svg + aria-hidden="true" + class="gl-badge-icon gl-icon s16 gl-mr-2" + data-testid="license-icon" + role="img" + > + <use + href="#license" + /> + </svg> + Ultimate + </span> + + <!----> + </div> + </div> +</div> +`; + +exports[`WorkItemsHierarchy App when licensePlan is premium matches the snapshot 1`] = ` +<div> + <div + class="gl-card gl-px-8 gl-py-6 gl-line-height-20 gl-mt-4 gl-px-5!" + > + <!----> + + <div + class="gl-card-body gl-display-flex gl-p-0!" + > + <div + class="gl-banner-illustration" + > + <img + alt="" + role="presentation" + src="/foo.svg" + /> + </div> + + <div + class="gl-banner-content" + > + <h1 + class="gl-banner-title" + > + Help us improve work items in GitLab! + </h1> + + <p> + + Is there a framework or type of work item you wish you had access to in GitLab? Give us your feedback and help us build the experiences valuable to you. + + </p> + + <a + class="btn btn-confirm btn-md gl-button" + data-testid="gl-banner-primary-button" + href="https://forms.gle/u1BmRp8rTbwj52iq5" + > + <!----> + + <!----> + + <span + class="gl-button-text" + > + Take the work items survey + </span> + </a> + + </div> + + <button + aria-label="Close banner" + class="btn gl-banner-close btn-default btn-sm gl-button btn-default-tertiary btn-icon" + type="button" + > + <!----> + + <svg + aria-hidden="true" + class="gl-button-icon gl-icon s16" + data-testid="close-icon" + role="img" + > + <use + href="#close" + /> + </svg> + + <!----> + </button> + </div> + + <!----> + </div> + + <h3 + class="gl-mt-5!" + > + Planning hierarchy + </h3> + + <p> + + Deliver value more efficiently by breaking down necessary work into a hierarchical structure. This structure helps teams understand scope, priorities, and how work cascades up toward larger goals. + + </p> + + <div + class="gl-font-weight-bold gl-mb-2" + > + Current structure + </div> + + <p + class="gl-mb-3!" + > + You can start using these items now. + </p> + + <div> + <div + class="gl-mb-3" + > + <span + class="gl-border-gray-100 gl-border-1 gl-border-solid gl-rounded-base gl-pl-2 gl-pt-2 gl-pb-2 gl-pr-3 gl-display-inline-flex gl-align-items-center gl-justify-content-center gl-line-height-normal" + data-testid="work-item-wrapper" + > + <span + class="gl-rounded-base gl-mr-2 gl-display-inline-flex justify-content-center align-items-center hierarchy-icon-wrapper" + style="background-color: rgb(225, 216, 249); color: rgb(105, 76, 192);" + > + <svg + aria-hidden="true" + class="gl-icon s12" + data-testid="epic-icon" + role="img" + > + <use + href="#epic" + /> + </svg> + </span> + + + Epic + + </span> + + <!----> + + <div + class="" + > + <!----> + + <div + class="gl-display-block gl-mt-2 gl-ml-6" + > + <svg + aria-hidden="true" + class="gl-text-gray-400 gl-icon s16" + data-testid="arrow-down-icon" + role="img" + > + <use + href="#arrow-down" + /> + </svg> + </div> + + <!----> + + <span + class="gl-border-gray-100 gl-border-1 gl-border-solid gl-rounded-base gl-pl-2 gl-pt-2 gl-pb-2 gl-pr-3 gl-display-inline-flex gl-align-items-center gl-justify-content-center gl-mt-2 gl-line-height-normal gl-ml-6" + > + <span + class="gl-rounded-base gl-mr-2 gl-display-inline-flex justify-content-center align-items-center hierarchy-icon-wrapper" + style="background-color: rgb(203, 226, 249); color: rgb(16, 104, 191);" + > + <svg + aria-hidden="true" + class="gl-icon s12" + data-testid="issues-icon" + role="img" + > + <use + href="#issues" + /> + </svg> + </span> + + + Issue + + </span> + </div> + </div> + <div + class="gl-mb-3" + > + <span + class="gl-border-gray-100 gl-border-1 gl-border-solid gl-rounded-base gl-pl-2 gl-pt-2 gl-pb-2 gl-pr-3 gl-display-inline-flex gl-align-items-center gl-justify-content-center gl-line-height-normal" + data-testid="work-item-wrapper" + > + <span + class="gl-rounded-base gl-mr-2 gl-display-inline-flex justify-content-center align-items-center hierarchy-icon-wrapper" + style="background-color: rgb(195, 230, 205); color: rgb(33, 118, 69);" + > + <svg + aria-hidden="true" + class="gl-icon s12" + data-testid="task-done-icon" + role="img" + > + <use + href="#task-done" + /> + </svg> + </span> + + + Task + + </span> + + <!----> + + <!----> + </div> + <div + class="gl-mb-3" + > + <span + class="gl-border-gray-100 gl-border-1 gl-border-solid gl-rounded-base gl-pl-2 gl-pt-2 gl-pb-2 gl-pr-3 gl-display-inline-flex gl-align-items-center gl-justify-content-center gl-line-height-normal" + data-testid="work-item-wrapper" + > + <span + class="gl-rounded-base gl-mr-2 gl-display-inline-flex justify-content-center align-items-center hierarchy-icon-wrapper" + style="background-color: rgb(219, 42, 15); color: rgb(253, 212, 205);" + > + <svg + aria-hidden="true" + class="gl-icon s16" + data-testid="issue-type-incident-icon" + role="img" + > + <use + href="#issue-type-incident" + /> + </svg> + </span> + + + Incident + + </span> + + <!----> + + <!----> + </div> + </div> + + <div + class="gl-font-weight-bold gl-mt-5 gl-mb-2" + data-testid="unavailable-structure" + > + + Unavailable structure + + </div> + + <p + class="gl-mb-3!" + > + + These items are unavailable in the current structure. + + </p> + + <div> + <div + class="gl-mb-3 flex" + > + <span + class="gl-border-gray-100 gl-border-1 gl-border-solid gl-rounded-base gl-pl-2 gl-pt-2 gl-pb-2 gl-pr-3 gl-display-inline-flex gl-align-items-center gl-justify-content-center gl-line-height-normal" + data-testid="work-item-wrapper" + > + <span + class="gl-rounded-base gl-mr-2 gl-display-inline-flex justify-content-center align-items-center hierarchy-icon-wrapper" + style="background-color: rgb(245, 217, 168); color: rgb(171, 97, 0);" + > + <svg + aria-hidden="true" + class="gl-icon s12" + data-testid="epic-icon" + role="img" + > + <use + href="#epic" + /> + </svg> + </span> + + + Child epic + + </span> + + <span + class="badge gl-ml-3 gl-align-self-center badge-info badge-pill gl-badge sm" + > + <svg + aria-hidden="true" + class="gl-badge-icon gl-icon s16 gl-mr-2" + data-testid="license-icon" + role="img" + > + <use + href="#license" + /> + </svg> + Ultimate + </span> + + <!----> + </div> + <div + class="gl-mb-3 flex" + > + <span + class="gl-border-gray-100 gl-border-1 gl-border-solid gl-rounded-base gl-pl-2 gl-pt-2 gl-pb-2 gl-pr-3 gl-display-inline-flex gl-align-items-center gl-justify-content-center gl-line-height-normal" + data-testid="work-item-wrapper" + > + <span + class="gl-rounded-base gl-mr-2 gl-display-inline-flex justify-content-center align-items-center hierarchy-icon-wrapper" + style="background-color: rgb(197, 227, 251); color: rgb(0, 104, 197);" + > + <svg + aria-hidden="true" + class="gl-icon s12" + data-testid="requirements-icon" + role="img" + > + <use + href="#requirements" + /> + </svg> + </span> + + + Requirement + + </span> + + <span + class="badge gl-ml-3 gl-align-self-center badge-info badge-pill gl-badge sm" + > + <svg + aria-hidden="true" + class="gl-badge-icon gl-icon s16 gl-mr-2" + data-testid="license-icon" + role="img" + > + <use + href="#license" + /> + </svg> + Ultimate + </span> + + <!----> + </div> + <div + class="gl-mb-3 flex" + > + <span + class="gl-border-gray-100 gl-border-1 gl-border-solid gl-rounded-base gl-pl-2 gl-pt-2 gl-pb-2 gl-pr-3 gl-display-inline-flex gl-align-items-center gl-justify-content-center gl-line-height-normal" + data-testid="work-item-wrapper" + > + <span + class="gl-rounded-base gl-mr-2 gl-display-inline-flex justify-content-center align-items-center hierarchy-icon-wrapper" + style="background-color: rgb(0, 122, 63); color: rgb(186, 232, 203);" + > + <svg + aria-hidden="true" + class="gl-icon s16" + data-testid="issue-type-test-case-icon" + role="img" + > + <use + href="#issue-type-test-case" + /> + </svg> + </span> + + + Test case + + </span> + + <span + class="badge gl-ml-3 gl-align-self-center badge-info badge-pill gl-badge sm" + > + <svg + aria-hidden="true" + class="gl-badge-icon gl-icon s16 gl-mr-2" + data-testid="license-icon" + role="img" + > + <use + href="#license" + /> + </svg> + Ultimate + </span> + + <!----> + </div> + </div> +</div> +`; + +exports[`WorkItemsHierarchy App when licensePlan is ultimate matches the snapshot 1`] = ` +<div> + <div + class="gl-card gl-px-8 gl-py-6 gl-line-height-20 gl-mt-4 gl-px-5!" + > + <!----> + + <div + class="gl-card-body gl-display-flex gl-p-0!" + > + <div + class="gl-banner-illustration" + > + <img + alt="" + role="presentation" + src="/foo.svg" + /> + </div> + + <div + class="gl-banner-content" + > + <h1 + class="gl-banner-title" + > + Help us improve work items in GitLab! + </h1> + + <p> + + Is there a framework or type of work item you wish you had access to in GitLab? Give us your feedback and help us build the experiences valuable to you. + + </p> + + <a + class="btn btn-confirm btn-md gl-button" + data-testid="gl-banner-primary-button" + href="https://forms.gle/u1BmRp8rTbwj52iq5" + > + <!----> + + <!----> + + <span + class="gl-button-text" + > + Take the work items survey + </span> + </a> + + </div> + + <button + aria-label="Close banner" + class="btn gl-banner-close btn-default btn-sm gl-button btn-default-tertiary btn-icon" + type="button" + > + <!----> + + <svg + aria-hidden="true" + class="gl-button-icon gl-icon s16" + data-testid="close-icon" + role="img" + > + <use + href="#close" + /> + </svg> + + <!----> + </button> + </div> + + <!----> + </div> + + <h3 + class="gl-mt-5!" + > + Planning hierarchy + </h3> + + <p> + + Deliver value more efficiently by breaking down necessary work into a hierarchical structure. This structure helps teams understand scope, priorities, and how work cascades up toward larger goals. + + </p> + + <div + class="gl-font-weight-bold gl-mb-2" + > + Current structure + </div> + + <p + class="gl-mb-3!" + > + You can start using these items now. + </p> + + <div> + <div + class="gl-mb-3" + > + <span + class="gl-border-gray-100 gl-border-1 gl-border-solid gl-rounded-base gl-pl-2 gl-pt-2 gl-pb-2 gl-pr-3 gl-display-inline-flex gl-align-items-center gl-justify-content-center gl-line-height-normal" + data-testid="work-item-wrapper" + > + <span + class="gl-rounded-base gl-mr-2 gl-display-inline-flex justify-content-center align-items-center hierarchy-icon-wrapper" + style="background-color: rgb(225, 216, 249); color: rgb(105, 76, 192);" + > + <svg + aria-hidden="true" + class="gl-icon s12" + data-testid="epic-icon" + role="img" + > + <use + href="#epic" + /> + </svg> + </span> + + + Epic + + </span> + + <!----> + + <div + class="gl-relative" + > + <svg + class="hierarchy-rounded-arrow-tail gl-text-gray-400" + data-testid="hierarchy-rounded-arrow-tail" + fill="none" + width="2" + xmlns="http://www.w3.org/2000/svg" + > + <line + stroke="currentColor" + stroke-linecap="round" + stroke-width="1.5" + x1="0.75" + x2="0.75" + y1="1" + y2="100%" + /> + </svg> + + <div + class="gl-display-block gl-mt-2 gl-ml-6" + > + <svg + aria-hidden="true" + class="gl-text-gray-400 gl-icon s16" + data-testid="arrow-down-icon" + role="img" + > + <use + href="#arrow-down" + /> + </svg> + </div> + + <!----> + + <span + class="gl-border-gray-100 gl-border-1 gl-border-solid gl-rounded-base gl-pl-2 gl-pt-2 gl-pb-2 gl-pr-3 gl-display-inline-flex gl-align-items-center gl-justify-content-center gl-mt-2 gl-line-height-normal gl-ml-6" + > + <span + class="gl-rounded-base gl-mr-2 gl-display-inline-flex justify-content-center align-items-center hierarchy-icon-wrapper" + style="background-color: rgb(245, 217, 168); color: rgb(171, 97, 0);" + > + <svg + aria-hidden="true" + class="gl-icon s12" + data-testid="epic-icon" + role="img" + > + <use + href="#epic" + /> + </svg> + </span> + + + Child epic + + </span> + <div + class="gl-display-block gl-mt-2 gl-ml-6" + > + <svg + aria-hidden="true" + class="gl-text-gray-400 gl-icon s16" + data-testid="arrow-down-icon" + role="img" + > + <use + href="#arrow-down" + /> + </svg> + </div> + + <svg + aria-hidden="true" + class="gl-text-gray-400 gl-ml-2 hierarchy-rounded-arrow gl-icon s16" + data-testid="level-up-icon" + role="img" + > + <use + href="#level-up" + /> + </svg> + + <span + class="gl-border-gray-100 gl-border-1 gl-border-solid gl-rounded-base gl-pl-2 gl-pt-2 gl-pb-2 gl-pr-3 gl-display-inline-flex gl-align-items-center gl-justify-content-center gl-mt-2 gl-line-height-normal gl-ml-0" + > + <span + class="gl-rounded-base gl-mr-2 gl-display-inline-flex justify-content-center align-items-center hierarchy-icon-wrapper" + style="background-color: rgb(203, 226, 249); color: rgb(16, 104, 191);" + > + <svg + aria-hidden="true" + class="gl-icon s12" + data-testid="issues-icon" + role="img" + > + <use + href="#issues" + /> + </svg> + </span> + + + Issue + + </span> + </div> + </div> + <div + class="gl-mb-3" + > + <span + class="gl-border-gray-100 gl-border-1 gl-border-solid gl-rounded-base gl-pl-2 gl-pt-2 gl-pb-2 gl-pr-3 gl-display-inline-flex gl-align-items-center gl-justify-content-center gl-line-height-normal" + data-testid="work-item-wrapper" + > + <span + class="gl-rounded-base gl-mr-2 gl-display-inline-flex justify-content-center align-items-center hierarchy-icon-wrapper" + style="background-color: rgb(195, 230, 205); color: rgb(33, 118, 69);" + > + <svg + aria-hidden="true" + class="gl-icon s12" + data-testid="task-done-icon" + role="img" + > + <use + href="#task-done" + /> + </svg> + </span> + + + Task + + </span> + + <!----> + + <!----> + </div> + <div + class="gl-mb-3" + > + <span + class="gl-border-gray-100 gl-border-1 gl-border-solid gl-rounded-base gl-pl-2 gl-pt-2 gl-pb-2 gl-pr-3 gl-display-inline-flex gl-align-items-center gl-justify-content-center gl-line-height-normal" + data-testid="work-item-wrapper" + > + <span + class="gl-rounded-base gl-mr-2 gl-display-inline-flex justify-content-center align-items-center hierarchy-icon-wrapper" + style="background-color: rgb(219, 42, 15); color: rgb(253, 212, 205);" + > + <svg + aria-hidden="true" + class="gl-icon s16" + data-testid="issue-type-incident-icon" + role="img" + > + <use + href="#issue-type-incident" + /> + </svg> + </span> + + + Incident + + </span> + + <!----> + + <!----> + </div> + <div + class="gl-mb-3" + > + <span + class="gl-border-gray-100 gl-border-1 gl-border-solid gl-rounded-base gl-pl-2 gl-pt-2 gl-pb-2 gl-pr-3 gl-display-inline-flex gl-align-items-center gl-justify-content-center gl-line-height-normal" + data-testid="work-item-wrapper" + > + <span + class="gl-rounded-base gl-mr-2 gl-display-inline-flex justify-content-center align-items-center hierarchy-icon-wrapper" + style="background-color: rgb(197, 227, 251); color: rgb(0, 104, 197);" + > + <svg + aria-hidden="true" + class="gl-icon s12" + data-testid="requirements-icon" + role="img" + > + <use + href="#requirements" + /> + </svg> + </span> + + + Requirement + + </span> + + <!----> + + <!----> + </div> + <div + class="gl-mb-3" + > + <span + class="gl-border-gray-100 gl-border-1 gl-border-solid gl-rounded-base gl-pl-2 gl-pt-2 gl-pb-2 gl-pr-3 gl-display-inline-flex gl-align-items-center gl-justify-content-center gl-line-height-normal" + data-testid="work-item-wrapper" + > + <span + class="gl-rounded-base gl-mr-2 gl-display-inline-flex justify-content-center align-items-center hierarchy-icon-wrapper" + style="background-color: rgb(0, 122, 63); color: rgb(186, 232, 203);" + > + <svg + aria-hidden="true" + class="gl-icon s16" + data-testid="issue-type-test-case-icon" + role="img" + > + <use + href="#issue-type-test-case" + /> + </svg> + </span> + + + Test case + + </span> + + <!----> + + <!----> + </div> + </div> + + <!----> + + <!----> + + <div /> +</div> +`; diff --git a/spec/frontend/work_items_hierarchy/components/app_spec.js b/spec/frontend/work_items_hierarchy/components/app_spec.js new file mode 100644 index 00000000000..c22c0fcb21c --- /dev/null +++ b/spec/frontend/work_items_hierarchy/components/app_spec.js @@ -0,0 +1,78 @@ +import { nextTick } from 'vue'; +import { createLocalVue, mount } from '@vue/test-utils'; +import VueApollo from 'vue-apollo'; +import { GlBanner } from '@gitlab/ui'; +import App from '~/work_items_hierarchy/components/app.vue'; +import { extendedWrapper } from 'helpers/vue_test_utils_helper'; + +const localVue = createLocalVue(); +localVue.use(VueApollo); + +describe('WorkItemsHierarchy App', () => { + let wrapper; + const createComponent = (props = {}, data = {}) => { + wrapper = extendedWrapper( + mount(App, { + localVue, + provide: { + illustrationPath: '/foo.svg', + licensePlan: 'free', + ...props, + }, + data() { + return data; + }, + }), + ); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + describe.each` + licensePlan + ${'free'} + ${'premium'} + ${'ultimate'} + `('when licensePlan is $licensePlan', ({ licensePlan }) => { + beforeEach(() => { + createComponent({ licensePlan }); + }); + + it('matches the snapshot', () => { + expect(wrapper.element).toMatchSnapshot(); + }); + }); + + describe('survey banner', () => { + it('shows when the banner is visible', () => { + createComponent({}, { bannerVisible: true }); + + expect(wrapper.find(GlBanner).exists()).toBe(true); + }); + + it('hide when close is called', async () => { + createComponent({}, { bannerVisible: true }); + + wrapper.findByTestId('close-icon').trigger('click'); + + await nextTick(); + + expect(wrapper.find(GlBanner).exists()).toBe(false); + }); + }); + + describe('Unavailable structure', () => { + it.each` + licensePlan | visible + ${'free'} | ${true} + ${'premium'} | ${true} + ${'ultimate'} | ${false} + `('visibility is $visible when plan is $licensePlan', ({ licensePlan, visible }) => { + createComponent({ licensePlan }); + + expect(wrapper.findByTestId('unavailable-structure').exists()).toBe(visible); + }); + }); +}); diff --git a/spec/frontend/work_items_hierarchy/components/hierarchy_spec.js b/spec/frontend/work_items_hierarchy/components/hierarchy_spec.js new file mode 100644 index 00000000000..14c15fb5cbe --- /dev/null +++ b/spec/frontend/work_items_hierarchy/components/hierarchy_spec.js @@ -0,0 +1,118 @@ +import { createLocalVue, mount } from '@vue/test-utils'; +import VueApollo from 'vue-apollo'; +import { GlBadge } from '@gitlab/ui'; +import Hierarchy from '~/work_items_hierarchy/components/hierarchy.vue'; +import { extendedWrapper } from 'helpers/vue_test_utils_helper'; +import RESPONSE from '~/work_items_hierarchy/static_response'; +import { workItemTypes } from '~/work_items/constants'; + +const localVue = createLocalVue(); +localVue.use(VueApollo); + +describe('WorkItemsHierarchy Hierarchy', () => { + let wrapper; + + const workItemsFromResponse = (response) => { + return response.reduce( + (itemTypes, item) => { + const key = item.available ? 'available' : 'unavailable'; + itemTypes[key].push({ + ...item, + ...workItemTypes[item.type], + nestedTypes: item.nestedTypes + ? item.nestedTypes.map((type) => workItemTypes[type]) + : null, + }); + return itemTypes; + }, + { available: [], unavailable: [] }, + ); + }; + + const createComponent = (props = {}) => { + wrapper = extendedWrapper( + mount(Hierarchy, { + localVue, + propsData: { + workItemTypes: props.workItemTypes, + ...props, + }, + }), + ); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + describe('available structure', () => { + let items = []; + + beforeEach(() => { + items = workItemsFromResponse(RESPONSE.ultimate).available; + createComponent({ workItemTypes: items }); + }); + + it('renders all work items', () => { + expect(wrapper.findAllByTestId('work-item-wrapper')).toHaveLength(items.length); + }); + + it('does not render badges', () => { + expect(wrapper.find(GlBadge).exists()).toBe(false); + }); + }); + + describe('unavailable structure', () => { + let items = []; + + beforeEach(() => { + items = workItemsFromResponse(RESPONSE.premium).unavailable; + createComponent({ workItemTypes: items }); + }); + + it('renders all work items', () => { + expect(wrapper.findAllByTestId('work-item-wrapper')).toHaveLength(items.length); + }); + + it('renders license badges for all work items', () => { + expect(wrapper.findAll(GlBadge)).toHaveLength(items.length); + }); + + it('does not render svg icon for linking', () => { + expect(wrapper.findByTestId('hierarchy-rounded-arrow-tail').exists()).toBe(false); + expect(wrapper.findByTestId('level-up-icon').exists()).toBe(false); + }); + }); + + describe('nested work items', () => { + describe.each` + licensePlan | arrowTailVisible | levelUpIconVisible | arrowDownIconVisible + ${'ultimate'} | ${true} | ${true} | ${true} + ${'premium'} | ${false} | ${false} | ${true} + ${'free'} | ${false} | ${false} | ${false} + `( + 'when $licensePlan license', + ({ licensePlan, arrowTailVisible, levelUpIconVisible, arrowDownIconVisible }) => { + let items = []; + beforeEach(() => { + items = workItemsFromResponse(RESPONSE[licensePlan]).available; + createComponent({ workItemTypes: items }); + }); + + it(`${arrowTailVisible ? 'render' : 'does not render'} arrow tail svg`, () => { + expect(wrapper.findByTestId('hierarchy-rounded-arrow-tail').exists()).toBe( + arrowTailVisible, + ); + }); + + it(`${levelUpIconVisible ? 'render' : 'does not render'} arrow tail svg`, () => { + expect(wrapper.findByTestId('level-up-icon').exists()).toBe(levelUpIconVisible); + }); + + it(`${arrowDownIconVisible ? 'render' : 'does not render'} arrow tail svg`, () => { + expect(wrapper.findByTestId('arrow-down-icon').exists()).toBe(arrowDownIconVisible); + }); + }, + ); + }); +}); diff --git a/spec/frontend/work_items_hierarchy/hierarchy_util_spec.js b/spec/frontend/work_items_hierarchy/hierarchy_util_spec.js new file mode 100644 index 00000000000..9042fa27d16 --- /dev/null +++ b/spec/frontend/work_items_hierarchy/hierarchy_util_spec.js @@ -0,0 +1,16 @@ +import { inferLicensePlan } from '~/work_items_hierarchy/hierarchy_util'; +import { LICENSE_PLAN } from '~/work_items_hierarchy/constants'; + +describe('inferLicensePlan', () => { + it.each` + epics | subEpics | licensePlan + ${true} | ${true} | ${LICENSE_PLAN.ULTIMATE} + ${true} | ${false} | ${LICENSE_PLAN.PREMIUM} + ${false} | ${false} | ${LICENSE_PLAN.FREE} + `( + 'returns $licensePlan when epic is $epics and sub-epic is $subEpics', + ({ epics, subEpics, licensePlan }) => { + expect(inferLicensePlan({ hasEpics: epics, hasSubEpics: subEpics })).toBe(licensePlan); + }, + ); +}); diff --git a/spec/lib/sidebars/concerns/work_item_hierarchy_spec.rb b/spec/lib/sidebars/concerns/work_item_hierarchy_spec.rb new file mode 100644 index 00000000000..f0a5e032764 --- /dev/null +++ b/spec/lib/sidebars/concerns/work_item_hierarchy_spec.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Sidebars::Concerns::WorkItemHierarchy do + shared_examples 'hierarchy menu' do + let(:item_id) { :hierarchy } + + context 'when the feature is disabled does not render' do + before do + stub_feature_flags(work_items_hierarchy: false) + end + + specify { is_expected.to be_nil } + end + + context 'when the feature is enabled does render' do + before do + stub_feature_flags(work_items_hierarchy: true) + end + + specify { is_expected.not_to be_nil } + end + end + + describe 'Project hierarchy menu item' do + let_it_be_with_reload(:project) { create(:project, :repository) } + + let(:user) { project.owner } + let(:context) { Sidebars::Projects::Context.new(current_user: user, container: project) } + + subject { Sidebars::Projects::Menus::ProjectInformationMenu.new(context).renderable_items.index { |e| e.item_id == item_id } } + + it_behaves_like 'hierarchy menu' + end +end diff --git a/spec/lib/sidebars/projects/menus/project_information_menu_spec.rb b/spec/lib/sidebars/projects/menus/project_information_menu_spec.rb index 7e8d0ab0518..5cbf0a13b66 100644 --- a/spec/lib/sidebars/projects/menus/project_information_menu_spec.rb +++ b/spec/lib/sidebars/projects/menus/project_information_menu_spec.rb @@ -59,5 +59,25 @@ RSpec.describe Sidebars::Projects::Menus::ProjectInformationMenu do specify { is_expected.to be_nil } end end + + describe 'Hierarchy' do + let(:item_id) { :hierarchy } + + context 'when the feature is disabled' do + before do + stub_feature_flags(work_items_hierarchy: false) + end + + specify { is_expected.to be_nil } + end + + context 'when the feature is enabled' do + before do + stub_feature_flags(work_items_hierarchy: true) + end + + specify { is_expected.not_to be_nil } + end + end end end diff --git a/spec/migrations/cleanup_after_add_primary_email_to_emails_if_user_confirmed_spec.rb b/spec/migrations/cleanup_after_add_primary_email_to_emails_if_user_confirmed_spec.rb new file mode 100644 index 00000000000..abff7c6aba1 --- /dev/null +++ b/spec/migrations/cleanup_after_add_primary_email_to_emails_if_user_confirmed_spec.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require 'spec_helper' +require_migration! + +RSpec.describe CleanupAfterAddPrimaryEmailToEmailsIfUserConfirmed, :sidekiq do + let(:migration) { described_class.new } + let(:users) { table(:users) } + let(:emails) { table(:emails) } + + let!(:user_1) { users.create!(name: 'confirmed-user-1', email: 'confirmed-1@example.com', confirmed_at: 3.days.ago, projects_limit: 100) } + let!(:user_2) { users.create!(name: 'confirmed-user-2', email: 'confirmed-2@example.com', confirmed_at: 1.day.ago, projects_limit: 100) } + let!(:user_3) { users.create!(name: 'confirmed-user-3', email: 'confirmed-3@example.com', confirmed_at: 1.day.ago, projects_limit: 100) } + let!(:user_4) { users.create!(name: 'unconfirmed-user', email: 'unconfirmed@example.com', confirmed_at: nil, projects_limit: 100) } + + let!(:email_1) { emails.create!(email: 'confirmed-1@example.com', user_id: user_1.id, confirmed_at: 1.day.ago) } + let!(:email_2) { emails.create!(email: 'other_2@example.com', user_id: user_2.id, confirmed_at: 1.day.ago) } + + before do + stub_const("#{described_class.name}::BATCH_SIZE", 2) + end + + it 'consume any pending background migration job' do + expect_next_instance_of(Gitlab::BackgroundMigration::JobCoordinator) do |coordinator| + expect(coordinator).to receive(:steal).with('AddPrimaryEmailToEmailsIfUserConfirmed').twice + end + + migration.up + end + + it 'adds the primary email to emails for leftover confirmed users that do not have their primary email in the emails table', :aggregate_failures do + original_email_1_confirmed_at = email_1.reload.confirmed_at + + expect { migration.up }.to change { emails.count }.by(2) + + expect(emails.find_by(user_id: user_2.id, email: 'confirmed-2@example.com').confirmed_at).to eq(user_2.reload.confirmed_at) + expect(emails.find_by(user_id: user_3.id, email: 'confirmed-3@example.com').confirmed_at).to eq(user_3.reload.confirmed_at) + expect(email_1.reload.confirmed_at).to eq(original_email_1_confirmed_at) + + expect(emails.exists?(user_id: user_4.id)).to be(false) + end + + it 'continues in case of errors with one email' do + allow(Email).to receive(:create) { raise 'boom!' } + + expect { migration.up }.not_to raise_error + end +end diff --git a/spec/support/shared_contexts/navbar_structure_context.rb b/spec/support/shared_contexts/navbar_structure_context.rb index 27967850389..576a8aa44fa 100644 --- a/spec/support/shared_contexts/navbar_structure_context.rb +++ b/spec/support/shared_contexts/navbar_structure_context.rb @@ -22,6 +22,7 @@ RSpec.shared_context 'project navbar structure' do nav_sub_items: [ _('Activity'), _('Labels'), + _('Planning hierarchy'), _('Members') ] }, diff --git a/spec/support/shared_contexts/policies/project_policy_shared_context.rb b/spec/support/shared_contexts/policies/project_policy_shared_context.rb index c39252cef13..aabd6454b88 100644 --- a/spec/support/shared_contexts/policies/project_policy_shared_context.rb +++ b/spec/support/shared_contexts/policies/project_policy_shared_context.rb @@ -17,7 +17,7 @@ RSpec.shared_context 'ProjectPolicy context' do %i[ award_emoji create_issue create_merge_request_in create_note create_project read_issue_board read_issue read_issue_iid read_issue_link - read_label read_issue_board_list read_milestone read_note read_project + read_label read_work_items_hierarchy read_issue_board_list read_milestone read_note read_project read_project_for_iids read_project_member read_release read_snippet read_wiki upload_file ] |