Commit b2d14975 authored by Mario de la Ossa's avatar Mario de la Ossa

Related Issues - move to FOSS

This is the final commit that moves Related Issues to FOSS
parent 787ad7eb
import initSidebarBundle from '~/sidebar/sidebar_bundle'; import initSidebarBundle from '~/sidebar/sidebar_bundle';
import initRelatedIssues from '~/related_issues';
import initShow from '../show'; import initShow from '../show';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
...@@ -6,4 +7,5 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -6,4 +7,5 @@ document.addEventListener('DOMContentLoaded', () => {
if (gon.features && !gon.features.vueIssuableSidebar) { if (gon.features && !gon.features.vueIssuableSidebar) {
initSidebarBundle(); initSidebarBundle();
} }
initRelatedIssues();
}); });
<script> <script>
import { GlLoadingIcon } from '@gitlab/ui'; import { GlLoadingIcon } from '@gitlab/ui';
import Sortable from 'sortablejs'; import Sortable from 'sortablejs';
import sortableConfig from 'ee/sortable/sortable_config'; import sortableConfig from 'ee_else_ce/sortable/sortable_config';
import RelatedIssuableItem from '~/vue_shared/components/issue/related_issuable_item.vue'; import RelatedIssuableItem from '~/vue_shared/components/issue/related_issuable_item.vue';
import tooltip from '~/vue_shared/directives/tooltip'; import tooltip from '~/vue_shared/directives/tooltip';
......
...@@ -17,6 +17,9 @@ export default function initRelatedIssues() { ...@@ -17,6 +17,9 @@ export default function initRelatedIssues() {
endpoint: relatedIssuesRootElement.dataset.endpoint, endpoint: relatedIssuesRootElement.dataset.endpoint,
canAdmin: parseBoolean(relatedIssuesRootElement.dataset.canAddRelatedIssues), canAdmin: parseBoolean(relatedIssuesRootElement.dataset.canAddRelatedIssues),
helpPath: relatedIssuesRootElement.dataset.helpPath, helpPath: relatedIssuesRootElement.dataset.helpPath,
showCategorizedIssues: parseBoolean(
relatedIssuesRootElement.dataset.showCategorizedIssues,
),
}, },
}), }),
}); });
......
- if can?(current_user, :read_issue_link, @project)
.js-related-issues-root{ data: { endpoint: project_issue_links_path(@project, @issue),
can_add_related_issues: "#{can?(current_user, :admin_issue_link, @issue)}",
help_path: help_page_path('user/project/issues/related_issues'),
show_categorized_issues: "false" } }
- render('projects/issues/related_issues_block')
.related-issues-block
.card.card-slim
.card-header.panel-empty-heading.border-bottom-0
%h3.card-title.mt-0.mb-0.h5
= _('Linked issues')
---
title: Move related issues to core
merge_request: 39779
author:
type: changed
<script> <script>
import { mapState, mapGetters } from 'vuex'; import { mapState, mapGetters } from 'vuex';
import { PathIdSeparator } from 'ee/related_issues/constants'; import { PathIdSeparator } from '~/related_issues/constants';
import IssuableBody from '~/issue_show/components/app.vue'; import IssuableBody from '~/issue_show/components/app.vue';
import IssuableSidebar from '~/issuable_sidebar/components/sidebar_app.vue'; import IssuableSidebar from '~/issuable_sidebar/components/sidebar_app.vue';
......
...@@ -11,7 +11,7 @@ import { ...@@ -11,7 +11,7 @@ import {
GlSprintf, GlSprintf,
} from '@gitlab/ui'; } from '@gitlab/ui';
import Api from 'ee/api'; import Api from 'ee/api';
import RelatedIssuesRoot from 'ee/related_issues/components/related_issues_root.vue'; import RelatedIssuesRoot from '~/related_issues/components/related_issues_root.vue';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import { deprecatedCreateFlash as flash, FLASH_TYPES } from '~/flash'; import { deprecatedCreateFlash as flash, FLASH_TYPES } from '~/flash';
import featureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import featureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
......
import initSidebarBundle from 'ee/sidebar/sidebar_bundle'; import initSidebarBundle from 'ee/sidebar/sidebar_bundle';
import initRelatedIssues from 'ee/related_issues';
import trackShowInviteMemberLink from 'ee/projects/track_invite_members'; import trackShowInviteMemberLink from 'ee/projects/track_invite_members';
import initRelatedIssues from '~/related_issues';
import initShow from '~/pages/projects/issues/show'; import initShow from '~/pages/projects/issues/show';
import UserCallout from '~/user_callout'; import UserCallout from '~/user_callout';
......
<script> <script>
import RelatedIssuableInput from 'ee/related_issues/components/related_issuable_input.vue'; import RelatedIssuableInput from '~/related_issues/components/related_issuable_input.vue';
import { issuableTypesMap } from 'ee/related_issues/constants'; import { issuableTypesMap } from '~/related_issues/constants';
export default { export default {
components: { components: {
......
...@@ -3,7 +3,7 @@ import { mapState, mapActions, mapGetters } from 'vuex'; ...@@ -3,7 +3,7 @@ import { mapState, mapActions, mapGetters } from 'vuex';
import { GlLoadingIcon } from '@gitlab/ui'; import { GlLoadingIcon } from '@gitlab/ui';
import AddItemForm from 'ee/related_issues/components/add_issuable_form.vue'; import AddItemForm from '~/related_issues/components/add_issuable_form.vue';
import SlotSwitch from '~/vue_shared/components/slot_switch.vue'; import SlotSwitch from '~/vue_shared/components/slot_switch.vue';
import CreateEpicForm from './create_epic_form.vue'; import CreateEpicForm from './create_epic_form.vue';
import CreateIssueForm from './create_issue_form.vue'; import CreateIssueForm from './create_issue_form.vue';
......
...@@ -3,7 +3,7 @@ import { mapState, mapActions } from 'vuex'; ...@@ -3,7 +3,7 @@ import { mapState, mapActions } from 'vuex';
import { GlTooltip, GlIcon } from '@gitlab/ui'; import { GlTooltip, GlIcon } from '@gitlab/ui';
import { issuableTypesMap } from 'ee/related_issues/constants'; import { issuableTypesMap } from '~/related_issues/constants';
import EpicActionsSplitButton from './epic_issue_actions_split_button.vue'; import EpicActionsSplitButton from './epic_issue_actions_split_button.vue';
import EpicHealthStatus from './epic_health_status.vue'; import EpicHealthStatus from './epic_health_status.vue';
......
...@@ -4,7 +4,7 @@ import { ...@@ -4,7 +4,7 @@ import {
itemAddFailureTypesMap, itemAddFailureTypesMap,
pathIndeterminateErrorMap, pathIndeterminateErrorMap,
relatedIssuesRemoveErrorMap, relatedIssuesRemoveErrorMap,
} from 'ee/related_issues/constants'; } from '~/related_issues/constants';
import { deprecatedCreateFlash as flash } from '~/flash'; import { deprecatedCreateFlash as flash } from '~/flash';
import { s__, __ } from '~/locale'; import { s__, __ } from '~/locale';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
......
import { issuableTypesMap, PathIdSeparator } from 'ee/related_issues/constants'; import { issuableTypesMap, PathIdSeparator } from '~/related_issues/constants';
export const autoCompleteSources = () => gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources; export const autoCompleteSources = () => gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources;
......
import Vue from 'vue'; import Vue from 'vue';
import { issuableTypesMap } from 'ee/related_issues/constants'; import { issuableTypesMap } from '~/related_issues/constants';
import * as types from './mutation_types'; import * as types from './mutation_types';
export default { export default {
......
import { PathIdSeparator } from 'ee/related_issues/constants'; import { PathIdSeparator } from '~/related_issues/constants';
import createGqClient, { fetchPolicies } from '~/lib/graphql'; import createGqClient, { fetchPolicies } from '~/lib/graphql';
import { ChildType } from '../constants'; import { ChildType } from '../constants';
......
<script> <script>
import axios from 'axios'; import axios from 'axios';
import { GlButton } from '@gitlab/ui'; import { GlButton } from '@gitlab/ui';
import RelatedIssuesStore from 'ee/related_issues/stores/related_issues_store'; import RelatedIssuesStore from '~/related_issues/stores/related_issues_store';
import RelatedIssuesBlock from 'ee/related_issues/components/related_issues_block.vue'; import RelatedIssuesBlock from '~/related_issues/components/related_issues_block.vue';
import { issuableTypesMap, PathIdSeparator } from 'ee/related_issues/constants'; import { issuableTypesMap, PathIdSeparator } from '~/related_issues/constants';
import { sprintf, __, s__ } from '~/locale'; import { sprintf, __, s__ } from '~/locale';
import { joinPaths, redirectTo } from '~/lib/utils/url_utility'; import { joinPaths, redirectTo } from '~/lib/utils/url_utility';
import { RELATED_ISSUES_ERRORS, FEEDBACK_TYPES } from '../constants'; import { RELATED_ISSUES_ERRORS, FEEDBACK_TYPES } from '../constants';
......
- if can?(current_user, :read_issue_link, @project) - if can?(current_user, :read_issue_link, @project)
.js-related-issues-root{ data: { endpoint: project_issue_links_path(@project, @issue), .js-related-issues-root{ data: { endpoint: project_issue_links_path(@project, @issue),
can_add_related_issues: "#{can?(current_user, :admin_issue_link, @issue)}", can_add_related_issues: "#{can?(current_user, :admin_issue_link, @issue)}",
help_path: help_page_path('user/project/issues/related_issues') } } help_path: help_page_path('user/project/issues/related_issues'),
.related-issues-block show_categorized_issues: "#{!!@project.feature_available?(:blocked_issues)}" } }
.card.card-slim - render('projects/issues/related_issues_block')
.card-header.panel-empty-heading.border-bottom-0
%h3.card-title.mt-0.mb-0.h5
= _('Linked issues')
...@@ -14,177 +14,6 @@ RSpec.describe 'Related issues', :js do ...@@ -14,177 +14,6 @@ RSpec.describe 'Related issues', :js do
let_it_be(:issue_project_b_a) { create(:issue, project: project_b) } let_it_be(:issue_project_b_a) { create(:issue, project: project_b) }
let_it_be(:issue_project_unauthorized_a) { create(:issue, project: project_unauthorized) } let_it_be(:issue_project_unauthorized_a) { create(:issue, project: project_unauthorized) }
context 'widget visibility' do
let_it_be(:issue, reload: true) { create(:issue, project: project) }
let_it_be(:project_private) { create(:project_empty_repo, :private) }
let_it_be(:project_internal) { create(:project_empty_repo, :internal) }
let_it_be(:issue_private) { create(:issue, project: project_private) }
let_it_be(:issue_internal) { create(:issue, project: project_internal) }
before do
stub_licensed_features(blocked_issues: true)
end
context 'when not logged in' do
it 'does not show widget when internal project' do
visit project_issue_path(project_internal, issue_internal)
expect(page).not_to have_css('.related-issues-block')
end
it 'does not show widget when private project' do
visit project_issue_path(project_private, issue_private)
expect(page).not_to have_css('.related-issues-block')
end
it 'shows widget when public project' do
visit project_issue_path(project, issue)
expect(page).to have_css('.related-issues-block')
expect(page).not_to have_selector('.js-issue-count-badge-add-button')
end
end
context 'when logged in but not a member' do
before do
gitlab_sign_in(user)
end
it 'shows widget when internal project' do
visit project_issue_path(project_internal, issue_internal)
expect(page).to have_css('.related-issues-block')
expect(page).not_to have_selector('.js-issue-count-badge-add-button')
end
it 'does not show widget when private project' do
visit project_issue_path(project_private, issue_private)
expect(page).not_to have_css('.related-issues-block')
end
it 'shows widget when public project' do
visit project_issue_path(project, issue)
expect(page).to have_css('.related-issues-block')
expect(page).not_to have_selector('.js-issue-count-badge-add-button')
end
it 'shows widget on their own public issue' do
issue.update!(author: user)
visit project_issue_path(project, issue)
expect(page).to have_css('.related-issues-block')
expect(page).not_to have_selector('.js-issue-count-badge-add-button')
end
end
context 'when logged in and a guest' do
before do
project_internal.add_guest(user)
project_private.add_guest(user)
project.add_guest(user)
gitlab_sign_in(user)
end
it 'shows widget when internal project' do
visit project_issue_path(project_internal, issue_internal)
expect(page).to have_css('.related-issues-block')
expect(page).not_to have_selector('.js-issue-count-badge-add-button')
end
it 'shows widget when private project' do
visit project_issue_path(project_private, issue_private)
expect(page).to have_css('.related-issues-block')
expect(page).not_to have_selector('.js-issue-count-badge-add-button')
end
it 'shows widget when public project' do
visit project_issue_path(project, issue)
expect(page).to have_css('.related-issues-block')
expect(page).not_to have_selector('.js-issue-count-badge-add-button')
end
end
context 'when logged in and a reporter' do
before do
project_internal.add_reporter(user)
project_private.add_reporter(user)
project.add_reporter(user)
gitlab_sign_in(user)
end
it 'shows widget when internal project' do
visit project_issue_path(project_internal, issue_internal)
expect(page).to have_css('.related-issues-block')
expect(page).to have_selector('.js-issue-count-badge-add-button')
end
it 'shows widget when private project' do
visit project_issue_path(project_private, issue_private)
expect(page).to have_css('.related-issues-block')
expect(page).to have_selector('.js-issue-count-badge-add-button')
end
it 'shows widget when public project' do
visit project_issue_path(project, issue)
expect(page).to have_css('.related-issues-block')
expect(page).to have_selector('.js-issue-count-badge-add-button')
end
it 'shows widget on their own public issue' do
issue.update!(author: user)
visit project_issue_path(project, issue)
expect(page).to have_css('.related-issues-block')
expect(page).to have_selector('.js-issue-count-badge-add-button')
end
end
end
context 'when user has no permission to manage related issues' do
let_it_be(:issue_link_b) { create :issue_link, source: issue_a, target: issue_b }
let_it_be(:issue_link_c) { create :issue_link, source: issue_a, target: issue_c }
before do
stub_licensed_features(blocked_issues: true)
project.add_guest(user)
gitlab_sign_in(user)
end
context 'visiting some issue someone else created' do
before do
visit project_issue_path(project, issue_a)
wait_for_requests
end
it 'shows related issues count' do
expect(find('.js-related-issues-header-issue-count')).to have_content('2')
end
end
context 'visiting issue_b which was targeted by issue_a' do
before do
visit project_issue_path(project, issue_b)
wait_for_requests
end
it 'shows related issues count' do
expect(find('.js-related-issues-header-issue-count')).to have_content('1')
end
end
end
context 'when user has permission to manage related issues' do context 'when user has permission to manage related issues' do
before do before do
project.add_maintainer(user) project.add_maintainer(user)
...@@ -192,300 +21,102 @@ RSpec.describe 'Related issues', :js do ...@@ -192,300 +21,102 @@ RSpec.describe 'Related issues', :js do
gitlab_sign_in(user) gitlab_sign_in(user)
end end
context 'with related_issues disabled' do context 'with "Relates to", "Blocks", "Is blocked by" groupings' do
let_it_be(:issue_link_b) { create :issue_link, source: issue_a, target: issue_b } def add_linked_issue(issue, radio_input_value)
let_it_be(:issue_link_c) { create :issue_link, source: issue_a, target: issue_c } find('.js-issue-count-badge-add-button').click
find('.js-add-issuable-form-input').set "#{issue.to_reference(project)} "
find("input[name=\"linked-issue-type-radio\"][value=\"#{radio_input_value}\"]").click
find('.js-add-issuable-form-add-button').click
before do
visit project_issue_path(project, issue_a)
wait_for_requests wait_for_requests
end end
it 'does not show the related issues block' do
expect(page).not_to have_selector('.js-related-issues-root')
end
end
context 'with related_issues enabled' do
before do before do
stub_licensed_features(blocked_issues: true) visit project_issue_path(project, issue_a)
wait_for_requests
end end
context 'without existing related issues' do context 'when adding a "relates_to" issue' do
before do before do
visit project_issue_path(project, issue_a) add_linked_issue(issue_b, "relates_to")
wait_for_requests
end end
it 'shows related issues count' do it 'shows "Relates to" heading' do
expect(find('.js-related-issues-header-issue-count')).to have_content('0') headings = all('.linked-issues-card-body h4')
end
it 'add related issue' do
find('.js-issue-count-badge-add-button').click
find('.js-add-issuable-form-input').set "#{issue_b.to_reference(project)} "
find('.js-add-issuable-form-add-button').click
wait_for_requests
items = all('.item-title a')
# Form gets hidden after submission expect(headings.count).to eq(1)
expect(page).not_to have_selector('.js-add-related-issues-form-area') expect(headings[0].text).to eq("Relates to")
# Check if related issues are present
expect(items.count).to eq(1)
expect(items[0].text).to eq(issue_b.title)
expect(find('.js-related-issues-header-issue-count')).to have_content('1')
end
it 'add cross-project related issue' do
find('.js-issue-count-badge-add-button').click
find('.js-add-issuable-form-input').set "#{issue_project_b_a.to_reference(project)} "
find('.js-add-issuable-form-add-button').click
wait_for_requests
items = all('.item-title a')
expect(items.count).to eq(1)
expect(items[0].text).to eq(issue_project_b_a.title)
expect(find('.js-related-issues-header-issue-count')).to have_content('1')
end end
it 'pressing enter should submit the form' do it 'shows the added issue' do
find('.js-issue-count-badge-add-button').click
find('.js-add-issuable-form-input').set "#{issue_project_b_a.to_reference(project)} "
find('.js-add-issuable-form-input').native.send_key(:enter)
wait_for_requests
items = all('.item-title a') items = all('.item-title a')
expect(items.count).to eq(1) expect(items[0].text).to eq(issue_b.title)
expect(items[0].text).to eq(issue_project_b_a.title)
expect(find('.js-related-issues-header-issue-count')).to have_content('1') expect(find('.js-related-issues-header-issue-count')).to have_content('1')
end end
it 'disallows duplicate entries' do
find('.js-issue-count-badge-add-button').click
find('.js-add-issuable-form-input').set 'duplicate duplicate duplicate'
items = all('.js-add-issuable-form-token-list-item')
expect(items.count).to eq(1)
expect(items[0].text).to eq('duplicate')
# Pending issues aren't counted towards the related issue count
expect(find('.js-related-issues-header-issue-count')).to have_content('0')
end
it 'allows us to remove pending issues' do
# Tests against https://gitlab.com/gitlab-org/gitlab/issues/11625
find('.js-issue-count-badge-add-button').click
find('.js-add-issuable-form-input').set 'issue1 issue2 issue3 '
items = all('.js-add-issuable-form-token-list-item')
expect(items.count).to eq(3)
expect(items[0].text).to eq('issue1')
expect(items[1].text).to eq('issue2')
expect(items[2].text).to eq('issue3')
# Remove pending issues left to right to make sure none get stuck
items[0].find('.js-issue-token-remove-button').click
items = all('.js-add-issuable-form-token-list-item')
expect(items.count).to eq(2)
expect(items[0].text).to eq('issue2')
expect(items[1].text).to eq('issue3')
items[0].find('.js-issue-token-remove-button').click
items = all('.js-add-issuable-form-token-list-item')
expect(items.count).to eq(1)
expect(items[0].text).to eq('issue3')
items[0].find('.js-issue-token-remove-button').click
items = all('.js-add-issuable-form-token-list-item')
expect(items.count).to eq(0)
end
end end
context 'with existing related issues' do context 'when adding a "blocks" issue' do
let_it_be(:issue_link_b) { create :issue_link, source: issue_a, target: issue_b }
let_it_be(:issue_link_c) { create :issue_link, source: issue_a, target: issue_c }
before do before do
visit project_issue_path(project, issue_a) add_linked_issue(issue_b, "blocks")
wait_for_requests
end
it 'shows related issues count' do
expect(find('.js-related-issues-header-issue-count')).to have_content('2')
end
it 'shows related issues' do
items = all('.item-title a')
expect(items.count).to eq(2)
expect(items[0].text).to eq(issue_b.title)
expect(items[1].text).to eq(issue_c.title)
end end
it 'allows us to remove a related issues' do it 'shows "Blocks" heading' do
items_before = all('.item-title a') headings = all('.linked-issues-card-body h4')
expect(items_before.count).to eq(2) expect(headings.count).to eq(1)
expect(headings[0].text).to eq("Blocks")
first('.js-issue-item-remove-button').click
wait_for_requests
items_after = all('.item-title a')
expect(items_after.count).to eq(1)
end end
it 'add related issue' do it 'shows the added issue' do
find('.js-issue-count-badge-add-button').click
find('.js-add-issuable-form-input').set "##{issue_d.iid} "
find('.js-add-issuable-form-add-button').click
wait_for_requests
items = all('.item-title a') items = all('.item-title a')
expect(items.count).to eq(3)
expect(items[0].text).to eq(issue_b.title) expect(items[0].text).to eq(issue_b.title)
expect(items[1].text).to eq(issue_c.title) expect(find('.js-related-issues-header-issue-count')).to have_content('1')
expect(items[2].text).to eq(issue_d.title)
expect(find('.js-related-issues-header-issue-count')).to have_content('3')
end end
end
it 'add invalid related issue' do context 'when adding an "is_blocked_by" issue' do
find('.js-issue-count-badge-add-button').click before do
find('.js-add-issuable-form-input').set "#9999999 " add_linked_issue(issue_b, "is_blocked_by")
find('.js-add-issuable-form-add-button').click
wait_for_requests
items = all('.item-title a')
expect(items.count).to eq(2)
expect(items[0].text).to eq(issue_b.title)
expect(items[1].text).to eq(issue_c.title)
expect(find('.js-related-issues-header-issue-count')).to have_content('2')
end end
it 'add unauthorized related issue' do it 'shows "Is blocked by" heading' do
find('.js-issue-count-badge-add-button').click headings = all('.linked-issues-card-body h4')
find('.js-add-issuable-form-input').set "#{issue_project_unauthorized_a.to_reference(project)} "
find('.js-add-issuable-form-add-button').click
wait_for_requests expect(headings.count).to eq(1)
expect(headings[0].text).to eq("Is blocked by")
end
it 'shows the added issue' do
items = all('.item-title a') items = all('.item-title a')
expect(items.count).to eq(2)
expect(items[0].text).to eq(issue_b.title) expect(items[0].text).to eq(issue_b.title)
expect(items[1].text).to eq(issue_c.title) expect(find('.js-related-issues-header-issue-count')).to have_content('1')
expect(find('.js-related-issues-header-issue-count')).to have_content('2')
end end
end end
context 'with "Relates to", "Blocks", "Is blocked by" groupings' do context 'when adding "relates_to", "blocks", and "is_blocked_by" issues' do
def add_linked_issue(issue, radio_input_value)
find('.js-issue-count-badge-add-button').click
find('.js-add-issuable-form-input').set "#{issue.to_reference(project)} "
find("input[name=\"linked-issue-type-radio\"][value=\"#{radio_input_value}\"]").click
find('.js-add-issuable-form-add-button').click
wait_for_requests
end
before do before do
visit project_issue_path(project, issue_a) add_linked_issue(issue_b, "relates_to")
wait_for_requests add_linked_issue(issue_c, "blocks")
add_linked_issue(issue_d, "is_blocked_by")
end end
context 'when adding a "relates_to" issue' do it 'shows "Blocks", "Is blocked by", and "Relates to" headings' do
before do headings = all('.linked-issues-card-body h4')
add_linked_issue(issue_b, "relates_to")
end
it 'shows "Relates to" heading' do expect(headings.count).to eq(3)
headings = all('.linked-issues-card-body h4') expect(headings[0].text).to eq("Blocks")
expect(headings[1].text).to eq("Is blocked by")
expect(headings.count).to eq(1) expect(headings[2].text).to eq("Relates to")
expect(headings[0].text).to eq("Relates to")
end
it 'shows the added issue' do
items = all('.item-title a')
expect(items[0].text).to eq(issue_b.title)
expect(find('.js-related-issues-header-issue-count')).to have_content('1')
end
end end
context 'when adding a "blocks" issue' do it 'shows all added issues' do
before do items = all('.item-title a')
add_linked_issue(issue_b, "blocks")
end
it 'shows "Blocks" heading' do
headings = all('.linked-issues-card-body h4')
expect(headings.count).to eq(1)
expect(headings[0].text).to eq("Blocks")
end
it 'shows the added issue' do
items = all('.item-title a')
expect(items[0].text).to eq(issue_b.title)
expect(find('.js-related-issues-header-issue-count')).to have_content('1')
end
end
context 'when adding an "is_blocked_by" issue' do
before do
add_linked_issue(issue_b, "is_blocked_by")
end
it 'shows "Is blocked by" heading' do
headings = all('.linked-issues-card-body h4')
expect(headings.count).to eq(1)
expect(headings[0].text).to eq("Is blocked by")
end
it 'shows the added issue' do
items = all('.item-title a')
expect(items[0].text).to eq(issue_b.title)
expect(find('.js-related-issues-header-issue-count')).to have_content('1')
end
end
context 'when adding "relates_to", "blocks", and "is_blocked_by" issues' do
before do
add_linked_issue(issue_b, "relates_to")
add_linked_issue(issue_c, "blocks")
add_linked_issue(issue_d, "is_blocked_by")
end
it 'shows "Blocks", "Is blocked by", and "Relates to" headings' do
headings = all('.linked-issues-card-body h4')
expect(headings.count).to eq(3)
expect(headings[0].text).to eq("Blocks")
expect(headings[1].text).to eq("Is blocked by")
expect(headings[2].text).to eq("Relates to")
end
it 'shows all added issues' do
items = all('.item-title a')
expect(items.count).to eq(3) expect(items.count).to eq(3)
expect(find('.js-related-issues-header-issue-count')).to have_content('3') expect(find('.js-related-issues-header-issue-count')).to have_content('3')
end
end end
end end
end end
......
...@@ -13,7 +13,7 @@ import { ...@@ -13,7 +13,7 @@ import {
LEGACY_FLAG, LEGACY_FLAG,
NEW_VERSION_FLAG, NEW_VERSION_FLAG,
} from 'ee/feature_flags/constants'; } from 'ee/feature_flags/constants';
import RelatedIssuesRoot from 'ee/related_issues/components/related_issues_root.vue'; import RelatedIssuesRoot from '~/related_issues/components/related_issues_root.vue';
import ToggleButton from '~/vue_shared/components/toggle_button.vue'; import ToggleButton from '~/vue_shared/components/toggle_button.vue';
import { featureFlag, userList, allUsersStrategy } from '../mock_data'; import { featureFlag, userList, allUsersStrategy } from '../mock_data';
......
import { mount, shallowMount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import { issuable1 } from 'jest/vue_shared/components/issue/related_issuable_mock_data';
import IssueWeight from 'ee/boards/components/issue_card_weight.vue'; import IssueWeight from 'ee/boards/components/issue_card_weight.vue';
import RelatedIssuesList from 'ee/related_issues/components/related_issues_list.vue'; import RelatedIssuesList from '~/related_issues/components/related_issues_list.vue';
import { import { PathIdSeparator } from '~/related_issues/constants';
issuable1,
issuable2,
issuable3,
issuable4,
issuable5,
} from 'jest/vue_shared/components/issue/related_issuable_mock_data';
import { PathIdSeparator } from 'ee/related_issues/constants';
import IssueDueDate from '~/boards/components/issue_due_date.vue';
/*
* Here we only test the behavior of Related Issues with Weight, as weight is an EE-only feature.
*/
describe('RelatedIssuesList', () => { describe('RelatedIssuesList', () => {
let wrapper; let wrapper;
...@@ -18,156 +14,6 @@ describe('RelatedIssuesList', () => { ...@@ -18,156 +14,6 @@ describe('RelatedIssuesList', () => {
wrapper.destroy(); wrapper.destroy();
}); });
describe('with defaults', () => {
const heading = 'Related to';
beforeEach(() => {
wrapper = shallowMount(RelatedIssuesList, {
propsData: {
pathIdSeparator: PathIdSeparator.Issue,
issuableType: 'issue',
heading,
},
});
});
it('shows a heading', () => {
expect(wrapper.find('h4').text()).toContain(heading);
});
it('should not show loading icon', () => {
expect(wrapper.vm.$refs.loadingIcon).toBeUndefined();
});
});
describe('with isFetching=true', () => {
beforeEach(() => {
wrapper = shallowMount(RelatedIssuesList, {
propsData: {
pathIdSeparator: PathIdSeparator.Issue,
isFetching: true,
issuableType: 'issue',
},
});
});
it('should show loading icon', () => {
expect(wrapper.vm.$refs.loadingIcon).toBeDefined();
});
});
describe('methods', () => {
beforeEach(() => {
wrapper = shallowMount(RelatedIssuesList, {
propsData: {
pathIdSeparator: PathIdSeparator.Issue,
relatedIssues: [issuable1, issuable2, issuable3, issuable4, issuable5],
issuableType: 'issue',
},
});
});
it('updates the order correctly when an item is moved to the top', () => {
const beforeAfterIds = wrapper.vm.getBeforeAfterId(
wrapper.vm.$el.querySelector('ul li:first-child'),
);
expect(beforeAfterIds.beforeId).toBeNull();
expect(beforeAfterIds.afterId).toBe(2);
});
it('updates the order correctly when an item is moved to the bottom', () => {
const beforeAfterIds = wrapper.vm.getBeforeAfterId(
wrapper.vm.$el.querySelector('ul li:last-child'),
);
expect(beforeAfterIds.beforeId).toBe(4);
expect(beforeAfterIds.afterId).toBeNull();
});
it('updates the order correctly when an item is swapped with adjacent item', () => {
const beforeAfterIds = wrapper.vm.getBeforeAfterId(
wrapper.vm.$el.querySelector('ul li:nth-child(3)'),
);
expect(beforeAfterIds.beforeId).toBe(2);
expect(beforeAfterIds.afterId).toBe(4);
});
it('updates the order correctly when an item is moved somewhere in the middle', () => {
const beforeAfterIds = wrapper.vm.getBeforeAfterId(
wrapper.vm.$el.querySelector('ul li:nth-child(4)'),
);
expect(beforeAfterIds.beforeId).toBe(3);
expect(beforeAfterIds.afterId).toBe(5);
});
});
describe('issuableOrderingId returns correct issuable order id when', () => {
it('issuableType is epic', () => {
wrapper = shallowMount(RelatedIssuesList, {
propsData: {
pathIdSeparator: PathIdSeparator.Issue,
issuableType: 'issue',
},
});
expect(wrapper.vm.issuableOrderingId(issuable1)).toBe(issuable1.epicIssueId);
});
it('issuableType is issue', () => {
wrapper = shallowMount(RelatedIssuesList, {
propsData: {
pathIdSeparator: PathIdSeparator.Issue,
issuableType: 'epic',
},
});
expect(wrapper.vm.issuableOrderingId(issuable1)).toBe(issuable1.id);
});
});
describe('renders correct ordering id when', () => {
let relatedIssues;
beforeAll(() => {
relatedIssues = [issuable1, issuable2, issuable3, issuable4, issuable5];
});
it('issuableType is epic', () => {
wrapper = shallowMount(RelatedIssuesList, {
propsData: {
pathIdSeparator: PathIdSeparator.Issue,
issuableType: 'epic',
relatedIssues,
},
});
const listItems = wrapper.vm.$el.querySelectorAll('.list-item');
Array.from(listItems).forEach((item, index) => {
expect(Number(item.dataset.orderingId)).toBe(relatedIssues[index].id);
});
});
it('issuableType is issue', () => {
wrapper = shallowMount(RelatedIssuesList, {
propsData: {
pathIdSeparator: PathIdSeparator.Issue,
issuableType: 'issue',
relatedIssues,
},
});
const listItems = wrapper.vm.$el.querySelectorAll('.list-item');
Array.from(listItems).forEach((item, index) => {
expect(Number(item.dataset.orderingId)).toBe(relatedIssues[index].epicIssueId);
});
});
});
describe('related item contents', () => { describe('related item contents', () => {
beforeAll(() => { beforeAll(() => {
wrapper = mount(RelatedIssuesList, { wrapper = mount(RelatedIssuesList, {
...@@ -187,14 +33,5 @@ describe('RelatedIssuesList', () => { ...@@ -187,14 +33,5 @@ describe('RelatedIssuesList', () => {
.text(), .text(),
).toBe(issuable1.weight.toString()); ).toBe(issuable1.weight.toString());
}); });
it('shows due date', () => {
expect(
wrapper
.find(IssueDueDate)
.find('.board-card-info-text')
.text(),
).toBe('Nov 22, 2010');
});
}); });
}); });
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import BlockingMrInputRoot from 'ee/projects/merge_requests/blocking_mr_input_root.vue'; import BlockingMrInputRoot from 'ee/projects/merge_requests/blocking_mr_input_root.vue';
import RelatedIssuableInput from 'ee/related_issues/components/related_issuable_input.vue'; import RelatedIssuableInput from '~/related_issues/components/related_issuable_input.vue';
describe('blocking mr input root', () => { describe('blocking mr input root', () => {
let wrapper; let wrapper;
......
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import { TEST_HOST } from 'helpers/test_constants'; import { TEST_HOST } from 'helpers/test_constants';
import { issuableTypesMap, PathIdSeparator } from 'ee/related_issues/constants'; import { issuableTypesMap, PathIdSeparator } from '~/related_issues/constants';
import RelatedIssuableInput from 'ee/related_issues/components/related_issuable_input.vue'; import RelatedIssuableInput from '~/related_issues/components/related_issuable_input.vue';
jest.mock('ee_else_ce/gfm_auto_complete', () => { jest.mock('ee_else_ce/gfm_auto_complete', () => {
return function gfmAutoComplete() { return function gfmAutoComplete() {
......
...@@ -5,10 +5,10 @@ import { GlLoadingIcon } from '@gitlab/ui'; ...@@ -5,10 +5,10 @@ import { GlLoadingIcon } from '@gitlab/ui';
import RelatedItemsTreeApp from 'ee/related_items_tree/components/related_items_tree_app.vue'; import RelatedItemsTreeApp from 'ee/related_items_tree/components/related_items_tree_app.vue';
import RelatedItemsTreeHeader from 'ee/related_items_tree/components/related_items_tree_header.vue'; import RelatedItemsTreeHeader from 'ee/related_items_tree/components/related_items_tree_header.vue';
import createDefaultStore from 'ee/related_items_tree/store'; import createDefaultStore from 'ee/related_items_tree/store';
import { issuableTypesMap } from 'ee/related_issues/constants';
import CreateIssueForm from 'ee/related_items_tree/components/create_issue_form.vue'; import CreateIssueForm from 'ee/related_items_tree/components/create_issue_form.vue';
import AxiosMockAdapter from 'axios-mock-adapter'; import AxiosMockAdapter from 'axios-mock-adapter';
import { getJSONFixture } from 'helpers/fixtures'; import { getJSONFixture } from 'helpers/fixtures';
import { issuableTypesMap } from '~/related_issues/constants';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { mockInitialConfig, mockParentItem, mockEpics, mockIssues } from '../mock_data'; import { mockInitialConfig, mockParentItem, mockEpics, mockIssues } from '../mock_data';
......
...@@ -5,11 +5,11 @@ import { GlTooltip, GlIcon } from '@gitlab/ui'; ...@@ -5,11 +5,11 @@ import { GlTooltip, GlIcon } from '@gitlab/ui';
import RelatedItemsTreeHeader from 'ee/related_items_tree/components/related_items_tree_header.vue'; import RelatedItemsTreeHeader from 'ee/related_items_tree/components/related_items_tree_header.vue';
import createDefaultStore from 'ee/related_items_tree/store'; import createDefaultStore from 'ee/related_items_tree/store';
import * as epicUtils from 'ee/related_items_tree/utils/epic_utils'; import * as epicUtils from 'ee/related_items_tree/utils/epic_utils';
import { issuableTypesMap } from 'ee/related_issues/constants';
import EpicActionsSplitButton from 'ee/related_items_tree/components/epic_issue_actions_split_button.vue'; import EpicActionsSplitButton from 'ee/related_items_tree/components/epic_issue_actions_split_button.vue';
import EpicHealthStatus from 'ee/related_items_tree/components/epic_health_status.vue'; import EpicHealthStatus from 'ee/related_items_tree/components/epic_health_status.vue';
import { issuableTypesMap } from '~/related_issues/constants';
import { mockInitialConfig, mockParentItem, mockQueryResponse } from '../mock_data'; import { mockInitialConfig, mockParentItem, mockQueryResponse } from '../mock_data';
const localVue = createLocalVue(); const localVue = createLocalVue();
......
...@@ -10,7 +10,7 @@ import StateTooltip from 'ee/related_items_tree/components/state_tooltip.vue'; ...@@ -10,7 +10,7 @@ import StateTooltip from 'ee/related_items_tree/components/state_tooltip.vue';
import createDefaultStore from 'ee/related_items_tree/store'; import createDefaultStore from 'ee/related_items_tree/store';
import * as epicUtils from 'ee/related_items_tree/utils/epic_utils'; import * as epicUtils from 'ee/related_items_tree/utils/epic_utils';
import { ChildType, ChildState } from 'ee/related_items_tree/constants'; import { ChildType, ChildState } from 'ee/related_items_tree/constants';
import { PathIdSeparator } from 'ee/related_issues/constants'; import { PathIdSeparator } from '~/related_issues/constants';
import ItemAssignees from '~/vue_shared/components/issue/issue_assignees.vue'; import ItemAssignees from '~/vue_shared/components/issue/issue_assignees.vue';
import ItemDueDate from '~/boards/components/issue_due_date.vue'; import ItemDueDate from '~/boards/components/issue_due_date.vue';
import ItemMilestone from '~/vue_shared/components/issue/issue_milestone.vue'; import ItemMilestone from '~/vue_shared/components/issue/issue_milestone.vue';
......
...@@ -7,7 +7,7 @@ import TreeItemRemoveModal from 'ee/related_items_tree/components/tree_item_remo ...@@ -7,7 +7,7 @@ import TreeItemRemoveModal from 'ee/related_items_tree/components/tree_item_remo
import createDefaultStore from 'ee/related_items_tree/store'; import createDefaultStore from 'ee/related_items_tree/store';
import * as epicUtils from 'ee/related_items_tree/utils/epic_utils'; import * as epicUtils from 'ee/related_items_tree/utils/epic_utils';
import { ChildType } from 'ee/related_items_tree/constants'; import { ChildType } from 'ee/related_items_tree/constants';
import { PathIdSeparator } from 'ee/related_issues/constants'; import { PathIdSeparator } from '~/related_issues/constants';
import { mockParentItem, mockQueryResponse, mockIssue1 } from '../mock_data'; import { mockParentItem, mockQueryResponse, mockIssue1 } from '../mock_data';
......
...@@ -9,7 +9,7 @@ import TreeRoot from 'ee/related_items_tree/components/tree_root.vue'; ...@@ -9,7 +9,7 @@ import TreeRoot from 'ee/related_items_tree/components/tree_root.vue';
import createDefaultStore from 'ee/related_items_tree/store'; import createDefaultStore from 'ee/related_items_tree/store';
import * as epicUtils from 'ee/related_items_tree/utils/epic_utils'; import * as epicUtils from 'ee/related_items_tree/utils/epic_utils';
import { ChildType, treeItemChevronBtnClassName } from 'ee/related_items_tree/constants'; import { ChildType, treeItemChevronBtnClassName } from 'ee/related_items_tree/constants';
import { PathIdSeparator } from 'ee/related_issues/constants'; import { PathIdSeparator } from '~/related_issues/constants';
import { mockParentItem, mockQueryResponse, mockEpic1 } from '../mock_data'; import { mockParentItem, mockQueryResponse, mockEpic1 } from '../mock_data';
......
...@@ -5,14 +5,14 @@ import * as types from 'ee/related_items_tree/store/mutation_types'; ...@@ -5,14 +5,14 @@ import * as types from 'ee/related_items_tree/store/mutation_types';
import * as epicUtils from 'ee/related_items_tree/utils/epic_utils'; import * as epicUtils from 'ee/related_items_tree/utils/epic_utils';
import { ChildType, ChildState } from 'ee/related_items_tree/constants'; import { ChildType, ChildState } from 'ee/related_items_tree/constants';
import testAction from 'helpers/vuex_action_helper';
import { TEST_HOST } from 'spec/test_constants';
import { import {
issuableTypesMap, issuableTypesMap,
itemAddFailureTypesMap, itemAddFailureTypesMap,
PathIdSeparator, PathIdSeparator,
} from 'ee/related_issues/constants'; } from '~/related_issues/constants';
import testAction from 'helpers/vuex_action_helper';
import { TEST_HOST } from 'spec/test_constants';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { deprecatedCreateFlash as createFlash } from '~/flash'; import { deprecatedCreateFlash as createFlash } from '~/flash';
......
import * as getters from 'ee/related_items_tree/store/getters'; import * as getters from 'ee/related_items_tree/store/getters';
import createDefaultState from 'ee/related_items_tree/store/state'; import createDefaultState from 'ee/related_items_tree/store/state';
import { issuableTypesMap } from 'ee/related_issues/constants'; import { issuableTypesMap } from '~/related_issues/constants';
import { mockEpic1, mockEpic2 } from '../mock_data'; import { mockEpic1, mockEpic2 } from '../mock_data';
......
import * as epicUtils from 'ee/related_items_tree/utils/epic_utils'; import * as epicUtils from 'ee/related_items_tree/utils/epic_utils';
import { PathIdSeparator } from 'ee/related_issues/constants';
import { ChildType } from 'ee/related_items_tree/constants'; import { ChildType } from 'ee/related_items_tree/constants';
import { PathIdSeparator } from '~/related_issues/constants';
import { mockQueryResponse2, mockEpic1, mockIssue1 } from '../mock_data'; import { mockQueryResponse2, mockEpic1, mockIssue1 } from '../mock_data';
......
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import RelatedIssues from 'ee/vulnerabilities/components/related_issues.vue'; import RelatedIssues from 'ee/vulnerabilities/components/related_issues.vue';
import RelatedIssuesBlock from 'ee/related_issues/components/related_issues_block.vue';
import { issuableTypesMap, PathIdSeparator } from 'ee/related_issues/constants';
import { FEEDBACK_TYPES } from 'ee/vulnerabilities/constants'; import { FEEDBACK_TYPES } from 'ee/vulnerabilities/constants';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
import RelatedIssuesBlock from '~/related_issues/components/related_issues_block.vue';
import { issuableTypesMap, PathIdSeparator } from '~/related_issues/constants';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { deprecatedCreateFlash as createFlash } from '~/flash'; import { deprecatedCreateFlash as createFlash } from '~/flash';
import httpStatusCodes from '~/lib/utils/http_status'; import httpStatusCodes from '~/lib/utils/http_status';
......
...@@ -12,11 +12,11 @@ module QA ...@@ -12,11 +12,11 @@ module QA
element :close_reopen_epic_button element :close_reopen_epic_button
end end
view 'ee/app/assets/javascripts/related_issues/components/add_issuable_form.vue' do view 'app/assets/javascripts/related_issues/components/add_issuable_form.vue' do
element :add_issue_button element :add_issue_button
end end
view 'ee/app/assets/javascripts/related_issues/components/related_issuable_input.vue' do view 'app/assets/javascripts/related_issues/components/related_issuable_input.vue' do
element :add_issue_input element :add_issue_input
end end
......
...@@ -12,23 +12,6 @@ module QA ...@@ -12,23 +12,6 @@ module QA
super super
base.class_eval do base.class_eval do
view 'ee/app/assets/javascripts/related_issues/components/add_issuable_form.vue' do
element :add_issue_button
end
view 'ee/app/assets/javascripts/related_issues/components/related_issuable_input.vue' do
element :add_issue_input
end
view 'ee/app/assets/javascripts/related_issues/components/related_issues_block.vue' do
element :related_issues_plus_button
end
view 'ee/app/assets/javascripts/related_issues/components/related_issues_list.vue' do
element :related_issuable_item
element :related_issues_loading_icon
end
view 'ee/app/assets/javascripts/sidebar/components/weight/weight.vue' do view 'ee/app/assets/javascripts/sidebar/components/weight/weight.vue' do
element :weight_label_value element :weight_label_value
element :edit_weight_link element :edit_weight_link
...@@ -43,26 +26,12 @@ module QA ...@@ -43,26 +26,12 @@ module QA
click_element(:remove_weight_link) click_element(:remove_weight_link)
end end
def relate_issue(issue)
click_element(:related_issues_plus_button)
fill_element(:add_issue_input, issue.web_url)
send_keys_to_element(:add_issue_input, :enter)
end
def related_issuable_item
find_element(:related_issuable_item)
end
def set_weight(weight) def set_weight(weight)
click_element(:edit_weight_link) click_element(:edit_weight_link)
fill_element(:weight_input_field, weight) fill_element(:weight_input_field, weight)
send_keys_to_element(:weight_input_field, :enter) send_keys_to_element(:weight_input_field, :enter)
end end
def wait_for_related_issues_to_load
has_no_element?(:related_issues_loading_icon, wait: QA::Support::Repeater::DEFAULT_MAX_WAIT_TIME)
end
def weight_label_value def weight_label_value
find_element(:weight_label_value) find_element(:weight_label_value)
end end
......
...@@ -38,6 +38,37 @@ module QA ...@@ -38,6 +38,37 @@ module QA
element :new_note_form, 'attr: :note' # rubocop:disable QA/ElementWithPattern element :new_note_form, 'attr: :note' # rubocop:disable QA/ElementWithPattern
end end
view 'app/assets/javascripts/related_issues/components/add_issuable_form.vue' do
element :add_issue_button
end
view 'app/assets/javascripts/related_issues/components/related_issuable_input.vue' do
element :add_issue_input
end
view 'app/assets/javascripts/related_issues/components/related_issues_block.vue' do
element :related_issues_plus_button
end
view 'app/assets/javascripts/related_issues/components/related_issues_list.vue' do
element :related_issuable_item
element :related_issues_loading_icon
end
def relate_issue(issue)
click_element(:related_issues_plus_button)
fill_element(:add_issue_input, issue.web_url)
send_keys_to_element(:add_issue_input, :enter)
end
def related_issuable_item
find_element(:related_issuable_item)
end
def wait_for_related_issues_to_load
has_no_element?(:related_issues_loading_icon, wait: QA::Support::Repeater::DEFAULT_MAX_WAIT_TIME)
end
def click_remove_related_issue_button def click_remove_related_issue_button
click_element(:remove_related_issue_button) click_element(:remove_related_issue_button)
end end
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Related issues', :js do
let(:user) { create(:user) }
let(:project) { create(:project_empty_repo, :public) }
let(:project_b) { create(:project_empty_repo, :public) }
let(:project_unauthorized) { create(:project_empty_repo, :public) }
let(:issue_a) { create(:issue, project: project) }
let(:issue_b) { create(:issue, project: project) }
let(:issue_c) { create(:issue, project: project) }
let(:issue_d) { create(:issue, project: project) }
let(:issue_project_b_a) { create(:issue, project: project_b) }
let(:issue_project_unauthorized_a) { create(:issue, project: project_unauthorized) }
context 'widget visibility' do
context 'when not logged in' do
it 'does not show widget when internal project' do
project = create :project_empty_repo, :internal
issue = create :issue, project: project
visit project_issue_path(project, issue)
expect(page).not_to have_css('.related-issues-block')
end
it 'does not show widget when private project' do
project = create :project_empty_repo, :private
issue = create :issue, project: project
visit project_issue_path(project, issue)
expect(page).not_to have_css('.related-issues-block')
end
it 'shows widget when public project' do
project = create :project_empty_repo, :public
issue = create :issue, project: project
visit project_issue_path(project, issue)
expect(page).to have_css('.related-issues-block')
expect(page).not_to have_selector('.js-issue-count-badge-add-button')
end
end
context 'when logged in but not a member' do
before do
gitlab_sign_in(user)
end
it 'shows widget when internal project' do
project = create :project_empty_repo, :internal
issue = create :issue, project: project
visit project_issue_path(project, issue)
expect(page).to have_css('.related-issues-block')
expect(page).not_to have_selector('.js-issue-count-badge-add-button')
end
it 'does not show widget when private project' do
project = create :project_empty_repo, :private
issue = create :issue, project: project
visit project_issue_path(project, issue)
expect(page).not_to have_css('.related-issues-block')
end
it 'shows widget when public project' do
project = create :project_empty_repo, :public
issue = create :issue, project: project
visit project_issue_path(project, issue)
expect(page).to have_css('.related-issues-block')
expect(page).not_to have_selector('.js-issue-count-badge-add-button')
end
it 'shows widget on their own public issue' do
project = create :project_empty_repo, :public
issue = create :issue, project: project, author: user
visit project_issue_path(project, issue)
expect(page).to have_css('.related-issues-block')
expect(page).not_to have_selector('.js-issue-count-badge-add-button')
end
end
context 'when logged in and a guest' do
before do
gitlab_sign_in(user)
end
it 'shows widget when internal project' do
project = create :project_empty_repo, :internal
issue = create :issue, project: project
project.add_guest(user)
visit project_issue_path(project, issue)
expect(page).to have_css('.related-issues-block')
expect(page).not_to have_selector('.js-issue-count-badge-add-button')
end
it 'shows widget when private project' do
project = create :project_empty_repo, :private
issue = create :issue, project: project
project.add_guest(user)
visit project_issue_path(project, issue)
expect(page).to have_css('.related-issues-block')
expect(page).not_to have_selector('.js-issue-count-badge-add-button')
end
it 'shows widget when public project' do
project = create :project_empty_repo, :public
issue = create :issue, project: project
project.add_guest(user)
visit project_issue_path(project, issue)
expect(page).to have_css('.related-issues-block')
expect(page).not_to have_selector('.js-issue-count-badge-add-button')
end
end
context 'when logged in and a reporter' do
before do
gitlab_sign_in(user)
end
it 'shows widget when internal project' do
project = create :project_empty_repo, :internal
issue = create :issue, project: project
project.add_reporter(user)
visit project_issue_path(project, issue)
expect(page).to have_css('.related-issues-block')
expect(page).to have_selector('.js-issue-count-badge-add-button')
end
it 'shows widget when private project' do
project = create :project_empty_repo, :private
issue = create :issue, project: project
project.add_reporter(user)
visit project_issue_path(project, issue)
expect(page).to have_css('.related-issues-block')
expect(page).to have_selector('.js-issue-count-badge-add-button')
end
it 'shows widget when public project' do
project = create :project_empty_repo, :public
issue = create :issue, project: project
project.add_reporter(user)
visit project_issue_path(project, issue)
expect(page).to have_css('.related-issues-block')
expect(page).to have_selector('.js-issue-count-badge-add-button')
end
it 'shows widget on their own public issue' do
project = create :project_empty_repo, :public
issue = create :issue, project: project, author: user
project.add_reporter(user)
visit project_issue_path(project, issue)
expect(page).to have_css('.related-issues-block')
expect(page).to have_selector('.js-issue-count-badge-add-button')
end
end
end
context 'when user has no permission to manage related issues' do
let!(:issue_link_b) { create :issue_link, source: issue_a, target: issue_b }
let!(:issue_link_c) { create :issue_link, source: issue_a, target: issue_c }
before do
project.add_guest(user)
gitlab_sign_in(user)
end
context 'visiting some issue someone else created' do
before do
visit project_issue_path(project, issue_a)
wait_for_requests
end
it 'shows related issues count' do
expect(find('.js-related-issues-header-issue-count')).to have_content('2')
end
end
context 'visiting issue_b which was targeted by issue_a' do
before do
visit project_issue_path(project, issue_b)
wait_for_requests
end
it 'shows related issues count' do
expect(find('.js-related-issues-header-issue-count')).to have_content('1')
end
end
end
context 'when user has permission to manage related issues' do
before do
project.add_maintainer(user)
project_b.add_maintainer(user)
gitlab_sign_in(user)
end
context 'without existing related issues' do
before do
visit project_issue_path(project, issue_a)
wait_for_requests
end
it 'shows related issues count' do
expect(find('.js-related-issues-header-issue-count')).to have_content('0')
end
it 'add related issue' do
find('.js-issue-count-badge-add-button').click
find('.js-add-issuable-form-input').set "#{issue_b.to_reference(project)} "
find('.js-add-issuable-form-add-button').click
wait_for_requests
items = all('.item-title a')
# Form gets hidden after submission
expect(page).not_to have_selector('.js-add-related-issues-form-area')
# Check if related issues are present
expect(items.count).to eq(1)
expect(items[0].text).to eq(issue_b.title)
expect(find('.js-related-issues-header-issue-count')).to have_content('1')
end
it 'add cross-project related issue' do
find('.js-issue-count-badge-add-button').click
find('.js-add-issuable-form-input').set "#{issue_project_b_a.to_reference(project)} "
find('.js-add-issuable-form-add-button').click
wait_for_requests
items = all('.item-title a')
expect(items.count).to eq(1)
expect(items[0].text).to eq(issue_project_b_a.title)
expect(find('.js-related-issues-header-issue-count')).to have_content('1')
end
it 'pressing enter should submit the form' do
find('.js-issue-count-badge-add-button').click
find('.js-add-issuable-form-input').set "#{issue_project_b_a.to_reference(project)} "
find('.js-add-issuable-form-input').native.send_key(:enter)
wait_for_requests
items = all('.item-title a')
expect(items.count).to eq(1)
expect(items[0].text).to eq(issue_project_b_a.title)
expect(find('.js-related-issues-header-issue-count')).to have_content('1')
end
it 'disallows duplicate entries' do
find('.js-issue-count-badge-add-button').click
find('.js-add-issuable-form-input').set 'duplicate duplicate duplicate'
items = all('.js-add-issuable-form-token-list-item')
expect(items.count).to eq(1)
expect(items[0].text).to eq('duplicate')
# Pending issues aren't counted towards the related issue count
expect(find('.js-related-issues-header-issue-count')).to have_content('0')
end
it 'allows us to remove pending issues' do
# Tests against https://gitlab.com/gitlab-org/gitlab/issues/11625
find('.js-issue-count-badge-add-button').click
find('.js-add-issuable-form-input').set 'issue1 issue2 issue3 '
items = all('.js-add-issuable-form-token-list-item')
expect(items.count).to eq(3)
expect(items[0].text).to eq('issue1')
expect(items[1].text).to eq('issue2')
expect(items[2].text).to eq('issue3')
# Remove pending issues left to right to make sure none get stuck
items[0].find('.js-issue-token-remove-button').click
items = all('.js-add-issuable-form-token-list-item')
expect(items.count).to eq(2)
expect(items[0].text).to eq('issue2')
expect(items[1].text).to eq('issue3')
items[0].find('.js-issue-token-remove-button').click
items = all('.js-add-issuable-form-token-list-item')
expect(items.count).to eq(1)
expect(items[0].text).to eq('issue3')
items[0].find('.js-issue-token-remove-button').click
items = all('.js-add-issuable-form-token-list-item')
expect(items.count).to eq(0)
end
end
context 'with existing related issues' do
let!(:issue_link_b) { create :issue_link, source: issue_a, target: issue_b }
let!(:issue_link_c) { create :issue_link, source: issue_a, target: issue_c }
before do
visit project_issue_path(project, issue_a)
wait_for_requests
end
it 'shows related issues count' do
expect(find('.js-related-issues-header-issue-count')).to have_content('2')
end
it 'shows related issues' do
items = all('.item-title a')
expect(items.count).to eq(2)
expect(items[0].text).to eq(issue_b.title)
expect(items[1].text).to eq(issue_c.title)
end
it 'allows us to remove a related issues' do
items_before = all('.item-title a')
expect(items_before.count).to eq(2)
first('.js-issue-item-remove-button').click
wait_for_requests
items_after = all('.item-title a')
expect(items_after.count).to eq(1)
end
it 'add related issue' do
find('.js-issue-count-badge-add-button').click
find('.js-add-issuable-form-input').set "##{issue_d.iid} "
find('.js-add-issuable-form-add-button').click
wait_for_requests
items = all('.item-title a')
expect(items.count).to eq(3)
expect(items[0].text).to eq(issue_b.title)
expect(items[1].text).to eq(issue_c.title)
expect(items[2].text).to eq(issue_d.title)
expect(find('.js-related-issues-header-issue-count')).to have_content('3')
end
it 'add invalid related issue' do
find('.js-issue-count-badge-add-button').click
find('.js-add-issuable-form-input').set "#9999999 "
find('.js-add-issuable-form-add-button').click
wait_for_requests
items = all('.item-title a')
expect(items.count).to eq(2)
expect(items[0].text).to eq(issue_b.title)
expect(items[1].text).to eq(issue_c.title)
expect(find('.js-related-issues-header-issue-count')).to have_content('2')
end
it 'add unauthorized related issue' do
find('.js-issue-count-badge-add-button').click
find('.js-add-issuable-form-input').set "#{issue_project_unauthorized_a.to_reference(project)} "
find('.js-add-issuable-form-add-button').click
wait_for_requests
items = all('.item-title a')
expect(items.count).to eq(2)
expect(items[0].text).to eq(issue_b.title)
expect(items[1].text).to eq(issue_c.title)
expect(find('.js-related-issues-header-issue-count')).to have_content('2')
end
end
end
end
import { mount, shallowMount } from '@vue/test-utils'; import { mount, shallowMount } from '@vue/test-utils';
import { import { issuableTypesMap, linkedIssueTypesMap, PathIdSeparator } from '~/related_issues/constants';
issuableTypesMap, import AddIssuableForm from '~/related_issues/components/add_issuable_form.vue';
linkedIssueTypesMap,
PathIdSeparator,
} from 'ee/related_issues/constants';
import AddIssuableForm from 'ee/related_issues/components/add_issuable_form.vue';
const issuable1 = { const issuable1 = {
id: 200, id: 200,
......
import Vue from 'vue'; import Vue from 'vue';
import { PathIdSeparator } from 'ee/related_issues/constants'; import { PathIdSeparator } from '~/related_issues/constants';
import issueToken from 'ee/related_issues/components/issue_token.vue'; import issueToken from '~/related_issues/components/issue_token.vue';
describe('IssueToken', () => { describe('IssueToken', () => {
const idKey = 200; const idKey = 200;
......
import { shallowMount, mount } from '@vue/test-utils'; import { shallowMount, mount } from '@vue/test-utils';
import { GlButton, GlIcon } from '@gitlab/ui'; import { GlButton, GlIcon } from '@gitlab/ui';
import RelatedIssuesBlock from 'ee/related_issues/components/related_issues_block.vue';
import { import {
issuable1, issuable1,
issuable2, issuable2,
issuable3, issuable3,
} from 'jest/vue_shared/components/issue/related_issuable_mock_data'; } from 'jest/vue_shared/components/issue/related_issuable_mock_data';
import RelatedIssuesBlock from '~/related_issues/components/related_issues_block.vue';
import { import {
linkedIssueTypesMap, linkedIssueTypesMap,
linkedIssueTypesTextMap, linkedIssueTypesTextMap,
PathIdSeparator, PathIdSeparator,
} from 'ee/related_issues/constants'; } from '~/related_issues/constants';
describe('RelatedIssuesBlock', () => { describe('RelatedIssuesBlock', () => {
let wrapper; let wrapper;
......
import { mount, shallowMount } from '@vue/test-utils';
import {
issuable1,
issuable2,
issuable3,
issuable4,
issuable5,
} from 'jest/vue_shared/components/issue/related_issuable_mock_data';
import IssueDueDate from '~/boards/components/issue_due_date.vue';
import RelatedIssuesList from '~/related_issues/components/related_issues_list.vue';
import { PathIdSeparator } from '~/related_issues/constants';
describe('RelatedIssuesList', () => {
let wrapper;
afterEach(() => {
wrapper.destroy();
});
describe('with defaults', () => {
const heading = 'Related to';
beforeEach(() => {
wrapper = shallowMount(RelatedIssuesList, {
propsData: {
pathIdSeparator: PathIdSeparator.Issue,
issuableType: 'issue',
heading,
},
});
});
it('shows a heading', () => {
expect(wrapper.find('h4').text()).toContain(heading);
});
it('should not show loading icon', () => {
expect(wrapper.vm.$refs.loadingIcon).toBeUndefined();
});
});
describe('with isFetching=true', () => {
beforeEach(() => {
wrapper = shallowMount(RelatedIssuesList, {
propsData: {
pathIdSeparator: PathIdSeparator.Issue,
isFetching: true,
issuableType: 'issue',
},
});
});
it('should show loading icon', () => {
expect(wrapper.vm.$refs.loadingIcon).toBeDefined();
});
});
describe('methods', () => {
beforeEach(() => {
wrapper = shallowMount(RelatedIssuesList, {
propsData: {
pathIdSeparator: PathIdSeparator.Issue,
relatedIssues: [issuable1, issuable2, issuable3, issuable4, issuable5],
issuableType: 'issue',
},
});
});
it('updates the order correctly when an item is moved to the top', () => {
const beforeAfterIds = wrapper.vm.getBeforeAfterId(
wrapper.vm.$el.querySelector('ul li:first-child'),
);
expect(beforeAfterIds.beforeId).toBeNull();
expect(beforeAfterIds.afterId).toBe(2);
});
it('updates the order correctly when an item is moved to the bottom', () => {
const beforeAfterIds = wrapper.vm.getBeforeAfterId(
wrapper.vm.$el.querySelector('ul li:last-child'),
);
expect(beforeAfterIds.beforeId).toBe(4);
expect(beforeAfterIds.afterId).toBeNull();
});
it('updates the order correctly when an item is swapped with adjacent item', () => {
const beforeAfterIds = wrapper.vm.getBeforeAfterId(
wrapper.vm.$el.querySelector('ul li:nth-child(3)'),
);
expect(beforeAfterIds.beforeId).toBe(2);
expect(beforeAfterIds.afterId).toBe(4);
});
it('updates the order correctly when an item is moved somewhere in the middle', () => {
const beforeAfterIds = wrapper.vm.getBeforeAfterId(
wrapper.vm.$el.querySelector('ul li:nth-child(4)'),
);
expect(beforeAfterIds.beforeId).toBe(3);
expect(beforeAfterIds.afterId).toBe(5);
});
});
describe('issuableOrderingId returns correct issuable order id when', () => {
it('issuableType is epic', () => {
wrapper = shallowMount(RelatedIssuesList, {
propsData: {
pathIdSeparator: PathIdSeparator.Issue,
issuableType: 'issue',
},
});
expect(wrapper.vm.issuableOrderingId(issuable1)).toBe(issuable1.epicIssueId);
});
it('issuableType is issue', () => {
wrapper = shallowMount(RelatedIssuesList, {
propsData: {
pathIdSeparator: PathIdSeparator.Issue,
issuableType: 'epic',
},
});
expect(wrapper.vm.issuableOrderingId(issuable1)).toBe(issuable1.id);
});
});
describe('renders correct ordering id when', () => {
let relatedIssues;
beforeAll(() => {
relatedIssues = [issuable1, issuable2, issuable3, issuable4, issuable5];
});
it('issuableType is epic', () => {
wrapper = shallowMount(RelatedIssuesList, {
propsData: {
pathIdSeparator: PathIdSeparator.Issue,
issuableType: 'epic',
relatedIssues,
},
});
const listItems = wrapper.vm.$el.querySelectorAll('.list-item');
Array.from(listItems).forEach((item, index) => {
expect(Number(item.dataset.orderingId)).toBe(relatedIssues[index].id);
});
});
it('issuableType is issue', () => {
wrapper = shallowMount(RelatedIssuesList, {
propsData: {
pathIdSeparator: PathIdSeparator.Issue,
issuableType: 'issue',
relatedIssues,
},
});
const listItems = wrapper.vm.$el.querySelectorAll('.list-item');
Array.from(listItems).forEach((item, index) => {
expect(Number(item.dataset.orderingId)).toBe(relatedIssues[index].epicIssueId);
});
});
});
describe('related item contents', () => {
beforeAll(() => {
wrapper = mount(RelatedIssuesList, {
propsData: {
issuableType: 'issue',
pathIdSeparator: PathIdSeparator.Issue,
relatedIssues: [issuable1],
},
});
});
it('shows due date', () => {
expect(
wrapper
.find(IssueDueDate)
.find('.board-card-info-text')
.text(),
).toBe('Nov 22, 2010');
});
});
});
import { mount, shallowMount } from '@vue/test-utils'; import { mount, shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
import RelatedIssuesRoot from 'ee/related_issues/components/related_issues_root.vue';
import relatedIssuesService from 'ee/related_issues/services/related_issues_service';
import { linkedIssueTypesMap } from 'ee/related_issues/constants';
import { import {
defaultProps, defaultProps,
issuable1, issuable1,
issuable2, issuable2,
} from 'jest/vue_shared/components/issue/related_issuable_mock_data'; } from 'jest/vue_shared/components/issue/related_issuable_mock_data';
import RelatedIssuesRoot from '~/related_issues/components/related_issues_root.vue';
import relatedIssuesService from '~/related_issues/services/related_issues_service';
import { linkedIssueTypesMap } from '~/related_issues/constants';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import { deprecatedCreateFlash as createFlash } from '~/flash'; import { deprecatedCreateFlash as createFlash } from '~/flash';
......
import RelatedIssuesStore from 'ee/related_issues/stores/related_issues_store';
import { import {
issuable1, issuable1,
issuable2, issuable2,
...@@ -7,6 +5,7 @@ import { ...@@ -7,6 +5,7 @@ import {
issuable4, issuable4,
issuable5, issuable5,
} from 'jest/vue_shared/components/issue/related_issuable_mock_data'; } from 'jest/vue_shared/components/issue/related_issuable_mock_data';
import RelatedIssuesStore from '~/related_issues/stores/related_issues_store';
describe('RelatedIssuesStore', () => { describe('RelatedIssuesStore', () => {
let store; let store;
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment