summaryrefslogtreecommitdiff
path: root/spec/frontend/issues_list/components/issuables_list_app_spec.js
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend/issues_list/components/issuables_list_app_spec.js')
-rw-r--r--spec/frontend/issues_list/components/issuables_list_app_spec.js595
1 files changed, 595 insertions, 0 deletions
diff --git a/spec/frontend/issues_list/components/issuables_list_app_spec.js b/spec/frontend/issues_list/components/issuables_list_app_spec.js
new file mode 100644
index 00000000000..1f80b4fc54a
--- /dev/null
+++ b/spec/frontend/issues_list/components/issuables_list_app_spec.js
@@ -0,0 +1,595 @@
+import axios from 'axios';
+import MockAdapter from 'axios-mock-adapter';
+import { shallowMount } from '@vue/test-utils';
+import {
+ GlEmptyState,
+ GlPagination,
+ GlDeprecatedSkeletonLoading as GlSkeletonLoading,
+} from '@gitlab/ui';
+import waitForPromises from 'helpers/wait_for_promises';
+import { TEST_HOST } from 'helpers/test_constants';
+import { deprecatedCreateFlash as flash } from '~/flash';
+import IssuablesListApp from '~/issues_list/components/issuables_list_app.vue';
+import Issuable from '~/issues_list/components/issuable.vue';
+import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
+import issueablesEventBus from '~/issues_list/eventhub';
+import { PAGE_SIZE, PAGE_SIZE_MANUAL, RELATIVE_POSITION } from '~/issues_list/constants';
+
+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 setUrl = query => {
+ window.location.href = `${TEST_LOCATION}${query}`;
+ window.location.search = query;
+};
+
+const MOCK_ISSUES = Array(PAGE_SIZE_MANUAL)
+ .fill(0)
+ .map((_, i) => ({
+ id: i,
+ web_url: `url${i}`,
+ }));
+
+describe('Issuables list component', () => {
+ let oldLocation;
+ 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);
+
+ oldLocation = window.location;
+ Object.defineProperty(window, 'location', {
+ writable: true,
+ value: { href: '', search: '' },
+ });
+ window.location.href = TEST_LOCATION;
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ mockAxios.restore();
+ window.location = oldLocation;
+ });
+
+ 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(flash).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(() => {
+ issueablesEventBus.$on.mockReset();
+ issueablesEventBus.$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', () => {
+ issueablesEventBus.$emit.mockReset();
+ const i1 = wrapper.vm.issuables[1];
+
+ wrapper.vm.onSelectIssuable({ issuable: i1, selected: true });
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(issueablesEventBus.$emit).toHaveBeenCalledTimes(1);
+ expect(issueablesEventBus.$emit).toHaveBeenCalledWith('issuables:updateBulkEdit');
+ });
+ });
+
+ it('does not broadcast a message to the bulk edit sidebar when a value is not added to selection', () => {
+ issueablesEventBus.$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(issueablesEventBus.$emit).toHaveBeenCalledTimes(0);
+ });
+ });
+
+ it('listens to a message to toggle bulk editing', () => {
+ expect(wrapper.vm.isBulkEditing).toBe(false);
+ expect(issueablesEventBus.$on.mock.calls[0][0]).toBe('issuables:toggleBulkEdit');
+ issueablesEventBus.$on.mock.calls[0][1](true); // Call the message handler
+
+ return waitForPromises()
+ .then(() => {
+ expect(wrapper.vm.isBulkEditing).toBe(true);
+ issueablesEventBus.$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',
+ utf8: '✓',
+ 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&utf8=%E2%9C%93&weight=0&not[label_name][]=Afterpod&not[milestone_title][]=13';
+
+ beforeEach(() => {
+ setUrl(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&utf8=%E2%9C%93&weight=0&page=3';
+
+ beforeEach(() => {
+ setUrl(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(() => {
+ window.location.href = `${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(() => {
+ window.location.search = '?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(() => {
+ window.location.search = '?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(() => {
+ window.location.search = '?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', () => {
+ setUrl(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';
+
+ setUrl(query);
+
+ factory({ type: 'jira' });
+
+ expect(findFilteredSearchBar().props('initialFilterValue')).toEqual(['free text']);
+ });
+ });
+ });
+});