import { shallowMount } from '@vue/test-utils'; import { GlSprintf, GlLabel, GlIcon } from '@gitlab/ui'; import { TEST_HOST } from 'helpers/test_constants'; import { trimText } from 'helpers/text_helper'; import initUserPopovers from '~/user_popovers'; import { formatDate } from '~/lib/utils/datetime_utility'; import { mergeUrlParams } from '~/lib/utils/url_utility'; import Issuable from '~/issues_list/components/issuable.vue'; import IssueAssignees from '~/vue_shared/components/issue/issue_assignees.vue'; import { simpleIssue, testAssignees, testLabels } from '../issuable_list_test_data'; import { isScopedLabel } from '~/lib/utils/common_utils'; jest.mock('~/user_popovers'); const TEST_NOW = '2019-08-28T20:03:04.713Z'; const TEST_MONTH_AGO = '2019-07-28'; const TEST_MONTH_LATER = '2019-09-30'; const DATE_FORMAT = 'mmm d, yyyy'; const TEST_USER_NAME = 'Tyler Durden'; const TEST_BASE_URL = `${TEST_HOST}/issues`; const TEST_TASK_STATUS = '50 of 100 tasks completed'; const TEST_MILESTONE = { title: 'Milestone title', web_url: `${TEST_HOST}/milestone/1`, }; const TEXT_CLOSED = 'CLOSED'; const TEST_META_COUNT = 100; // Use FixedDate so that time sensitive info in snapshots don't fail class FixedDate extends Date { constructor(date = TEST_NOW) { super(date); } } describe('Issuable component', () => { let issuable; let DateOrig; let wrapper; const factory = (props = {}, scopedLabelsAvailable = false) => { wrapper = shallowMount(Issuable, { propsData: { issuable: simpleIssue, baseUrl: TEST_BASE_URL, ...props, }, provide: { scopedLabelsAvailable, }, stubs: { 'gl-sprintf': GlSprintf, }, }); }; beforeEach(() => { issuable = { ...simpleIssue }; }); afterEach(() => { wrapper.destroy(); wrapper = null; }); beforeAll(() => { DateOrig = window.Date; window.Date = FixedDate; }); afterAll(() => { window.Date = DateOrig; }); const checkExists = (findFn) => () => findFn().exists(); const hasIcon = (iconName, iconWrapper = wrapper) => iconWrapper.findAll(GlIcon).wrappers.some((icon) => icon.props('name') === iconName); const hasConfidentialIcon = () => hasIcon('eye-slash'); const findTaskStatus = () => wrapper.find('.task-status'); const findOpenedAgoContainer = () => wrapper.find('[data-testid="openedByMessage"]'); const findAuthor = () => wrapper.find({ ref: 'openedAgoByContainer' }); const findMilestone = () => wrapper.find('.js-milestone'); const findMilestoneTooltip = () => findMilestone().attributes('title'); const findDueDate = () => wrapper.find('.js-due-date'); const findLabels = () => wrapper.findAll(GlLabel); const findWeight = () => wrapper.find('[data-testid="weight"]'); const findAssignees = () => wrapper.find(IssueAssignees); const findBlockingIssuesCount = () => wrapper.find('[data-testid="blocking-issues"]'); const findMergeRequestsCount = () => wrapper.find('[data-testid="merge-requests"]'); const findUpvotes = () => wrapper.find('[data-testid="upvotes"]'); const findDownvotes = () => wrapper.find('[data-testid="downvotes"]'); const findNotes = () => wrapper.find('[data-testid="notes-count"]'); const findBulkCheckbox = () => wrapper.find('input.selected-issuable'); const findScopedLabels = () => findLabels().filter((w) => isScopedLabel({ title: w.text() })); const findUnscopedLabels = () => findLabels().filter((w) => !isScopedLabel({ title: w.text() })); const findIssuableTitle = () => wrapper.find('[data-testid="issuable-title"]'); const findIssuableStatus = () => wrapper.find('[data-testid="issuable-status"]'); const containsJiraLogo = () => wrapper.find('[data-testid="jira-logo"]').exists(); const findHealthStatus = () => wrapper.find('.health-status'); describe('when mounted', () => { it('initializes user popovers', () => { expect(initUserPopovers).not.toHaveBeenCalled(); factory(); expect(initUserPopovers).toHaveBeenCalledWith([wrapper.vm.$refs.openedAgoByContainer.$el]); }); }); describe('when scopedLabels feature is available', () => { beforeEach(() => { issuable.labels = [...testLabels]; factory({ issuable }, true); }); describe('when label is scoped', () => { it('returns label with correct props', () => { const scopedLabel = findScopedLabels().at(0); expect(scopedLabel.props('scoped')).toBe(true); }); }); describe('when label is not scoped', () => { it('returns label with correct props', () => { const notScopedLabel = findUnscopedLabels().at(0); expect(notScopedLabel.props('scoped')).toBe(false); }); }); }); describe('when scopedLabels feature is not available', () => { beforeEach(() => { issuable.labels = [...testLabels]; factory({ issuable }); }); describe('when label is scoped', () => { it('label scoped props is false', () => { const scopedLabel = findScopedLabels().at(0); expect(scopedLabel.props('scoped')).toBe(false); }); }); describe('when label is not scoped', () => { it('label scoped props is false', () => { const notScopedLabel = findUnscopedLabels().at(0); expect(notScopedLabel.props('scoped')).toBe(false); }); }); }); describe('with simple issuable', () => { beforeEach(() => { Object.assign(issuable, { has_tasks: false, task_status: TEST_TASK_STATUS, created_at: TEST_MONTH_AGO, author: { ...issuable.author, name: TEST_USER_NAME, }, labels: [], }); factory({ issuable }); }); it.each` desc | check ${'bulk editing checkbox'} | ${checkExists(findBulkCheckbox)} ${'confidential icon'} | ${hasConfidentialIcon} ${'task status'} | ${checkExists(findTaskStatus)} ${'milestone'} | ${checkExists(findMilestone)} ${'due date'} | ${checkExists(findDueDate)} ${'labels'} | ${checkExists(findLabels)} ${'weight'} | ${checkExists(findWeight)} ${'blocking issues count'} | ${checkExists(findBlockingIssuesCount)} ${'merge request count'} | ${checkExists(findMergeRequestsCount)} ${'upvotes'} | ${checkExists(findUpvotes)} ${'downvotes'} | ${checkExists(findDownvotes)} `('does not render $desc', ({ check }) => { expect(check()).toBe(false); }); it('show relative reference path', () => { expect(wrapper.find('.js-ref-path').text()).toBe(issuable.references.relative); }); it('does not have closed text', () => { expect(wrapper.text()).not.toContain(TEXT_CLOSED); }); it('does not have closed class', () => { expect(wrapper.classes('closed')).toBe(false); }); it('renders fuzzy opened date and author', () => { expect(trimText(findOpenedAgoContainer().text())).toContain( `opened 1 month ago by ${TEST_USER_NAME}`, ); }); it('renders no comments', () => { expect(findNotes().classes('no-comments')).toBe(true); }); }); describe('with confidential issuable', () => { beforeEach(() => { issuable.confidential = true; factory({ issuable }); }); it('renders the confidential icon', () => { expect(hasConfidentialIcon()).toBe(true); }); }); describe('with Jira issuable', () => { beforeEach(() => { issuable.external_tracker = 'jira'; factory({ issuable }); }); it('renders the Jira icon', () => { expect(containsJiraLogo()).toBe(true); }); it('opens issuable in a new tab', () => { expect(findIssuableTitle().props('target')).toBe('_blank'); }); it('opens author in a new tab', () => { expect(findAuthor().props('target')).toBe('_blank'); }); describe('with Jira status', () => { const expectedStatus = 'In Progress'; beforeEach(() => { issuable.status = expectedStatus; factory({ issuable }); }); it('renders the Jira status', () => { expect(findIssuableStatus().text()).toBe(expectedStatus); }); }); }); describe('with task status', () => { beforeEach(() => { Object.assign(issuable, { has_tasks: true, task_status: TEST_TASK_STATUS, }); factory({ issuable }); }); it('renders task status', () => { expect(findTaskStatus().exists()).toBe(true); expect(findTaskStatus().text()).toBe(TEST_TASK_STATUS); }); }); describe.each` desc | dueDate | expectedTooltipPart ${'past due'} | ${TEST_MONTH_AGO} | ${'Past due'} ${'future due'} | ${TEST_MONTH_LATER} | ${'1 month remaining'} `('with milestone with $desc', ({ dueDate, expectedTooltipPart }) => { beforeEach(() => { issuable.milestone = { ...TEST_MILESTONE, due_date: dueDate }; factory({ issuable }); }); it('renders milestone', () => { expect(findMilestone().exists()).toBe(true); expect(hasIcon('clock', findMilestone())).toBe(true); expect(findMilestone().text()).toEqual(TEST_MILESTONE.title); }); it('renders tooltip', () => { expect(findMilestoneTooltip()).toBe( `${formatDate(dueDate, DATE_FORMAT)} (${expectedTooltipPart})`, ); }); it('renders milestone with the correct href', () => { const { title } = issuable.milestone; const expected = mergeUrlParams({ milestone_title: title }, TEST_BASE_URL); expect(findMilestone().attributes('href')).toBe(expected); }); }); describe.each` dueDate | hasClass | desc ${TEST_MONTH_LATER} | ${false} | ${'with future due date'} ${TEST_MONTH_AGO} | ${true} | ${'with past due date'} `('$desc', ({ dueDate, hasClass }) => { beforeEach(() => { issuable.due_date = dueDate; factory({ issuable }); }); it('renders due date', () => { expect(findDueDate().exists()).toBe(true); expect(findDueDate().text()).toBe(formatDate(dueDate, DATE_FORMAT)); }); it(hasClass ? 'has cred class' : 'does not have cred class', () => { expect(findDueDate().classes('cred')).toEqual(hasClass); }); }); describe('with labels', () => { beforeEach(() => { issuable.labels = [...testLabels]; factory({ issuable }); }); it('renders labels', () => { factory({ issuable }); const labels = findLabels().wrappers.map((label) => ({ href: label.props('target'), text: label.text(), tooltip: label.attributes('description'), })); const expected = testLabels.map((label) => ({ href: mergeUrlParams({ 'label_name[]': label.name }, TEST_BASE_URL), text: label.name, tooltip: label.description, })); expect(labels).toEqual(expected); }); }); describe('with labels for Jira issuable', () => { beforeEach(() => { issuable.labels = [...testLabels]; issuable.external_tracker = 'jira'; factory({ issuable }); }); it('renders labels', () => { factory({ issuable }); const labels = findLabels().wrappers.map((label) => ({ href: label.props('target'), text: label.text(), tooltip: label.attributes('description'), })); const expected = testLabels.map((label) => ({ href: mergeUrlParams({ 'labels[]': label.name }, TEST_BASE_URL), text: label.name, tooltip: label.description, })); expect(labels).toEqual(expected); }); }); describe.each` weight ${0} ${10} ${12345} `('with weight $weight', ({ weight }) => { beforeEach(() => { issuable.weight = weight; factory({ issuable }); }); it('renders weight', () => { expect(findWeight().exists()).toBe(true); expect(findWeight().text()).toEqual(weight.toString()); }); }); describe('with closed state', () => { beforeEach(() => { issuable.state = 'closed'; factory({ issuable }); }); it('renders closed text', () => { expect(wrapper.text()).toContain(TEXT_CLOSED); }); it('has closed class', () => { expect(wrapper.classes('closed')).toBe(true); }); }); describe('with assignees', () => { beforeEach(() => { issuable.assignees = testAssignees; factory({ issuable }); }); it('renders assignees', () => { expect(findAssignees().exists()).toBe(true); expect(findAssignees().props('assignees')).toEqual(testAssignees); }); }); describe.each` desc | key | finder ${'with blocking issues count'} | ${'blocking_issues_count'} | ${findBlockingIssuesCount} ${'with merge requests count'} | ${'merge_requests_count'} | ${findMergeRequestsCount} ${'with upvote count'} | ${'upvotes'} | ${findUpvotes} ${'with downvote count'} | ${'downvotes'} | ${findDownvotes} ${'with notes count'} | ${'user_notes_count'} | ${findNotes} `('$desc', ({ key, finder }) => { beforeEach(() => { issuable[key] = TEST_META_COUNT; factory({ issuable }); }); it('renders correct count', () => { expect(finder().exists()).toBe(true); expect(finder().text()).toBe(TEST_META_COUNT.toString()); expect(finder().classes('no-comments')).toBe(false); }); }); describe('with bulk editing', () => { describe.each` selected | desc ${true} | ${'when selected'} ${false} | ${'when unselected'} `('$desc', ({ selected }) => { beforeEach(() => { factory({ isBulkEditing: true, selected }); }); it(`renders checked is ${selected}`, () => { expect(findBulkCheckbox().element.checked).toBe(selected); }); it('emits select when clicked', () => { expect(wrapper.emitted().select).toBeUndefined(); findBulkCheckbox().trigger('click'); return wrapper.vm.$nextTick().then(() => { expect(wrapper.emitted().select).toEqual([[{ issuable, selected: !selected }]]); }); }); }); }); if (IS_EE) { describe('with health status', () => { it('renders health status tag', () => { factory({ issuable }); expect(findHealthStatus().exists()).toBe(true); }); it('does not render when health status is absent', () => { issuable.health_status = null; factory({ issuable }); expect(findHealthStatus().exists()).toBe(false); }); }); } });