Commit c71dad2e authored by Florie Guibert's avatar Florie Guibert Committed by Simon Knox

Board refactor - Decouple boardsStore from GraphQL boards

Duplicate components to help decouple boardsStore
parent 1e1b44a0
<script>
import IssueCardInner from './issue_card_inner.vue';
import IssueCardInnerDeprecated from './issue_card_inner_deprecated.vue';
import boardsStore from '../stores/boards_store';
export default {
name: 'BoardsIssueCard',
components: {
IssueCardInner,
IssueCardInner: gon.features?.graphqlBoardLists ? IssueCardInner : IssueCardInnerDeprecated,
},
props: {
list: {
......
<script>
import { sortBy } from 'lodash';
import { mapState } from 'vuex';
import { mapActions, mapState } from 'vuex';
import { GlLabel, GlTooltipDirective, GlIcon } from '@gitlab/ui';
import issueCardInner from 'ee_else_ce/boards/mixins/issue_card_inner';
import { sprintf, __, n__ } from '~/locale';
......@@ -8,9 +8,10 @@ import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import IssueDueDate from './issue_due_date.vue';
import IssueTimeEstimate from './issue_time_estimate.vue';
import boardsStore from '../stores/boards_store';
import eventHub from '../eventhub';
import { isScopedLabel } from '~/lib/utils/common_utils';
import { ListType } from '../constants';
import { updateHistory } from '~/lib/utils/url_utility';
export default {
components: {
......@@ -42,7 +43,7 @@ export default {
default: false,
},
},
inject: ['groupId', 'rootPath'],
inject: ['groupId', 'rootPath', 'scopedLabelsAvailable'],
data() {
return {
limitBeforeCounter: 2,
......@@ -52,6 +53,16 @@ export default {
},
computed: {
...mapState(['isShowingLabels']),
cappedAssignees() {
// e.g. maxRender is 4,
// Render up to all 4 assignees if there are only 4 assigness
// Otherwise render up to the limitBeforeCounter
if (this.issue.assignees.length <= this.maxRender) {
return this.issue.assignees.slice(0, this.maxRender);
}
return this.issue.assignees.slice(0, this.limitBeforeCounter);
},
numberOverLimit() {
return this.issue.assignees.length - this.limitBeforeCounter;
},
......@@ -98,19 +109,10 @@ export default {
},
},
methods: {
...mapActions(['performSearch']),
isIndexLessThanlimit(index) {
return index < this.limitBeforeCounter;
},
shouldRenderAssignee(index) {
// Eg. maxRender is 4,
// Render up to all 4 assignees if there are only 4 assigness
// Otherwise render up to the limitBeforeCounter
if (this.issue.assignees.length <= this.maxRender) {
return index < this.maxRender;
}
return index < this.limitBeforeCounter;
},
assigneeUrl(assignee) {
if (!assignee) return '';
return `${this.rootPath}${assignee.username}`;
......@@ -118,6 +120,9 @@ export default {
avatarUrlTitle(assignee) {
return sprintf(__(`Avatar for %{assigneeName}`), { assigneeName: assignee.name });
},
avatarUrl(assignee) {
return assignee.avatarUrl || assignee.avatar || gon.default_avatar_url;
},
showLabel(label) {
if (!label.id) return false;
return true;
......@@ -133,13 +138,19 @@ export default {
},
filterByLabel(label) {
if (!this.updateFilters) return;
const labelTitle = encodeURIComponent(label.title);
const filter = `label_name[]=${labelTitle}`;
const filterPath = window.location.search ? `${window.location.search}&` : '?';
const filter = `label_name[]=${encodeURIComponent(label.title)}`;
boardsStore.toggleFilter(filter);
if (!filterPath.includes(filter)) {
updateHistory({
url: `${filterPath}${filter}`,
});
this.performSearch();
eventHub.$emit('updateTokens');
}
},
showScopedLabel(label) {
return boardsStore.scopedLabels.enabled && isScopedLabel(label);
return this.scopedLabelsAvailable && isScopedLabel(label);
},
},
};
......@@ -222,12 +233,11 @@ export default {
</div>
<div class="board-card-assignee gl-display-flex">
<user-avatar-link
v-for="(assignee, index) in issue.assignees"
v-if="shouldRenderAssignee(index)"
v-for="assignee in cappedAssignees"
:key="assignee.id"
:link-href="assigneeUrl(assignee)"
:img-alt="avatarUrlTitle(assignee)"
:img-src="assignee.avatarUrl || assignee.avatar || assignee.avatar_url"
:img-src="avatarUrl(assignee)"
:img-size="24"
class="js-no-trigger"
tooltip-placement="bottom"
......
<script>
import { sortBy } from 'lodash';
import { mapState } from 'vuex';
import { GlLabel, GlTooltipDirective, GlIcon } from '@gitlab/ui';
import issueCardInner from 'ee_else_ce/boards/mixins/issue_card_inner';
import { sprintf, __, n__ } from '~/locale';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import UserAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import IssueDueDate from './issue_due_date.vue';
import IssueTimeEstimate from './issue_time_estimate_deprecated.vue';
import boardsStore from '../stores/boards_store';
import { isScopedLabel } from '~/lib/utils/common_utils';
export default {
components: {
GlLabel,
GlIcon,
UserAvatarLink,
TooltipOnTruncate,
IssueDueDate,
IssueTimeEstimate,
IssueCardWeight: () => import('ee_component/boards/components/issue_card_weight.vue'),
},
directives: {
GlTooltip: GlTooltipDirective,
},
mixins: [issueCardInner],
props: {
issue: {
type: Object,
required: true,
},
list: {
type: Object,
required: false,
default: () => ({}),
},
updateFilters: {
type: Boolean,
required: false,
default: false,
},
},
inject: ['groupId', 'rootPath'],
data() {
return {
limitBeforeCounter: 2,
maxRender: 3,
maxCounter: 99,
};
},
computed: {
...mapState(['isShowingLabels']),
numberOverLimit() {
return this.issue.assignees.length - this.limitBeforeCounter;
},
assigneeCounterTooltip() {
const { numberOverLimit, maxCounter } = this;
const count = numberOverLimit > maxCounter ? maxCounter : numberOverLimit;
return sprintf(__('%{count} more assignees'), { count });
},
assigneeCounterLabel() {
if (this.numberOverLimit > this.maxCounter) {
return `${this.maxCounter}+`;
}
return `+${this.numberOverLimit}`;
},
shouldRenderCounter() {
if (this.issue.assignees.length <= this.maxRender) {
return false;
}
return this.issue.assignees.length > this.numberOverLimit;
},
issueId() {
if (this.issue.iid) {
return `#${this.issue.iid}`;
}
return false;
},
showLabelFooter() {
return this.isShowingLabels && this.issue.labels.find(this.showLabel);
},
issueReferencePath() {
const { referencePath, groupId } = this.issue;
return !groupId ? referencePath.split('#')[0] : null;
},
orderedLabels() {
return sortBy(this.issue.labels.filter(this.isNonListLabel), 'title');
},
blockedLabel() {
if (this.issue.blockedByCount) {
return n__(`Blocked by %d issue`, `Blocked by %d issues`, this.issue.blockedByCount);
}
return __('Blocked issue');
},
},
methods: {
isIndexLessThanlimit(index) {
return index < this.limitBeforeCounter;
},
shouldRenderAssignee(index) {
// Eg. maxRender is 4,
// Render up to all 4 assignees if there are only 4 assigness
// Otherwise render up to the limitBeforeCounter
if (this.issue.assignees.length <= this.maxRender) {
return index < this.maxRender;
}
return index < this.limitBeforeCounter;
},
assigneeUrl(assignee) {
if (!assignee) return '';
return `${this.rootPath}${assignee.username}`;
},
avatarUrlTitle(assignee) {
return sprintf(__(`Avatar for %{assigneeName}`), { assigneeName: assignee.name });
},
showLabel(label) {
if (!label.id) return false;
return true;
},
isNonListLabel(label) {
return label.id && !(this.list.type === 'label' && this.list.title === label.title);
},
filterByLabel(label) {
if (!this.updateFilters) return;
const labelTitle = encodeURIComponent(label.title);
const filter = `label_name[]=${labelTitle}`;
boardsStore.toggleFilter(filter);
},
showScopedLabel(label) {
return boardsStore.scopedLabels.enabled && isScopedLabel(label);
},
},
};
</script>
<template>
<div>
<div class="gl-display-flex" dir="auto">
<h4 class="board-card-title gl-mb-0 gl-mt-0">
<gl-icon
v-if="issue.blocked"
v-gl-tooltip
name="issue-block"
:title="blockedLabel"
class="issue-blocked-icon gl-mr-2"
:aria-label="blockedLabel"
data-testid="issue-blocked-icon"
/>
<gl-icon
v-if="issue.confidential"
v-gl-tooltip
name="eye-slash"
:title="__('Confidential')"
class="confidential-icon gl-mr-2"
:aria-label="__('Confidential')"
/>
<a
:href="issue.path || issue.webUrl || ''"
:title="issue.title"
class="js-no-trigger"
@mousemove.stop
>{{ issue.title }}</a
>
</h4>
</div>
<div v-if="showLabelFooter" class="board-card-labels gl-mt-2 gl-display-flex gl-flex-wrap">
<template v-for="label in orderedLabels">
<gl-label
:key="label.id"
:background-color="label.color"
:title="label.title"
:description="label.description"
size="sm"
:scoped="showScopedLabel(label)"
@click="filterByLabel(label)"
/>
</template>
</div>
<div
class="board-card-footer gl-display-flex gl-justify-content-space-between gl-align-items-flex-end"
>
<div
class="gl-display-flex align-items-start flex-wrap-reverse board-card-number-container gl-overflow-hidden js-board-card-number-container"
>
<span
v-if="issue.referencePath"
class="board-card-number gl-overflow-hidden gl-display-flex gl-mr-3 gl-mt-3"
>
<tooltip-on-truncate
v-if="issueReferencePath"
:title="issueReferencePath"
placement="bottom"
class="board-issue-path gl-text-truncate gl-font-weight-bold"
>{{ issueReferencePath }}</tooltip-on-truncate
>
#{{ issue.iid }}
</span>
<span class="board-info-items gl-mt-3 gl-display-inline-block">
<issue-due-date
v-if="issue.dueDate"
:date="issue.dueDate"
:closed="issue.closed || Boolean(issue.closedAt)"
/>
<issue-time-estimate v-if="issue.timeEstimate" :estimate="issue.timeEstimate" />
<issue-card-weight
v-if="validIssueWeight"
:weight="issue.weight"
@click="filterByWeight(issue.weight)"
/>
</span>
</div>
<div class="board-card-assignee gl-display-flex">
<user-avatar-link
v-for="(assignee, index) in issue.assignees"
v-if="shouldRenderAssignee(index)"
:key="assignee.id"
:link-href="assigneeUrl(assignee)"
:img-alt="avatarUrlTitle(assignee)"
:img-src="assignee.avatarUrl || assignee.avatar || assignee.avatar_url"
:img-size="24"
class="js-no-trigger"
tooltip-placement="bottom"
>
<span class="js-assignee-tooltip">
<span class="gl-font-weight-bold gl-display-block">{{ __('Assignee') }}</span>
{{ assignee.name }}
<span class="text-white-50">@{{ assignee.username }}</span>
</span>
</user-avatar-link>
<span
v-if="shouldRenderCounter"
v-gl-tooltip
:title="assigneeCounterTooltip"
class="avatar-counter"
data-placement="bottom"
>{{ assigneeCounterLabel }}</span
>
</div>
</div>
</div>
</template>
<script>
import { GlTooltip, GlIcon } from '@gitlab/ui';
import { __ } from '~/locale';
import { parseSeconds, stringifyTime } from '~/lib/utils/datetime_utility';
import boardsStore from '../stores/boards_store';
export default {
i18n: {
timeEstimate: __('Time estimate'),
},
components: {
GlIcon,
GlTooltip,
......@@ -14,17 +17,18 @@ export default {
required: true,
},
},
data() {
return {
limitToHours: boardsStore.timeTracking.limitToHours,
};
},
inject: ['timeTrackingLimitToHours'],
computed: {
title() {
return stringifyTime(parseSeconds(this.estimate, { limitToHours: this.limitToHours }), true);
return stringifyTime(
parseSeconds(this.estimate, { limitToHours: this.timeTrackingLimitToHours }),
true,
);
},
timeEstimate() {
return stringifyTime(parseSeconds(this.estimate, { limitToHours: this.limitToHours }));
return stringifyTime(
parseSeconds(this.estimate, { limitToHours: this.timeTrackingLimitToHours }),
);
},
},
};
......@@ -33,16 +37,16 @@ export default {
<template>
<span>
<span ref="issueTimeEstimate" class="board-card-info card-number">
<gl-icon name="hourglass" class="board-card-info-icon" /><time class="board-card-info-text">{{
timeEstimate
}}</time>
<gl-icon name="hourglass" class="board-card-info-icon" />
<time class="board-card-info-text">{{ timeEstimate }}</time>
</span>
<gl-tooltip
:target="() => $refs.issueTimeEstimate"
placement="bottom"
class="js-issue-time-estimate"
>
<span class="bold d-block">{{ __('Time estimate') }}</span> {{ title }}
<span class="gl-font-weight-bold gl-display-block">{{ $options.i18n.timeEstimate }}</span>
{{ title }}
</gl-tooltip>
</span>
</template>
<script>
import { GlTooltip, GlIcon } from '@gitlab/ui';
import { parseSeconds, stringifyTime } from '~/lib/utils/datetime_utility';
import boardsStore from '../stores/boards_store';
export default {
components: {
GlIcon,
GlTooltip,
},
props: {
estimate: {
type: Number,
required: true,
},
},
data() {
return {
limitToHours: boardsStore.timeTracking.limitToHours,
};
},
computed: {
title() {
return stringifyTime(parseSeconds(this.estimate, { limitToHours: this.limitToHours }), true);
},
timeEstimate() {
return stringifyTime(parseSeconds(this.estimate, { limitToHours: this.limitToHours }));
},
},
};
</script>
<template>
<span>
<span ref="issueTimeEstimate" class="board-card-info card-number">
<gl-icon name="hourglass" class="board-card-info-icon" /><time class="board-card-info-text">{{
timeEstimate
}}</time>
</span>
<gl-tooltip
:target="() => $refs.issueTimeEstimate"
placement="bottom"
class="js-issue-time-estimate"
>
<span class="bold d-block">{{ __('Time estimate') }}</span> {{ title }}
</gl-tooltip>
</span>
</template>
......@@ -117,16 +117,9 @@ export default () => {
},
},
created() {
const endpoints = {
boardsEndpoint: this.boardsEndpoint,
recentBoardsEndpoint: this.recentBoardsEndpoint,
listsEndpoint: this.listsEndpoint,
bulkUpdatePath: this.bulkUpdatePath,
this.setInitialBoardData({
boardId: $boardApp.dataset.boardId,
fullPath: $boardApp.dataset.fullPath,
};
this.setInitialBoardData({
...endpoints,
boardType: this.parent,
disabled: this.disabled,
boardConfig: {
......@@ -141,7 +134,14 @@ export default () => {
: null,
},
});
boardsStore.setEndpoints(endpoints);
boardsStore.setEndpoints({
boardsEndpoint: this.boardsEndpoint,
recentBoardsEndpoint: this.recentBoardsEndpoint,
listsEndpoint: this.listsEndpoint,
bulkUpdatePath: this.bulkUpdatePath,
boardId: $boardApp.dataset.boardId,
fullPath: $boardApp.dataset.fullPath,
});
boardsStore.rootPath = this.boardsEndpoint;
eventHub.$on('updateTokens', this.updateTokens);
......
......@@ -78,8 +78,7 @@ export default {
},
fetchLists: ({ commit, state, dispatch }) => {
const { endpoints, boardType, filterParams } = state;
const { fullPath, boardId } = endpoints;
const { boardType, filterParams, fullPath, boardId } = state;
const variables = {
fullPath,
......@@ -106,7 +105,7 @@ export default {
},
createList: ({ state, commit, dispatch }, { backlog, labelId, milestoneId, assigneeId }) => {
const { boardId } = state.endpoints;
const { boardId } = state;
gqlClient
.mutate({
......@@ -135,8 +134,7 @@ export default {
},
fetchLabels: ({ state, commit }, searchTerm) => {
const { endpoints, boardType } = state;
const { fullPath } = endpoints;
const { fullPath, boardType } = state;
const variables = {
fullPath,
......@@ -227,8 +225,7 @@ export default {
fetchIssuesForList: ({ state, commit }, { listId, fetchNext = false }) => {
commit(types.REQUEST_ISSUES_FOR_LIST, { listId, fetchNext });
const { endpoints, boardType, filterParams } = state;
const { fullPath, boardId } = endpoints;
const { fullPath, boardId, boardType, filterParams } = state;
const variables = {
fullPath,
......@@ -271,7 +268,7 @@ export default {
const originalIndex = fromList.indexOf(Number(issueId));
commit(types.MOVE_ISSUE, { originalIssue, fromListId, toListId, moveBeforeId, moveAfterId });
const { boardId } = state.endpoints;
const { boardId } = state;
const [fullProjectPath] = issuePath.split(/[#]/);
gqlClient
......@@ -357,9 +354,9 @@ export default {
createNewIssue: ({ commit, state }, issueInput) => {
const input = issueInput;
const { boardType, endpoints } = state;
const { boardType, fullPath } = state;
if (boardType === BoardType.project) {
input.projectPath = endpoints.fullPath;
input.projectPath = fullPath;
}
return gqlClient
......
......@@ -32,8 +32,9 @@ export const addIssueToList = ({ state, listId, issueId, moveBeforeId, moveAfter
export default {
[mutationTypes.SET_INITIAL_BOARD_DATA](state, data) {
const { boardType, disabled, boardConfig, ...endpoints } = data;
state.endpoints = endpoints;
const { boardType, disabled, boardId, fullPath, boardConfig } = data;
state.boardId = boardId;
state.fullPath = fullPath;
state.boardType = boardType;
state.disabled = disabled;
state.boardConfig = boardConfig;
......
......@@ -45,8 +45,7 @@ export const gqlClient = createGqClient(
);
const fetchAndFormatListIssues = (state, extraVariables) => {
const { endpoints, boardType, filterParams } = state;
const { fullPath, boardId } = endpoints;
const { fullPath, boardId, boardType, filterParams } = state;
const variables = {
fullPath,
......@@ -126,8 +125,7 @@ export default {
},
fetchEpicsSwimlanes({ state, commit, dispatch }, { withLists = true, endCursor = null }) {
const { endpoints, boardType, filterParams } = state;
const { fullPath, boardId } = endpoints;
const { fullPath, boardId, boardType, filterParams } = state;
const variables = {
fullPath,
......@@ -176,9 +174,7 @@ export default {
},
updateBoardEpicUserPreferences({ commit, state }, { epicId, collapsed }) {
const {
endpoints: { boardId },
} = state;
const { boardId } = state;
const variables = {
boardId: fullBoardId(boardId),
......@@ -394,7 +390,7 @@ export default {
epicId,
});
const { boardId } = state.endpoints;
const { boardId } = state;
const [fullProjectPath] = issuePath.split(/[#]/);
gqlClient
......
import { GlLabel } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import IssueCardWeight from 'ee/boards/components/issue_card_weight.vue';
import ListIssueEE from 'ee/boards/models/issue';
import IssueCardInner from '~/boards/components/issue_card_inner.vue';
import ListLabel from '~/boards/models/label';
import defaultStore from '~/boards/stores';
describe('Issue card component', () => {
......@@ -22,6 +20,7 @@ describe('Issue card component', () => {
provide: {
groupId: null,
rootPath: '/',
scopedLabelsAvailable: false,
},
});
};
......@@ -31,7 +30,7 @@ describe('Issue card component', () => {
id: 300,
position: 0,
title: 'Test',
list_type: 'label',
listType: 'label',
label: {
id: 5000,
title: 'Testing',
......@@ -41,19 +40,19 @@ describe('Issue card component', () => {
},
};
issue = new ListIssueEE({
issue = {
title: 'Testing',
id: 1,
iid: 1,
confidential: false,
labels: [list.label],
assignees: [],
reference_path: '#1',
real_path: '/test/1',
referencePath: '#1',
webUrl: '/test/1',
weight: 1,
blocked: true,
blockedByCount: 2,
});
};
});
afterEach(() => {
......@@ -63,15 +62,15 @@ describe('Issue card component', () => {
describe('labels', () => {
beforeEach(() => {
const label1 = new ListLabel({
const label1 = {
id: 3,
title: 'testing 123',
color: '#000cff',
text_color: 'white',
textColor: 'white',
description: 'test',
});
};
issue.addLabel(label1);
issue.labels = [...issue.labels, label1];
});
it.each`
......@@ -79,14 +78,15 @@ describe('Issue card component', () => {
${'GroupLabel'} | ${'Group label'} | ${'shows group labels on group boards'}
${'ProjectLabel'} | ${'Project label'} | ${'shows project labels on group boards'}
`('$desc', ({ type, title }) => {
issue.addLabel(
new ListLabel({
issue.labels = [
...issue.labels,
{
id: 9001,
type,
title,
color: '#000000',
}),
);
},
];
createComponent({ groupId: 1 });
......
......@@ -137,10 +137,8 @@ describe('performSearch', () => {
describe('fetchEpicsSwimlanes', () => {
const state = {
endpoints: {
fullPath: 'gitlab-org',
boardId: 1,
},
fullPath: 'gitlab-org',
boardId: 1,
filterParams: {},
boardType: 'group',
};
......@@ -230,9 +228,7 @@ describe('fetchEpicsSwimlanes', () => {
describe('updateBoardEpicUserPreferences', () => {
const state = {
endpoints: {
boardId: 1,
},
boardId: 1,
};
const queryResponse = (collapsed = false) => ({
......@@ -392,10 +388,8 @@ describe('fetchIssuesForEpic', () => {
const epicId = mockEpic.id;
const state = {
endpoints: {
fullPath: 'gitlab-org',
boardId: 1,
},
fullPath: 'gitlab-org',
boardId: 1,
filterParams: {},
boardType: 'group',
};
......@@ -463,10 +457,8 @@ describe('toggleEpicSwimlanes', () => {
const state = {
isShowingEpicsSwimlanes: false,
endpoints: {
fullPath: 'gitlab-org',
boardId: 1,
},
fullPath: 'gitlab-org',
boardId: 1,
};
return testAction(
......@@ -495,10 +487,8 @@ describe('toggleEpicSwimlanes', () => {
const state = {
isShowingEpicsSwimlanes: true,
endpoints: {
fullPath: 'gitlab-org',
boardId: 1,
},
fullPath: 'gitlab-org',
boardId: 1,
};
return testAction(
......@@ -635,7 +625,8 @@ describe('moveIssue', () => {
};
const state = {
endpoints: { fullPath: 'gitlab-org', boardId: '1' },
fullPath: 'gitlab-org',
boardId: 1,
boardType: 'group',
disabled: false,
boardLists: mockLists,
......
......@@ -38,6 +38,7 @@ describe('Board card layout', () => {
provide: {
groupId: null,
rootPath: '/',
scopedLabelsAvailable: false,
},
});
};
......
......@@ -45,6 +45,7 @@ describe('BoardCard', () => {
provide: {
groupId: null,
rootPath: '/',
scopedLabelsAvailable: false,
},
});
};
......
import { shallowMount } from '@vue/test-utils';
import IssueTimeEstimate from '~/boards/components/issue_time_estimate_deprecated.vue';
import boardsStore from '~/boards/stores/boards_store';
describe('Issue Time Estimate component', () => {
let wrapper;
beforeEach(() => {
boardsStore.create();
});
afterEach(() => {
wrapper.destroy();
});
describe('when limitToHours is false', () => {
beforeEach(() => {
boardsStore.timeTracking.limitToHours = false;
wrapper = shallowMount(IssueTimeEstimate, {
propsData: {
estimate: 374460,
},
});
});
it('renders the correct time estimate', () => {
expect(
wrapper
.find('time')
.text()
.trim(),
).toEqual('2w 3d 1m');
});
it('renders expanded time estimate in tooltip', () => {
expect(wrapper.find('.js-issue-time-estimate').text()).toContain('2 weeks 3 days 1 minute');
});
it('prevents tooltip xss', done => {
const alertSpy = jest.spyOn(window, 'alert');
wrapper.setProps({ estimate: 'Foo <script>alert("XSS")</script>' });
wrapper.vm.$nextTick(() => {
expect(alertSpy).not.toHaveBeenCalled();
expect(
wrapper
.find('time')
.text()
.trim(),
).toEqual('0m');
expect(wrapper.find('.js-issue-time-estimate').text()).toContain('0m');
done();
});
});
});
describe('when limitToHours is true', () => {
beforeEach(() => {
boardsStore.timeTracking.limitToHours = true;
wrapper = shallowMount(IssueTimeEstimate, {
propsData: {
estimate: 374460,
},
});
});
it('renders the correct time estimate', () => {
expect(
wrapper
.find('time')
.text()
.trim(),
).toEqual('104h 1m');
});
it('renders expanded time estimate in tooltip', () => {
expect(wrapper.find('.js-issue-time-estimate').text()).toContain('104 hours 1 minute');
});
});
});
import { shallowMount } from '@vue/test-utils';
import IssueTimeEstimate from '~/boards/components/issue_time_estimate.vue';
import boardsStore from '~/boards/stores/boards_store';
describe('Issue Time Estimate component', () => {
let wrapper;
beforeEach(() => {
boardsStore.create();
});
afterEach(() => {
wrapper.destroy();
});
describe('when limitToHours is false', () => {
beforeEach(() => {
boardsStore.timeTracking.limitToHours = false;
wrapper = shallowMount(IssueTimeEstimate, {
propsData: {
estimate: 374460,
},
provide: {
timeTrackingLimitToHours: false,
},
});
});
......@@ -55,11 +52,13 @@ describe('Issue Time Estimate component', () => {
describe('when limitToHours is true', () => {
beforeEach(() => {
boardsStore.timeTracking.limitToHours = true;
wrapper = shallowMount(IssueTimeEstimate, {
propsData: {
estimate: 374460,
},
provide: {
timeTrackingLimitToHours: true,
},
});
});
......
......@@ -6,7 +6,7 @@ import '~/boards/models/assignee';
import '~/boards/models/issue';
import '~/boards/models/list';
import { GlLabel } from '@gitlab/ui';
import IssueCardInner from '~/boards/components/issue_card_inner.vue';
import IssueCardInner from '~/boards/components/issue_card_inner_deprecated.vue';
import { listObj } from './mock_data';
import store from '~/boards/stores';
......
import { mount } from '@vue/test-utils';
import { range } from 'lodash';
import { GlLabel } from '@gitlab/ui';
import IssueCardInner from '~/boards/components/issue_card_inner.vue';
import { mockLabelList } from './mock_data';
import defaultStore from '~/boards/stores';
import eventHub from '~/boards/eventhub';
import { updateHistory } from '~/lib/utils/url_utility';
jest.mock('~/lib/utils/url_utility');
jest.mock('~/boards/eventhub');
describe('Issue card component', () => {
const user = {
id: 1,
name: 'testing 123',
username: 'test',
avatarUrl: 'test_image',
};
const label1 = {
id: 3,
title: 'testing 123',
color: '#000CFF',
textColor: 'white',
description: 'test',
};
let wrapper;
let issue;
let list;
const createWrapper = (props = {}, store = defaultStore) => {
wrapper = mount(IssueCardInner, {
store,
propsData: {
list,
issue,
...props,
},
stubs: {
GlLabel: true,
},
provide: {
groupId: null,
rootPath: '/',
scopedLabelsAvailable: false,
},
});
};
beforeEach(() => {
list = mockLabelList;
issue = {
title: 'Testing',
id: 1,
iid: 1,
confidential: false,
labels: [list.label],
assignees: [],
referencePath: '#1',
webUrl: '/test/1',
weight: 1,
};
createWrapper({ issue, list });
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
jest.clearAllMocks();
});
it('renders issue title', () => {
expect(wrapper.find('.board-card-title').text()).toContain(issue.title);
});
it('includes issue base in link', () => {
expect(wrapper.find('.board-card-title a').attributes('href')).toContain('/test');
});
it('includes issue title on link', () => {
expect(wrapper.find('.board-card-title a').attributes('title')).toBe(issue.title);
});
it('does not render confidential icon', () => {
expect(wrapper.find('.confidential-icon').exists()).toBe(false);
});
it('does not render blocked icon', () => {
expect(wrapper.find('.issue-blocked-icon').exists()).toBe(false);
});
it('renders issue ID with #', () => {
expect(wrapper.find('.board-card-number').text()).toContain(`#${issue.id}`);
});
it('does not render assignee', () => {
expect(wrapper.find('.board-card-assignee .avatar').exists()).toBe(false);
});
describe('confidential issue', () => {
beforeEach(() => {
wrapper.setProps({
issue: {
...wrapper.props('issue'),
confidential: true,
},
});
});
it('renders confidential icon', () => {
expect(wrapper.find('.confidential-icon').exists()).toBe(true);
});
});
describe('with assignee', () => {
describe('with avatar', () => {
beforeEach(() => {
wrapper.setProps({
issue: {
...wrapper.props('issue'),
assignees: [user],
updateData(newData) {
Object.assign(this, newData);
},
},
});
});
it('renders assignee', () => {
expect(wrapper.find('.board-card-assignee .avatar').exists()).toBe(true);
});
it('sets title', () => {
expect(wrapper.find('.js-assignee-tooltip').text()).toContain(`${user.name}`);
});
it('sets users path', () => {
expect(wrapper.find('.board-card-assignee a').attributes('href')).toBe('/test');
});
it('renders avatar', () => {
expect(wrapper.find('.board-card-assignee img').exists()).toBe(true);
});
it('renders the avatar using avatarUrl property', async () => {
wrapper.props('issue').updateData({
...wrapper.props('issue'),
assignees: [
{
id: '1',
name: 'test',
state: 'active',
username: 'test_name',
avatarUrl: 'test_image_from_avatar_url',
},
],
});
await wrapper.vm.$nextTick();
expect(wrapper.find('.board-card-assignee img').attributes('src')).toBe(
'test_image_from_avatar_url?width=24',
);
});
});
describe('with default avatar', () => {
beforeEach(() => {
global.gon.default_avatar_url = 'default_avatar';
wrapper.setProps({
issue: {
...wrapper.props('issue'),
assignees: [
{
id: 1,
name: 'testing 123',
username: 'test',
},
],
},
});
});
afterEach(() => {
global.gon.default_avatar_url = null;
});
it('displays defaults avatar if users avatar is null', () => {
expect(wrapper.find('.board-card-assignee img').exists()).toBe(true);
expect(wrapper.find('.board-card-assignee img').attributes('src')).toBe(
'default_avatar?width=24',
);
});
});
});
describe('multiple assignees', () => {
beforeEach(() => {
wrapper.setProps({
issue: {
...wrapper.props('issue'),
assignees: [
{
id: 2,
name: 'user2',
username: 'user2',
avatarUrl: 'test_image',
},
{
id: 3,
name: 'user3',
username: 'user3',
avatarUrl: 'test_image',
},
{
id: 4,
name: 'user4',
username: 'user4',
avatarUrl: 'test_image',
},
],
},
});
});
it('renders all three assignees', () => {
expect(wrapper.findAll('.board-card-assignee .avatar').length).toEqual(3);
});
describe('more than three assignees', () => {
beforeEach(() => {
const { assignees } = wrapper.props('issue');
assignees.push({
id: 5,
name: 'user5',
username: 'user5',
avatarUrl: 'test_image',
});
wrapper.setProps({
issue: {
...wrapper.props('issue'),
assignees,
},
});
});
it('renders more avatar counter', () => {
expect(
wrapper
.find('.board-card-assignee .avatar-counter')
.text()
.trim(),
).toEqual('+2');
});
it('renders two assignees', () => {
expect(wrapper.findAll('.board-card-assignee .avatar').length).toEqual(2);
});
it('renders 99+ avatar counter', async () => {
const assignees = [
...wrapper.props('issue').assignees,
...range(5, 103).map(i => ({
id: i,
name: 'name',
username: 'username',
avatarUrl: 'test_image',
})),
];
wrapper.setProps({
issue: {
...wrapper.props('issue'),
assignees,
},
});
await wrapper.vm.$nextTick();
expect(
wrapper
.find('.board-card-assignee .avatar-counter')
.text()
.trim(),
).toEqual('99+');
});
});
});
describe('labels', () => {
beforeEach(() => {
wrapper.setProps({ issue: { ...issue, labels: [list.label, label1] } });
});
it('does not render list label but renders all other labels', () => {
expect(wrapper.findAll(GlLabel).length).toBe(1);
const label = wrapper.find(GlLabel);
expect(label.props('title')).toEqual(label1.title);
expect(label.props('description')).toEqual(label1.description);
expect(label.props('backgroundColor')).toEqual(label1.color);
});
it('does not render label if label does not have an ID', async () => {
wrapper.setProps({ issue: { ...issue, labels: [label1, { title: 'closed' }] } });
await wrapper.vm.$nextTick();
expect(wrapper.findAll(GlLabel).length).toBe(1);
expect(wrapper.text()).not.toContain('closed');
});
});
describe('blocked', () => {
beforeEach(() => {
wrapper.setProps({
issue: {
...wrapper.props('issue'),
blocked: true,
},
});
});
it('renders blocked icon if issue is blocked', () => {
expect(wrapper.find('.issue-blocked-icon').exists()).toBe(true);
});
});
describe('filterByLabel method', () => {
beforeEach(() => {
delete window.location;
wrapper.setProps({
updateFilters: true,
});
});
describe('when selected label is not in the filter', () => {
beforeEach(() => {
jest.spyOn(wrapper.vm, 'performSearch').mockImplementation(() => {});
window.location = { search: '' };
wrapper.vm.filterByLabel(label1);
});
it('calls updateHistory', () => {
expect(updateHistory).toHaveBeenCalledTimes(1);
});
it('dispatches performSearch vuex action', () => {
expect(wrapper.vm.performSearch).toHaveBeenCalledTimes(1);
});
it('emits updateTokens event', () => {
expect(eventHub.$emit).toHaveBeenCalledTimes(1);
expect(eventHub.$emit).toHaveBeenCalledWith('updateTokens');
});
});
describe('when selected label is already in the filter', () => {
beforeEach(() => {
jest.spyOn(wrapper.vm, 'performSearch').mockImplementation(() => {});
window.location = { search: '?label_name[]=testing%20123' };
wrapper.vm.filterByLabel(label1);
});
it('does not call updateHistory', () => {
expect(updateHistory).not.toHaveBeenCalled();
});
it('does not dispatch performSearch vuex action', () => {
expect(wrapper.vm.performSearch).not.toHaveBeenCalled();
});
it('does not emit updateTokens event', () => {
expect(eventHub.$emit).not.toHaveBeenCalled();
});
});
});
});
......@@ -108,10 +108,8 @@ describe('setActiveId', () => {
describe('fetchLists', () => {
const state = {
endpoints: {
fullPath: 'gitlab-org',
boardId: 1,
},
fullPath: 'gitlab-org',
boardId: '1',
filterParams: {},
boardType: 'group',
};
......@@ -201,7 +199,8 @@ describe('createList', () => {
);
const state = {
endpoints: { fullPath: 'gitlab-org', boardId: '1' },
fullPath: 'gitlab-org',
boardId: '1',
boardType: 'group',
disabled: false,
boardLists: [{ type: 'closed' }],
......@@ -230,7 +229,8 @@ describe('createList', () => {
);
const state = {
endpoints: { fullPath: 'gitlab-org', boardId: '1' },
fullPath: 'gitlab-org',
boardId: '1',
boardType: 'group',
disabled: false,
boardLists: [{ type: 'closed' }],
......@@ -255,7 +255,8 @@ describe('moveList', () => {
};
const state = {
endpoints: { fullPath: 'gitlab-org', boardId: '1' },
fullPath: 'gitlab-org',
boardId: '1',
boardType: 'group',
disabled: false,
boardLists: initialBoardListsState,
......@@ -297,7 +298,8 @@ describe('moveList', () => {
};
const state = {
endpoints: { fullPath: 'gitlab-org', boardId: '1' },
fullPath: 'gitlab-org',
boardId: '1',
boardType: 'group',
disabled: false,
boardLists: initialBoardListsState,
......@@ -330,7 +332,8 @@ describe('updateList', () => {
});
const state = {
endpoints: { fullPath: 'gitlab-org', boardId: '1' },
fullPath: 'gitlab-org',
boardId: '1',
boardType: 'group',
disabled: false,
boardLists: [{ type: 'closed' }],
......@@ -429,10 +432,8 @@ describe('fetchIssuesForList', () => {
const listId = mockLists[0].id;
const state = {
endpoints: {
fullPath: 'gitlab-org',
boardId: 1,
},
fullPath: 'gitlab-org',
boardId: '1',
filterParams: {},
boardType: 'group',
};
......@@ -530,7 +531,8 @@ describe('moveIssue', () => {
};
const state = {
endpoints: { fullPath: 'gitlab-org', boardId: '1' },
fullPath: 'gitlab-org',
boardId: '1',
boardType: 'group',
disabled: false,
boardLists: mockLists,
......@@ -582,7 +584,7 @@ describe('moveIssue', () => {
mutation: issueMoveListMutation,
variables: {
projectPath: getProjectPath(mockIssue.referencePath),
boardId: fullBoardId(state.endpoints.boardId),
boardId: fullBoardId(state.boardId),
iid: mockIssue.iid,
fromListId: 1,
toListId: 2,
......@@ -724,9 +726,7 @@ describe('setAssignees', () => {
describe('createNewIssue', () => {
const state = {
boardType: 'group',
endpoints: {
fullPath: 'gitlab-org/gitlab',
},
fullPath: 'gitlab-org/gitlab',
};
it('should return issue from API on success', async () => {
......
......@@ -23,14 +23,8 @@ describe('Board Store Mutations', () => {
describe('SET_INITIAL_BOARD_DATA', () => {
it('Should set initial Boards data to state', () => {
const endpoints = {
boardsEndpoint: '/boards/',
recentBoardsEndpoint: '/boards/',
listsEndpoint: '/boards/lists',
bulkUpdatePath: '/boards/bulkUpdate',
boardId: 1,
fullPath: 'gitlab-org',
};
const boardId = 1;
const fullPath = 'gitlab-org';
const boardType = 'group';
const disabled = false;
const boardConfig = {
......@@ -38,13 +32,15 @@ describe('Board Store Mutations', () => {
};
mutations[types.SET_INITIAL_BOARD_DATA](state, {
...endpoints,
boardId,
fullPath,
boardType,
disabled,
boardConfig,
});
expect(state.endpoints).toEqual(endpoints);
expect(state.boardId).toEqual(boardId);
expect(state.fullPath).toEqual(fullPath);
expect(state.boardType).toEqual(boardType);
expect(state.disabled).toEqual(disabled);
expect(state.boardConfig).toEqual(boardConfig);
......
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