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
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend/issues')
-rw-r--r--spec/frontend/issues/issue_spec.js13
-rw-r--r--spec/frontend/issues/list/components/issues_list_app_spec.js175
-rw-r--r--spec/frontend/issues/list/mock_data.js14
-rw-r--r--spec/frontend/issues/list/utils_spec.js29
-rw-r--r--spec/frontend/issues/show/components/app_spec.js53
-rw-r--r--spec/frontend/issues/show/components/description_spec.js91
-rw-r--r--spec/frontend/issues/show/components/fields/description_spec.js1
-rw-r--r--spec/frontend/issues/show/components/title_spec.js7
-rw-r--r--spec/frontend/issues/show/mock_data/mock_data.js17
-rw-r--r--spec/frontend/issues/show/utils_spec.js40
10 files changed, 329 insertions, 111 deletions
diff --git a/spec/frontend/issues/issue_spec.js b/spec/frontend/issues/issue_spec.js
index 8a089b372ff..089ea8dbbad 100644
--- a/spec/frontend/issues/issue_spec.js
+++ b/spec/frontend/issues/issue_spec.js
@@ -1,5 +1,6 @@
import { getByText } from '@testing-library/dom';
import MockAdapter from 'axios-mock-adapter';
+import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { EVENT_ISSUABLE_VUE_APP_CHANGE } from '~/issuable/constants';
import Issue from '~/issues/issue';
import axios from '~/lib/utils/axios_utils';
@@ -24,11 +25,11 @@ describe('Issue', () => {
const getIssueCounter = () => document.querySelector('.issue_counter');
const getOpenStatusBox = () =>
getByText(document, (_, el) => el.textContent.match(/Open/), {
- selector: '.status-box-open',
+ selector: '.issuable-status-badge-open',
});
const getClosedStatusBox = () =>
getByText(document, (_, el) => el.textContent.match(/Closed/), {
- selector: '.status-box-issue-closed',
+ selector: '.issuable-status-badge-closed',
});
describe.each`
@@ -38,9 +39,9 @@ describe('Issue', () => {
`('$desc', ({ isIssueInitiallyOpen, expectedCounterText }) => {
beforeEach(() => {
if (isIssueInitiallyOpen) {
- loadFixtures('issues/open-issue.html');
+ loadHTMLFixture('issues/open-issue.html');
} else {
- loadFixtures('issues/closed-issue.html');
+ loadHTMLFixture('issues/closed-issue.html');
}
testContext.issueCounter = getIssueCounter();
@@ -50,6 +51,10 @@ describe('Issue', () => {
testContext.issueCounter.textContent = '1,001';
});
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
it(`has the proper visible status box when ${isIssueInitiallyOpen ? 'open' : 'closed'}`, () => {
if (isIssueInitiallyOpen) {
expect(testContext.statusBoxClosed).toHaveClass('hidden');
diff --git a/spec/frontend/issues/list/components/issues_list_app_spec.js b/spec/frontend/issues/list/components/issues_list_app_spec.js
index 5a9bd1ff8e4..d92ba527b5c 100644
--- a/spec/frontend/issues/list/components/issues_list_app_spec.js
+++ b/spec/frontend/issues/list/components/issues_list_app_spec.js
@@ -5,8 +5,11 @@ import AxiosMockAdapter from 'axios-mock-adapter';
import { cloneDeep } from 'lodash';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
+import VueRouter from 'vue-router';
import getIssuesQuery from 'ee_else_ce/issues/list/queries/get_issues.query.graphql';
import getIssuesCountsQuery from 'ee_else_ce/issues/list/queries/get_issues_counts.query.graphql';
+import getIssuesWithoutCrmQuery from 'ee_else_ce/issues/list/queries/get_issues_without_crm.query.graphql';
+import getIssuesCountsWithoutCrmQuery from 'ee_else_ce/issues/list/queries/get_issues_counts_without_crm.query.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
import setWindowLocation from 'helpers/set_window_location_helper';
import { TEST_HOST } from 'helpers/test_constants';
@@ -58,6 +61,7 @@ describe('CE IssuesListApp component', () => {
let wrapper;
Vue.use(VueApollo);
+ Vue.use(VueRouter);
const defaultProvide = {
autocompleteAwardEmojisPath: 'autocomplete/award/emojis/path',
@@ -78,6 +82,7 @@ describe('CE IssuesListApp component', () => {
isAnonymousSearchDisabled: false,
isIssueRepositioningDisabled: false,
isProject: true,
+ isPublicVisibilityRestricted: false,
isSignedIn: true,
jiraIntegrationPath: 'jira/integration/path',
newIssuePath: 'new/issue/path',
@@ -107,6 +112,7 @@ describe('CE IssuesListApp component', () => {
const mountComponent = ({
provide = {},
+ data = {},
issuesQueryResponse = jest.fn().mockResolvedValue(defaultQueryResponse),
issuesCountsQueryResponse = jest.fn().mockResolvedValue(getIssuesCountsQueryResponse),
sortPreferenceMutationResponse = jest.fn().mockResolvedValue(setSortPreferenceMutationResponse),
@@ -115,16 +121,21 @@ describe('CE IssuesListApp component', () => {
const requestHandlers = [
[getIssuesQuery, issuesQueryResponse],
[getIssuesCountsQuery, issuesCountsQueryResponse],
+ [getIssuesWithoutCrmQuery, issuesQueryResponse],
+ [getIssuesCountsWithoutCrmQuery, issuesCountsQueryResponse],
[setSortPreferenceMutation, sortPreferenceMutationResponse],
];
- const apolloProvider = createMockApollo(requestHandlers);
return mountFn(IssuesListApp, {
- apolloProvider,
+ apolloProvider: createMockApollo(requestHandlers),
+ router: new VueRouter({ mode: 'history' }),
provide: {
...defaultProvide,
...provide,
},
+ data() {
+ return data;
+ },
});
};
@@ -139,10 +150,10 @@ describe('CE IssuesListApp component', () => {
});
describe('IssuableList', () => {
- beforeEach(async () => {
+ beforeEach(() => {
wrapper = mountComponent();
jest.runOnlyPendingTimers();
- await waitForPromises();
+ return waitForPromises();
});
it('renders', () => {
@@ -167,10 +178,6 @@ describe('CE IssuesListApp component', () => {
useKeysetPagination: true,
hasPreviousPage: getIssuesQueryResponse.data.project.issues.pageInfo.hasPreviousPage,
hasNextPage: getIssuesQueryResponse.data.project.issues.pageInfo.hasNextPage,
- urlParams: {
- sort: urlSortParams[CREATED_DESC],
- state: IssuableStates.Opened,
- },
});
});
});
@@ -200,7 +207,7 @@ describe('CE IssuesListApp component', () => {
describe('csv import/export component', () => {
describe('when user is signed in', () => {
- beforeEach(async () => {
+ beforeEach(() => {
setWindowLocation('?search=refactor&state=opened');
wrapper = mountComponent({
@@ -209,12 +216,12 @@ describe('CE IssuesListApp component', () => {
});
jest.runOnlyPendingTimers();
- await waitForPromises();
+ return waitForPromises();
});
it('renders', () => {
expect(findCsvImportExportButtons().props()).toMatchObject({
- exportCsvPath: `${defaultProvide.exportCsvPath}?search=refactor&sort=created_date&state=opened`,
+ exportCsvPath: `${defaultProvide.exportCsvPath}?search=refactor&state=opened`,
issuableCount: 1,
});
});
@@ -252,11 +259,9 @@ describe('CE IssuesListApp component', () => {
it('emits "issuables:enableBulkEdit" event to legacy bulk edit class', async () => {
wrapper = mountComponent({ provide: { canBulkUpdate: true }, mountFn: mount });
-
jest.spyOn(eventHub, '$emit');
findGlButtonAt(2).vm.$emit('click');
-
await waitForPromises();
expect(eventHub.$emit).toHaveBeenCalledWith('issuables:enableBulkEdit');
@@ -297,32 +302,25 @@ describe('CE IssuesListApp component', () => {
describe('page', () => {
it('page_after is set from the url params', () => {
setWindowLocation('?page_after=randomCursorString');
-
wrapper = mountComponent();
- expect(findIssuableList().props('urlParams')).toMatchObject({
- page_after: 'randomCursorString',
- });
+ expect(wrapper.vm.$route.query).toMatchObject({ page_after: 'randomCursorString' });
});
it('page_before is set from the url params', () => {
setWindowLocation('?page_before=anotherRandomCursorString');
-
wrapper = mountComponent();
- expect(findIssuableList().props('urlParams')).toMatchObject({
- page_before: 'anotherRandomCursorString',
- });
+ expect(wrapper.vm.$route.query).toMatchObject({ page_before: 'anotherRandomCursorString' });
});
});
describe('search', () => {
it('is set from the url params', () => {
setWindowLocation(locationSearch);
-
wrapper = mountComponent();
- expect(findIssuableList().props('urlParams')).toMatchObject({ search: 'find issues' });
+ expect(wrapper.vm.$route.query).toMatchObject({ search: 'find issues' });
});
});
@@ -333,10 +331,7 @@ describe('CE IssuesListApp component', () => {
it.each(oldEnumSortValues)('initial sort is set with value %s', (sort) => {
wrapper = mountComponent({ provide: { initialSort: sort } });
- expect(findIssuableList().props()).toMatchObject({
- initialSortBy: getSortKey(sort),
- urlParams: { sort },
- });
+ expect(findIssuableList().props('initialSortBy')).toBe(getSortKey(sort));
});
});
@@ -346,10 +341,7 @@ describe('CE IssuesListApp component', () => {
it.each(graphQLEnumSortValues)('initial sort is set with value %s', (sort) => {
wrapper = mountComponent({ provide: { initialSort: sort.toLowerCase() } });
- expect(findIssuableList().props()).toMatchObject({
- initialSortBy: sort,
- urlParams: { sort: urlSortParams[sort] },
- });
+ expect(findIssuableList().props('initialSortBy')).toBe(sort);
});
});
@@ -359,10 +351,7 @@ describe('CE IssuesListApp component', () => {
(sort) => {
wrapper = mountComponent({ provide: { initialSort: sort } });
- expect(findIssuableList().props()).toMatchObject({
- initialSortBy: CREATED_DESC,
- urlParams: { sort: urlSortParams[CREATED_DESC] },
- });
+ expect(findIssuableList().props('initialSortBy')).toBe(CREATED_DESC);
},
);
});
@@ -375,10 +364,7 @@ describe('CE IssuesListApp component', () => {
});
it('changes the sort to the default of created descending', () => {
- expect(findIssuableList().props()).toMatchObject({
- initialSortBy: CREATED_DESC,
- urlParams: { sort: urlSortParams[CREATED_DESC] },
- });
+ expect(findIssuableList().props('initialSortBy')).toBe(CREATED_DESC);
});
it('shows an alert to tell the user that manual reordering is disabled', () => {
@@ -393,9 +379,7 @@ describe('CE IssuesListApp component', () => {
describe('state', () => {
it('is set from the url params', () => {
const initialState = IssuableStates.All;
-
setWindowLocation(`?state=${initialState}`);
-
wrapper = mountComponent();
expect(findIssuableList().props('currentTab')).toBe(initialState);
@@ -405,7 +389,6 @@ describe('CE IssuesListApp component', () => {
describe('filter tokens', () => {
it('is set from the url params', () => {
setWindowLocation(locationSearch);
-
wrapper = mountComponent();
expect(findIssuableList().props('initialFilterValue')).toEqual(filteredTokens);
@@ -414,7 +397,6 @@ describe('CE IssuesListApp component', () => {
describe('when anonymous searching is performed', () => {
beforeEach(() => {
setWindowLocation(locationSearch);
-
wrapper = mountComponent({
provide: { isAnonymousSearchDisabled: true, isSignedIn: false },
});
@@ -649,12 +631,12 @@ describe('CE IssuesListApp component', () => {
${'fetching issues'} | ${'issuesQueryResponse'} | ${IssuesListApp.i18n.errorFetchingIssues}
${'fetching issue counts'} | ${'issuesCountsQueryResponse'} | ${IssuesListApp.i18n.errorFetchingCounts}
`('when there is an error $error', ({ mountOption, message }) => {
- beforeEach(async () => {
+ beforeEach(() => {
wrapper = mountComponent({
[mountOption]: jest.fn().mockRejectedValue(new Error('ERROR')),
});
jest.runOnlyPendingTimers();
- await waitForPromises();
+ return waitForPromises();
});
it('shows an error message', () => {
@@ -676,29 +658,51 @@ describe('CE IssuesListApp component', () => {
describe('when "click-tab" event is emitted by IssuableList', () => {
beforeEach(() => {
wrapper = mountComponent();
+ jest.spyOn(wrapper.vm.$router, 'push');
findIssuableList().vm.$emit('click-tab', IssuableStates.Closed);
});
- it('updates to the new tab', () => {
+ it('updates ui to the new tab', () => {
expect(findIssuableList().props('currentTab')).toBe(IssuableStates.Closed);
});
- });
- describe.each(['next-page', 'previous-page'])(
- 'when "%s" event is emitted by IssuableList',
- (event) => {
- beforeEach(() => {
- wrapper = mountComponent();
+ it('updates url to the new tab', () => {
+ expect(wrapper.vm.$router.push).toHaveBeenCalledWith({
+ query: expect.objectContaining({ state: IssuableStates.Closed }),
+ });
+ });
+ });
- findIssuableList().vm.$emit(event);
+ describe.each`
+ event | paramName | paramValue
+ ${'next-page'} | ${'page_after'} | ${'endCursor'}
+ ${'previous-page'} | ${'page_before'} | ${'startCursor'}
+ `('when "$event" event is emitted by IssuableList', ({ event, paramName, paramValue }) => {
+ beforeEach(() => {
+ wrapper = mountComponent({
+ data: {
+ pageInfo: {
+ endCursor: 'endCursor',
+ startCursor: 'startCursor',
+ },
+ },
});
+ jest.spyOn(wrapper.vm.$router, 'push');
+
+ findIssuableList().vm.$emit(event);
+ });
+
+ it('scrolls to the top', () => {
+ expect(scrollUp).toHaveBeenCalled();
+ });
- it('scrolls to the top', () => {
- expect(scrollUp).toHaveBeenCalled();
+ it(`updates url with "${paramName}" param`, () => {
+ expect(wrapper.vm.$router.push).toHaveBeenCalledWith({
+ query: expect.objectContaining({ [paramName]: paramValue }),
});
- },
- );
+ });
+ });
describe('when "reorder" event is emitted by IssuableList', () => {
const issueOne = {
@@ -752,18 +756,17 @@ describe('CE IssuesListApp component', () => {
`(
'when moving issue $description',
({ issueToMove, oldIndex, newIndex, moveBeforeId, moveAfterId }) => {
- beforeEach(async () => {
+ beforeEach(() => {
wrapper = mountComponent({
provide: { isProject },
issuesQueryResponse: jest.fn().mockResolvedValue(response(isProject)),
});
jest.runOnlyPendingTimers();
- await waitForPromises();
+ return waitForPromises();
});
it('makes API call to reorder the issue', async () => {
findIssuableList().vm.$emit('reorder', { oldIndex, newIndex });
-
await waitForPromises();
expect(axiosMock.history.put[0]).toMatchObject({
@@ -780,19 +783,18 @@ describe('CE IssuesListApp component', () => {
});
describe('when unsuccessful', () => {
- beforeEach(async () => {
+ beforeEach(() => {
wrapper = mountComponent({
issuesQueryResponse: jest.fn().mockResolvedValue(response()),
});
jest.runOnlyPendingTimers();
- await waitForPromises();
+ return waitForPromises();
});
it('displays an error message', async () => {
axiosMock.onPut(joinPaths(issueOne.webPath, 'reorder')).reply(500);
findIssuableList().vm.$emit('reorder', { oldIndex: 0, newIndex: 1 });
-
await waitForPromises();
expect(findIssuableList().props('error')).toBe(IssuesListApp.i18n.reorderError);
@@ -808,14 +810,14 @@ describe('CE IssuesListApp component', () => {
'updates to the new sort when payload is `%s`',
async (sortKey) => {
wrapper = mountComponent();
+ jest.spyOn(wrapper.vm.$router, 'push');
findIssuableList().vm.$emit('sort', sortKey);
-
jest.runOnlyPendingTimers();
await nextTick();
- expect(findIssuableList().props('urlParams')).toMatchObject({
- sort: urlSortParams[sortKey],
+ expect(wrapper.vm.$router.push).toHaveBeenCalledWith({
+ query: expect.objectContaining({ sort: urlSortParams[sortKey] }),
});
},
);
@@ -827,14 +829,13 @@ describe('CE IssuesListApp component', () => {
wrapper = mountComponent({
provide: { initialSort, isIssueRepositioningDisabled: true },
});
+ jest.spyOn(wrapper.vm.$router, 'push');
findIssuableList().vm.$emit('sort', RELATIVE_POSITION_ASC);
});
it('does not update the sort to manual', () => {
- expect(findIssuableList().props('urlParams')).toMatchObject({
- sort: urlSortParams[initialSort],
- });
+ expect(wrapper.vm.$router.push).not.toHaveBeenCalled();
});
it('shows an alert to tell the user that manual reordering is disabled', () => {
@@ -899,11 +900,14 @@ describe('CE IssuesListApp component', () => {
describe('when "filter" event is emitted by IssuableList', () => {
it('updates IssuableList with url params', async () => {
wrapper = mountComponent();
+ jest.spyOn(wrapper.vm.$router, 'push');
findIssuableList().vm.$emit('filter', filteredTokens);
await nextTick();
- expect(findIssuableList().props('urlParams')).toMatchObject(urlParams);
+ expect(wrapper.vm.$router.push).toHaveBeenCalledWith({
+ query: expect.objectContaining(urlParams),
+ });
});
describe('when anonymous searching is performed', () => {
@@ -911,19 +915,13 @@ describe('CE IssuesListApp component', () => {
wrapper = mountComponent({
provide: { isAnonymousSearchDisabled: true, isSignedIn: false },
});
+ jest.spyOn(wrapper.vm.$router, 'push');
findIssuableList().vm.$emit('filter', filteredTokens);
});
- it('does not update IssuableList with url params ', async () => {
- const defaultParams = {
- page_after: null,
- page_before: null,
- sort: 'created_date',
- state: 'opened',
- };
-
- expect(findIssuableList().props('urlParams')).toEqual(defaultParams);
+ it('does not update url params', () => {
+ expect(wrapper.vm.$router.push).not.toHaveBeenCalled();
});
it('shows an alert to tell the user they must be signed in to search', () => {
@@ -935,4 +933,23 @@ describe('CE IssuesListApp component', () => {
});
});
});
+
+ describe('public visibility', () => {
+ it.each`
+ description | isPublicVisibilityRestricted | isSignedIn | hideUsers
+ ${'shows users when public visibility is not restricted and is not signed in'} | ${false} | ${false} | ${false}
+ ${'shows users when public visibility is not restricted and is signed in'} | ${false} | ${true} | ${false}
+ ${'hides users when public visibility is restricted and is not signed in'} | ${true} | ${false} | ${true}
+ ${'shows users when public visibility is restricted and is signed in'} | ${true} | ${true} | ${false}
+ `('$description', ({ isPublicVisibilityRestricted, isSignedIn, hideUsers }) => {
+ const mockQuery = jest.fn().mockResolvedValue(defaultQueryResponse);
+ wrapper = mountComponent({
+ provide: { isPublicVisibilityRestricted, isSignedIn },
+ issuesQueryResponse: mockQuery,
+ });
+ jest.runOnlyPendingTimers();
+
+ expect(mockQuery).toHaveBeenCalledWith(expect.objectContaining({ hideUsers }));
+ });
+ });
});
diff --git a/spec/frontend/issues/list/mock_data.js b/spec/frontend/issues/list/mock_data.js
index b1a135ceb18..42f2d08082e 100644
--- a/spec/frontend/issues/list/mock_data.js
+++ b/spec/frontend/issues/list/mock_data.js
@@ -117,6 +117,7 @@ export const locationSearch = [
'not[author_username]=marge',
'assignee_username[]=bart',
'assignee_username[]=lisa',
+ 'assignee_username[]=5',
'not[assignee_username][]=patty',
'not[assignee_username][]=selma',
'milestone_title=season+3',
@@ -146,6 +147,8 @@ export const locationSearch = [
'not[epic_id]=34',
'weight=1',
'not[weight]=3',
+ 'crm_contact_id=123',
+ 'crm_organization_id=456',
].join('&');
export const locationSearchWithSpecialValues = [
@@ -165,6 +168,7 @@ export const filteredTokens = [
{ type: 'author_username', value: { data: 'marge', operator: OPERATOR_IS_NOT } },
{ type: 'assignee_username', value: { data: 'bart', operator: OPERATOR_IS } },
{ type: 'assignee_username', value: { data: 'lisa', operator: OPERATOR_IS } },
+ { type: 'assignee_username', value: { data: '5', operator: OPERATOR_IS } },
{ type: 'assignee_username', value: { data: 'patty', operator: OPERATOR_IS_NOT } },
{ type: 'assignee_username', value: { data: 'selma', operator: OPERATOR_IS_NOT } },
{ type: 'milestone', value: { data: 'season 3', operator: OPERATOR_IS } },
@@ -194,6 +198,8 @@ export const filteredTokens = [
{ type: 'epic_id', value: { data: '34', operator: OPERATOR_IS_NOT } },
{ type: 'weight', value: { data: '1', operator: OPERATOR_IS } },
{ type: 'weight', value: { data: '3', operator: OPERATOR_IS_NOT } },
+ { type: 'crm_contact', value: { data: '123', operator: OPERATOR_IS } },
+ { type: 'crm_organization', value: { data: '456', operator: OPERATOR_IS } },
{ type: 'filtered-search-term', value: { data: 'find' } },
{ type: 'filtered-search-term', value: { data: 'issues' } },
];
@@ -212,7 +218,7 @@ export const filteredTokensWithSpecialValues = [
export const apiParams = {
authorUsername: 'homer',
- assigneeUsernames: ['bart', 'lisa'],
+ assigneeUsernames: ['bart', 'lisa', '5'],
milestoneTitle: ['season 3', 'season 4'],
labelName: ['cartoon', 'tv'],
releaseTag: ['v3', 'v4'],
@@ -222,6 +228,8 @@ export const apiParams = {
iterationId: ['4', '12'],
epicId: '12',
weight: '1',
+ crmContactId: '123',
+ crmOrganizationId: '456',
not: {
authorUsername: 'marge',
assigneeUsernames: ['patty', 'selma'],
@@ -251,7 +259,7 @@ export const apiParamsWithSpecialValues = {
export const urlParams = {
author_username: 'homer',
'not[author_username]': 'marge',
- 'assignee_username[]': ['bart', 'lisa'],
+ 'assignee_username[]': ['bart', 'lisa', '5'],
'not[assignee_username][]': ['patty', 'selma'],
milestone_title: ['season 3', 'season 4'],
'not[milestone_title]': ['season 20', 'season 30'],
@@ -270,6 +278,8 @@ export const urlParams = {
'not[epic_id]': '34',
weight: '1',
'not[weight]': '3',
+ crm_contact_id: '123',
+ crm_organization_id: '456',
};
export const urlParamsWithSpecialValues = {
diff --git a/spec/frontend/issues/list/utils_spec.js b/spec/frontend/issues/list/utils_spec.js
index a60350d91c5..ce0477883d7 100644
--- a/spec/frontend/issues/list/utils_spec.js
+++ b/spec/frontend/issues/list/utils_spec.js
@@ -1,3 +1,5 @@
+import setWindowLocation from 'helpers/set_window_location_helper';
+import { TEST_HOST } from 'helpers/test_constants';
import {
apiParams,
apiParamsWithSpecialValues,
@@ -24,6 +26,7 @@ import {
getSortOptions,
isSortKey,
} from '~/issues/list/utils';
+import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
describe('getInitialPageParams', () => {
it.each(Object.keys(urlSortParams))(
@@ -124,24 +127,50 @@ describe('getFilterTokens', () => {
filteredTokensWithSpecialValues,
);
});
+
+ it.each`
+ description | argument
+ ${'an undefined value'} | ${undefined}
+ ${'an irrelevant value'} | ${'?unrecognised=parameter'}
+ `('returns an empty filtered search term given $description', ({ argument }) => {
+ expect(getFilterTokens(argument)).toEqual([
+ {
+ id: expect.any(String),
+ type: FILTERED_SEARCH_TERM,
+ value: { data: '' },
+ },
+ ]);
+ });
});
describe('convertToApiParams', () => {
+ beforeEach(() => {
+ setWindowLocation(TEST_HOST);
+ });
+
it('returns api params given filtered tokens', () => {
expect(convertToApiParams(filteredTokens)).toEqual(apiParams);
});
it('returns api params given filtered tokens with special values', () => {
+ setWindowLocation('?assignee_id=123');
+
expect(convertToApiParams(filteredTokensWithSpecialValues)).toEqual(apiParamsWithSpecialValues);
});
});
describe('convertToUrlParams', () => {
+ beforeEach(() => {
+ setWindowLocation(TEST_HOST);
+ });
+
it('returns url params given filtered tokens', () => {
expect(convertToUrlParams(filteredTokens)).toEqual(urlParams);
});
it('returns url params given filtered tokens with special values', () => {
+ setWindowLocation('?assignee_id=123');
+
expect(convertToUrlParams(filteredTokensWithSpecialValues)).toEqual(urlParamsWithSpecialValues);
});
});
diff --git a/spec/frontend/issues/show/components/app_spec.js b/spec/frontend/issues/show/components/app_spec.js
index 5ab64d8e9ca..27604b8ccf3 100644
--- a/spec/frontend/issues/show/components/app_spec.js
+++ b/spec/frontend/issues/show/components/app_spec.js
@@ -1,10 +1,12 @@
-import { GlIntersectionObserver } from '@gitlab/ui';
+import { GlIcon, GlIntersectionObserver } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
import '~/behaviors/markdown/render_gfm';
-import { IssuableStatus, IssuableStatusText } from '~/issues/constants';
+import { IssuableStatus, IssuableStatusText, IssuableType } from '~/issues/constants';
import IssuableApp from '~/issues/show/components/app.vue';
import DescriptionComponent from '~/issues/show/components/description.vue';
import EditedComponent from '~/issues/show/components/edited.vue';
@@ -70,7 +72,7 @@ describe('Issuable output', () => {
};
beforeEach(() => {
- setFixtures(`
+ setHTMLFixture(`
<div>
<title>Title</title>
<div class="detail-page-description content-block">
@@ -105,6 +107,7 @@ describe('Issuable output', () => {
realtimeRequestCount = 0;
wrapper.vm.poll.stop();
wrapper.destroy();
+ resetHTMLFixture();
});
it('should render a title/description/edited and update title/description/edited on update', () => {
@@ -465,6 +468,31 @@ describe('Issuable output', () => {
expect(findStickyHeader().text()).toContain('Sticky header title');
});
+ it('shows with title for an epic', async () => {
+ wrapper.setProps({ issuableType: 'epic' });
+
+ await nextTick();
+
+ expect(findStickyHeader().text()).toContain('Sticky header title');
+ });
+
+ it.each`
+ issuableType | issuableStatus | statusIcon
+ ${IssuableType.Issue} | ${IssuableStatus.Open} | ${'issues'}
+ ${IssuableType.Issue} | ${IssuableStatus.Closed} | ${'issue-closed'}
+ ${IssuableType.Epic} | ${IssuableStatus.Open} | ${'epic'}
+ ${IssuableType.Epic} | ${IssuableStatus.Closed} | ${'epic-closed'}
+ `(
+ 'shows with state icon "$statusIcon" for $issuableType when status is $issuableStatus',
+ async ({ issuableType, issuableStatus, statusIcon }) => {
+ wrapper.setProps({ issuableType, issuableStatus });
+
+ await nextTick();
+
+ expect(findStickyHeader().findComponent(GlIcon).props('name')).toBe(statusIcon);
+ },
+ );
+
it.each`
title | state
${'shows with Open when status is opened'} | ${IssuableStatus.Open}
@@ -487,7 +515,14 @@ describe('Issuable output', () => {
await nextTick();
- expect(findConfidentialBadge().exists()).toBe(isConfidential);
+ const confidentialEl = findConfidentialBadge();
+ expect(confidentialEl.exists()).toBe(isConfidential);
+ if (isConfidential) {
+ expect(confidentialEl.props()).toMatchObject({
+ workspaceType: 'project',
+ issuableType: 'issue',
+ });
+ }
});
it.each`
@@ -613,4 +648,14 @@ describe('Issuable output', () => {
expect(wrapper.vm.updateStoreState).toHaveBeenCalled();
});
});
+
+ describe('listItemReorder event', () => {
+ it('makes request to update issue', async () => {
+ const description = 'I have been updated!';
+ findDescription().vm.$emit('listItemReorder', description);
+ await waitForPromises();
+
+ expect(mock.history.put[0].data).toContain(description);
+ });
+ });
});
diff --git a/spec/frontend/issues/show/components/description_spec.js b/spec/frontend/issues/show/components/description_spec.js
index 0b3daadae1d..1ae04531a6b 100644
--- a/spec/frontend/issues/show/components/description_spec.js
+++ b/spec/frontend/issues/show/components/description_spec.js
@@ -1,14 +1,20 @@
import $ from 'jquery';
-import { nextTick } from 'vue';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
import '~/behaviors/markdown/render_gfm';
import { GlTooltip, GlModal } from '@gitlab/ui';
+
import setWindowLocation from 'helpers/set_window_location_helper';
import { stubComponent } from 'helpers/stub_component';
import { TEST_HOST } from 'helpers/test_constants';
import { mockTracking } from 'helpers/tracking_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+
import Description from '~/issues/show/components/description.vue';
import { updateHistory } from '~/lib/utils/url_utility';
+import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
import TaskList from '~/task_list';
import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue';
import CreateWorkItem from '~/work_items/pages/create_work_item.vue';
@@ -27,17 +33,29 @@ jest.mock('~/task_list');
const showModal = jest.fn();
const hideModal = jest.fn();
+const showDetailsModal = jest.fn();
const $toast = {
show: jest.fn(),
};
+const workItemQueryResponse = {
+ data: {
+ workItem: null,
+ },
+};
+
+const queryHandler = jest.fn().mockResolvedValue(workItemQueryResponse);
+
describe('Description component', () => {
let wrapper;
+ Vue.use(VueApollo);
+
const findGfmContent = () => wrapper.find('[data-testid="gfm-content"]');
const findTextarea = () => wrapper.find('[data-testid="textarea"]');
const findTaskActionButtons = () => wrapper.findAll('.js-add-task');
const findConvertToTaskButton = () => wrapper.find('.js-add-task');
+ const findTaskLink = () => wrapper.find('a.gfm-issue');
const findTooltips = () => wrapper.findAllComponents(GlTooltip);
const findModal = () => wrapper.findComponent(GlModal);
@@ -52,6 +70,7 @@ describe('Description component', () => {
...props,
},
provide,
+ apolloProvider: createMockApollo([[workItemQuery, queryHandler]]),
mocks: {
$toast,
},
@@ -62,6 +81,11 @@ describe('Description component', () => {
hide: hideModal,
},
}),
+ WorkItemDetailModal: stubComponent(WorkItemDetailModal, {
+ methods: {
+ show: showDetailsModal,
+ },
+ }),
},
});
}
@@ -296,15 +320,15 @@ describe('Description component', () => {
});
it('shows toast after delete success', async () => {
- findWorkItemDetailModal().vm.$emit('workItemDeleted');
+ const newDesc = 'description';
+ findWorkItemDetailModal().vm.$emit('workItemDeleted', newDesc);
+ expect(wrapper.emitted('updateDescription')).toEqual([[newDesc]]);
expect($toast.show).toHaveBeenCalledWith('Work item deleted');
});
});
describe('work items detail', () => {
- const findTaskLink = () => wrapper.find('a.gfm-issue');
-
describe('when opening and closing', () => {
beforeEach(() => {
createComponent({
@@ -319,11 +343,9 @@ describe('Description component', () => {
});
it('opens when task button is clicked', async () => {
- expect(findWorkItemDetailModal().props('visible')).toBe(false);
-
await findTaskLink().trigger('click');
- expect(findWorkItemDetailModal().props('visible')).toBe(true);
+ expect(showDetailsModal).toHaveBeenCalled();
expect(updateHistory).toHaveBeenCalledWith({
url: `${TEST_HOST}/?work_item_id=2`,
replace: true,
@@ -333,12 +355,9 @@ describe('Description component', () => {
it('closes from an open state', async () => {
await findTaskLink().trigger('click');
- expect(findWorkItemDetailModal().props('visible')).toBe(true);
-
findWorkItemDetailModal().vm.$emit('close');
await nextTick();
- expect(findWorkItemDetailModal().props('visible')).toBe(false);
expect(updateHistory).toHaveBeenLastCalledWith({
url: `${TEST_HOST}/`,
replace: true,
@@ -364,16 +383,17 @@ describe('Description component', () => {
describe('when url query `work_item_id` exists', () => {
it.each`
- behavior | workItemId | visible
- ${'opens'} | ${'123'} | ${true}
- ${'does not open'} | ${'123e'} | ${false}
- ${'does not open'} | ${'12e3'} | ${false}
- ${'does not open'} | ${'1e23'} | ${false}
- ${'does not open'} | ${'x'} | ${false}
- ${'does not open'} | ${'undefined'} | ${false}
+ behavior | workItemId | modalOpened
+ ${'opens'} | ${'2'} | ${1}
+ ${'does not open'} | ${'123'} | ${0}
+ ${'does not open'} | ${'123e'} | ${0}
+ ${'does not open'} | ${'12e3'} | ${0}
+ ${'does not open'} | ${'1e23'} | ${0}
+ ${'does not open'} | ${'x'} | ${0}
+ ${'does not open'} | ${'undefined'} | ${0}
`(
'$behavior when url contains `work_item_id=$workItemId`',
- async ({ workItemId, visible }) => {
+ async ({ workItemId, modalOpened }) => {
setWindowLocation(`?work_item_id=${workItemId}`);
createComponent({
@@ -381,10 +401,43 @@ describe('Description component', () => {
provide: { glFeatures: { workItems: true } },
});
- expect(findWorkItemDetailModal().props('visible')).toBe(visible);
+ expect(showDetailsModal).toHaveBeenCalledTimes(modalOpened);
},
);
});
});
+
+ describe('when hovering task links', () => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ descriptionHtml: descriptionHtmlWithTask,
+ },
+ provide: {
+ glFeatures: { workItems: true },
+ },
+ });
+ return nextTick();
+ });
+
+ it('prefetches work item detail after work item link is hovered for 150ms', async () => {
+ await findTaskLink().trigger('mouseover');
+ jest.advanceTimersByTime(150);
+ await waitForPromises();
+
+ expect(queryHandler).toHaveBeenCalledWith({
+ id: 'gid://gitlab/WorkItem/2',
+ });
+ });
+
+ it('does not work item detail after work item link is hovered for less than 150ms', async () => {
+ await findTaskLink().trigger('mouseover');
+ await findTaskLink().trigger('mouseout');
+ jest.advanceTimersByTime(150);
+ await waitForPromises();
+
+ expect(queryHandler).not.toHaveBeenCalled();
+ });
+ });
});
});
diff --git a/spec/frontend/issues/show/components/fields/description_spec.js b/spec/frontend/issues/show/components/fields/description_spec.js
index 0dcd70ac19b..d0e33f0b980 100644
--- a/spec/frontend/issues/show/components/fields/description_spec.js
+++ b/spec/frontend/issues/show/components/fields/description_spec.js
@@ -24,7 +24,6 @@ describe('Description field component', () => {
beforeEach(() => {
jest.spyOn(eventHub, '$emit');
- gon.features = { markdownContinueLists: true };
});
afterEach(() => {
diff --git a/spec/frontend/issues/show/components/title_spec.js b/spec/frontend/issues/show/components/title_spec.js
index 29b5353ef1c..7560b733ae6 100644
--- a/spec/frontend/issues/show/components/title_spec.js
+++ b/spec/frontend/issues/show/components/title_spec.js
@@ -1,4 +1,5 @@
import Vue, { nextTick } from 'vue';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import titleComponent from '~/issues/show/components/title.vue';
import eventHub from '~/issues/show/event_hub';
import Store from '~/issues/show/stores';
@@ -6,7 +7,7 @@ import Store from '~/issues/show/stores';
describe('Title component', () => {
let vm;
beforeEach(() => {
- setFixtures(`<title />`);
+ setHTMLFixture(`<title />`);
const Component = Vue.extend(titleComponent);
const store = new Store({
@@ -25,6 +26,10 @@ describe('Title component', () => {
}).$mount();
});
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
it('renders title HTML', () => {
expect(vm.$el.querySelector('.title').innerHTML.trim()).toBe('Testing <img>');
});
diff --git a/spec/frontend/issues/show/mock_data/mock_data.js b/spec/frontend/issues/show/mock_data/mock_data.js
index 7b0b8ca686a..909789b7a0f 100644
--- a/spec/frontend/issues/show/mock_data/mock_data.js
+++ b/spec/frontend/issues/show/mock_data/mock_data.js
@@ -77,7 +77,22 @@ export const descriptionHtmlWithTask = `
<ul data-sourcepos="1:1-3:7" class="task-list" dir="auto">
<li data-sourcepos="1:1-1:10" class="task-list-item">
<input type="checkbox" class="task-list-item-checkbox" disabled>
- <a href="/gitlab-org/gitlab-test/-/issues/48" data-original="#48+" data-link="false" data-link-reference="false" data-project="1" data-issue="2" data-reference-format="+" data-reference-type="task" data-container="body" data-placement="top" title="1" class="gfm gfm-issue has-tooltip">1 (#48)</a>
+ <a href="/gitlab-org/gitlab-test/-/issues/48" data-original="#48+" data-link="false" data-link-reference="false" data-project="1" data-issue="2" data-reference-format="+" data-reference-type="task" data-container="body" data-placement="top" title="1" class="gfm gfm-issue has-tooltip" data-issue-type="task">1 (#48)</a>
+ </li>
+ <li data-sourcepos="2:1-2:7" class="task-list-item">
+ <input type="checkbox" class="task-list-item-checkbox" disabled> 2
+ </li>
+ <li data-sourcepos="3:1-3:7" class="task-list-item">
+ <input type="checkbox" class="task-list-item-checkbox" disabled> 3
+ </li>
+ </ul>
+`;
+
+export const descriptionHtmlWithIssue = `
+ <ul data-sourcepos="1:1-3:7" class="task-list" dir="auto">
+ <li data-sourcepos="1:1-1:10" class="task-list-item">
+ <input type="checkbox" class="task-list-item-checkbox" disabled>
+ <a href="/gitlab-org/gitlab-test/-/issues/48" data-original="#48+" data-link="false" data-link-reference="false" data-project="1" data-issue="2" data-reference-format="+" data-reference-type="task" data-container="body" data-placement="top" title="1" class="gfm gfm-issue has-tooltip" data-issue-type="issue">1 (#48)</a>
</li>
<li data-sourcepos="2:1-2:7" class="task-list-item">
<input type="checkbox" class="task-list-item-checkbox" disabled> 2
diff --git a/spec/frontend/issues/show/utils_spec.js b/spec/frontend/issues/show/utils_spec.js
new file mode 100644
index 00000000000..e5f14cfc01a
--- /dev/null
+++ b/spec/frontend/issues/show/utils_spec.js
@@ -0,0 +1,40 @@
+import { convertDescriptionWithNewSort } from '~/issues/show/utils';
+
+describe('app/assets/javascripts/issues/show/utils.js', () => {
+ describe('convertDescriptionWithNewSort', () => {
+ it('converts markdown description with new list sort order', () => {
+ const description = `I am text
+
+- Item 1
+- Item 2
+ - Item 3
+ - Item 4
+- Item 5`;
+
+ // Drag Item 2 + children to Item 1's position
+ const html = `<ul data-sourcepos="3:1-8:0">
+ <li data-sourcepos="4:1-4:8">
+ Item 2
+ <ul data-sourcepos="5:1-6:10">
+ <li data-sourcepos="5:1-5:10">Item 3</li>
+ <li data-sourcepos="6:1-6:10">Item 4</li>
+ </ul>
+ </li>
+ <li data-sourcepos="3:1-3:8">Item 1</li>
+ <li data-sourcepos="7:1-8:0">Item 5</li>
+ <ul>`;
+ const list = document.createElement('div');
+ list.innerHTML = html;
+
+ const expected = `I am text
+
+- Item 2
+ - Item 3
+ - Item 4
+- Item 1
+- Item 5`;
+
+ expect(convertDescriptionWithNewSort(description, list.firstChild)).toBe(expected);
+ });
+ });
+});