summaryrefslogtreecommitdiff
path: root/spec/frontend/incidents/components/incidents_list_spec.js
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend/incidents/components/incidents_list_spec.js')
-rw-r--r--spec/frontend/incidents/components/incidents_list_spec.js362
1 files changed, 362 insertions, 0 deletions
diff --git a/spec/frontend/incidents/components/incidents_list_spec.js b/spec/frontend/incidents/components/incidents_list_spec.js
new file mode 100644
index 00000000000..33ddd06d6d9
--- /dev/null
+++ b/spec/frontend/incidents/components/incidents_list_spec.js
@@ -0,0 +1,362 @@
+import { mount } from '@vue/test-utils';
+import {
+ GlAlert,
+ GlLoadingIcon,
+ GlTable,
+ GlAvatar,
+ GlPagination,
+ GlSearchBoxByType,
+ GlTab,
+ GlTabs,
+ GlBadge,
+ GlEmptyState,
+} from '@gitlab/ui';
+import { visitUrl, joinPaths, mergeUrlParams } from '~/lib/utils/url_utility';
+import IncidentsList from '~/incidents/components/incidents_list.vue';
+import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import { I18N, INCIDENT_STATUS_TABS } from '~/incidents/constants';
+import mockIncidents from '../mocks/incidents.json';
+
+jest.mock('~/lib/utils/url_utility', () => ({
+ visitUrl: jest.fn().mockName('visitUrlMock'),
+ joinPaths: jest.fn().mockName('joinPaths'),
+ mergeUrlParams: jest.fn().mockName('mergeUrlParams'),
+}));
+
+describe('Incidents List', () => {
+ let wrapper;
+ const newIssuePath = 'namespace/project/-/issues/new';
+ const emptyListSvgPath = '/assets/empty.svg';
+ const incidentTemplateName = 'incident';
+ const incidentType = 'incident';
+ const incidentsCount = {
+ opened: 14,
+ closed: 1,
+ all: 16,
+ };
+
+ const findTable = () => wrapper.find(GlTable);
+ const findTableRows = () => wrapper.findAll('table tbody tr');
+ const findAlert = () => wrapper.find(GlAlert);
+ const findLoader = () => wrapper.find(GlLoadingIcon);
+ const findTimeAgo = () => wrapper.findAll(TimeAgoTooltip);
+ const findDateColumnHeader = () =>
+ wrapper.find('[data-testid="incident-management-created-at-sort"]');
+ const findSearch = () => wrapper.find(GlSearchBoxByType);
+ const findAssingees = () => wrapper.findAll('[data-testid="incident-assignees"]');
+ const findCreateIncidentBtn = () => wrapper.find('[data-testid="createIncidentBtn"]');
+ const findClosedIcon = () => wrapper.findAll("[data-testid='incident-closed']");
+ const findPagination = () => wrapper.find(GlPagination);
+ const findStatusFilterTabs = () => wrapper.findAll(GlTab);
+ const findStatusFilterBadge = () => wrapper.findAll(GlBadge);
+ const findStatusTabs = () => wrapper.find(GlTabs);
+ const findEmptyState = () => wrapper.find(GlEmptyState);
+
+ function mountComponent({ data = { incidents: [], incidentsCount: {} }, loading = false }) {
+ wrapper = mount(IncidentsList, {
+ data() {
+ return data;
+ },
+ mocks: {
+ $apollo: {
+ queries: {
+ incidents: {
+ loading,
+ },
+ },
+ },
+ },
+ provide: {
+ projectPath: '/project/path',
+ newIssuePath,
+ incidentTemplateName,
+ incidentType,
+ issuePath: '/project/isssues',
+ publishedAvailable: true,
+ emptyListSvgPath,
+ },
+ stubs: {
+ GlButton: true,
+ GlAvatar: true,
+ },
+ });
+ }
+
+ afterEach(() => {
+ if (wrapper) {
+ wrapper.destroy();
+ wrapper = null;
+ }
+ });
+
+ it('shows the loading state', () => {
+ mountComponent({
+ loading: true,
+ });
+ expect(findLoader().exists()).toBe(true);
+ });
+
+ it('shows empty state', () => {
+ mountComponent({
+ data: { incidents: { list: [] }, incidentsCount: {} },
+ loading: false,
+ });
+ expect(findEmptyState().exists()).toBe(true);
+ });
+
+ it('shows error state', () => {
+ mountComponent({
+ data: { incidents: { list: [] }, incidentsCount: { all: 0 }, errored: true },
+ loading: false,
+ });
+ expect(findTable().text()).toContain(I18N.noIncidents);
+ expect(findAlert().exists()).toBe(true);
+ });
+
+ describe('Incident Management list', () => {
+ beforeEach(() => {
+ mountComponent({
+ data: { incidents: { list: mockIncidents }, incidentsCount },
+ loading: false,
+ });
+ });
+
+ it('renders rows based on provided data', () => {
+ expect(findTableRows().length).toBe(mockIncidents.length);
+ });
+
+ it('renders a createdAt with timeAgo component per row', () => {
+ expect(findTimeAgo().length).toBe(mockIncidents.length);
+ });
+
+ describe('Assignees', () => {
+ it('shows Unassigned when there are no assignees', () => {
+ expect(
+ findAssingees()
+ .at(0)
+ .text(),
+ ).toBe(I18N.unassigned);
+ });
+
+ it('renders an avatar component when there is an assignee', () => {
+ const avatar = findAssingees()
+ .at(1)
+ .find(GlAvatar);
+ const { src, label } = avatar.attributes();
+ const { name, avatarUrl } = mockIncidents[1].assignees.nodes[0];
+
+ expect(avatar.exists()).toBe(true);
+ expect(label).toBe(name);
+ expect(src).toBe(avatarUrl);
+ });
+
+ it('contains a link to the issue details', () => {
+ findTableRows()
+ .at(0)
+ .trigger('click');
+ expect(visitUrl).toHaveBeenCalledWith(joinPaths(`/project/isssues/`, mockIncidents[0].iid));
+ });
+
+ it('renders a closed icon for closed incidents', () => {
+ expect(findClosedIcon().length).toBe(
+ mockIncidents.filter(({ state }) => state === 'closed').length,
+ );
+ });
+ });
+ });
+
+ describe('Create Incident', () => {
+ beforeEach(() => {
+ mountComponent({
+ data: { incidents: { list: mockIncidents }, incidentsCount: {} },
+ loading: false,
+ });
+ });
+
+ it('shows the button linking to new incidents page with prefilled incident template when clicked', () => {
+ expect(findCreateIncidentBtn().exists()).toBe(true);
+ findCreateIncidentBtn().trigger('click');
+ expect(mergeUrlParams).toHaveBeenCalledWith(
+ { issuable_template: incidentTemplateName, 'issue[issue_type]': incidentType },
+ newIssuePath,
+ );
+ });
+
+ it('sets button loading on click', () => {
+ findCreateIncidentBtn().vm.$emit('click');
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findCreateIncidentBtn().attributes('loading')).toBe('true');
+ });
+ });
+ });
+
+ describe('Pagination', () => {
+ beforeEach(() => {
+ mountComponent({
+ data: {
+ incidents: {
+ list: mockIncidents,
+ pageInfo: { hasNextPage: true, hasPreviousPage: true },
+ },
+ incidentsCount,
+ errored: false,
+ },
+ loading: false,
+ });
+ });
+
+ it('should render pagination', () => {
+ expect(wrapper.find(GlPagination).exists()).toBe(true);
+ });
+
+ describe('prevPage', () => {
+ it('returns prevPage button', () => {
+ findPagination().vm.$emit('input', 3);
+
+ return wrapper.vm.$nextTick(() => {
+ expect(
+ findPagination()
+ .findAll('.page-item')
+ .at(0)
+ .text(),
+ ).toBe('Prev');
+ });
+ });
+
+ it('returns prevPage number', () => {
+ findPagination().vm.$emit('input', 3);
+
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.vm.prevPage).toBe(2);
+ });
+ });
+
+ it('returns 0 when it is the first page', () => {
+ findPagination().vm.$emit('input', 1);
+
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.vm.prevPage).toBe(0);
+ });
+ });
+ });
+
+ describe('nextPage', () => {
+ it('returns nextPage button', () => {
+ findPagination().vm.$emit('input', 3);
+
+ return wrapper.vm.$nextTick(() => {
+ expect(
+ findPagination()
+ .findAll('.page-item')
+ .at(1)
+ .text(),
+ ).toBe('Next');
+ });
+ });
+
+ it('returns nextPage number', () => {
+ mountComponent({
+ data: {
+ incidents: {
+ list: [...mockIncidents, ...mockIncidents, ...mockIncidents],
+ pageInfo: { hasNextPage: true, hasPreviousPage: true },
+ },
+ incidentsCount,
+ errored: false,
+ },
+ loading: false,
+ });
+ findPagination().vm.$emit('input', 1);
+
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.vm.nextPage).toBe(2);
+ });
+ });
+
+ it('returns `null` when currentPage is already last page', () => {
+ findStatusTabs().vm.$emit('input', 1);
+ findPagination().vm.$emit('input', 1);
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.vm.nextPage).toBeNull();
+ });
+ });
+ });
+
+ describe('Search', () => {
+ beforeEach(() => {
+ mountComponent({
+ data: {
+ incidents: {
+ list: mockIncidents,
+ pageInfo: { hasNextPage: true, hasPreviousPage: true },
+ },
+ incidentsCount,
+ errored: false,
+ },
+ loading: false,
+ });
+ });
+
+ it('renders the search component for incidents', () => {
+ expect(findSearch().exists()).toBe(true);
+ });
+
+ it('sets the `searchTerm` graphql variable', () => {
+ const SEARCH_TERM = 'Simple Incident';
+
+ findSearch().vm.$emit('input', SEARCH_TERM);
+
+ expect(wrapper.vm.$data.searchTerm).toBe(SEARCH_TERM);
+ });
+ });
+
+ describe('Status Filter Tabs', () => {
+ beforeEach(() => {
+ mountComponent({
+ data: { incidents: mockIncidents, incidentsCount },
+ loading: false,
+ stubs: {
+ GlTab: true,
+ },
+ });
+ });
+
+ it('should display filter tabs', () => {
+ const tabs = findStatusFilterTabs().wrappers;
+
+ tabs.forEach((tab, i) => {
+ expect(tab.attributes('data-testid')).toContain(INCIDENT_STATUS_TABS[i].status);
+ });
+ });
+
+ it('should display filter tabs with alerts count badge for each status', () => {
+ const tabs = findStatusFilterTabs().wrappers;
+ const badges = findStatusFilterBadge();
+
+ tabs.forEach((tab, i) => {
+ const status = INCIDENT_STATUS_TABS[i].status.toLowerCase();
+ expect(tab.attributes('data-testid')).toContain(INCIDENT_STATUS_TABS[i].status);
+ expect(badges.at(i).text()).toContain(incidentsCount[status]);
+ });
+ });
+ });
+ });
+
+ describe('sorting the incident list by column', () => {
+ beforeEach(() => {
+ mountComponent({
+ data: { incidents: mockIncidents, incidentsCount },
+ loading: false,
+ });
+ });
+
+ it('updates sort with new direction and column key', () => {
+ expect(findDateColumnHeader().attributes('aria-sort')).toBe('descending');
+
+ findDateColumnHeader().trigger('click');
+ return wrapper.vm.$nextTick(() => {
+ expect(findDateColumnHeader().attributes('aria-sort')).toBe('ascending');
+ });
+ });
+ });
+});