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>2022-01-17 18:16:12 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-01-17 18:16:12 +0300
commit8432be20de0a29f4dde4980efe37d013c9e90034 (patch)
treeaf163db5a7c8ac17ca4da59d505c75552452956c /spec
parentdd33e917374b611cd5a596c7fa51b47af6e153f6 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
-rw-r--r--spec/controllers/concerns/work_items_hierarchy_spec.rb39
-rw-r--r--spec/features/profile_spec.rb63
-rw-r--r--spec/features/profiles/personal_access_tokens_spec.rb11
-rw-r--r--spec/features/projects/features_visibility_spec.rb4
-rw-r--r--spec/features/projects/settings/project_settings_spec.rb4
-rw-r--r--spec/features/projects/settings/user_manages_merge_requests_settings_spec.rb6
-rw-r--r--spec/features/projects/user_changes_project_visibility_spec.rb88
-rw-r--r--spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js42
-rw-r--r--spec/frontend/work_items_hierarchy/components/__snapshots__/app_spec.js.snap1197
-rw-r--r--spec/frontend/work_items_hierarchy/components/app_spec.js78
-rw-r--r--spec/frontend/work_items_hierarchy/components/hierarchy_spec.js118
-rw-r--r--spec/frontend/work_items_hierarchy/hierarchy_util_spec.js16
-rw-r--r--spec/lib/sidebars/concerns/work_item_hierarchy_spec.rb36
-rw-r--r--spec/lib/sidebars/projects/menus/project_information_menu_spec.rb20
-rw-r--r--spec/migrations/cleanup_after_add_primary_email_to_emails_if_user_confirmed_spec.rb48
-rw-r--r--spec/support/shared_contexts/navbar_structure_context.rb1
-rw-r--r--spec/support/shared_contexts/policies/project_policy_shared_context.rb2
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
]