From 643e971e16331076e19aa3085c36d91a7b5f0669 Mon Sep 17 00:00:00 2001 From: Jordan Yoshihara Date: Mon, 6 Jul 2020 18:41:53 -0700 Subject: [PATCH] Added admin page tests --- .../components/ClipboardChip.vue | 7 +- .../components/ConfirmationDialog.vue | 8 +- .../__tests__/clipboardChip.spec.js | 23 +++ .../__tests__/confirmationDialog.spec.js | 27 +++ .../pages/Channels/ChannelActionsDropdown.vue | 32 ++-- .../pages/Channels/ChannelDetails.vue | 149 +++++++++++++++++ .../pages/Channels/ChannelItem.vue | 6 +- .../pages/Channels/ChannelTable.vue | 14 +- .../__tests__/channelActionsDropdown.spec.js | 105 ++++++++++++ .../Channels/__tests__/channelDetails.spec.js | 73 ++++++++ .../Channels/__tests__/channelItem.spec.js | 75 +++++++++ .../Channels/__tests__/channelTable.spec.js | 111 +++++++++++++ .../pages/Users/EmailUsersDialog.vue | 10 +- .../pages/Users/UserActionsDropdown.vue | 14 +- .../pages/Users/UserDetails.vue | 12 +- .../pages/Users/UserPrivilegeModal.vue | 15 +- .../pages/Users/UserStorage.vue | 51 ++++-- .../administration/pages/Users/UserTable.vue | 3 +- .../Users/__tests__/emailUsersDialog.spec.js | 100 +++++++++++ .../__tests__/userActionsDropdown.spec.js | 90 ++++++++++ .../pages/Users/__tests__/userDetails.spec.js | 93 +++++++++++ .../pages/Users/__tests__/userItem.spec.js | 50 ++++++ .../__tests__/userPrivilegeModal.spec.js | 64 +++++++ .../pages/Users/__tests__/userStorage.spec.js | 64 +++++++ .../pages/Users/__tests__/userTable.spec.js | 99 +++++++++++ .../frontend/administration/router.js | 4 +- .../channelAdmin/__tests__/module.spec.js | 105 ++++++++++++ .../vuex/channelAdmin/actions.js | 2 +- .../vuex/userAdmin/__tests__/module.spec.js | 157 ++++++++++++++++++ .../administration/vuex/userAdmin/actions.js | 2 +- .../views/channel/ChannelDetailsModal.vue | 98 ++++------- 31 files changed, 1537 insertions(+), 126 deletions(-) create mode 100644 contentcuration/contentcuration/frontend/administration/components/__tests__/clipboardChip.spec.js create mode 100644 contentcuration/contentcuration/frontend/administration/components/__tests__/confirmationDialog.spec.js create mode 100644 contentcuration/contentcuration/frontend/administration/pages/Channels/ChannelDetails.vue create mode 100644 contentcuration/contentcuration/frontend/administration/pages/Channels/__tests__/channelActionsDropdown.spec.js create mode 100644 contentcuration/contentcuration/frontend/administration/pages/Channels/__tests__/channelDetails.spec.js create mode 100644 contentcuration/contentcuration/frontend/administration/pages/Channels/__tests__/channelItem.spec.js create mode 100644 contentcuration/contentcuration/frontend/administration/pages/Channels/__tests__/channelTable.spec.js create mode 100644 contentcuration/contentcuration/frontend/administration/pages/Users/__tests__/emailUsersDialog.spec.js create mode 100644 contentcuration/contentcuration/frontend/administration/pages/Users/__tests__/userActionsDropdown.spec.js create mode 100644 contentcuration/contentcuration/frontend/administration/pages/Users/__tests__/userDetails.spec.js create mode 100644 contentcuration/contentcuration/frontend/administration/pages/Users/__tests__/userItem.spec.js create mode 100644 contentcuration/contentcuration/frontend/administration/pages/Users/__tests__/userPrivilegeModal.spec.js create mode 100644 contentcuration/contentcuration/frontend/administration/pages/Users/__tests__/userStorage.spec.js create mode 100644 contentcuration/contentcuration/frontend/administration/pages/Users/__tests__/userTable.spec.js create mode 100644 contentcuration/contentcuration/frontend/administration/vuex/channelAdmin/__tests__/module.spec.js create mode 100644 contentcuration/contentcuration/frontend/administration/vuex/userAdmin/__tests__/module.spec.js diff --git a/contentcuration/contentcuration/frontend/administration/components/ClipboardChip.vue b/contentcuration/contentcuration/frontend/administration/components/ClipboardChip.vue index 5ae9817e53..74f86ea157 100644 --- a/contentcuration/contentcuration/frontend/administration/components/ClipboardChip.vue +++ b/contentcuration/contentcuration/frontend/administration/components/ClipboardChip.vue @@ -6,7 +6,7 @@ {{ value }} - + content_copy @@ -21,7 +21,10 @@ export default { name: 'ClipboardChip', props: { - value: String, + value: { + type: String, + required: true, + }, successMessage: { default: 'Value copied to clipboard', type: String, diff --git a/contentcuration/contentcuration/frontend/administration/components/ConfirmationDialog.vue b/contentcuration/contentcuration/frontend/administration/components/ConfirmationDialog.vue index 7dbcfc40ee..a696dac605 100644 --- a/contentcuration/contentcuration/frontend/administration/components/ConfirmationDialog.vue +++ b/contentcuration/contentcuration/frontend/administration/components/ConfirmationDialog.vue @@ -6,10 +6,10 @@ :text="text" > @@ -42,10 +42,6 @@ type: String, required: true, }, - confirmHandler: { - type: Function, - required: true, - }, cancelButtonText: { type: String, default: 'Cancel', diff --git a/contentcuration/contentcuration/frontend/administration/components/__tests__/clipboardChip.spec.js b/contentcuration/contentcuration/frontend/administration/components/__tests__/clipboardChip.spec.js new file mode 100644 index 0000000000..6aae663d9c --- /dev/null +++ b/contentcuration/contentcuration/frontend/administration/components/__tests__/clipboardChip.spec.js @@ -0,0 +1,23 @@ +import { mount } from '@vue/test-utils'; +import ClipboardChip from '../ClipboardChip.vue'; + +function makeWrapper() { + return mount(ClipboardChip, { + propsData: { + value: 'testtoken', + }, + }); +} + +describe('clipboardChip', () => { + let wrapper; + beforeEach(() => { + wrapper = makeWrapper(); + }); + it('should fire a copy operation on button click', () => { + const copyToClipboard = jest.fn(); + wrapper.setMethods({ copyToClipboard }); + wrapper.find('[data-test="copy"]').trigger('click'); + expect(copyToClipboard).toHaveBeenCalled(); + }); +}); diff --git a/contentcuration/contentcuration/frontend/administration/components/__tests__/confirmationDialog.spec.js b/contentcuration/contentcuration/frontend/administration/components/__tests__/confirmationDialog.spec.js new file mode 100644 index 0000000000..1fa1616c03 --- /dev/null +++ b/contentcuration/contentcuration/frontend/administration/components/__tests__/confirmationDialog.spec.js @@ -0,0 +1,27 @@ +import { mount } from '@vue/test-utils'; +import ConfirmationDialog from '../ConfirmationDialog.vue'; + +function makeWrapper() { + return mount(ConfirmationDialog, { + propsData: { + title: 'title', + text: 'text', + confirmButtonText: 'confirm', + }, + }); +} + +describe('confirmationDialog', () => { + let wrapper; + beforeEach(() => { + wrapper = makeWrapper(); + }); + it('should emit input with false value on close', () => { + wrapper.find('[data-test="close"]').trigger('click'); + expect(wrapper.emitted('input')[0][0]).toBe(false); + }); + it('should emit confirm event when confirm button is clicked', () => { + wrapper.find('[data-test="confirm"]').trigger('click'); + expect(wrapper.emitted('confirm')).toHaveLength(1); + }); +}); diff --git a/contentcuration/contentcuration/frontend/administration/pages/Channels/ChannelActionsDropdown.vue b/contentcuration/contentcuration/frontend/administration/pages/Channels/ChannelActionsDropdown.vue index 695bcf20d0..df8ba90d1f 100644 --- a/contentcuration/contentcuration/frontend/administration/pages/Channels/ChannelActionsDropdown.vue +++ b/contentcuration/contentcuration/frontend/administration/pages/Channels/ChannelActionsDropdown.vue @@ -5,34 +5,38 @@ v-model="restoreDialog" title="Restore channel" :text="`Are you sure you want to restore ${name} and make it active again?`" + data-test="confirm-restore" confirmButtonText="Restore" - :confirmHandler="restoreHandler" + @confirm="restoreHandler" /> @@ -179,7 +187,7 @@ }, }, watch: { - '$route.query': { + $route: { deep: true, handler(newRoute, oldRoute) { if (newRoute.name === oldRoute.name && newRoute.name === RouterNames.CHANNELS) diff --git a/contentcuration/contentcuration/frontend/administration/pages/Channels/__tests__/channelActionsDropdown.spec.js b/contentcuration/contentcuration/frontend/administration/pages/Channels/__tests__/channelActionsDropdown.spec.js new file mode 100644 index 0000000000..57462444e6 --- /dev/null +++ b/contentcuration/contentcuration/frontend/administration/pages/Channels/__tests__/channelActionsDropdown.spec.js @@ -0,0 +1,105 @@ +import { mount } from '@vue/test-utils'; +import router from '../../../router'; +import store from '../../../store'; +import ChannelActionsDropdown from '../ChannelActionsDropdown'; + +const channelId = '11111111111111111111111111111111'; +const updateChannel = jest.fn().mockReturnValue(Promise.resolve()); +const channel = { + id: channelId, + name: 'Channel Test', + created: new Date(), + modified: new Date(), + public: true, + published: true, + primary_token: 'testytesty', + deleted: false, + demo_server_url: 'demo.com', + source_url: 'source.com', +}; + +function makeWrapper(channelProps = {}) { + return mount(ChannelActionsDropdown, { + router, + store, + propsData: { channelId }, + computed: { + channel() { + return { + ...channel, + ...channelProps, + }; + }, + }, + methods: { updateChannel }, + }); +} + +describe('channelActionsDropdown', () => { + let wrapper; + beforeEach(() => { + updateChannel.mockClear(); + }); + + describe('deleted channel actions', () => { + beforeEach(() => { + wrapper = makeWrapper({ deleted: true }); + }); + it('restore channel should open restore confirmation', () => { + wrapper.find('[data-test="restore"]').trigger('click'); + expect(wrapper.vm.restoreDialog).toBe(true); + }); + it('confirm restore channel should call updateChannel with deleted = false', () => { + wrapper.find('[data-test="confirm-restore"]').vm.$emit('confirm'); + expect(updateChannel).toHaveBeenCalledWith({ id: channelId, deleted: false }); + }); + it('delete channel should open delete confirmation', () => { + wrapper.find('[data-test="delete"]').trigger('click'); + expect(wrapper.vm.deleteDialog).toBe(true); + }); + it('confirm delete channel should call deleteChannel', () => { + const deleteChannel = jest.fn().mockReturnValue(Promise.resolve()); + wrapper.setMethods({ deleteChannel }); + wrapper.find('[data-test="confirm-delete"]').vm.$emit('confirm'); + expect(deleteChannel).toHaveBeenCalledWith(channelId); + }); + }); + describe('live channel actions', () => { + beforeEach(() => { + wrapper = makeWrapper({ public: false }); + }); + it('download PDF button should call downloadPDF', () => { + const downloadPDF = jest.fn(); + wrapper.setMethods({ downloadPDF }); + wrapper.find('[data-test="pdf"]').trigger('click'); + expect(downloadPDF).toHaveBeenCalled(); + }); + it('download CSV button should call downloadCSV', () => { + const downloadCSV = jest.fn(); + wrapper.setMethods({ downloadCSV }); + wrapper.find('[data-test="csv"]').trigger('click'); + expect(downloadCSV).toHaveBeenCalled(); + }); + it('make public button should open make public confirmation', () => { + wrapper.find('[data-test="public"]').trigger('click'); + expect(wrapper.vm.makePublicDialog).toBe(true); + }); + it('confirm make public should call updateChannel with isPublic = true', () => { + wrapper.find('[data-test="confirm-public"]').vm.$emit('confirm'); + expect(updateChannel).toHaveBeenCalledWith({ id: channelId, isPublic: true }); + }); + }); + describe('public channel actions', () => { + beforeEach(() => { + wrapper = makeWrapper(); + }); + it('make private button should open make private confirmation', () => { + wrapper.find('[data-test="private"]').trigger('click'); + expect(wrapper.vm.makePrivateDialog).toBe(true); + }); + it('confirm make private should call updateChannel with isPublic = false', () => { + wrapper.find('[data-test="confirm-private"]').vm.$emit('confirm'); + expect(updateChannel).toHaveBeenCalledWith({ id: channelId, isPublic: false }); + }); + }); +}); diff --git a/contentcuration/contentcuration/frontend/administration/pages/Channels/__tests__/channelDetails.spec.js b/contentcuration/contentcuration/frontend/administration/pages/Channels/__tests__/channelDetails.spec.js new file mode 100644 index 0000000000..cde7a58b83 --- /dev/null +++ b/contentcuration/contentcuration/frontend/administration/pages/Channels/__tests__/channelDetails.spec.js @@ -0,0 +1,73 @@ +import { mount } from '@vue/test-utils'; +import router from '../../../router'; +import store from '../../../store'; +import { RouterNames } from '../../../constants'; +import ChannelDetails from './../ChannelDetails'; + +const channelId = '11111111111111111111111111111111'; + +function makeWrapper() { + router.replace({ name: RouterNames.CHANNEL, params: { channelId } }); + return mount(ChannelDetails, { + router, + store, + propsData: { + channelId, + }, + computed: { + channel() { + return { + name: 'test', + }; + }, + }, + stubs: { + ChannelActionsDropdown: true, + ChannelSharing: true, + }, + }); +} + +describe('channelDetails', () => { + let wrapper; + beforeEach(() => { + wrapper = makeWrapper(); + }); + it('clicking close should close the modal', () => { + wrapper.vm.dialog = false; + expect(wrapper.vm.$route.name).toBe(RouterNames.CHANNELS); + }); + describe('load', () => { + it('should automatically close if loadChannel does not find a channel', () => { + wrapper.setMethods({ + loadChannel: jest.fn().mockReturnValue(Promise.resolve()), + loadChannelDetails: jest.fn().mockReturnValue(Promise.resolve()), + }); + return wrapper.vm.load().then(() => { + expect(wrapper.vm.$route.name).toBe(RouterNames.CHANNELS); + }); + }); + it('load should call loadChannel and loadChannelDetails', () => { + const loadChannel = jest.fn().mockReturnValue(Promise.resolve({ id: channelId })); + const loadChannelDetails = jest.fn().mockReturnValue(Promise.resolve()); + wrapper.setMethods({ loadChannel, loadChannelDetails }); + return wrapper.vm.load().then(() => { + expect(loadChannel).toHaveBeenCalled(); + expect(loadChannelDetails).toHaveBeenCalled(); + }); + }); + }); + it('clicking info tab should navigate to info tab', () => { + wrapper.vm.tab = 'share'; + wrapper.find('[data-test="info-tab"] a').trigger('click'); + wrapper.vm.$nextTick(() => { + expect(wrapper.vm.tab).toBe('info'); + }); + }); + it('clicking share tab should navigate to share tab', () => { + wrapper.find('[data-test="share-tab"] a').trigger('click'); + wrapper.vm.$nextTick(() => { + expect(wrapper.vm.tab).toBe('share'); + }); + }); +}); diff --git a/contentcuration/contentcuration/frontend/administration/pages/Channels/__tests__/channelItem.spec.js b/contentcuration/contentcuration/frontend/administration/pages/Channels/__tests__/channelItem.spec.js new file mode 100644 index 0000000000..44536a3ff3 --- /dev/null +++ b/contentcuration/contentcuration/frontend/administration/pages/Channels/__tests__/channelItem.spec.js @@ -0,0 +1,75 @@ +import { mount } from '@vue/test-utils'; +import router from '../../../router'; +import store from '../../../store'; +import { RouterNames } from '../../../constants'; +import ChannelItem from '../ChannelItem'; + +const channelId = '11111111111111111111111111111111'; +const channel = { + id: channelId, + name: 'Channel Test', + created: new Date(), + modified: new Date(), + public: true, + published: true, + primary_token: 'testytesty', + deleted: false, + demo_server_url: 'demo.com', + source_url: 'source.com', +}; + +function makeWrapper() { + router.replace({ name: RouterNames.CHANNELS }); + return mount(ChannelItem, { + router, + store, + propsData: { + channelId, + value: [], + }, + computed: { + channel() { + return channel; + }, + }, + stubs: { + ChannelActionsDropdown: true, + }, + }); +} + +describe('channelItem', () => { + let wrapper; + beforeEach(() => { + wrapper = makeWrapper(); + }); + it('selecting the channel should emit list with channel id', () => { + wrapper.vm.selected = true; + expect(wrapper.emitted('input')[0][0]).toEqual([channelId]); + }); + it('deselecting the channel should emit list without channel id', () => { + wrapper.setProps({ value: [channelId] }); + wrapper.vm.selected = false; + expect(wrapper.emitted('input')[0][0]).toEqual([]); + }); + it('saveDemoServerUrl should call updateChannel with new demo_server_url', () => { + const updateChannel = jest.fn().mockReturnValue(Promise.resolve()); + wrapper.setMethods({ updateChannel }); + return wrapper.vm.saveDemoServerUrl().then(() => { + expect(updateChannel).toHaveBeenCalledWith({ + id: channelId, + demo_server_url: channel.demo_server_url, + }); + }); + }); + it('saveSourceUrl should call updateChannel with new source_url', () => { + const updateChannel = jest.fn().mockReturnValue(Promise.resolve()); + wrapper.setMethods({ updateChannel }); + return wrapper.vm.saveSourceUrl().then(() => { + expect(updateChannel).toHaveBeenCalledWith({ + id: channelId, + source_url: channel.source_url, + }); + }); + }); +}); diff --git a/contentcuration/contentcuration/frontend/administration/pages/Channels/__tests__/channelTable.spec.js b/contentcuration/contentcuration/frontend/administration/pages/Channels/__tests__/channelTable.spec.js new file mode 100644 index 0000000000..0459c816d2 --- /dev/null +++ b/contentcuration/contentcuration/frontend/administration/pages/Channels/__tests__/channelTable.spec.js @@ -0,0 +1,111 @@ +import { mount } from '@vue/test-utils'; +import router from '../../../router'; +import store from '../../../store'; +import { RouterNames } from '../../../constants'; +import ChannelTable from '../ChannelTable'; + +const loadChannels = jest.fn().mockReturnValue(Promise.resolve()); +const channelList = ['test', 'channel', 'table']; +function makeWrapper() { + router.replace({ name: RouterNames.CHANNELS }); + return mount(ChannelTable, { + router, + store, + sync: false, + computed: { + count() { + return 10; + }, + channels() { + return channelList; + }, + }, + methods: { + loadChannels, + }, + stubs: { + ChannelItem: true, + }, + }); +} + +describe('channelTable', () => { + let wrapper; + beforeEach(() => { + wrapper = makeWrapper(); + }); + describe('filters', () => { + it('changing filter should set query params', () => { + wrapper.vm.filter = 'public'; + expect(router.currentRoute.query.public).toBe(true); + }); + it('changing language should set query params', () => { + wrapper.vm.language = 'en'; + expect(router.currentRoute.query.languages).toBe('en'); + }); + it('changing search text should set query params', () => { + wrapper.vm.keywords = 'keyword test'; + expect(router.currentRoute.query.keywords).toBe('keyword test'); + }); + }); + describe('selection', () => { + it('selectAll should set selected to channel list', () => { + wrapper.vm.selectAll = true; + expect(wrapper.vm.selected).toEqual(channelList); + }); + it('removing selectAll should set selected to empty list', () => { + wrapper.vm.selected = channelList; + wrapper.vm.selectAll = false; + wrapper.vm.$nextTick(() => { + expect(wrapper.vm.selected).toEqual([]); + }); + }); + it('selectedCount should match the selected length', () => { + wrapper.vm.selected = ['test']; + expect(wrapper.vm.selectedCount).toBe(1); + }); + it('selected should clear on query changes', () => { + wrapper.vm.selected = ['test']; + router.push({ + ...wrapper.vm.$route, + query: { + param: 'test', + }, + }); + wrapper.vm.$nextTick(() => { + expect(wrapper.vm.selected).toEqual([]); + }); + }); + }); + describe('bulk actions', () => { + it('should be hidden if no items are selected', () => { + expect(wrapper.find('[data-test="csv"]').exists()).toBe(false); + expect(wrapper.find('[data-test="pdf"]').exists()).toBe(false); + }); + it('should be visible if items are selected', () => { + wrapper.vm.selected = channelList; + wrapper.vm.$nextTick(() => { + expect(wrapper.find('[data-test="csv"]').exists()).toBe(true); + expect(wrapper.find('[data-test="pdf"]').exists()).toBe(true); + }); + }); + it('download PDF should call downloadPDF', () => { + const downloadPDF = jest.fn(); + wrapper.setMethods({ downloadPDF }); + wrapper.vm.selected = channelList; + wrapper.vm.$nextTick(() => { + wrapper.find('[data-test="pdf"] .v-btn').trigger('click'); + expect(downloadPDF).toHaveBeenCalled(); + }); + }); + it('download CSV should call downloadCSV', () => { + const downloadCSV = jest.fn(); + wrapper.setMethods({ downloadCSV }); + wrapper.vm.selected = channelList; + wrapper.vm.$nextTick(() => { + wrapper.find('[data-test="csv"] .v-btn').trigger('click'); + expect(downloadCSV).toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/contentcuration/contentcuration/frontend/administration/pages/Users/EmailUsersDialog.vue b/contentcuration/contentcuration/frontend/administration/pages/Users/EmailUsersDialog.vue index 52aec89e04..9dbee338c5 100644 --- a/contentcuration/contentcuration/frontend/administration/pages/Users/EmailUsersDialog.vue +++ b/contentcuration/contentcuration/frontend/administration/pages/Users/EmailUsersDialog.vue @@ -29,6 +29,7 @@ @@ -81,10 +82,10 @@ - + Cancel - + Send email @@ -95,7 +96,8 @@ text="Draft will be lost upon exiting this editor. Are you sure you want to continue?" confirmButtonText="Discard draft" cancelButtonText="Keep open" - :confirmHandler="close" + data-test="confirm" + @confirm="close" /> @@ -219,7 +221,7 @@ } }, addPlaceholder(placeholder) { - this.message += placeholder; + this.message += ` ${placeholder}`; }, remove(id) { this.selected = this.selected.filter(u => u !== id); diff --git a/contentcuration/contentcuration/frontend/administration/pages/Users/UserActionsDropdown.vue b/contentcuration/contentcuration/frontend/administration/pages/Users/UserActionsDropdown.vue index c8a8290dab..fa2d1cb7bf 100644 --- a/contentcuration/contentcuration/frontend/administration/pages/Users/UserActionsDropdown.vue +++ b/contentcuration/contentcuration/frontend/administration/pages/Users/UserActionsDropdown.vue @@ -6,7 +6,8 @@ title="Delete user" :text="`Are you sure you want to permanently delete ${user.name}'s account?`" confirmButtonText="Delete" - :confirmHandler="deleteHandler" + data-test="confirm-delete" + @confirm="deleteHandler" /> - + Email - + - +

{{ user.name }}

@@ -30,6 +35,7 @@ @@ -243,7 +249,7 @@ methods: { ...mapActions('userAdmin', ['loadUser', 'loadUserDetails']), load() { - const userPromise = this.user ? Promise.resolve(this.user) : this.loadUser(this.userId); + const userPromise = this.loadUser(this.userId); this.loading = true; return Promise.all([userPromise, this.loadUserDetails(this.userId)]) .then(([user, details]) => { diff --git a/contentcuration/contentcuration/frontend/administration/pages/Users/UserPrivilegeModal.vue b/contentcuration/contentcuration/frontend/administration/pages/Users/UserPrivilegeModal.vue index 0270f5c2ba..ccf32679b4 100644 --- a/contentcuration/contentcuration/frontend/administration/pages/Users/UserPrivilegeModal.vue +++ b/contentcuration/contentcuration/frontend/administration/pages/Users/UserPrivilegeModal.vue @@ -5,7 +5,7 @@ header="Remove admin privileges" :text="`Are you sure you want to remove admin privileges from user '${user.name}'?`" > - +

Enter your email address to continue

-