diff options
Diffstat (limited to 'spec/frontend/issues_list')
11 files changed, 0 insertions, 2916 deletions
diff --git a/spec/frontend/issues_list/components/__snapshots__/issuables_list_app_spec.js.snap b/spec/frontend/issues_list/components/__snapshots__/issuables_list_app_spec.js.snap deleted file mode 100644 index c327b7de827..00000000000 --- a/spec/frontend/issues_list/components/__snapshots__/issuables_list_app_spec.js.snap +++ /dev/null @@ -1,14 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Issuables list component with empty issues response with all state should display a catch-all if there are no issues to show 1`] = ` -<gl-empty-state-stub - svgpath="/emptySvg" - title="There are no issues to show" -/> -`; - -exports[`Issuables list component with empty issues response with closed state should display a message "There are no closed issues" if there are no closed issues 1`] = `"There are no closed issues"`; - -exports[`Issuables list component with empty issues response with empty query should display the message "There are no open issues" 1`] = `"There are no open issues"`; - -exports[`Issuables list component with empty issues response with query in window location should display "Sorry, your filter produced no results" if filters are too specific 1`] = `"Sorry, your filter produced no results"`; diff --git a/spec/frontend/issues_list/components/issuable_spec.js b/spec/frontend/issues_list/components/issuable_spec.js deleted file mode 100644 index f3c2ae1f9dc..00000000000 --- a/spec/frontend/issues_list/components/issuable_spec.js +++ /dev/null @@ -1,508 +0,0 @@ -import { GlSprintf, GlLabel, GlIcon, GlLink } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import { TEST_HOST } from 'helpers/test_constants'; -import { trimText } from 'helpers/text_helper'; -import Issuable from '~/issues_list/components/issuable.vue'; -import { isScopedLabel } from '~/lib/utils/common_utils'; -import { formatDate } from '~/lib/utils/datetime_utility'; -import { mergeUrlParams } from '~/lib/utils/url_utility'; -import initUserPopovers from '~/user_popovers'; -import IssueAssignees from '~/issuable/components/issue_assignees.vue'; -import { simpleIssue, testAssignees, testLabels } from '../issuable_list_test_data'; - -jest.mock('~/user_popovers'); - -const TODAY = new Date(); - -const createTestDateFromDelta = (timeDelta) => - formatDate(new Date(TODAY.getTime() + timeDelta), 'yyyy-mm-dd'); - -// TODO: Encapsulate date helpers https://gitlab.com/gitlab-org/gitlab/-/issues/320883 -const MONTHS_IN_MS = 1000 * 60 * 60 * 24 * 31; -const TEST_MONTH_AGO = createTestDateFromDelta(-MONTHS_IN_MS); -const TEST_MONTH_LATER = createTestDateFromDelta(MONTHS_IN_MS); -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; -const MOCK_GITLAB_URL = 'http://0.0.0.0:3000'; - -describe('Issuable component', () => { - let issuable; - 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 }; - gon.gitlab_url = MOCK_GITLAB_URL; - }); - - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - - 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 created date and author', () => { - expect(trimText(findOpenedAgoContainer().text())).toContain( - `created 1 month ago by ${TEST_USER_NAME}`, - ); - }); - - it('renders no comments', () => { - expect(findNotes().classes('no-comments')).toBe(true); - }); - - it.each` - gitlabWebUrl | webUrl | expectedHref | expectedTarget | isExternal - ${undefined} | ${`${MOCK_GITLAB_URL}/issue`} | ${`${MOCK_GITLAB_URL}/issue`} | ${undefined} | ${false} - ${undefined} | ${'https://jira.com/issue'} | ${'https://jira.com/issue'} | ${'_blank'} | ${true} - ${'/gitlab-org/issue'} | ${'https://jira.com/issue'} | ${'/gitlab-org/issue'} | ${undefined} | ${false} - `( - 'renders issuable title correctly when `gitlabWebUrl` is `$gitlabWebUrl` and webUrl is `$webUrl`', - async ({ webUrl, gitlabWebUrl, expectedHref, expectedTarget, isExternal }) => { - factory({ - issuable: { - ...issuable, - web_url: webUrl, - gitlab_web_url: gitlabWebUrl, - }, - }); - - const titleEl = findIssuableTitle(); - - expect(titleEl.exists()).toBe(true); - expect(titleEl.find(GlLink).attributes('href')).toBe(expectedHref); - expect(titleEl.find(GlLink).attributes('target')).toBe(expectedTarget); - expect(titleEl.find(GlLink).text()).toBe(issuable.title); - - expect(titleEl.find(GlIcon).exists()).toBe(isExternal); - }, - ); - }); - - 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); - }); - }); - } -}); diff --git a/spec/frontend/issues_list/components/issuables_list_app_spec.js b/spec/frontend/issues_list/components/issuables_list_app_spec.js deleted file mode 100644 index 11854db534e..00000000000 --- a/spec/frontend/issues_list/components/issuables_list_app_spec.js +++ /dev/null @@ -1,653 +0,0 @@ -import { - GlEmptyState, - GlPagination, - GlDeprecatedSkeletonLoading as GlSkeletonLoading, -} from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import axios from 'axios'; -import MockAdapter from 'axios-mock-adapter'; -import setWindowLocation from 'helpers/set_window_location_helper'; -import { TEST_HOST } from 'helpers/test_constants'; -import waitForPromises from 'helpers/wait_for_promises'; -import createFlash from '~/flash'; -import Issuable from '~/issues_list/components/issuable.vue'; -import IssuablesListApp from '~/issues_list/components/issuables_list_app.vue'; -import { PAGE_SIZE, PAGE_SIZE_MANUAL, RELATIVE_POSITION } from '~/issues_list/constants'; -import issuablesEventBus from '~/issues_list/eventhub'; -import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue'; - -jest.mock('~/flash'); -jest.mock('~/issues_list/eventhub'); -jest.mock('~/lib/utils/common_utils', () => ({ - ...jest.requireActual('~/lib/utils/common_utils'), - scrollToElement: () => {}, -})); - -const TEST_LOCATION = `${TEST_HOST}/issues`; -const TEST_ENDPOINT = '/issues'; -const TEST_CREATE_ISSUES_PATH = '/createIssue'; -const TEST_SVG_PATH = '/emptySvg'; - -const MOCK_ISSUES = Array(PAGE_SIZE_MANUAL) - .fill(0) - .map((_, i) => ({ - id: i, - web_url: `url${i}`, - })); - -describe('Issuables list component', () => { - let mockAxios; - let wrapper; - let apiSpy; - - const setupApiMock = (cb) => { - apiSpy = jest.fn(cb); - - mockAxios.onGet(TEST_ENDPOINT).reply((cfg) => apiSpy(cfg)); - }; - - const factory = (props = { sortKey: 'priority' }) => { - const emptyStateMeta = { - createIssuePath: TEST_CREATE_ISSUES_PATH, - svgPath: TEST_SVG_PATH, - }; - - wrapper = shallowMount(IssuablesListApp, { - propsData: { - endpoint: TEST_ENDPOINT, - emptyStateMeta, - ...props, - }, - }); - }; - - const findLoading = () => wrapper.find(GlSkeletonLoading); - const findIssuables = () => wrapper.findAll(Issuable); - const findFilteredSearchBar = () => wrapper.find(FilteredSearchBar); - const findFirstIssuable = () => findIssuables().wrappers[0]; - const findEmptyState = () => wrapper.find(GlEmptyState); - - beforeEach(() => { - mockAxios = new MockAdapter(axios); - - setWindowLocation(TEST_LOCATION); - }); - - afterEach(() => { - wrapper.destroy(); - wrapper = null; - mockAxios.restore(); - }); - - describe('with failed issues response', () => { - beforeEach(() => { - setupApiMock(() => [500]); - - factory(); - - return waitForPromises(); - }); - - it('does not show loading', () => { - expect(wrapper.vm.loading).toBe(false); - }); - - it('flashes an error', () => { - expect(createFlash).toHaveBeenCalledTimes(1); - }); - }); - - describe('with successful issues response', () => { - beforeEach(() => { - setupApiMock(() => [ - 200, - MOCK_ISSUES.slice(0, PAGE_SIZE), - { - 'x-total': 100, - 'x-page': 2, - }, - ]); - }); - - it('has default props and data', () => { - factory(); - expect(wrapper.vm).toMatchObject({ - // Props - canBulkEdit: false, - emptyStateMeta: { - createIssuePath: TEST_CREATE_ISSUES_PATH, - svgPath: TEST_SVG_PATH, - }, - // Data - filters: { - state: 'opened', - }, - isBulkEditing: false, - issuables: [], - loading: true, - page: 1, - selection: {}, - totalItems: 0, - }); - }); - - it('does not call API until mounted', () => { - factory(); - expect(apiSpy).not.toHaveBeenCalled(); - }); - - describe('when mounted', () => { - beforeEach(() => { - factory(); - }); - - it('calls API', () => { - expect(apiSpy).toHaveBeenCalled(); - }); - - it('shows loading', () => { - expect(findLoading().exists()).toBe(true); - expect(findIssuables().length).toBe(0); - expect(findEmptyState().exists()).toBe(false); - }); - }); - - describe('when finished loading', () => { - beforeEach(() => { - factory(); - - return waitForPromises(); - }); - - it('does not display empty state', () => { - expect(wrapper.vm.issuables.length).toBeGreaterThan(0); - expect(wrapper.vm.emptyState).toEqual({}); - expect(wrapper.find(GlEmptyState).exists()).toBe(false); - }); - - it('sets the proper page and total items', () => { - expect(wrapper.vm.totalItems).toBe(100); - expect(wrapper.vm.page).toBe(2); - }); - - it('renders one page of issuables and pagination', () => { - expect(findIssuables().length).toBe(PAGE_SIZE); - expect(wrapper.find(GlPagination).exists()).toBe(true); - }); - }); - - it('does not render FilteredSearchBar', () => { - factory(); - - expect(findFilteredSearchBar().exists()).toBe(false); - }); - }); - - describe('with bulk editing enabled', () => { - beforeEach(() => { - issuablesEventBus.$on.mockReset(); - issuablesEventBus.$emit.mockReset(); - - setupApiMock(() => [200, MOCK_ISSUES.slice(0)]); - factory({ canBulkEdit: true }); - - return waitForPromises(); - }); - - it('is not enabled by default', () => { - expect(wrapper.vm.isBulkEditing).toBe(false); - }); - - it('does not select issues by default', () => { - expect(wrapper.vm.selection).toEqual({}); - }); - - it('"Select All" checkbox toggles all visible issuables"', () => { - wrapper.vm.onSelectAll(); - expect(wrapper.vm.selection).toEqual( - wrapper.vm.issuables.reduce((acc, i) => ({ ...acc, [i.id]: true }), {}), - ); - - wrapper.vm.onSelectAll(); - expect(wrapper.vm.selection).toEqual({}); - }); - - it('"Select All checkbox" selects all issuables if only some are selected"', () => { - wrapper.vm.selection = { [wrapper.vm.issuables[0].id]: true }; - wrapper.vm.onSelectAll(); - expect(wrapper.vm.selection).toEqual( - wrapper.vm.issuables.reduce((acc, i) => ({ ...acc, [i.id]: true }), {}), - ); - }); - - it('selects and deselects issuables', () => { - const [i0, i1, i2] = wrapper.vm.issuables; - - expect(wrapper.vm.selection).toEqual({}); - wrapper.vm.onSelectIssuable({ issuable: i0, selected: false }); - expect(wrapper.vm.selection).toEqual({}); - wrapper.vm.onSelectIssuable({ issuable: i1, selected: true }); - expect(wrapper.vm.selection).toEqual({ 1: true }); - wrapper.vm.onSelectIssuable({ issuable: i0, selected: true }); - expect(wrapper.vm.selection).toEqual({ 1: true, 0: true }); - wrapper.vm.onSelectIssuable({ issuable: i2, selected: true }); - expect(wrapper.vm.selection).toEqual({ 1: true, 0: true, 2: true }); - wrapper.vm.onSelectIssuable({ issuable: i2, selected: true }); - expect(wrapper.vm.selection).toEqual({ 1: true, 0: true, 2: true }); - wrapper.vm.onSelectIssuable({ issuable: i0, selected: false }); - expect(wrapper.vm.selection).toEqual({ 1: true, 2: true }); - }); - - it('broadcasts a message to the bulk edit sidebar when a value is added to selection', () => { - issuablesEventBus.$emit.mockReset(); - const i1 = wrapper.vm.issuables[1]; - - wrapper.vm.onSelectIssuable({ issuable: i1, selected: true }); - - return wrapper.vm.$nextTick().then(() => { - expect(issuablesEventBus.$emit).toHaveBeenCalledTimes(1); - expect(issuablesEventBus.$emit).toHaveBeenCalledWith('issuables:updateBulkEdit'); - }); - }); - - it('does not broadcast a message to the bulk edit sidebar when a value is not added to selection', () => { - issuablesEventBus.$emit.mockReset(); - - return wrapper.vm - .$nextTick() - .then(waitForPromises) - .then(() => { - const i1 = wrapper.vm.issuables[1]; - - wrapper.vm.onSelectIssuable({ issuable: i1, selected: false }); - }) - .then(wrapper.vm.$nextTick) - .then(() => { - expect(issuablesEventBus.$emit).toHaveBeenCalledTimes(0); - }); - }); - - it('listens to a message to toggle bulk editing', () => { - expect(wrapper.vm.isBulkEditing).toBe(false); - expect(issuablesEventBus.$on.mock.calls[0][0]).toBe('issuables:toggleBulkEdit'); - issuablesEventBus.$on.mock.calls[0][1](true); // Call the message handler - - return waitForPromises() - .then(() => { - expect(wrapper.vm.isBulkEditing).toBe(true); - issuablesEventBus.$on.mock.calls[0][1](false); - }) - .then(() => { - expect(wrapper.vm.isBulkEditing).toBe(false); - }); - }); - }); - - describe('with query params in window.location', () => { - const expectedFilters = { - assignee_username: 'root', - author_username: 'root', - confidential: 'yes', - my_reaction_emoji: 'airplane', - scope: 'all', - state: 'opened', - weight: '0', - milestone: 'v3.0', - labels: 'Aquapod,Astro', - order_by: 'milestone_due', - sort: 'desc', - }; - - describe('when page is not present in params', () => { - const query = - '?assignee_username=root&author_username=root&confidential=yes&label_name%5B%5D=Aquapod&label_name%5B%5D=Astro&milestone_title=v3.0&my_reaction_emoji=airplane&scope=all&sort=priority&state=opened&weight=0¬[label_name][]=Afterpod¬[milestone_title][]=13'; - - beforeEach(() => { - setWindowLocation(query); - - setupApiMock(() => [200, MOCK_ISSUES.slice(0)]); - factory({ sortKey: 'milestone_due_desc' }); - - return waitForPromises(); - }); - - afterEach(() => { - apiSpy.mockClear(); - }); - - it('applies filters and sorts', () => { - expect(wrapper.vm.hasFilters).toBe(true); - expect(wrapper.vm.filters).toEqual({ - ...expectedFilters, - 'not[milestone]': ['13'], - 'not[labels]': ['Afterpod'], - }); - - expect(apiSpy).toHaveBeenCalledWith( - expect.objectContaining({ - params: { - ...expectedFilters, - with_labels_details: true, - page: 1, - per_page: PAGE_SIZE, - 'not[milestone]': ['13'], - 'not[labels]': ['Afterpod'], - }, - }), - ); - }); - - it('passes the base url to issuable', () => { - expect(findFirstIssuable().props('baseUrl')).toBe(TEST_LOCATION); - }); - }); - - describe('when page is present in the param', () => { - const query = - '?assignee_username=root&author_username=root&confidential=yes&label_name%5B%5D=Aquapod&label_name%5B%5D=Astro&milestone_title=v3.0&my_reaction_emoji=airplane&scope=all&sort=priority&state=opened&weight=0&page=3'; - - beforeEach(() => { - setWindowLocation(query); - - setupApiMock(() => [200, MOCK_ISSUES.slice(0)]); - factory({ sortKey: 'milestone_due_desc' }); - - return waitForPromises(); - }); - - afterEach(() => { - apiSpy.mockClear(); - }); - - it('applies filters and sorts', () => { - expect(apiSpy).toHaveBeenCalledWith( - expect.objectContaining({ - params: { - ...expectedFilters, - with_labels_details: true, - page: 3, - per_page: PAGE_SIZE, - }, - }), - ); - }); - }); - }); - - describe('with hash in window.location', () => { - beforeEach(() => { - setWindowLocation(`${TEST_LOCATION}#stuff`); - setupApiMock(() => [200, MOCK_ISSUES.slice(0)]); - factory(); - return waitForPromises(); - }); - - it('passes the base url to issuable', () => { - expect(findFirstIssuable().props('baseUrl')).toBe(TEST_LOCATION); - }); - }); - - describe('with manual sort', () => { - beforeEach(() => { - setupApiMock(() => [200, MOCK_ISSUES.slice(0)]); - factory({ sortKey: RELATIVE_POSITION }); - }); - - it('uses manual page size', () => { - expect(apiSpy).toHaveBeenCalledWith( - expect.objectContaining({ - params: expect.objectContaining({ - per_page: PAGE_SIZE_MANUAL, - }), - }), - ); - }); - }); - - describe('with empty issues response', () => { - beforeEach(() => { - setupApiMock(() => [200, []]); - }); - - describe('with query in window location', () => { - beforeEach(() => { - setWindowLocation('?weight=Any'); - - factory(); - - return waitForPromises().then(() => wrapper.vm.$nextTick()); - }); - - it('should display "Sorry, your filter produced no results" if filters are too specific', () => { - expect(findEmptyState().props('title')).toMatchSnapshot(); - }); - }); - - describe('with closed state', () => { - beforeEach(() => { - setWindowLocation('?state=closed'); - - factory(); - - return waitForPromises().then(() => wrapper.vm.$nextTick()); - }); - - it('should display a message "There are no closed issues" if there are no closed issues', () => { - expect(findEmptyState().props('title')).toMatchSnapshot(); - }); - }); - - describe('with all state', () => { - beforeEach(() => { - setWindowLocation('?state=all'); - - factory(); - - return waitForPromises().then(() => wrapper.vm.$nextTick()); - }); - - it('should display a catch-all if there are no issues to show', () => { - expect(findEmptyState().element).toMatchSnapshot(); - }); - }); - - describe('with empty query', () => { - beforeEach(() => { - factory(); - - return wrapper.vm.$nextTick().then(waitForPromises); - }); - - it('should display the message "There are no open issues"', () => { - expect(findEmptyState().props('title')).toMatchSnapshot(); - }); - }); - }); - - describe('when paginates', () => { - const newPage = 3; - - describe('when total-items is defined in response headers', () => { - beforeEach(() => { - window.history.pushState = jest.fn(); - setupApiMock(() => [ - 200, - MOCK_ISSUES.slice(0, PAGE_SIZE), - { - 'x-total': 100, - 'x-page': 2, - }, - ]); - - factory(); - - return waitForPromises(); - }); - - afterEach(() => { - // reset to original value - window.history.pushState.mockRestore(); - }); - - it('calls window.history.pushState one time', () => { - // Trigger pagination - wrapper.find(GlPagination).vm.$emit('input', newPage); - - expect(window.history.pushState).toHaveBeenCalledTimes(1); - }); - - it('sets params in the url', () => { - // Trigger pagination - wrapper.find(GlPagination).vm.$emit('input', newPage); - - expect(window.history.pushState).toHaveBeenCalledWith( - {}, - '', - `${TEST_LOCATION}?state=opened&order_by=priority&sort=asc&page=${newPage}`, - ); - }); - }); - - describe('when total-items is not defined in the headers', () => { - const page = 2; - const prevPage = page - 1; - const nextPage = page + 1; - - beforeEach(() => { - setupApiMock(() => [ - 200, - MOCK_ISSUES.slice(0, PAGE_SIZE), - { - 'x-page': page, - }, - ]); - - factory(); - - return waitForPromises(); - }); - - it('finds the correct props applied to GlPagination', () => { - expect(wrapper.find(GlPagination).props()).toMatchObject({ - nextPage, - prevPage, - value: page, - }); - }); - }); - }); - - describe('when type is "jira"', () => { - it('renders FilteredSearchBar', () => { - factory({ type: 'jira' }); - - expect(findFilteredSearchBar().exists()).toBe(true); - }); - - describe('initialSortBy', () => { - const query = '?sort=updated_asc'; - - it('sets default value', () => { - factory({ type: 'jira' }); - - expect(findFilteredSearchBar().props('initialSortBy')).toBe('created_desc'); - }); - - it('sets value according to query', () => { - setWindowLocation(query); - - factory({ type: 'jira' }); - - expect(findFilteredSearchBar().props('initialSortBy')).toBe('updated_asc'); - }); - }); - - describe('initialFilterValue', () => { - it('does not set value when no query', () => { - factory({ type: 'jira' }); - - expect(findFilteredSearchBar().props('initialFilterValue')).toEqual([]); - }); - - it('sets value according to query', () => { - const query = '?search=free+text'; - - setWindowLocation(query); - - factory({ type: 'jira' }); - - expect(findFilteredSearchBar().props('initialFilterValue')).toEqual(['free text']); - }); - }); - - describe('on filter search', () => { - beforeEach(() => { - factory({ type: 'jira' }); - - window.history.pushState = jest.fn(); - }); - - afterEach(() => { - window.history.pushState.mockRestore(); - }); - - const emitOnFilter = (filter) => findFilteredSearchBar().vm.$emit('onFilter', filter); - - describe('empty filter', () => { - const mockFilter = []; - - it('updates URL with correct params', () => { - emitOnFilter(mockFilter); - - expect(window.history.pushState).toHaveBeenCalledWith( - {}, - '', - `${TEST_LOCATION}?state=opened`, - ); - }); - }); - - describe('filter with search term', () => { - const mockFilter = [ - { - type: 'filtered-search-term', - value: { data: 'free' }, - }, - ]; - - it('updates URL with correct params', () => { - emitOnFilter(mockFilter); - - expect(window.history.pushState).toHaveBeenCalledWith( - {}, - '', - `${TEST_LOCATION}?state=opened&search=free`, - ); - }); - }); - - describe('filter with multiple search terms', () => { - const mockFilter = [ - { - type: 'filtered-search-term', - value: { data: 'free' }, - }, - { - type: 'filtered-search-term', - value: { data: 'text' }, - }, - ]; - - it('updates URL with correct params', () => { - emitOnFilter(mockFilter); - - expect(window.history.pushState).toHaveBeenCalledWith( - {}, - '', - `${TEST_LOCATION}?state=opened&search=free+text`, - ); - }); - }); - }); - }); -}); diff --git a/spec/frontend/issues_list/components/issue_card_time_info_spec.js b/spec/frontend/issues_list/components/issue_card_time_info_spec.js deleted file mode 100644 index 7c5faeb8dc1..00000000000 --- a/spec/frontend/issues_list/components/issue_card_time_info_spec.js +++ /dev/null @@ -1,122 +0,0 @@ -import { GlIcon, GlLink } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import { useFakeDate } from 'helpers/fake_date'; -import IssueCardTimeInfo from '~/issues_list/components/issue_card_time_info.vue'; - -describe('CE IssueCardTimeInfo component', () => { - useFakeDate(2020, 11, 11); - - let wrapper; - - const issue = { - milestone: { - dueDate: '2020-12-17', - startDate: '2020-12-10', - title: 'My milestone', - webPath: '/milestone/webPath', - }, - dueDate: '2020-12-12', - humanTimeEstimate: '1w', - }; - - const findMilestone = () => wrapper.find('[data-testid="issuable-milestone"]'); - const findMilestoneTitle = () => findMilestone().find(GlLink).attributes('title'); - const findDueDate = () => wrapper.find('[data-testid="issuable-due-date"]'); - - const mountComponent = ({ - closedAt = null, - dueDate = issue.dueDate, - milestoneDueDate = issue.milestone.dueDate, - milestoneStartDate = issue.milestone.startDate, - } = {}) => - shallowMount(IssueCardTimeInfo, { - propsData: { - issue: { - ...issue, - milestone: { - ...issue.milestone, - dueDate: milestoneDueDate, - startDate: milestoneStartDate, - }, - closedAt, - dueDate, - }, - }, - }); - - afterEach(() => { - wrapper.destroy(); - }); - - describe('milestone', () => { - it('renders', () => { - wrapper = mountComponent(); - - const milestone = findMilestone(); - - expect(milestone.text()).toBe(issue.milestone.title); - expect(milestone.find(GlIcon).props('name')).toBe('clock'); - expect(milestone.find(GlLink).attributes('href')).toBe(issue.milestone.webPath); - }); - - describe.each` - time | text | milestoneDueDate | milestoneStartDate | expected - ${'due date is in past'} | ${'Past due'} | ${'2020-09-09'} | ${null} | ${'Sep 9, 2020 (Past due)'} - ${'due date is today'} | ${'Today'} | ${'2020-12-11'} | ${null} | ${'Dec 11, 2020 (Today)'} - ${'start date is in future'} | ${'Upcoming'} | ${'2021-03-01'} | ${'2021-02-01'} | ${'Mar 1, 2021 (Upcoming)'} - ${'due date is in future'} | ${'2 weeks remaining'} | ${'2020-12-25'} | ${null} | ${'Dec 25, 2020 (2 weeks remaining)'} - `('when $description', ({ text, milestoneDueDate, milestoneStartDate, expected }) => { - it(`renders with "${text}"`, () => { - wrapper = mountComponent({ milestoneDueDate, milestoneStartDate }); - - expect(findMilestoneTitle()).toBe(expected); - }); - }); - }); - - describe('due date', () => { - describe('when upcoming', () => { - it('renders', () => { - wrapper = mountComponent(); - - const dueDate = findDueDate(); - - expect(dueDate.text()).toBe('Dec 12, 2020'); - expect(dueDate.attributes('title')).toBe('Due date'); - expect(dueDate.find(GlIcon).props('name')).toBe('calendar'); - expect(dueDate.classes()).not.toContain('gl-text-red-500'); - }); - }); - - describe('when in the past', () => { - describe('when issue is open', () => { - it('renders in red', () => { - wrapper = mountComponent({ dueDate: new Date('2020-10-10') }); - - expect(findDueDate().classes()).toContain('gl-text-red-500'); - }); - }); - - describe('when issue is closed', () => { - it('does not render in red', () => { - wrapper = mountComponent({ - dueDate: new Date('2020-10-10'), - closedAt: '2020-09-05T13:06:25Z', - }); - - expect(findDueDate().classes()).not.toContain('gl-text-red-500'); - }); - }); - }); - }); - - it('renders time estimate', () => { - wrapper = mountComponent(); - - const timeEstimate = wrapper.find('[data-testid="time-estimate"]'); - - expect(timeEstimate.text()).toBe(issue.humanTimeEstimate); - expect(timeEstimate.attributes('title')).toBe('Estimate'); - expect(timeEstimate.find(GlIcon).props('name')).toBe('timer'); - }); -}); diff --git a/spec/frontend/issues_list/components/issues_list_app_spec.js b/spec/frontend/issues_list/components/issues_list_app_spec.js deleted file mode 100644 index f24c090fa92..00000000000 --- a/spec/frontend/issues_list/components/issues_list_app_spec.js +++ /dev/null @@ -1,829 +0,0 @@ -import { GlButton, GlEmptyState, GlLink } from '@gitlab/ui'; -import * as Sentry from '@sentry/browser'; -import { mount, shallowMount } from '@vue/test-utils'; -import AxiosMockAdapter from 'axios-mock-adapter'; -import { cloneDeep } from 'lodash'; -import Vue, { nextTick } from 'vue'; -import VueApollo from 'vue-apollo'; -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 createMockApollo from 'helpers/mock_apollo_helper'; -import setWindowLocation from 'helpers/set_window_location_helper'; -import { TEST_HOST } from 'helpers/test_constants'; -import waitForPromises from 'helpers/wait_for_promises'; -import { - getIssuesCountsQueryResponse, - getIssuesQueryResponse, - filteredTokens, - locationSearch, - urlParams, -} from 'jest/issues_list/mock_data'; -import createFlash, { FLASH_TYPES } from '~/flash'; -import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils'; -import CsvImportExportButtons from '~/issuable/components/csv_import_export_buttons.vue'; -import IssuableByEmail from '~/issuable/components/issuable_by_email.vue'; -import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_root.vue'; -import { IssuableListTabs, IssuableStates } from '~/vue_shared/issuable/list/constants'; -import IssuesListApp from '~/issues_list/components/issues_list_app.vue'; -import NewIssueDropdown from '~/issues_list/components/new_issue_dropdown.vue'; -import { - CREATED_DESC, - DUE_DATE_OVERDUE, - PARAM_DUE_DATE, - RELATIVE_POSITION, - RELATIVE_POSITION_ASC, - TOKEN_TYPE_ASSIGNEE, - TOKEN_TYPE_AUTHOR, - TOKEN_TYPE_CONFIDENTIAL, - TOKEN_TYPE_LABEL, - TOKEN_TYPE_MILESTONE, - TOKEN_TYPE_MY_REACTION, - TOKEN_TYPE_RELEASE, - TOKEN_TYPE_TYPE, - urlSortParams, -} from '~/issues_list/constants'; -import eventHub from '~/issues_list/eventhub'; -import { getSortOptions } from '~/issues_list/utils'; -import axios from '~/lib/utils/axios_utils'; -import { scrollUp } from '~/lib/utils/scroll_utils'; -import { joinPaths } from '~/lib/utils/url_utility'; - -jest.mock('@sentry/browser'); -jest.mock('~/flash'); -jest.mock('~/lib/utils/scroll_utils', () => ({ - scrollUp: jest.fn().mockName('scrollUpMock'), -})); - -describe('CE IssuesListApp component', () => { - let axiosMock; - let wrapper; - - Vue.use(VueApollo); - - const defaultProvide = { - calendarPath: 'calendar/path', - canBulkUpdate: false, - emptyStateSvgPath: 'empty-state.svg', - exportCsvPath: 'export/csv/path', - fullPath: 'path/to/project', - hasAnyIssues: true, - hasAnyProjects: true, - hasBlockedIssuesFeature: true, - hasIssuableHealthStatusFeature: true, - hasIssueWeightsFeature: true, - hasIterationsFeature: true, - isProject: true, - isSignedIn: true, - jiraIntegrationPath: 'jira/integration/path', - newIssuePath: 'new/issue/path', - rssPath: 'rss/path', - showNewIssueLink: true, - signInPath: 'sign/in/path', - }; - - let defaultQueryResponse = getIssuesQueryResponse; - if (IS_EE) { - defaultQueryResponse = cloneDeep(getIssuesQueryResponse); - defaultQueryResponse.data.project.issues.nodes[0].blockingCount = 1; - defaultQueryResponse.data.project.issues.nodes[0].healthStatus = null; - defaultQueryResponse.data.project.issues.nodes[0].weight = 5; - } - - const findCsvImportExportButtons = () => wrapper.findComponent(CsvImportExportButtons); - const findIssuableByEmail = () => wrapper.findComponent(IssuableByEmail); - const findGlButton = () => wrapper.findComponent(GlButton); - const findGlButtons = () => wrapper.findAllComponents(GlButton); - const findGlButtonAt = (index) => findGlButtons().at(index); - const findGlEmptyState = () => wrapper.findComponent(GlEmptyState); - const findGlLink = () => wrapper.findComponent(GlLink); - const findIssuableList = () => wrapper.findComponent(IssuableList); - const findNewIssueDropdown = () => wrapper.findComponent(NewIssueDropdown); - - const mountComponent = ({ - provide = {}, - issuesQueryResponse = jest.fn().mockResolvedValue(defaultQueryResponse), - issuesCountsQueryResponse = jest.fn().mockResolvedValue(getIssuesCountsQueryResponse), - mountFn = shallowMount, - } = {}) => { - const requestHandlers = [ - [getIssuesQuery, issuesQueryResponse], - [getIssuesCountsQuery, issuesCountsQueryResponse], - ]; - const apolloProvider = createMockApollo(requestHandlers); - - return mountFn(IssuesListApp, { - apolloProvider, - provide: { - ...defaultProvide, - ...provide, - }, - }); - }; - - beforeEach(() => { - setWindowLocation(TEST_HOST); - axiosMock = new AxiosMockAdapter(axios); - }); - - afterEach(() => { - axiosMock.reset(); - wrapper.destroy(); - }); - - describe('IssuableList', () => { - beforeEach(() => { - wrapper = mountComponent(); - jest.runOnlyPendingTimers(); - }); - - it('renders', () => { - expect(findIssuableList().props()).toMatchObject({ - namespace: defaultProvide.fullPath, - recentSearchesStorageKey: 'issues', - searchInputPlaceholder: IssuesListApp.i18n.searchPlaceholder, - sortOptions: getSortOptions(true, true), - initialSortBy: CREATED_DESC, - issuables: getIssuesQueryResponse.data.project.issues.nodes, - tabs: IssuableListTabs, - currentTab: IssuableStates.Opened, - tabCounts: { - opened: 1, - closed: 1, - all: 1, - }, - issuablesLoading: false, - isManualOrdering: false, - showBulkEditSidebar: false, - showPaginationControls: true, - useKeysetPagination: true, - hasPreviousPage: getIssuesQueryResponse.data.project.issues.pageInfo.hasPreviousPage, - hasNextPage: getIssuesQueryResponse.data.project.issues.pageInfo.hasNextPage, - urlParams: { - sort: urlSortParams[CREATED_DESC], - state: IssuableStates.Opened, - }, - }); - }); - }); - - describe('header action buttons', () => { - it('renders rss button', () => { - wrapper = mountComponent({ mountFn: mount }); - - expect(findGlButtonAt(0).props('icon')).toBe('rss'); - expect(findGlButtonAt(0).attributes()).toMatchObject({ - href: defaultProvide.rssPath, - 'aria-label': IssuesListApp.i18n.rssLabel, - }); - }); - - it('renders calendar button', () => { - wrapper = mountComponent({ mountFn: mount }); - - expect(findGlButtonAt(1).props('icon')).toBe('calendar'); - expect(findGlButtonAt(1).attributes()).toMatchObject({ - href: defaultProvide.calendarPath, - 'aria-label': IssuesListApp.i18n.calendarLabel, - }); - }); - - describe('csv import/export component', () => { - describe('when user is signed in', () => { - const search = '?search=refactor&sort=created_date&state=opened'; - - beforeEach(() => { - setWindowLocation(search); - - wrapper = mountComponent({ provide: { isSignedIn: true }, mountFn: mount }); - - jest.runOnlyPendingTimers(); - }); - - it('renders', () => { - expect(findCsvImportExportButtons().props()).toMatchObject({ - exportCsvPath: `${defaultProvide.exportCsvPath}${search}`, - issuableCount: 1, - }); - }); - }); - - describe('when user is not signed in', () => { - it('does not render', () => { - wrapper = mountComponent({ provide: { isSignedIn: false }, mountFn: mount }); - - expect(findCsvImportExportButtons().exists()).toBe(false); - }); - }); - - describe('when in a group context', () => { - it('does not render', () => { - wrapper = mountComponent({ provide: { isProject: false }, mountFn: mount }); - - expect(findCsvImportExportButtons().exists()).toBe(false); - }); - }); - }); - - describe('bulk edit button', () => { - it('renders when user has permissions', () => { - wrapper = mountComponent({ provide: { canBulkUpdate: true }, mountFn: mount }); - - expect(findGlButtonAt(2).text()).toBe('Edit issues'); - }); - - it('does not render when user does not have permissions', () => { - wrapper = mountComponent({ provide: { canBulkUpdate: false }, mountFn: mount }); - - expect(findGlButtons().filter((button) => button.text() === 'Edit issues')).toHaveLength(0); - }); - - 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'); - }); - }); - - describe('new issue button', () => { - it('renders when user has permissions', () => { - wrapper = mountComponent({ provide: { showNewIssueLink: true }, mountFn: mount }); - - expect(findGlButtonAt(2).text()).toBe('New issue'); - expect(findGlButtonAt(2).attributes('href')).toBe(defaultProvide.newIssuePath); - }); - - it('does not render when user does not have permissions', () => { - wrapper = mountComponent({ provide: { showNewIssueLink: false }, mountFn: mount }); - - expect(findGlButtons().filter((button) => button.text() === 'New issue')).toHaveLength(0); - }); - }); - - describe('new issue split dropdown', () => { - it('does not render in a project context', () => { - wrapper = mountComponent({ provide: { isProject: true }, mountFn: mount }); - - expect(findNewIssueDropdown().exists()).toBe(false); - }); - - it('renders in a group context', () => { - wrapper = mountComponent({ provide: { isProject: false }, mountFn: mount }); - - expect(findNewIssueDropdown().exists()).toBe(true); - }); - }); - }); - - describe('initial url params', () => { - describe('due_date', () => { - it('is set from the url params', () => { - setWindowLocation(`?${PARAM_DUE_DATE}=${DUE_DATE_OVERDUE}`); - - wrapper = mountComponent(); - - expect(findIssuableList().props('urlParams')).toMatchObject({ due_date: DUE_DATE_OVERDUE }); - }); - }); - - describe('search', () => { - it('is set from the url params', () => { - setWindowLocation(locationSearch); - - wrapper = mountComponent(); - - expect(findIssuableList().props('urlParams')).toMatchObject({ search: 'find issues' }); - }); - }); - - describe('sort', () => { - it.each(Object.keys(urlSortParams))('is set as %s from the url params', (sortKey) => { - setWindowLocation(`?sort=${urlSortParams[sortKey]}`); - - wrapper = mountComponent(); - - expect(findIssuableList().props()).toMatchObject({ - initialSortBy: sortKey, - urlParams: { - sort: urlSortParams[sortKey], - }, - }); - }); - - describe('when issue repositioning is disabled and the sort is manual', () => { - beforeEach(() => { - setWindowLocation(`?sort=${RELATIVE_POSITION}`); - wrapper = mountComponent({ provide: { isIssueRepositioningDisabled: true } }); - }); - - it('changes the sort to the default of created descending', () => { - expect(findIssuableList().props()).toMatchObject({ - initialSortBy: CREATED_DESC, - urlParams: { - sort: urlSortParams[CREATED_DESC], - }, - }); - }); - - it('shows an alert to tell the user that manual reordering is disabled', () => { - expect(createFlash).toHaveBeenCalledWith({ - message: IssuesListApp.i18n.issueRepositioningMessage, - type: FLASH_TYPES.NOTICE, - }); - }); - }); - }); - - describe('state', () => { - it('is set from the url params', () => { - const initialState = IssuableStates.All; - - setWindowLocation(`?state=${initialState}`); - - wrapper = mountComponent(); - - expect(findIssuableList().props('currentTab')).toBe(initialState); - }); - }); - - describe('filter tokens', () => { - it('is set from the url params', () => { - setWindowLocation(locationSearch); - - wrapper = mountComponent(); - - expect(findIssuableList().props('initialFilterValue')).toEqual(filteredTokens); - }); - - describe('when anonymous searching is performed', () => { - beforeEach(() => { - setWindowLocation(locationSearch); - - wrapper = mountComponent({ - provide: { isAnonymousSearchDisabled: true, isSignedIn: false }, - }); - }); - - it('is not set from url params', () => { - expect(findIssuableList().props('initialFilterValue')).toEqual([]); - }); - - it('shows an alert to tell the user they must be signed in to search', () => { - expect(createFlash).toHaveBeenCalledWith({ - message: IssuesListApp.i18n.anonymousSearchingMessage, - type: FLASH_TYPES.NOTICE, - }); - }); - }); - }); - }); - - describe('bulk edit', () => { - describe.each([true, false])( - 'when "issuables:toggleBulkEdit" event is received with payload `%s`', - (isBulkEdit) => { - beforeEach(() => { - wrapper = mountComponent(); - - eventHub.$emit('issuables:toggleBulkEdit', isBulkEdit); - }); - - it(`${isBulkEdit ? 'enables' : 'disables'} bulk edit`, () => { - expect(findIssuableList().props('showBulkEditSidebar')).toBe(isBulkEdit); - }); - }, - ); - }); - - describe('IssuableByEmail component', () => { - describe.each([true, false])(`when issue creation by email is enabled=%s`, (enabled) => { - it(`${enabled ? 'renders' : 'does not render'}`, () => { - wrapper = mountComponent({ provide: { initialEmail: enabled } }); - - expect(findIssuableByEmail().exists()).toBe(enabled); - }); - }); - }); - - describe('empty states', () => { - describe('when there are issues', () => { - describe('when search returns no results', () => { - beforeEach(() => { - setWindowLocation(`?search=no+results`); - - wrapper = mountComponent({ provide: { hasAnyIssues: true }, mountFn: mount }); - }); - - it('shows empty state', () => { - expect(findGlEmptyState().props()).toMatchObject({ - description: IssuesListApp.i18n.noSearchResultsDescription, - title: IssuesListApp.i18n.noSearchResultsTitle, - svgPath: defaultProvide.emptyStateSvgPath, - }); - }); - }); - - describe('when "Open" tab has no issues', () => { - beforeEach(() => { - wrapper = mountComponent({ provide: { hasAnyIssues: true }, mountFn: mount }); - }); - - it('shows empty state', () => { - expect(findGlEmptyState().props()).toMatchObject({ - description: IssuesListApp.i18n.noOpenIssuesDescription, - title: IssuesListApp.i18n.noOpenIssuesTitle, - svgPath: defaultProvide.emptyStateSvgPath, - }); - }); - }); - - describe('when "Closed" tab has no issues', () => { - beforeEach(() => { - setWindowLocation(`?state=${IssuableStates.Closed}`); - - wrapper = mountComponent({ provide: { hasAnyIssues: true }, mountFn: mount }); - }); - - it('shows empty state', () => { - expect(findGlEmptyState().props()).toMatchObject({ - title: IssuesListApp.i18n.noClosedIssuesTitle, - svgPath: defaultProvide.emptyStateSvgPath, - }); - }); - }); - }); - - describe('when there are no issues', () => { - describe('when user is logged in', () => { - beforeEach(() => { - wrapper = mountComponent({ - provide: { hasAnyIssues: false, isSignedIn: true }, - mountFn: mount, - }); - }); - - it('shows empty state', () => { - expect(findGlEmptyState().props()).toMatchObject({ - description: IssuesListApp.i18n.noIssuesSignedInDescription, - title: IssuesListApp.i18n.noIssuesSignedInTitle, - svgPath: defaultProvide.emptyStateSvgPath, - }); - }); - - it('shows "New issue" and import/export buttons', () => { - expect(findGlButton().text()).toBe(IssuesListApp.i18n.newIssueLabel); - expect(findGlButton().attributes('href')).toBe(defaultProvide.newIssuePath); - expect(findCsvImportExportButtons().props()).toMatchObject({ - exportCsvPath: defaultProvide.exportCsvPath, - issuableCount: 0, - }); - }); - - it('shows Jira integration information', () => { - const paragraphs = wrapper.findAll('p'); - expect(paragraphs.at(1).text()).toContain(IssuesListApp.i18n.jiraIntegrationTitle); - expect(paragraphs.at(2).text()).toContain( - 'Enable the Jira integration to view your Jira issues in GitLab.', - ); - expect(paragraphs.at(3).text()).toContain( - IssuesListApp.i18n.jiraIntegrationSecondaryMessage, - ); - expect(findGlLink().text()).toBe('Enable the Jira integration'); - expect(findGlLink().attributes('href')).toBe(defaultProvide.jiraIntegrationPath); - }); - }); - - describe('when user is logged out', () => { - beforeEach(() => { - wrapper = mountComponent({ - provide: { hasAnyIssues: false, isSignedIn: false }, - }); - }); - - it('shows empty state', () => { - expect(findGlEmptyState().props()).toMatchObject({ - description: IssuesListApp.i18n.noIssuesSignedOutDescription, - title: IssuesListApp.i18n.noIssuesSignedOutTitle, - svgPath: defaultProvide.emptyStateSvgPath, - primaryButtonText: IssuesListApp.i18n.noIssuesSignedOutButtonText, - primaryButtonLink: defaultProvide.signInPath, - }); - }); - }); - }); - }); - - describe('tokens', () => { - const mockCurrentUser = { - id: 1, - name: 'Administrator', - username: 'root', - avatar_url: 'avatar/url', - }; - - describe('when user is signed out', () => { - beforeEach(() => { - wrapper = mountComponent({ provide: { isSignedIn: false } }); - }); - - it('does not render My-Reaction or Confidential tokens', () => { - expect(findIssuableList().props('searchTokens')).not.toMatchObject([ - { type: TOKEN_TYPE_AUTHOR, preloadedAuthors: [mockCurrentUser] }, - { type: TOKEN_TYPE_ASSIGNEE, preloadedAuthors: [mockCurrentUser] }, - { type: TOKEN_TYPE_MY_REACTION }, - { type: TOKEN_TYPE_CONFIDENTIAL }, - ]); - }); - }); - - describe('when all tokens are available', () => { - const originalGon = window.gon; - - beforeEach(() => { - window.gon = { - ...originalGon, - current_user_id: mockCurrentUser.id, - current_user_fullname: mockCurrentUser.name, - current_username: mockCurrentUser.username, - current_user_avatar_url: mockCurrentUser.avatar_url, - }; - - wrapper = mountComponent({ provide: { isSignedIn: true } }); - }); - - afterEach(() => { - window.gon = originalGon; - }); - - it('renders all tokens alphabetically', () => { - const preloadedAuthors = [ - { ...mockCurrentUser, id: convertToGraphQLId('User', mockCurrentUser.id) }, - ]; - - expect(findIssuableList().props('searchTokens')).toMatchObject([ - { type: TOKEN_TYPE_ASSIGNEE, preloadedAuthors }, - { type: TOKEN_TYPE_AUTHOR, preloadedAuthors }, - { type: TOKEN_TYPE_CONFIDENTIAL }, - { type: TOKEN_TYPE_LABEL }, - { type: TOKEN_TYPE_MILESTONE }, - { type: TOKEN_TYPE_MY_REACTION }, - { type: TOKEN_TYPE_RELEASE }, - { type: TOKEN_TYPE_TYPE }, - ]); - }); - }); - }); - - describe('errors', () => { - describe.each` - error | mountOption | message - ${'fetching issues'} | ${'issuesQueryResponse'} | ${IssuesListApp.i18n.errorFetchingIssues} - ${'fetching issue counts'} | ${'issuesCountsQueryResponse'} | ${IssuesListApp.i18n.errorFetchingCounts} - `('when there is an error $error', ({ mountOption, message }) => { - beforeEach(() => { - wrapper = mountComponent({ - [mountOption]: jest.fn().mockRejectedValue(new Error('ERROR')), - }); - jest.runOnlyPendingTimers(); - }); - - it('shows an error message', () => { - expect(findIssuableList().props('error')).toBe(message); - expect(Sentry.captureException).toHaveBeenCalledWith(new Error('Network error: ERROR')); - }); - }); - - it('clears error message when "dismiss-alert" event is emitted from IssuableList', () => { - wrapper = mountComponent({ issuesQueryResponse: jest.fn().mockRejectedValue(new Error()) }); - - findIssuableList().vm.$emit('dismiss-alert'); - - expect(findIssuableList().props('error')).toBeNull(); - }); - }); - - describe('events', () => { - describe('when "click-tab" event is emitted by IssuableList', () => { - beforeEach(() => { - wrapper = mountComponent(); - - findIssuableList().vm.$emit('click-tab', IssuableStates.Closed); - }); - - it('updates 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(); - - findIssuableList().vm.$emit(event); - }); - - it('scrolls to the top', () => { - expect(scrollUp).toHaveBeenCalled(); - }); - }, - ); - - describe('when "reorder" event is emitted by IssuableList', () => { - const issueOne = { - ...defaultQueryResponse.data.project.issues.nodes[0], - id: 'gid://gitlab/Issue/1', - iid: '101', - reference: 'group/project#1', - webPath: '/group/project/-/issues/1', - }; - const issueTwo = { - ...defaultQueryResponse.data.project.issues.nodes[0], - id: 'gid://gitlab/Issue/2', - iid: '102', - reference: 'group/project#2', - webPath: '/group/project/-/issues/2', - }; - const issueThree = { - ...defaultQueryResponse.data.project.issues.nodes[0], - id: 'gid://gitlab/Issue/3', - iid: '103', - reference: 'group/project#3', - webPath: '/group/project/-/issues/3', - }; - const issueFour = { - ...defaultQueryResponse.data.project.issues.nodes[0], - id: 'gid://gitlab/Issue/4', - iid: '104', - reference: 'group/project#4', - webPath: '/group/project/-/issues/4', - }; - const response = (isProject = true) => ({ - data: { - [isProject ? 'project' : 'group']: { - id: '1', - issues: { - ...defaultQueryResponse.data.project.issues, - nodes: [issueOne, issueTwo, issueThree, issueFour], - }, - }, - }, - }); - - describe('when successful', () => { - describe.each([true, false])('when isProject=%s', (isProject) => { - describe.each` - description | issueToMove | oldIndex | newIndex | moveBeforeId | moveAfterId - ${'to the beginning of the list'} | ${issueThree} | ${2} | ${0} | ${null} | ${issueOne.id} - ${'down the list'} | ${issueOne} | ${0} | ${1} | ${issueTwo.id} | ${issueThree.id} - ${'up the list'} | ${issueThree} | ${2} | ${1} | ${issueOne.id} | ${issueTwo.id} - ${'to the end of the list'} | ${issueTwo} | ${1} | ${3} | ${issueFour.id} | ${null} - `( - 'when moving issue $description', - ({ issueToMove, oldIndex, newIndex, moveBeforeId, moveAfterId }) => { - beforeEach(() => { - wrapper = mountComponent({ - provide: { isProject }, - issuesQueryResponse: jest.fn().mockResolvedValue(response(isProject)), - }); - jest.runOnlyPendingTimers(); - }); - - it('makes API call to reorder the issue', async () => { - findIssuableList().vm.$emit('reorder', { oldIndex, newIndex }); - - await waitForPromises(); - - expect(axiosMock.history.put[0]).toMatchObject({ - url: joinPaths(issueToMove.webPath, 'reorder'), - data: JSON.stringify({ - move_before_id: getIdFromGraphQLId(moveBeforeId), - move_after_id: getIdFromGraphQLId(moveAfterId), - group_full_path: isProject ? undefined : defaultProvide.fullPath, - }), - }); - }); - }, - ); - }); - }); - - describe('when unsuccessful', () => { - beforeEach(() => { - wrapper = mountComponent({ - issuesQueryResponse: jest.fn().mockResolvedValue(response()), - }); - jest.runOnlyPendingTimers(); - }); - - 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); - expect(Sentry.captureException).toHaveBeenCalledWith( - new Error('Request failed with status code 500'), - ); - }); - }); - }); - - describe('when "sort" event is emitted by IssuableList', () => { - it.each(Object.keys(urlSortParams))( - 'updates to the new sort when payload is `%s`', - async (sortKey) => { - wrapper = mountComponent(); - - findIssuableList().vm.$emit('sort', sortKey); - - jest.runOnlyPendingTimers(); - await nextTick(); - - expect(findIssuableList().props('urlParams')).toMatchObject({ - sort: urlSortParams[sortKey], - }); - }, - ); - - describe('when issue repositioning is disabled', () => { - const initialSort = CREATED_DESC; - - beforeEach(() => { - setWindowLocation(`?sort=${initialSort}`); - wrapper = mountComponent({ provide: { isIssueRepositioningDisabled: true } }); - - findIssuableList().vm.$emit('sort', RELATIVE_POSITION_ASC); - }); - - it('does not update the sort to manual', () => { - expect(findIssuableList().props('urlParams')).toMatchObject({ - sort: urlSortParams[initialSort], - }); - }); - - it('shows an alert to tell the user that manual reordering is disabled', () => { - expect(createFlash).toHaveBeenCalledWith({ - message: IssuesListApp.i18n.issueRepositioningMessage, - type: FLASH_TYPES.NOTICE, - }); - }); - }); - }); - - describe('when "update-legacy-bulk-edit" event is emitted by IssuableList', () => { - beforeEach(() => { - wrapper = mountComponent(); - jest.spyOn(eventHub, '$emit'); - - findIssuableList().vm.$emit('update-legacy-bulk-edit'); - }); - - it('emits an "issuables:updateBulkEdit" event to the legacy bulk edit class', () => { - expect(eventHub.$emit).toHaveBeenCalledWith('issuables:updateBulkEdit'); - }); - }); - - describe('when "filter" event is emitted by IssuableList', () => { - it('updates IssuableList with url params', async () => { - wrapper = mountComponent(); - - findIssuableList().vm.$emit('filter', filteredTokens); - await nextTick(); - - expect(findIssuableList().props('urlParams')).toMatchObject(urlParams); - }); - - describe('when anonymous searching is performed', () => { - beforeEach(() => { - wrapper = mountComponent({ - provide: { isAnonymousSearchDisabled: true, isSignedIn: false }, - }); - - findIssuableList().vm.$emit('filter', filteredTokens); - }); - - it('does not update IssuableList with url params ', async () => { - const defaultParams = { sort: 'created_date', state: 'opened' }; - - expect(findIssuableList().props('urlParams')).toEqual(defaultParams); - }); - - it('shows an alert to tell the user they must be signed in to search', () => { - expect(createFlash).toHaveBeenCalledWith({ - message: IssuesListApp.i18n.anonymousSearchingMessage, - type: FLASH_TYPES.NOTICE, - }); - }); - }); - }); - }); -}); diff --git a/spec/frontend/issues_list/components/jira_issues_import_status_app_spec.js b/spec/frontend/issues_list/components/jira_issues_import_status_app_spec.js deleted file mode 100644 index 633799816d8..00000000000 --- a/spec/frontend/issues_list/components/jira_issues_import_status_app_spec.js +++ /dev/null @@ -1,117 +0,0 @@ -import { GlAlert, GlLabel } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import Vue from 'vue'; -import JiraIssuesImportStatus from '~/issues_list/components/jira_issues_import_status_app.vue'; - -describe('JiraIssuesImportStatus', () => { - const issuesPath = 'gitlab-org/gitlab-test/-/issues'; - const label = { - color: '#333', - title: 'jira-import::MTG-3', - }; - let wrapper; - - const findAlert = () => wrapper.find(GlAlert); - - const findAlertLabel = () => wrapper.find(GlAlert).find(GlLabel); - - const mountComponent = ({ - shouldShowFinishedAlert = false, - shouldShowInProgressAlert = false, - } = {}) => - shallowMount(JiraIssuesImportStatus, { - propsData: { - canEdit: true, - isJiraConfigured: true, - issuesPath, - projectPath: 'gitlab-org/gitlab-test', - }, - data() { - return { - jiraImport: { - importedIssuesCount: 1, - label, - shouldShowFinishedAlert, - shouldShowInProgressAlert, - }, - }; - }, - }); - - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - - describe('when Jira import is neither in progress nor finished', () => { - beforeEach(() => { - wrapper = mountComponent(); - }); - - it('does not show an alert', () => { - expect(wrapper.find(GlAlert).exists()).toBe(false); - }); - }); - - describe('when Jira import is in progress', () => { - it('shows an alert that tells the user a Jira import is in progress', () => { - wrapper = mountComponent({ - shouldShowInProgressAlert: true, - }); - - expect(findAlert().text()).toBe( - 'Import in progress. Refresh page to see newly added issues.', - ); - }); - }); - - describe('when Jira import has finished', () => { - beforeEach(() => { - wrapper = mountComponent({ - shouldShowFinishedAlert: true, - }); - }); - - describe('shows an alert', () => { - it('tells the user the Jira import has finished', () => { - expect(findAlert().text()).toBe('1 issue successfully imported with the label'); - }); - - it('contains the label title associated with the Jira import', () => { - const alertLabelTitle = findAlertLabel().props('title'); - - expect(alertLabelTitle).toBe(label.title); - }); - - it('contains the correct label color', () => { - const alertLabelTitle = findAlertLabel().props('backgroundColor'); - - expect(alertLabelTitle).toBe(label.color); - }); - - it('contains a link within the label', () => { - const alertLabelTarget = findAlertLabel().props('target'); - - expect(alertLabelTarget).toBe( - `${issuesPath}?label_name[]=${encodeURIComponent(label.title)}`, - ); - }); - }); - }); - - describe('alert message', () => { - it('is hidden when dismissed', () => { - wrapper = mountComponent({ - shouldShowInProgressAlert: true, - }); - - expect(wrapper.find(GlAlert).exists()).toBe(true); - - findAlert().vm.$emit('dismiss'); - - return Vue.nextTick(() => { - expect(wrapper.find(GlAlert).exists()).toBe(false); - }); - }); - }); -}); diff --git a/spec/frontend/issues_list/components/new_issue_dropdown_spec.js b/spec/frontend/issues_list/components/new_issue_dropdown_spec.js deleted file mode 100644 index 1c9a87e8af2..00000000000 --- a/spec/frontend/issues_list/components/new_issue_dropdown_spec.js +++ /dev/null @@ -1,131 +0,0 @@ -import { GlDropdown, GlDropdownItem, GlSearchBoxByType } from '@gitlab/ui'; -import { createLocalVue, mount, shallowMount } from '@vue/test-utils'; -import VueApollo from 'vue-apollo'; -import createMockApollo from 'helpers/mock_apollo_helper'; -import NewIssueDropdown from '~/issues_list/components/new_issue_dropdown.vue'; -import searchProjectsQuery from '~/issues_list/queries/search_projects.query.graphql'; -import { DASH_SCOPE, joinPaths } from '~/lib/utils/url_utility'; -import { - emptySearchProjectsQueryResponse, - project1, - project3, - searchProjectsQueryResponse, -} from '../mock_data'; - -describe('NewIssueDropdown component', () => { - let wrapper; - - const localVue = createLocalVue(); - localVue.use(VueApollo); - - const mountComponent = ({ - search = '', - queryResponse = searchProjectsQueryResponse, - mountFn = shallowMount, - } = {}) => { - const requestHandlers = [[searchProjectsQuery, jest.fn().mockResolvedValue(queryResponse)]]; - const apolloProvider = createMockApollo(requestHandlers); - - return mountFn(NewIssueDropdown, { - localVue, - apolloProvider, - provide: { - fullPath: 'mushroom-kingdom', - }, - data() { - return { search }; - }, - }); - }; - - const findDropdown = () => wrapper.findComponent(GlDropdown); - const findInput = () => wrapper.findComponent(GlSearchBoxByType); - const showDropdown = async () => { - findDropdown().vm.$emit('shown'); - await wrapper.vm.$apollo.queries.projects.refetch(); - jest.runOnlyPendingTimers(); - }; - - afterEach(() => { - wrapper.destroy(); - }); - - it('renders a split dropdown', () => { - wrapper = mountComponent(); - - expect(findDropdown().props('split')).toBe(true); - }); - - it('renders a label for the dropdown toggle button', () => { - wrapper = mountComponent(); - - expect(findDropdown().attributes('toggle-text')).toBe(NewIssueDropdown.i18n.toggleButtonLabel); - }); - - it('focuses on input when dropdown is shown', async () => { - wrapper = mountComponent({ mountFn: mount }); - - const inputSpy = jest.spyOn(findInput().vm, 'focusInput'); - - await showDropdown(); - - expect(inputSpy).toHaveBeenCalledTimes(1); - }); - - it('renders projects with issues enabled', async () => { - wrapper = mountComponent({ mountFn: mount }); - - await showDropdown(); - - const listItems = wrapper.findAll('li'); - - expect(listItems.at(0).text()).toBe(project1.nameWithNamespace); - expect(listItems.at(1).text()).toBe(project3.nameWithNamespace); - }); - - it('renders `No matches found` when there are no matches', async () => { - wrapper = mountComponent({ - search: 'no matches', - queryResponse: emptySearchProjectsQueryResponse, - mountFn: mount, - }); - - await showDropdown(); - - expect(wrapper.find('li').text()).toBe(NewIssueDropdown.i18n.noMatchesFound); - }); - - describe('when no project is selected', () => { - beforeEach(() => { - wrapper = mountComponent(); - }); - - it('dropdown button is not a link', () => { - expect(findDropdown().attributes('split-href')).toBeUndefined(); - }); - - it('displays default text on the dropdown button', () => { - expect(findDropdown().props('text')).toBe(NewIssueDropdown.i18n.defaultDropdownText); - }); - }); - - describe('when a project is selected', () => { - beforeEach(async () => { - wrapper = mountComponent({ mountFn: mount }); - - await showDropdown(); - - wrapper.findComponent(GlDropdownItem).vm.$emit('click', project1); - }); - - it('dropdown button is a link', () => { - const href = joinPaths(project1.webUrl, DASH_SCOPE, 'issues/new'); - - expect(findDropdown().attributes('split-href')).toBe(href); - }); - - it('displays project name on the dropdown button', () => { - expect(findDropdown().props('text')).toBe(`New issue in ${project1.name}`); - }); - }); -}); diff --git a/spec/frontend/issues_list/issuable_list_test_data.js b/spec/frontend/issues_list/issuable_list_test_data.js deleted file mode 100644 index 313aa15bd31..00000000000 --- a/spec/frontend/issues_list/issuable_list_test_data.js +++ /dev/null @@ -1,77 +0,0 @@ -export const simpleIssue = { - id: 442, - iid: 31, - title: 'Dismiss Cipher with no integrity', - state: 'opened', - created_at: '2019-08-26T19:06:32.667Z', - updated_at: '2019-08-28T19:53:58.314Z', - labels: [], - milestone: null, - assignees: [], - author: { - id: 3, - name: 'Elnora Bernhard', - username: 'treva.lesch', - state: 'active', - avatar_url: 'https://www.gravatar.com/avatar/a8c0d9c2882406cf2a9b71494625a796?s=80&d=identicon', - web_url: 'http://localhost:3001/treva.lesch', - }, - assignee: null, - user_notes_count: 0, - blocking_issues_count: 0, - merge_requests_count: 0, - upvotes: 0, - downvotes: 0, - due_date: null, - confidential: false, - web_url: 'http://localhost:3001/h5bp/html5-boilerplate/issues/31', - has_tasks: false, - weight: null, - references: { - relative: 'html-boilerplate#45', - }, - health_status: 'on_track', -}; - -export const testLabels = [ - { - id: 1, - name: 'Tanuki', - description: 'A cute animal', - color: '#ff0000', - text_color: '#ffffff', - }, - { - id: 2, - name: 'Octocat', - description: 'A grotesque mish-mash of whiskers and tentacles', - color: '#333333', - text_color: '#000000', - }, - { - id: 3, - name: 'scoped::label', - description: 'A scoped label', - color: '#00ff00', - text_color: '#ffffff', - }, -]; - -export const testAssignees = [ - { - id: 1, - name: 'Administrator', - username: 'root', - state: 'active', - avatar_url: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', - web_url: 'http://localhost:3001/root', - }, - { - id: 22, - name: 'User 0', - username: 'user0', - state: 'active', - avatar_url: 'https://www.gravatar.com/avatar/52e4ce24a915fb7e51e1ad3b57f4b00a?s=80&d=identicon', - web_url: 'http://localhost:3001/user0', - }, -]; diff --git a/spec/frontend/issues_list/mock_data.js b/spec/frontend/issues_list/mock_data.js deleted file mode 100644 index 948699876ce..00000000000 --- a/spec/frontend/issues_list/mock_data.js +++ /dev/null @@ -1,310 +0,0 @@ -import { - OPERATOR_IS, - OPERATOR_IS_NOT, -} from '~/vue_shared/components/filtered_search_bar/constants'; - -export const getIssuesQueryResponse = { - data: { - project: { - id: '1', - issues: { - pageInfo: { - hasNextPage: true, - hasPreviousPage: false, - startCursor: 'startcursor', - endCursor: 'endcursor', - }, - nodes: [ - { - id: 'gid://gitlab/Issue/123456', - iid: '789', - closedAt: null, - confidential: false, - createdAt: '2021-05-22T04:08:01Z', - downvotes: 2, - dueDate: '2021-05-29', - hidden: false, - humanTimeEstimate: null, - mergeRequestsCount: false, - moved: false, - title: 'Issue title', - updatedAt: '2021-05-22T04:08:01Z', - upvotes: 3, - userDiscussionsCount: 4, - webPath: 'project/-/issues/789', - webUrl: 'project/-/issues/789', - assignees: { - nodes: [ - { - id: 'gid://gitlab/User/234', - avatarUrl: 'avatar/url', - name: 'Marge Simpson', - username: 'msimpson', - webUrl: 'url/msimpson', - }, - ], - }, - author: { - id: 'gid://gitlab/User/456', - avatarUrl: 'avatar/url', - name: 'Homer Simpson', - username: 'hsimpson', - webUrl: 'url/hsimpson', - }, - labels: { - nodes: [ - { - id: 'gid://gitlab/ProjectLabel/456', - color: '#333', - title: 'Label title', - description: 'Label description', - }, - ], - }, - milestone: null, - taskCompletionStatus: { - completedCount: 1, - count: 2, - }, - }, - ], - }, - }, - }, -}; - -export const getIssuesCountsQueryResponse = { - data: { - project: { - id: '1', - openedIssues: { - count: 1, - }, - closedIssues: { - count: 1, - }, - allIssues: { - count: 1, - }, - }, - }, -}; - -export const locationSearch = [ - '?search=find+issues', - 'author_username=homer', - 'not[author_username]=marge', - 'assignee_username[]=bart', - 'assignee_username[]=lisa', - 'not[assignee_username][]=patty', - 'not[assignee_username][]=selma', - 'milestone_title=season+3', - 'milestone_title=season+4', - 'not[milestone_title]=season+20', - 'not[milestone_title]=season+30', - 'label_name[]=cartoon', - 'label_name[]=tv', - 'not[label_name][]=live action', - 'not[label_name][]=drama', - 'release_tag=v3', - 'release_tag=v4', - 'not[release_tag]=v20', - 'not[release_tag]=v30', - 'type[]=issue', - 'type[]=feature', - 'not[type][]=bug', - 'not[type][]=incident', - 'my_reaction_emoji=thumbsup', - 'not[my_reaction_emoji]=thumbsdown', - 'confidential=yes', - 'iteration_id=4', - 'iteration_id=12', - 'not[iteration_id]=20', - 'not[iteration_id]=42', - 'epic_id=12', - 'not[epic_id]=34', - 'weight=1', - 'not[weight]=3', -].join('&'); - -export const locationSearchWithSpecialValues = [ - 'assignee_id=123', - 'assignee_username=bart', - 'my_reaction_emoji=None', - 'iteration_id=Current', - 'label_name[]=None', - 'release_tag=None', - 'milestone_title=Upcoming', - 'epic_id=None', - 'weight=None', -].join('&'); - -export const filteredTokens = [ - { type: 'author_username', value: { data: 'homer', operator: OPERATOR_IS } }, - { 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: 'patty', operator: OPERATOR_IS_NOT } }, - { type: 'assignee_username', value: { data: 'selma', operator: OPERATOR_IS_NOT } }, - { type: 'milestone', value: { data: 'season 3', operator: OPERATOR_IS } }, - { type: 'milestone', value: { data: 'season 4', operator: OPERATOR_IS } }, - { type: 'milestone', value: { data: 'season 20', operator: OPERATOR_IS_NOT } }, - { type: 'milestone', value: { data: 'season 30', operator: OPERATOR_IS_NOT } }, - { type: 'labels', value: { data: 'cartoon', operator: OPERATOR_IS } }, - { type: 'labels', value: { data: 'tv', operator: OPERATOR_IS } }, - { type: 'labels', value: { data: 'live action', operator: OPERATOR_IS_NOT } }, - { type: 'labels', value: { data: 'drama', operator: OPERATOR_IS_NOT } }, - { type: 'release', value: { data: 'v3', operator: OPERATOR_IS } }, - { type: 'release', value: { data: 'v4', operator: OPERATOR_IS } }, - { type: 'release', value: { data: 'v20', operator: OPERATOR_IS_NOT } }, - { type: 'release', value: { data: 'v30', operator: OPERATOR_IS_NOT } }, - { type: 'type', value: { data: 'issue', operator: OPERATOR_IS } }, - { type: 'type', value: { data: 'feature', operator: OPERATOR_IS } }, - { type: 'type', value: { data: 'bug', operator: OPERATOR_IS_NOT } }, - { type: 'type', value: { data: 'incident', operator: OPERATOR_IS_NOT } }, - { type: 'my_reaction_emoji', value: { data: 'thumbsup', operator: OPERATOR_IS } }, - { type: 'my_reaction_emoji', value: { data: 'thumbsdown', operator: OPERATOR_IS_NOT } }, - { type: 'confidential', value: { data: 'yes', operator: OPERATOR_IS } }, - { type: 'iteration', value: { data: '4', operator: OPERATOR_IS } }, - { type: 'iteration', value: { data: '12', operator: OPERATOR_IS } }, - { type: 'iteration', value: { data: '20', operator: OPERATOR_IS_NOT } }, - { type: 'iteration', value: { data: '42', operator: OPERATOR_IS_NOT } }, - { type: 'epic_id', value: { data: '12', operator: OPERATOR_IS } }, - { 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: 'filtered-search-term', value: { data: 'find' } }, - { type: 'filtered-search-term', value: { data: 'issues' } }, -]; - -export const filteredTokensWithSpecialValues = [ - { type: 'assignee_username', value: { data: '123', operator: OPERATOR_IS } }, - { type: 'assignee_username', value: { data: 'bart', operator: OPERATOR_IS } }, - { type: 'my_reaction_emoji', value: { data: 'None', operator: OPERATOR_IS } }, - { type: 'iteration', value: { data: 'Current', operator: OPERATOR_IS } }, - { type: 'labels', value: { data: 'None', operator: OPERATOR_IS } }, - { type: 'release', value: { data: 'None', operator: OPERATOR_IS } }, - { type: 'milestone', value: { data: 'Upcoming', operator: OPERATOR_IS } }, - { type: 'epic_id', value: { data: 'None', operator: OPERATOR_IS } }, - { type: 'weight', value: { data: 'None', operator: OPERATOR_IS } }, -]; - -export const apiParams = { - authorUsername: 'homer', - assigneeUsernames: ['bart', 'lisa'], - milestoneTitle: ['season 3', 'season 4'], - labelName: ['cartoon', 'tv'], - releaseTag: ['v3', 'v4'], - types: ['ISSUE', 'FEATURE'], - myReactionEmoji: 'thumbsup', - confidential: true, - iterationId: ['4', '12'], - epicId: '12', - weight: '1', - not: { - authorUsername: 'marge', - assigneeUsernames: ['patty', 'selma'], - milestoneTitle: ['season 20', 'season 30'], - labelName: ['live action', 'drama'], - releaseTag: ['v20', 'v30'], - types: ['BUG', 'INCIDENT'], - myReactionEmoji: 'thumbsdown', - iterationId: ['20', '42'], - epicId: '34', - weight: '3', - }, -}; - -export const apiParamsWithSpecialValues = { - assigneeId: '123', - assigneeUsernames: 'bart', - labelName: 'None', - myReactionEmoji: 'None', - releaseTagWildcardId: 'NONE', - iterationWildcardId: 'CURRENT', - milestoneWildcardId: 'UPCOMING', - epicId: 'None', - weight: 'None', -}; - -export const urlParams = { - author_username: 'homer', - 'not[author_username]': 'marge', - 'assignee_username[]': ['bart', 'lisa'], - 'not[assignee_username][]': ['patty', 'selma'], - milestone_title: ['season 3', 'season 4'], - 'not[milestone_title]': ['season 20', 'season 30'], - 'label_name[]': ['cartoon', 'tv'], - 'not[label_name][]': ['live action', 'drama'], - release_tag: ['v3', 'v4'], - 'not[release_tag]': ['v20', 'v30'], - 'type[]': ['issue', 'feature'], - 'not[type][]': ['bug', 'incident'], - my_reaction_emoji: 'thumbsup', - 'not[my_reaction_emoji]': 'thumbsdown', - confidential: 'yes', - iteration_id: ['4', '12'], - 'not[iteration_id]': ['20', '42'], - epic_id: '12', - 'not[epic_id]': '34', - weight: '1', - 'not[weight]': '3', -}; - -export const urlParamsWithSpecialValues = { - assignee_id: '123', - 'assignee_username[]': 'bart', - 'label_name[]': 'None', - release_tag: 'None', - my_reaction_emoji: 'None', - iteration_id: 'Current', - milestone_title: 'Upcoming', - epic_id: 'None', - weight: 'None', -}; - -export const project1 = { - id: 'gid://gitlab/Group/26', - issuesEnabled: true, - name: 'Super Mario Project', - nameWithNamespace: 'Mushroom Kingdom / Super Mario Project', - webUrl: 'https://127.0.0.1:3000/mushroom-kingdom/super-mario-project', -}; - -export const project2 = { - id: 'gid://gitlab/Group/59', - issuesEnabled: false, - name: 'Mario Kart Project', - nameWithNamespace: 'Mushroom Kingdom / Mario Kart Project', - webUrl: 'https://127.0.0.1:3000/mushroom-kingdom/mario-kart-project', -}; - -export const project3 = { - id: 'gid://gitlab/Group/103', - issuesEnabled: true, - name: 'Mario Party Project', - nameWithNamespace: 'Mushroom Kingdom / Mario Party Project', - webUrl: 'https://127.0.0.1:3000/mushroom-kingdom/mario-party-project', -}; - -export const searchProjectsQueryResponse = { - data: { - group: { - id: '1', - projects: { - nodes: [project1, project2, project3], - }, - }, - }, -}; - -export const emptySearchProjectsQueryResponse = { - data: { - group: { - id: '1', - projects: { - nodes: [], - }, - }, - }, -}; diff --git a/spec/frontend/issues_list/service_desk_helper_spec.js b/spec/frontend/issues_list/service_desk_helper_spec.js deleted file mode 100644 index 16aee853341..00000000000 --- a/spec/frontend/issues_list/service_desk_helper_spec.js +++ /dev/null @@ -1,28 +0,0 @@ -import { emptyStateHelper, generateMessages } from '~/issues_list/service_desk_helper'; - -describe('service desk helper', () => { - const emptyStateMessages = generateMessages({}); - - // Note: isServiceDeskEnabled must not be true when isServiceDeskSupported is false (it's an invalid case). - describe.each` - isServiceDeskSupported | isServiceDeskEnabled | canEditProjectSettings | expectedMessage - ${true} | ${true} | ${true} | ${'serviceDeskEnabledAndCanEditProjectSettings'} - ${true} | ${true} | ${false} | ${'serviceDeskEnabledAndCannotEditProjectSettings'} - ${true} | ${false} | ${true} | ${'serviceDeskDisabledAndCanEditProjectSettings'} - ${true} | ${false} | ${false} | ${'serviceDeskDisabledAndCannotEditProjectSettings'} - ${false} | ${false} | ${true} | ${'serviceDeskIsNotSupported'} - ${false} | ${false} | ${false} | ${'serviceDeskIsNotEnabled'} - `( - 'isServiceDeskSupported = $isServiceDeskSupported, isServiceDeskEnabled = $isServiceDeskEnabled, canEditProjectSettings = $canEditProjectSettings', - ({ isServiceDeskSupported, isServiceDeskEnabled, canEditProjectSettings, expectedMessage }) => { - it(`displays ${expectedMessage} message`, () => { - const emptyStateMeta = { - isServiceDeskEnabled, - isServiceDeskSupported, - canEditProjectSettings, - }; - expect(emptyStateHelper(emptyStateMeta)).toEqual(emptyStateMessages[expectedMessage]); - }); - }, - ); -}); diff --git a/spec/frontend/issues_list/utils_spec.js b/spec/frontend/issues_list/utils_spec.js deleted file mode 100644 index 8e1d70db92d..00000000000 --- a/spec/frontend/issues_list/utils_spec.js +++ /dev/null @@ -1,127 +0,0 @@ -import { - apiParams, - apiParamsWithSpecialValues, - filteredTokens, - filteredTokensWithSpecialValues, - locationSearch, - locationSearchWithSpecialValues, - urlParams, - urlParamsWithSpecialValues, -} from 'jest/issues_list/mock_data'; -import { - defaultPageSizeParams, - DUE_DATE_VALUES, - largePageSizeParams, - RELATIVE_POSITION_ASC, - urlSortParams, -} from '~/issues_list/constants'; -import { - convertToApiParams, - convertToSearchQuery, - convertToUrlParams, - getDueDateValue, - getFilterTokens, - getInitialPageParams, - getSortKey, - getSortOptions, -} from '~/issues_list/utils'; - -describe('getInitialPageParams', () => { - it.each(Object.keys(urlSortParams))( - 'returns the correct page params for sort key %s', - (sortKey) => { - const expectedPageParams = - sortKey === RELATIVE_POSITION_ASC ? largePageSizeParams : defaultPageSizeParams; - - expect(getInitialPageParams(sortKey)).toBe(expectedPageParams); - }, - ); -}); - -describe('getSortKey', () => { - it.each(Object.keys(urlSortParams))('returns %s given the correct inputs', (sortKey) => { - const sort = urlSortParams[sortKey]; - expect(getSortKey(sort)).toBe(sortKey); - }); -}); - -describe('getDueDateValue', () => { - it.each(DUE_DATE_VALUES)('returns the argument when it is `%s`', (value) => { - expect(getDueDateValue(value)).toBe(value); - }); - - it('returns undefined when the argument is invalid', () => { - expect(getDueDateValue('invalid value')).toBeUndefined(); - }); -}); - -describe('getSortOptions', () => { - describe.each` - hasIssueWeightsFeature | hasBlockedIssuesFeature | length | containsWeight | containsBlocking - ${false} | ${false} | ${9} | ${false} | ${false} - ${true} | ${false} | ${10} | ${true} | ${false} - ${false} | ${true} | ${10} | ${false} | ${true} - ${true} | ${true} | ${11} | ${true} | ${true} - `( - 'when hasIssueWeightsFeature=$hasIssueWeightsFeature and hasBlockedIssuesFeature=$hasBlockedIssuesFeature', - ({ - hasIssueWeightsFeature, - hasBlockedIssuesFeature, - length, - containsWeight, - containsBlocking, - }) => { - const sortOptions = getSortOptions(hasIssueWeightsFeature, hasBlockedIssuesFeature); - - it('returns the correct length of sort options', () => { - expect(sortOptions).toHaveLength(length); - }); - - it(`${containsWeight ? 'contains' : 'does not contain'} weight option`, () => { - expect(sortOptions.some((option) => option.title === 'Weight')).toBe(containsWeight); - }); - - it(`${containsBlocking ? 'contains' : 'does not contain'} blocking option`, () => { - expect(sortOptions.some((option) => option.title === 'Blocking')).toBe(containsBlocking); - }); - }, - ); -}); - -describe('getFilterTokens', () => { - it('returns filtered tokens given "window.location.search"', () => { - expect(getFilterTokens(locationSearch)).toEqual(filteredTokens); - }); - - it('returns filtered tokens given "window.location.search" with special values', () => { - expect(getFilterTokens(locationSearchWithSpecialValues)).toEqual( - filteredTokensWithSpecialValues, - ); - }); -}); - -describe('convertToApiParams', () => { - it('returns api params given filtered tokens', () => { - expect(convertToApiParams(filteredTokens)).toEqual(apiParams); - }); - - it('returns api params given filtered tokens with special values', () => { - expect(convertToApiParams(filteredTokensWithSpecialValues)).toEqual(apiParamsWithSpecialValues); - }); -}); - -describe('convertToUrlParams', () => { - it('returns url params given filtered tokens', () => { - expect(convertToUrlParams(filteredTokens)).toEqual(urlParams); - }); - - it('returns url params given filtered tokens with special values', () => { - expect(convertToUrlParams(filteredTokensWithSpecialValues)).toEqual(urlParamsWithSpecialValues); - }); -}); - -describe('convertToSearchQuery', () => { - it('returns search string given filtered tokens', () => { - expect(convertToSearchQuery(filteredTokens)).toBe('find issues'); - }); -}); |