diff options
Diffstat (limited to 'spec/frontend/notifications')
-rw-r--r-- | spec/frontend/notifications/components/custom_notifications_modal_spec.js | 267 | ||||
-rw-r--r-- | spec/frontend/notifications/components/notifications_dropdown_spec.js | 274 |
2 files changed, 541 insertions, 0 deletions
diff --git a/spec/frontend/notifications/components/custom_notifications_modal_spec.js b/spec/frontend/notifications/components/custom_notifications_modal_spec.js new file mode 100644 index 00000000000..3e87f3107bd --- /dev/null +++ b/spec/frontend/notifications/components/custom_notifications_modal_spec.js @@ -0,0 +1,267 @@ +import { GlSprintf, GlModal, GlFormGroup, GlFormCheckbox, GlLoadingIcon } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import axios from 'axios'; +import MockAdapter from 'axios-mock-adapter'; +import { extendedWrapper } from 'helpers/vue_test_utils_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import httpStatus from '~/lib/utils/http_status'; +import CustomNotificationsModal from '~/notifications/components/custom_notifications_modal.vue'; +import { i18n } from '~/notifications/constants'; + +const mockNotificationSettingsResponses = { + default: { + level: 'custom', + events: { + new_release: true, + new_note: false, + }, + }, + updated: { + level: 'custom', + events: { + new_release: true, + new_note: true, + }, + }, +}; + +const mockToastShow = jest.fn(); + +describe('CustomNotificationsModal', () => { + let wrapper; + let mockAxios; + + function createComponent(options = {}) { + const { injectedProperties = {}, props = {} } = options; + return extendedWrapper( + shallowMount(CustomNotificationsModal, { + props: { + ...props, + }, + provide: { + ...injectedProperties, + }, + mocks: { + $toast: { + show: mockToastShow, + }, + }, + stubs: { + GlModal, + GlFormGroup, + GlFormCheckbox, + }, + }), + ); + } + + const findModalBodyDescription = () => wrapper.find(GlSprintf); + const findAllCheckboxes = () => wrapper.findAll(GlFormCheckbox); + const findCheckboxAt = (index) => findAllCheckboxes().at(index); + + beforeEach(() => { + gon.api_version = 'v4'; + mockAxios = new MockAdapter(axios); + }); + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + mockAxios.restore(); + }); + + describe('template', () => { + beforeEach(() => { + wrapper = createComponent(); + }); + + it('displays the body title and the body message', () => { + expect(wrapper.findByTestId('modalBodyTitle').text()).toBe( + i18n.customNotificationsModal.bodyTitle, + ); + expect(findModalBodyDescription().attributes('message')).toContain( + i18n.customNotificationsModal.bodyMessage, + ); + }); + + describe('checkbox items', () => { + beforeEach(async () => { + wrapper = createComponent(); + + wrapper.setData({ + events: [ + { id: 'new_release', enabled: true, name: 'New release', loading: false }, + { id: 'new_note', enabled: false, name: 'New note', loading: true }, + ], + }); + + await wrapper.vm.$nextTick(); + }); + + it.each` + index | eventId | eventName | enabled | loading + ${0} | ${'new_release'} | ${'New release'} | ${true} | ${false} + ${1} | ${'new_note'} | ${'New note'} | ${false} | ${true} + `( + 'renders a checkbox for "$eventName" with checked=$enabled', + async ({ index, eventName, enabled, loading }) => { + const checkbox = findCheckboxAt(index); + expect(checkbox.text()).toContain(eventName); + expect(checkbox.vm.$attrs.checked).toBe(enabled); + expect(checkbox.find(GlLoadingIcon).exists()).toBe(loading); + }, + ); + }); + }); + + describe('API calls', () => { + describe('load notification settings', () => { + beforeEach(() => { + jest.spyOn(axios, 'get'); + }); + + it.each` + projectId | groupId | endpointUrl | notificationType | condition + ${1} | ${null} | ${'/api/v4/projects/1/notification_settings'} | ${'project'} | ${'a projectId is given'} + ${null} | ${1} | ${'/api/v4/groups/1/notification_settings'} | ${'group'} | ${'a groupId is given'} + ${null} | ${null} | ${'/api/v4/notification_settings'} | ${'global'} | ${'neither projectId nor groupId are given'} + `( + 'requests $notificationType notification settings when $condition', + async ({ projectId, groupId, endpointUrl }) => { + const injectedProperties = { + projectId, + groupId, + }; + + mockAxios + .onGet(endpointUrl) + .reply(httpStatus.OK, mockNotificationSettingsResponses.default); + + wrapper = createComponent({ injectedProperties }); + + wrapper.find(GlModal).vm.$emit('show'); + + await waitForPromises(); + + expect(axios.get).toHaveBeenCalledWith(endpointUrl); + }, + ); + + it('updates the loading state and the events property', async () => { + const endpointUrl = '/api/v4/notification_settings'; + + mockAxios + .onGet(endpointUrl) + .reply(httpStatus.OK, mockNotificationSettingsResponses.default); + + wrapper = createComponent(); + + wrapper.find(GlModal).vm.$emit('show'); + expect(wrapper.vm.isLoading).toBe(true); + + await waitForPromises(); + + expect(axios.get).toHaveBeenCalledWith(endpointUrl); + expect(wrapper.vm.isLoading).toBe(false); + expect(wrapper.vm.events).toEqual([ + { id: 'new_release', enabled: true, name: 'New release', loading: false }, + { id: 'new_note', enabled: false, name: 'New note', loading: false }, + ]); + }); + + it('shows a toast message when the request fails', async () => { + mockAxios.onGet('/api/v4/notification_settings').reply(httpStatus.NOT_FOUND, {}); + wrapper = createComponent(); + + wrapper.find(GlModal).vm.$emit('show'); + + await waitForPromises(); + + expect( + mockToastShow, + ).toHaveBeenCalledWith( + 'An error occured while loading the notification settings. Please try again.', + { type: 'error' }, + ); + }); + }); + + describe('update notification settings', () => { + beforeEach(() => { + jest.spyOn(axios, 'put'); + }); + + it.each` + projectId | groupId | endpointUrl | notificationType | condition + ${1} | ${null} | ${'/api/v4/projects/1/notification_settings'} | ${'project'} | ${'a projectId is given'} + ${null} | ${1} | ${'/api/v4/groups/1/notification_settings'} | ${'group'} | ${'a groupId is given'} + ${null} | ${null} | ${'/api/v4/notification_settings'} | ${'global'} | ${'neither projectId nor groupId are given'} + `( + 'updates the $notificationType notification settings when $condition and the user clicks the checkbox ', + async ({ projectId, groupId, endpointUrl }) => { + mockAxios + .onGet(endpointUrl) + .reply(httpStatus.OK, mockNotificationSettingsResponses.default); + + mockAxios + .onPut(endpointUrl) + .reply(httpStatus.OK, mockNotificationSettingsResponses.updated); + + const injectedProperties = { + projectId, + groupId, + }; + + wrapper = createComponent({ injectedProperties }); + + wrapper.setData({ + events: [ + { id: 'new_release', enabled: true, name: 'New release', loading: false }, + { id: 'new_note', enabled: false, name: 'New note', loading: false }, + ], + }); + + await wrapper.vm.$nextTick(); + + findCheckboxAt(1).vm.$emit('change', true); + + await waitForPromises(); + + expect(axios.put).toHaveBeenCalledWith(endpointUrl, { + new_note: true, + }); + + expect(wrapper.vm.events).toEqual([ + { id: 'new_release', enabled: true, name: 'New release', loading: false }, + { id: 'new_note', enabled: true, name: 'New note', loading: false }, + ]); + }, + ); + + it('shows a toast message when the request fails', async () => { + mockAxios.onPut('/api/v4/notification_settings').reply(httpStatus.NOT_FOUND, {}); + wrapper = createComponent(); + + wrapper.setData({ + events: [ + { id: 'new_release', enabled: true, name: 'New release', loading: false }, + { id: 'new_note', enabled: false, name: 'New note', loading: false }, + ], + }); + + await wrapper.vm.$nextTick(); + + findCheckboxAt(1).vm.$emit('change', true); + + await waitForPromises(); + + expect( + mockToastShow, + ).toHaveBeenCalledWith( + 'An error occured while updating the notification settings. Please try again.', + { type: 'error' }, + ); + }); + }); + }); +}); diff --git a/spec/frontend/notifications/components/notifications_dropdown_spec.js b/spec/frontend/notifications/components/notifications_dropdown_spec.js new file mode 100644 index 00000000000..0673fb51a91 --- /dev/null +++ b/spec/frontend/notifications/components/notifications_dropdown_spec.js @@ -0,0 +1,274 @@ +import { GlButtonGroup, GlButton, GlDropdown, GlDropdownItem } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import axios from 'axios'; +import MockAdapter from 'axios-mock-adapter'; +import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; +import waitForPromises from 'helpers/wait_for_promises'; +import httpStatus from '~/lib/utils/http_status'; +import CustomNotificationsModal from '~/notifications/components/custom_notifications_modal.vue'; +import NotificationsDropdown from '~/notifications/components/notifications_dropdown.vue'; +import NotificationsDropdownItem from '~/notifications/components/notifications_dropdown_item.vue'; + +const mockDropdownItems = ['global', 'watch', 'participating', 'mention', 'disabled']; +const mockToastShow = jest.fn(); + +describe('NotificationsDropdown', () => { + let wrapper; + let mockAxios; + let glModalDirective; + + function createComponent(injectedProperties = {}) { + glModalDirective = jest.fn(); + + return shallowMount(NotificationsDropdown, { + stubs: { + GlButtonGroup, + GlDropdown, + GlDropdownItem, + NotificationsDropdownItem, + CustomNotificationsModal, + }, + directives: { + GlTooltip: createMockDirective(), + glModal: { + bind(_, { value }) { + glModalDirective(value); + }, + }, + }, + provide: { + dropdownItems: mockDropdownItems, + initialNotificationLevel: 'global', + ...injectedProperties, + }, + mocks: { + $toast: { + show: mockToastShow, + }, + }, + }); + } + + const findButtonGroup = () => wrapper.find(GlButtonGroup); + const findButton = () => wrapper.find(GlButton); + const findDropdown = () => wrapper.find(GlDropdown); + const findByTestId = (testId) => wrapper.find(`[data-testid="${testId}"]`); + const findAllNotificationsDropdownItems = () => wrapper.findAll(NotificationsDropdownItem); + const findDropdownItemAt = (index) => + findAllNotificationsDropdownItems().at(index).find(GlDropdownItem); + + const clickDropdownItemAt = async (index) => { + const dropdownItem = findDropdownItemAt(index); + dropdownItem.vm.$emit('click'); + + await waitForPromises(); + }; + + beforeEach(() => { + gon.api_version = 'v4'; + mockAxios = new MockAdapter(axios); + }); + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + mockAxios.restore(); + }); + + describe('template', () => { + describe('when notification level is "custom"', () => { + beforeEach(() => { + wrapper = createComponent({ + initialNotificationLevel: 'custom', + }); + }); + + it('renders a button group', () => { + expect(findButtonGroup().exists()).toBe(true); + }); + + it('shows the button text when showLabel is true', () => { + wrapper = createComponent({ + initialNotificationLevel: 'custom', + showLabel: true, + }); + + expect(findButton().text()).toBe('Custom'); + }); + + it("doesn't show the button text when showLabel is false", () => { + wrapper = createComponent({ + initialNotificationLevel: 'custom', + showLabel: false, + }); + + expect(findButton().text()).toBe(''); + }); + + it('opens the modal when the user clicks the button', async () => { + jest.spyOn(axios, 'put'); + mockAxios.onPut('/api/v4/notification_settings').reply(httpStatus.OK, {}); + + wrapper = createComponent({ + initialNotificationLevel: 'custom', + }); + + findButton().vm.$emit('click'); + + expect(glModalDirective).toHaveBeenCalled(); + }); + }); + + describe('when notification level is not "custom"', () => { + beforeEach(() => { + wrapper = createComponent({ + initialNotificationLevel: 'global', + }); + }); + + it('does not render a button group', () => { + expect(findButtonGroup().exists()).toBe(false); + }); + + it('shows the button text when showLabel is true', () => { + wrapper = createComponent({ + showLabel: true, + }); + + expect(findDropdown().props('text')).toBe('Global'); + }); + + it("doesn't show the button text when showLabel is false", () => { + wrapper = createComponent({ + showLabel: false, + }); + + expect(findDropdown().props('text')).toBe(null); + }); + }); + + describe('button tooltip', () => { + const tooltipTitlePrefix = 'Notification setting'; + it.each` + level | title + ${'global'} | ${'Global'} + ${'watch'} | ${'Watch'} + ${'participating'} | ${'Participate'} + ${'mention'} | ${'On mention'} + ${'disabled'} | ${'Disabled'} + ${'custom'} | ${'Custom'} + `(`renders "${tooltipTitlePrefix} - $title" for "$level" level`, ({ level, title }) => { + wrapper = createComponent({ + initialNotificationLevel: level, + }); + + const tooltipElement = findByTestId('notificationButton'); + const tooltip = getBinding(tooltipElement.element, 'gl-tooltip'); + + expect(tooltip.value.title).toBe(`${tooltipTitlePrefix} - ${title}`); + }); + }); + + describe('button icon', () => { + beforeEach(() => { + wrapper = createComponent({ + initialNotificationLevel: 'disabled', + }); + }); + + it('renders the "notifications-off" icon when notification level is "disabled"', () => { + expect(findDropdown().props('icon')).toBe('notifications-off'); + }); + + it('renders the "notifications" icon when notification level is not "disabled"', () => { + wrapper = createComponent(); + + expect(findDropdown().props('icon')).toBe('notifications'); + }); + }); + + describe('dropdown items', () => { + it.each` + dropdownIndex | level | title | description + ${0} | ${'global'} | ${'Global'} | ${'Use your global notification setting'} + ${1} | ${'watch'} | ${'Watch'} | ${'You will receive notifications for any activity'} + ${2} | ${'participating'} | ${'Participate'} | ${'You will only receive notifications for threads you have participated in'} + ${3} | ${'mention'} | ${'On mention'} | ${'You will receive notifications only for comments in which you were @mentioned'} + ${4} | ${'disabled'} | ${'Disabled'} | ${'You will not get any notifications via email'} + ${5} | ${'custom'} | ${'Custom'} | ${'You will only receive notifications for the events you choose'} + `('displays "$title" and "$description"', ({ dropdownIndex, title, description }) => { + wrapper = createComponent(); + + expect(findAllNotificationsDropdownItems().at(dropdownIndex).props('title')).toBe(title); + expect(findAllNotificationsDropdownItems().at(dropdownIndex).props('description')).toBe( + description, + ); + }); + }); + }); + + describe('when selecting an item', () => { + beforeEach(() => { + jest.spyOn(axios, 'put'); + }); + + it.each` + projectId | groupId | endpointUrl | endpointType | condition + ${1} | ${null} | ${'/api/v4/projects/1/notification_settings'} | ${'project notifications'} | ${'a projectId is given'} + ${null} | ${1} | ${'/api/v4/groups/1/notification_settings'} | ${'group notifications'} | ${'a groupId is given'} + ${null} | ${null} | ${'/api/v4/notification_settings'} | ${'global notifications'} | ${'when neither projectId nor groupId are given'} + `( + 'calls the $endpointType endpoint when $condition', + async ({ projectId, groupId, endpointUrl }) => { + wrapper = createComponent({ + projectId, + groupId, + }); + + await clickDropdownItemAt(1); + + expect(axios.put).toHaveBeenCalledWith(endpointUrl, { + level: 'watch', + }); + }, + ); + + it('updates the selectedNotificationLevel and marks the item with a checkmark', async () => { + mockAxios.onPut('/api/v4/notification_settings').reply(httpStatus.OK, {}); + wrapper = createComponent(); + + const dropdownItem = findDropdownItemAt(1); + + await clickDropdownItemAt(1); + + expect(wrapper.vm.selectedNotificationLevel).toBe('watch'); + expect(dropdownItem.props('isChecked')).toBe(true); + }); + + it("won't update the selectedNotificationLevel and shows a toast message when the request fails and ", async () => { + mockAxios.onPut('/api/v4/notification_settings').reply(httpStatus.NOT_FOUND, {}); + wrapper = createComponent(); + + await clickDropdownItemAt(1); + + expect(wrapper.vm.selectedNotificationLevel).toBe('global'); + expect( + mockToastShow, + ).toHaveBeenCalledWith( + 'An error occured while updating the notification settings. Please try again.', + { type: 'error' }, + ); + }); + + it('opens the modal when the user clicks on the "Custom" dropdown item', async () => { + mockAxios.onPut('/api/v4/notification_settings').reply(httpStatus.OK, {}); + wrapper = createComponent(); + + const mockModalShow = jest.spyOn(wrapper.vm.$refs.customNotificationsModal, 'open'); + + await clickDropdownItemAt(5); + + expect(mockModalShow).toHaveBeenCalled(); + }); + }); +}); |