diff --git a/src/__mocks__/router.js b/src/__mocks__/router.js index f62163e43a8..d2b86effaa8 100644 --- a/src/__mocks__/router.js +++ b/src/__mocks__/router.js @@ -2,51 +2,9 @@ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */ -import VueRouter from 'vue-router' +import { createTalkRouter } from '../router/router.ts' -const Stub = { - name: 'Stub', - template: '
', -} +const router = createTalkRouter() +router.addRoute({ path: '/', name: 'none', redirect: '/apps/spreed', component: { template: '
' } }) -export default new VueRouter({ - linkActiveClass: 'active', - routes: [ - { - path: '/apps/spreed', - name: 'root', - component: Stub, - props: true, - }, - { - path: '/apps/spreed/not-found', - name: 'notfound', - component: Stub, - props: true, - }, - { - path: '/apps/spreed/forbidden', - name: 'forbidden', - component: Stub, - props: true, - }, - { - path: '/apps/spreed/duplicate-session', - name: 'duplicatesession', - component: Stub, - props: true, - }, - { - path: '/call/:token', - name: 'conversation', - component: Stub, - props: true, - }, - { - path: '/call/:token/recording', - name: 'recording', - component: Stub, - props: true, - }, - ], -}) +export default router diff --git a/src/components/AvatarWrapper/AvatarWrapper.spec.js b/src/components/AvatarWrapper/AvatarWrapper.spec.js index 1a552625941..74b7ce0c448 100644 --- a/src/components/AvatarWrapper/AvatarWrapper.spec.js +++ b/src/components/AvatarWrapper/AvatarWrapper.spec.js @@ -83,7 +83,7 @@ describe('AvatarWrapper.vue', () => { expect(avatar.props('displayName')).toBe(USER_NAME) expect(avatar.props('hideStatus')).toBe(false) expect(avatar.props('verboseStatus')).toBe(true) - expect(avatar.props('preloadedUserStatus')).toBe(PRELOADED_USER_STATUS) + expect(avatar.props('preloadedUserStatus')).toStrictEqual(PRELOADED_USER_STATUS) expect(avatar.props('size')).toBe(AVATAR.SIZE.DEFAULT) }) }) diff --git a/src/components/CallView/shared/VideoBottomBar.spec.js b/src/components/CallView/shared/VideoBottomBar.spec.js index 5049f73e497..3584b4bf659 100644 --- a/src/components/CallView/shared/VideoBottomBar.spec.js +++ b/src/components/CallView/shared/VideoBottomBar.spec.js @@ -5,16 +5,16 @@ import { emit } from '@nextcloud/event-bus' import { t } from '@nextcloud/l10n' -import { shallowMount } from '@vue/test-utils' +import { mount } from '@vue/test-utils' import { cloneDeep } from 'lodash' import { createPinia, setActivePinia } from 'pinia' import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' import { createStore } from 'vuex' import NcButton from '@nextcloud/vue/components/NcButton' import IconAlertCircleOutline from 'vue-material-design-icons/AlertCircleOutline.vue' -import IconHandBackLeftOutline from 'vue-material-design-icons/HandBackLeftOutline.vue' +import IconHandBackLeft from 'vue-material-design-icons/HandBackLeft.vue' +import IconVideo from 'vue-material-design-icons/Video.vue' import IconVideoOffOutline from 'vue-material-design-icons/VideoOffOutline.vue' -import IconVideoOutline from 'vue-material-design-icons/VideoOutline.vue' import VideoBottomBar from './VideoBottomBar.vue' import { CONVERSATION, PARTICIPANT } from '../../../constants.ts' import storeConfig from '../../../store/storeConfig.js' @@ -93,82 +93,60 @@ describe('VideoBottomBar.vue', () => { vi.clearAllMocks() }) + /** + * Shared function to mount component + */ + function mountVideoBottomBar(props) { + return mount(VideoBottomBar, { + global: { + plugins: [store], + }, + props, + }) + } + describe('unit tests', () => { describe('render component', () => { test('component renders properly', async () => { - const wrapper = shallowMount(VideoBottomBar, { - global: { - plugins: [store], - }, - props: componentProps, - }) + const wrapper = mountVideoBottomBar(componentProps) expect(wrapper.exists()).toBeTruthy() expect(wrapper.classes('wrapper')).toBeDefined() }) test('component has class "wrapper--big" for main view', async () => { componentProps.isBig = true - const wrapper = shallowMount(VideoBottomBar, { - global: { - plugins: [store], - }, - props: componentProps, - }) + const wrapper = mountVideoBottomBar(componentProps) expect(wrapper.exists()).toBeTruthy() expect(wrapper.classes('wrapper--big')).toBeDefined() }) test('component renders all indicators by default', async () => { - const wrapper = shallowMount(VideoBottomBar, { - global: { - plugins: [store], - }, - props: componentProps, - }) - + const wrapper = mountVideoBottomBar(componentProps) const indicators = wrapper.findAllComponents(NcButton) expect(indicators).toHaveLength(3) }) test('component does not render indicators for Screen.vue component', async () => { componentProps.isScreen = true - const wrapper = shallowMount(VideoBottomBar, { - global: { - plugins: [store], - }, - props: componentProps, - }) - + const wrapper = mountVideoBottomBar(componentProps) const indicators = wrapper.findAllComponents(NcButton) expect(indicators).toHaveLength(0) }) test('component does not show indicators after video overlay is off', async () => { - const wrapper = shallowMount(VideoBottomBar, { - global: { - plugins: [store], - }, - props: componentProps, - }) - + const wrapper = mountVideoBottomBar(componentProps) componentProps.showVideoOverlay = false await wrapper.setProps(cloneDeep(componentProps)) const indicators = wrapper.findAllComponents(NcButton) - indicators.wrappers.forEach((indicator) => { + indicators.forEach((indicator) => { expect(indicator.isVisible()).toBeFalsy() }) }) test('component does not render anything when used in sidebar', async () => { componentProps.isSidebar = true - const wrapper = shallowMount(VideoBottomBar, { - global: { - plugins: [store], - }, - props: componentProps, - }) - + const wrapper = mountVideoBottomBar(componentProps) const participantName = wrapper.find('.participant-name') expect(participantName.exists()).toBeFalsy() @@ -179,13 +157,7 @@ describe('VideoBottomBar.vue', () => { describe('render participant name', () => { test('name is shown by default', () => { - const wrapper = shallowMount(VideoBottomBar, { - global: { - plugins: [store], - }, - props: componentProps, - }) - + const wrapper = mountVideoBottomBar(componentProps) const participantName = wrapper.find('.participant-name') expect(participantName.isVisible()).toBeTruthy() expect(participantName.text()).toBe(PARTICIPANT_NAME) @@ -193,13 +165,7 @@ describe('VideoBottomBar.vue', () => { test('name is not shown if all checks are falsy', () => { componentProps.showVideoOverlay = false - const wrapper = shallowMount(VideoBottomBar, { - global: { - plugins: [store], - }, - props: componentProps, - }) - + const wrapper = mountVideoBottomBar(componentProps) const participantName = wrapper.find('.participant-name') expect(participantName.isVisible()).toBeFalsy() }) @@ -209,21 +175,16 @@ describe('VideoBottomBar.vue', () => { describe('connection failed indicator', () => { test('indicator is not shown by default, other indicators are visible', () => { componentProps.model.attributes.raisedHand.state = true - const wrapper = shallowMount(VideoBottomBar, { - global: { - plugins: [store], - }, - props: componentProps, - }) + const wrapper = mountVideoBottomBar(componentProps) const iceFailedIndicator = wrapper.findComponent(IconAlertCircleOutline) expect(iceFailedIndicator.exists()).toBeFalsy() - const raiseHandIndicator = wrapper.findComponent(IconHandBackLeftOutline) + const raiseHandIndicator = wrapper.findComponent(IconHandBackLeft) expect(raiseHandIndicator.exists()).toBeTruthy() const indicators = wrapper.findAllComponents(NcButton) - indicators.wrappers.forEach((indicator) => { + indicators.forEach((indicator) => { expect(indicator.isVisible()).toBeTruthy() }) }) @@ -231,21 +192,16 @@ describe('VideoBottomBar.vue', () => { test('indicator is shown when model prop is true, other indicators are hidden', () => { componentProps.model.attributes.raisedHand.state = true componentProps.model.attributes.connectionState = ConnectionState.FAILED_NO_RESTART - const wrapper = shallowMount(VideoBottomBar, { - global: { - plugins: [store], - }, - props: componentProps, - }) + const wrapper = mountVideoBottomBar(componentProps) const iceFailedIndicator = wrapper.findComponent(IconAlertCircleOutline) expect(iceFailedIndicator.exists()).toBeTruthy() - const raiseHandIndicator = wrapper.findComponent(IconHandBackLeftOutline) + const raiseHandIndicator = wrapper.findComponent(IconHandBackLeft) expect(raiseHandIndicator.exists()).toBeFalsy() const indicators = wrapper.findAllComponents(NcButton) - indicators.wrappers.forEach((indicator) => { + indicators.forEach((indicator) => { expect(indicator.isVisible()).toBeFalsy() }) }) @@ -253,27 +209,17 @@ describe('VideoBottomBar.vue', () => { describe('raise hand indicator', () => { test('indicator is not shown by default', () => { - const wrapper = shallowMount(VideoBottomBar, { - global: { - plugins: [store], - }, - props: componentProps, - }) + const wrapper = mountVideoBottomBar(componentProps) - const raiseHandIndicator = wrapper.findComponent(IconHandBackLeftOutline) + const raiseHandIndicator = wrapper.findComponent(IconHandBackLeft) expect(raiseHandIndicator.exists()).toBeFalsy() }) test('indicator is shown when model prop is true', () => { componentProps.model.attributes.raisedHand.state = true - const wrapper = shallowMount(VideoBottomBar, { - global: { - plugins: [store], - }, - props: componentProps, - }) + const wrapper = mountVideoBottomBar(componentProps) - const raiseHandIndicator = wrapper.findComponent(IconHandBackLeftOutline) + const raiseHandIndicator = wrapper.findComponent(IconHandBackLeft) expect(raiseHandIndicator.exists()).toBeTruthy() }) }) @@ -282,37 +228,20 @@ describe('VideoBottomBar.vue', () => { describe('render buttons', () => { describe('audio indicator', () => { test('button is rendered properly', () => { - const wrapper = shallowMount(VideoBottomBar, { - global: { - plugins: [store], - }, - props: componentProps, - }) + const wrapper = mountVideoBottomBar(componentProps) const audioIndicator = findNcButton(wrapper, audioIndicatorAriaLabels) expect(audioIndicator.exists()).toBeTruthy() }) test('button is visible for moderators when audio is available', () => { - const wrapper = shallowMount(VideoBottomBar, { - global: { - plugins: [store], - }, - props: componentProps, - }) - + const wrapper = mountVideoBottomBar(componentProps) const audioIndicator = findNcButton(wrapper, audioIndicatorAriaLabels) expect(audioIndicator.isVisible()).toBeTruthy() }) test('button is not rendered for non-moderators when audio is available', () => { conversationProps.participantType = PARTICIPANT.TYPE.USER - const wrapper = shallowMount(VideoBottomBar, { - global: { - plugins: [store], - }, - props: componentProps, - }) - + const wrapper = mountVideoBottomBar(componentProps) const audioIndicator = findNcButton(wrapper, audioIndicatorAriaLabels) expect(audioIndicator.exists()).toBeFalsy() }) @@ -320,52 +249,26 @@ describe('VideoBottomBar.vue', () => { test('button is visible for everyone when audio is unavailable', () => { conversationProps.participantType = PARTICIPANT.TYPE.USER componentProps.model.attributes.audioAvailable = false - const wrapper = shallowMount(VideoBottomBar, { - global: { - plugins: [store], - }, - props: componentProps, - }) - + const wrapper = mountVideoBottomBar(componentProps) const audioIndicator = findNcButton(wrapper, audioIndicatorAriaLabels) expect(audioIndicator.isVisible()).toBeTruthy() }) test('button is enabled for moderators', () => { - const wrapper = shallowMount(VideoBottomBar, { - global: { - plugins: [store], - }, - props: componentProps, - }) - + const wrapper = mountVideoBottomBar(componentProps) const audioIndicator = findNcButton(wrapper, audioIndicatorAriaLabels) - expect(audioIndicator.attributes('disabled')).toBeFalsy() + expect(audioIndicator.attributes('disabled')).toBeUndefined() }) test('button is disabled when audio is unavailable', () => { componentProps.model.attributes.audioAvailable = false - const wrapper = shallowMount(VideoBottomBar, { - global: { - plugins: [store], - }, - props: componentProps, - }) + const wrapper = mountVideoBottomBar(componentProps) const audioIndicator = findNcButton(wrapper, audioIndicatorAriaLabels) - expect(audioIndicator.attributes('disabled')).toBeTruthy() + expect(audioIndicator.attributes('disabled')).toBeDefined() }) test('method is called after click', async () => { - const wrapper = shallowMount(VideoBottomBar, { - global: { - plugins: [store], - stubs: { - NcButton, - }, - }, - props: componentProps, - }) - + const wrapper = mountVideoBottomBar(componentProps) const audioIndicator = findNcButton(wrapper, audioIndicatorAriaLabels) await audioIndicator.trigger('click') @@ -375,86 +278,39 @@ describe('VideoBottomBar.vue', () => { describe('video indicator', () => { test('button is rendered properly', () => { - const wrapper = shallowMount(VideoBottomBar, { - global: { - plugins: [store], - }, - props: componentProps, - }) + const wrapper = mountVideoBottomBar(componentProps) const videoIndicator = findNcButton(wrapper, videoIndicatorAriaLabels) expect(videoIndicator.exists()).toBeTruthy() }) test('button is visible when video is available', () => { - const wrapper = shallowMount(VideoBottomBar, { - global: { - plugins: [store], - }, - props: componentProps, - }) - + const wrapper = mountVideoBottomBar(componentProps) const videoIndicator = findNcButton(wrapper, videoIndicatorAriaLabels) expect(videoIndicator.isVisible()).toBeTruthy() }) test('button is not rendered when video is unavailable', () => { componentProps.model.attributes.videoAvailable = false - const wrapper = shallowMount(VideoBottomBar, { - global: { - plugins: [store], - }, - props: componentProps, - }) - + const wrapper = mountVideoBottomBar(componentProps) const videoIndicator = findNcButton(wrapper, videoIndicatorAriaLabels) expect(videoIndicator.exists()).toBeFalsy() }) test('button shows proper icon if video is enabled', () => { - const wrapper = shallowMount(VideoBottomBar, { - global: { - plugins: [store], - stubs: { - NcButton, - }, - }, - - props: componentProps, - }) - - const videoOnIcon = wrapper.findComponent(IconVideoOutline) + const wrapper = mountVideoBottomBar(componentProps) + const videoOnIcon = wrapper.findComponent(IconVideo) expect(videoOnIcon.exists()).toBeTruthy() }) test('button shows proper icon if video is blocked', () => { componentProps.sharedData.remoteVideoBlocker.isVideoEnabled.mockReturnValue(false) - const wrapper = shallowMount(VideoBottomBar, { - global: { - plugins: [store], - stubs: { - NcButton, - }, - }, - - props: componentProps, - }) - + const wrapper = mountVideoBottomBar(componentProps) const videoOffIcon = wrapper.findComponent(IconVideoOffOutline) expect(videoOffIcon.exists()).toBeTruthy() }) test('method is called after click', async () => { - const wrapper = shallowMount(VideoBottomBar, { - global: { - plugins: [store], - stubs: { - NcButton, - }, - }, - - props: componentProps, - }) - + const wrapper = mountVideoBottomBar(componentProps) const videoIndicator = findNcButton(wrapper, videoIndicatorAriaLabels) await videoIndicator.trigger('click') @@ -465,54 +321,26 @@ describe('VideoBottomBar.vue', () => { describe('screen sharing indicator', () => { test('button is rendered properly', () => { - const wrapper = shallowMount(VideoBottomBar, { - global: { - plugins: [store], - }, - props: componentProps, - }) - + const wrapper = mountVideoBottomBar(componentProps) const screenSharingIndicator = findNcButton(wrapper, screenSharingAriaLabel) expect(screenSharingIndicator.exists()).toBeTruthy() }) test('button is visible when screen is available', () => { - const wrapper = shallowMount(VideoBottomBar, { - global: { - plugins: [store], - }, - props: componentProps, - }) - + const wrapper = mountVideoBottomBar(componentProps) const screenSharingIndicator = findNcButton(wrapper, screenSharingAriaLabel) expect(screenSharingIndicator.isVisible()).toBeTruthy() }) test('button is not rendered when screen is unavailable', () => { componentProps.model.attributes.screen = false - const wrapper = shallowMount(VideoBottomBar, { - global: { - plugins: [store], - }, - props: componentProps, - }) - + const wrapper = mountVideoBottomBar(componentProps) const screenSharingIndicator = findNcButton(wrapper, screenSharingAriaLabel) expect(screenSharingIndicator.exists()).toBeFalsy() }) test('component emits peer id after click', async () => { - const wrapper = shallowMount(VideoBottomBar, { - global: { - plugins: [store], - stubs: { - NcButton, - }, - }, - - props: componentProps, - }) - + const wrapper = mountVideoBottomBar(componentProps) const screenSharingIndicator = findNcButton(wrapper, screenSharingAriaLabel) await screenSharingIndicator.trigger('click') @@ -522,24 +350,14 @@ describe('VideoBottomBar.vue', () => { describe('following button', () => { test('button is not rendered for participants by default', () => { - const wrapper = shallowMount(VideoBottomBar, { - global: { - plugins: [store], - }, - props: componentProps, - }) + const wrapper = mountVideoBottomBar(componentProps) const followingButton = findNcButton(wrapper, followingButtonAriaLabel) expect(followingButton.exists()).toBeFalsy() }) test('button is not rendered for main speaker by default', () => { componentProps.isBig = true - const wrapper = shallowMount(VideoBottomBar, { - global: { - plugins: [store], - }, - props: componentProps, - }) + const wrapper = mountVideoBottomBar(componentProps) const followingButton = findNcButton(wrapper, followingButtonAriaLabel) expect(followingButton.exists()).toBeFalsy() }) @@ -547,13 +365,7 @@ describe('VideoBottomBar.vue', () => { test('button is rendered when source is selected', () => { callViewStore.setSelectedVideoPeerId(PEER_ID) componentProps.isBig = true - const wrapper = shallowMount(VideoBottomBar, { - global: { - plugins: [store], - }, - props: componentProps, - }) - + const wrapper = mountVideoBottomBar(componentProps) const followingButton = findNcButton(wrapper, followingButtonAriaLabel) expect(followingButton.exists()).toBeTruthy() }) @@ -565,16 +377,7 @@ describe('VideoBottomBar.vue', () => { expect(callViewStore.presentationStarted).toBeTruthy() componentProps.isBig = true - const wrapper = shallowMount(VideoBottomBar, { - global: { - plugins: [store], - stubs: { - NcButton, - }, - }, - - props: componentProps, - }) + const wrapper = mountVideoBottomBar(componentProps) const followingButton = findNcButton(wrapper, followingButtonAriaLabel) await followingButton.trigger('click') diff --git a/src/components/CallView/shared/VideoVue.spec.js b/src/components/CallView/shared/VideoVue.spec.js index 79f8c7c1197..809354a929b 100644 --- a/src/components/CallView/shared/VideoVue.spec.js +++ b/src/components/CallView/shared/VideoVue.spec.js @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -import { shallowMount } from '@vue/test-utils' +import { mount } from '@vue/test-utils' import { cloneDeep } from 'lodash' import { createPinia, setActivePinia } from 'pinia' import { beforeEach, describe, expect, test, vi } from 'vitest' @@ -97,7 +97,7 @@ describe('VideoVue.vue', () => { * async). */ function setupWrapper() { - wrapper = shallowMount(VideoVue, { + wrapper = mount(VideoVue, { global: { plugins: [store] }, props: { model: callParticipantModel, diff --git a/src/components/LeftSidebar/ConversationsList/Conversation.spec.js b/src/components/LeftSidebar/ConversationsList/Conversation.spec.js index 121768f24ca..6b234c99208 100644 --- a/src/components/LeftSidebar/ConversationsList/Conversation.spec.js +++ b/src/components/LeftSidebar/ConversationsList/Conversation.spec.js @@ -4,26 +4,29 @@ */ import { showError, showSuccess } from '@nextcloud/dialogs' -import { flushPromises, mount, shallowMount } from '@vue/test-utils' +import { flushPromises, mount } from '@vue/test-utils' import { cloneDeep } from 'lodash' import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' import { createStore } from 'vuex' -import NcActionButton from '@nextcloud/vue/components/NcActionButton' import NcButton from '@nextcloud/vue/components/NcButton' +import NcDialog from '@nextcloud/vue/components/NcDialog' import NcListItem from '@nextcloud/vue/components/NcListItem' +import IconFileOutline from 'vue-material-design-icons/FileOutline.vue' +import ConversationIcon from '../../ConversationIcon.vue' import Conversation from './Conversation.vue' import router from '../../../__mocks__/router.js' import { ATTENDEE, CONVERSATION, PARTICIPANT } from '../../../constants.ts' import { leaveConversation } from '../../../services/participantsService.js' import storeConfig from '../../../store/storeConfig.js' -import { findNcButton } from '../../../test-helpers.js' +import { findNcActionButton, findNcButton } from '../../../test-helpers.js' vi.mock('../../../services/participantsService', () => ({ leaveConversation: vi.fn(), })) -// TODO fix after RouterLinkStub can support slots https://github.com/vuejs/vue-test-utils/issues/1803 -const RouterLinkStub = true +const ComponentStub = { + template: '
', +} describe('Conversation.vue', () => { const TOKEN = 'XXTOKENXX' @@ -32,6 +35,26 @@ describe('Conversation.vue', () => { let item let messagesMock + /** + * Shared function to mount component + */ + function mountConversation(isSearchResult = false) { + return mount(Conversation, { + global: { + plugins: [router, store], + stubs: { + NcModal: ComponentStub, + NcPopover: ComponentStub, + }, + }, + + props: { + isSearchResult, + item, + }, + }) + } + beforeEach(() => { testStoreConfig = cloneDeep(storeConfig) messagesMock = vi.fn().mockReturnValue({}) @@ -73,24 +96,13 @@ describe('Conversation.vue', () => { }) test('renders conversation entry', () => { - const wrapper = mount(Conversation, { - global: { - plugins: [store], - stubs: { - RouterLink: RouterLinkStub, - }, - }, - props: { - isSearchResult: false, - item, - }, - }) + const wrapper = mountConversation(false) - const el = wrapper.findComponent({ name: 'NcListItem' }) + const el = wrapper.findComponent(NcListItem) expect(el.exists()).toBe(true) expect(el.props('name')).toBe('conversation one') - const icon = el.findComponent({ name: 'ConversationIcon' }) + const icon = el.findComponent(ConversationIcon) expect(icon.props('item')).toStrictEqual(item) expect(icon.props('hideFavorite')).toStrictEqual(false) expect(icon.props('hideCall')).toStrictEqual(false) @@ -102,106 +114,93 @@ describe('Conversation.vue', () => { * @param {string} expectedText Expected subname of the conversation item * @param {boolean} isSearchResult Whether or not the item is a search result (has no … menu) */ - function testConversationLabel(item, expectedText, isSearchResult = false) { - const wrapper = shallowMount(Conversation, { - global: { - plugins: [store], - }, - props: { - isSearchResult, - item, - }, - }) + async function testConversationLabel(item, expectedText, isSearchResult = false) { + const wrapper = mountConversation(isSearchResult) + await flushPromises() const el = wrapper.find('.conversation__subname') + expect(el.exists()).toBeTruthy() expect(el.text()).toMatch(expectedText) return wrapper } - test('display joining conversation message when not joined yet', () => { + test('display joining conversation message when not joined yet', async () => { item.actorId = null - testConversationLabel(item, 'Joining conversation …') + await testConversationLabel(item, 'Joining conversation …') }) - test('displays nothing when there is no last chat message', () => { + test('displays nothing when there is no last chat message', async () => { delete item.lastMessage - testConversationLabel(item, 'No messages') + await testConversationLabel(item, 'No messages') }) describe('author name', () => { - test('displays last chat message with shortened author name', () => { - testConversationLabel(item, /^Alice:\s+hello$/) + // items are padded from each other visually + test('displays last chat message with shortened author name', async () => { + await testConversationLabel(item, 'Alice:hello') }) - test('displays last chat message with author name if no space in name', () => { + test('displays last chat message with author name if no space in name', async () => { item.lastMessage.actorDisplayName = 'Bob' - testConversationLabel(item, /^Bob:\s+hello$/) + await testConversationLabel(item, 'Bob:hello') }) - test('displays own last chat message with "You" as author', () => { + test('displays own last chat message with "You" as author', async () => { item.lastMessage.actorId = 'actor-id-1' - testConversationLabel(item, /^You:\s+hello$/) + await testConversationLabel(item, 'You:hello') }) - test('displays last system message without author', () => { + test('displays last system message without author', async () => { item.lastMessage.message = 'Alice has joined the call' item.lastMessage.systemMessage = 'call_joined' - testConversationLabel(item, 'Alice has joined the call') + await testConversationLabel(item, 'Alice has joined the call') }) - test('displays last message without author in one to one conversations', () => { + test('displays last message without author in one to one conversations', async () => { item.type = CONVERSATION.TYPE.ONE_TO_ONE - testConversationLabel(item, 'hello') + await testConversationLabel(item, 'hello') }) - test('displays own last message with "You" author in one to one conversations', () => { + test('displays own last message with "You" author in one to one conversations', async () => { item.type = CONVERSATION.TYPE.ONE_TO_ONE item.lastMessage.actorId = 'actor-id-1' - testConversationLabel(item, /^You:\s+hello$/) + await testConversationLabel(item, 'You:hello') }) - test('displays last guest message with default author when none set', () => { + test('displays last guest message with default author when none set', async () => { item.type = CONVERSATION.TYPE.PUBLIC item.lastMessage.actorDisplayName = '' item.lastMessage.actorType = ATTENDEE.ACTOR_TYPE.GUESTS - testConversationLabel(item, /^Guest:\s+hello$/) + await testConversationLabel(item, 'Guest:hello') }) - test('displays description for search results', () => { + test('displays description for search results', async () => { // search results have no actor id item.actorId = null item.description = 'This is a description' - testConversationLabel(item, 'This is a description', true) + await testConversationLabel(item, 'This is a description', true) }) }) - test('replaces placeholders in rich object of last message', () => { + test('replaces placeholders in rich object of last message', async () => { item.lastMessage.message = '{file}' item.lastMessage.messageParameters = { file: { name: 'filename.jpg', }, } - const wrapper = testConversationLabel(item, /^Alice:\s+filename.jpg$/) - expect(wrapper.findComponent({ name: 'FileIcon' }).exists()).toBeTruthy() + const wrapper = await testConversationLabel(item, 'Alice:filename.jpg') + expect(wrapper.findComponent(IconFileOutline).exists()).toBeTruthy() }) test('hides subname for sensitive conversations', () => { item.isSensitive = true - const wrapper = shallowMount(Conversation, { - global: { - plugins: [store], - }, - props: { - isSearchResult: false, - item, - }, - }) + const wrapper = mountConversation(false) const el = wrapper.find('.conversation__subname') expect(el.exists()).toBe(false) @@ -216,20 +215,9 @@ describe('Conversation.vue', () => { * @param {boolean} expectedHighlighted Whether or not the unread counter is highlighted with primary color */ function testCounter(item, expectedCounterText, expectedOutlined, expectedHighlighted) { - const wrapper = mount(Conversation, { - global: { - plugins: [store], - stubs: { - RouterLink: RouterLinkStub, - }, - }, - props: { - isSearchResult: false, - item, - }, - }) + const wrapper = mountConversation(false) - const el = wrapper.findComponent({ name: 'NcListItem' }) + const el = wrapper.findComponent(NcListItem) expect(el.exists()).toBe(true) expect(el.props('counterNumber')).toBe(expectedCounterText) @@ -268,95 +256,38 @@ describe('Conversation.vue', () => { }) test('does not render counter when no unread messages', () => { - const wrapper = mount(Conversation, { - global: { - plugins: [store], - stubs: { - RouterLink: RouterLinkStub, - }, - }, - props: { - isSearchResult: false, - item, - }, - }) + const wrapper = mountConversation(false) - const el = wrapper.findComponent({ name: 'NcListItem' }) + const el = wrapper.findComponent(NcListItem) expect(el.exists()).toBe(true) expect(el.vm.$slots.counter).not.toBeDefined() }) }) - describe('actions (real router)', () => { + describe('actions and routing', () => { test('change route on click event', async () => { - const wrapper = mount(Conversation, { - global: { - plugins: [router, store], - stubs: { - NcListItem, - }, - }, - props: { - isSearchResult: false, - item, - }, - }) + await router.isReady() + const wrapper = mountConversation(false) - const el = wrapper.findComponent({ name: 'NcListItem' }) + const el = wrapper.findComponent(NcListItem) expect(el.exists()).toBe(true) await el.find('a').trigger('click') + await flushPromises() expect(wrapper.vm.$route.name).toBe('conversation') expect(wrapper.vm.$route.params).toStrictEqual({ token: TOKEN }) }) - }) - - describe('actions (mock router)', () => { - let $router - - beforeEach(() => { - $router = { push: vi.fn() } - }) - - /** - * @param {object} wrapper Parent element to search the text in - * @param {string} text Text to find within the wrapper - */ - function findNcActionButton(wrapper, text) { - const actionButtons = wrapper.findAllComponents(NcActionButton) - const items = actionButtons.filter((actionButton) => { - return actionButton.text() === text - }) - if (!items.exists()) { - return items - } - return items.at(0) - } /** * @param {string} actionName The name of the action to shallow */ function shallowMountAndGetAction(actionName) { store = createStore(testStoreConfig) - const wrapper = shallowMount(Conversation, { - global: { - plugins: [store], - stubs: { - NcActionButton, - }, - mocks: { - $router, - }, - }, - props: { - isSearchResult: false, - item, - }, - }) + const wrapper = mountConversation(false) - const el = wrapper.findComponent({ name: 'NcListItem' }) + const el = wrapper.findComponent(NcListItem) expect(el.exists()).toBe(true) return findNcActionButton(el, actionName) @@ -367,23 +298,8 @@ describe('Conversation.vue', () => { * @param {number} buttonsAmount The amount of buttons to be shown in dialog */ async function shallowMountAndOpenDialog(actionName, buttonsAmount) { - const wrapper = shallowMount(Conversation, { - global: { - plugins: [store], - stubs: { - NcActionButton, - NcButton, - }, - mocks: { - $router, - }, - }, - props: { - isSearchResult: false, - item, - }, - }) - const el = wrapper.findComponent({ name: 'NcListItem' }) + const wrapper = mountConversation(false) + const el = wrapper.findComponent(NcListItem) const action = findNcActionButton(el, actionName) expect(action.exists()).toBeTruthy() @@ -392,10 +308,9 @@ describe('Conversation.vue', () => { await action.find('button').trigger('click') // Assert 1 - const dialog = wrapper.findComponent({ name: 'NcDialog' }) + const dialog = wrapper.findComponent(NcDialog) expect(dialog.exists).toBeTruthy() - const buttons = dialog.findAllComponents({ name: 'NcButton' }) - expect(buttons.exists()).toBeTruthy() + const buttons = dialog.findAllComponents(NcButton) expect(buttons).toHaveLength(buttonsAmount) return dialog @@ -418,7 +333,7 @@ describe('Conversation.vue', () => { // Act: click on the 'confirm' button await findNcButton(dialog, 'Yes').find('button').trigger('click') - + await flushPromises() // Assert expect(actionHandler).toHaveBeenCalledWith(expect.anything(), { token: TOKEN }) }) @@ -474,6 +389,7 @@ describe('Conversation.vue', () => { let actionHandler beforeEach(() => { + vi.spyOn(router, 'push') actionHandler = vi.fn().mockResolvedValueOnce() testStoreConfig.modules.conversationsStore.actions.deleteConversationFromServer = actionHandler store = createStore(testStoreConfig) @@ -485,10 +401,11 @@ describe('Conversation.vue', () => { // Act: click on the 'confirm' button await findNcButton(dialog, 'Yes').find('button').trigger('click') + await flushPromises() // Assert expect(actionHandler).toHaveBeenCalledWith(expect.anything(), { token: TOKEN }) - expect($router.push).not.toHaveBeenCalled() + expect(router.push).not.toHaveBeenCalled() }) test('does not delete conversation when not confirmed', async () => { @@ -497,10 +414,11 @@ describe('Conversation.vue', () => { // Act: click on the 'decline' button await findNcButton(dialog, 'No').find('button').trigger('click') + await flushPromises() // Assert expect(actionHandler).not.toHaveBeenCalled() - expect($router.push).not.toHaveBeenCalled() + expect(router.push).not.toHaveBeenCalled() }) test('hides "delete conversation" action when not allowed', async () => { @@ -514,19 +432,7 @@ describe('Conversation.vue', () => { test('copies link conversation', async () => { store = createStore(testStoreConfig) const copyTextMock = vi.fn().mockResolvedValueOnce() - const wrapper = shallowMount(Conversation, { - global: { - plugins: [store], - stubs: { - NcActionButton, - }, - }, - - props: { - isSearchResult: false, - item, - }, - }) + const wrapper = mountConversation(false) Object.assign(navigator, { clipboard: { @@ -534,7 +440,7 @@ describe('Conversation.vue', () => { }, }) - const el = wrapper.findComponent({ name: 'NcListItem' }) + const el = wrapper.findComponent(NcListItem) expect(el.exists()).toBe(true) const action = findNcActionButton(el, 'Copy link') @@ -552,21 +458,9 @@ describe('Conversation.vue', () => { testStoreConfig.modules.conversationsStore.actions.toggleFavorite = toggleFavoriteAction store = createStore(testStoreConfig) - const wrapper = shallowMount(Conversation, { - global: { - plugins: [store], - stubs: { - NcActionButton, - }, - }, + const wrapper = mountConversation(false) - props: { - isSearchResult: false, - item, - }, - }) - - const el = wrapper.findComponent({ name: 'NcListItem' }) + const el = wrapper.findComponent(NcListItem) expect(el.exists()).toBe(true) const action = findNcActionButton(el, 'Add to favorites') @@ -586,21 +480,9 @@ describe('Conversation.vue', () => { item.isFavorite = true store = createStore(testStoreConfig) - const wrapper = shallowMount(Conversation, { - global: { - plugins: [store], - stubs: { - NcActionButton, - }, - }, + const wrapper = mountConversation(false) - props: { - isSearchResult: false, - item, - }, - }) - - const el = wrapper.findComponent({ name: 'NcListItem' }) + const el = wrapper.findComponent(NcListItem) expect(el.exists()).toBe(true) const action = findNcActionButton(el, 'Remove from favorites') @@ -637,26 +519,11 @@ describe('Conversation.vue', () => { }) test('does not show all actions for search result (open conversations)', () => { store = createStore(testStoreConfig) - const wrapper = shallowMount(Conversation, { - global: { - plugins: [store], - stubs: { - NcActionButton, - }, - }, + const wrapper = mountConversation(true) - props: { - isSearchResult: true, - item, - }, - }) - - const el = wrapper.findComponent({ name: 'NcListItem' }) + const el = wrapper.findComponent(NcListItem) expect(el.exists()).toBe(true) - const actionButtons = wrapper.findAllComponents(NcActionButton) - expect(actionButtons.exists()).toBe(true) - // Join conversation and Copy link actions are intended expect(findNcActionButton(el, 'Join conversation').exists()).toBe(true) expect(findNcActionButton(el, 'Copy link').exists()).toBe(true) diff --git a/src/components/LeftSidebar/LeftSidebar.spec.js b/src/components/LeftSidebar/LeftSidebar.spec.js index 4287b0cbe05..5846f048b0a 100644 --- a/src/components/LeftSidebar/LeftSidebar.spec.js +++ b/src/components/LeftSidebar/LeftSidebar.spec.js @@ -12,6 +12,7 @@ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' import { createStore } from 'vuex' import LeftSidebar from './LeftSidebar.vue' import router from '../../__mocks__/router.js' +import { localCapabilities } from '../../services/CapabilitiesManager.ts' import { searchListedConversations } from '../../services/conversationsService.ts' import { autocompleteQuery } from '../../services/coreService.ts' import { EventBus } from '../../services/EventBus.ts' @@ -27,22 +28,6 @@ vi.mock('../../services/coreService', () => ({ autocompleteQuery: vi.fn(), })) -// Test actions with 'can-create' config -let mockCanCreateConversations = true -vi.mock('../../services/CapabilitiesManager', async () => { - const CapabilitiesManager = await vi.importActual('../../services/CapabilitiesManager') - return { - ...CapabilitiesManager, - getTalkConfig: vi.fn((...args) => { - if (args[0] === 'local' && args[1] === 'conversations' && args[2] === 'can-create') { - return mockCanCreateConversations - } else { - return CapabilitiesManager.getTalkConfig(...args) - } - }), - } -}) - // short-circuit debounce vi.mock('debounce', () => ({ default: vi.fn().mockImplementation((fn) => fn), @@ -59,6 +44,9 @@ describe('LeftSidebar.vue', () => { const SEARCH_TERM = 'search' + const ComponentStub = { + template: '
', + } const RecycleScrollerStub = { props: { items: Array, @@ -69,7 +57,13 @@ describe('LeftSidebar.vue', () => { `, } - const mountComponent = () => { + const HAS_APP_NAVIGATION_KEY = Symbol.for('NcContent:setHasAppNavigation') + const NC_ACTIONS_CLOSE_MENU = Symbol.for('NcActions:closeMenu') + + /** + * Shared function to mount component + */ + function mountComponent() { return mount(LeftSidebar, { global: { plugins: [router, store], @@ -77,13 +71,14 @@ describe('LeftSidebar.vue', () => { // to prevent user status fetching NcAvatar: true, // to prevent complex dialog logic - NcActions: true, - NcModal: true, + NcActions: ComponentStub, + NcModal: ComponentStub, RecycleScroller: RecycleScrollerStub, }, - }, - provide: { - 'NcContent:setHasAppNavigation': () => {}, + provide: { + [HAS_APP_NAVIGATION_KEY]: () => {}, + [NC_ACTIONS_CLOSE_MENU]: () => {}, + }, }, }) } @@ -122,7 +117,7 @@ describe('LeftSidebar.vue', () => { }) afterEach(() => { - mockCanCreateConversations = true + localCapabilities.spreed.config.conversations['can-create'] = true vi.clearAllMocks() }) @@ -184,9 +179,7 @@ describe('LeftSidebar.vue', () => { expect(conversationListItems.at(0).text()).toStrictEqual(normalConversationsList[0].displayName) expect(conversationListItems.at(1).text()).toStrictEqual(normalConversationsList[1].displayName) - expect(conversationsReceivedEvent).toHaveBeenCalledWith({ - singleConversation: false, - }) + expect(conversationsReceivedEvent).toHaveBeenCalled() }) test('re-fetches conversations every 30 seconds', async () => { @@ -422,7 +415,6 @@ describe('LeftSidebar.vue', () => { ) const itemsListNames = prepareExpectedResults(usersResults, groupsResults, circlesResults, listedResults, 'Other sources') const itemsList = wrapper.findAll('.vue-recycle-scroller-STUB-item') - expect(itemsList.exists()).toBeTruthy() expect(itemsList).toHaveLength(itemsListNames.length) itemsListNames.forEach((name, index) => { expect(itemsList.at(index).text()).toStrictEqual(name) @@ -430,7 +422,7 @@ describe('LeftSidebar.vue', () => { }) test('only shows user search results when cannot create conversations', async () => { - mockCanCreateConversations = false + localCapabilities.spreed.config.conversations['can-create'] = false const wrapper = await testSearch( SEARCH_TERM, @@ -443,7 +435,6 @@ describe('LeftSidebar.vue', () => { const itemsListNames = prepareExpectedResults(usersResults, groupsResults, circlesResults, listedResults, 'Groups and teams', true, false) const itemsList = wrapper.findAll('.vue-recycle-scroller-STUB-item') - expect(itemsList.exists()).toBeTruthy() expect(itemsList).toHaveLength(itemsListNames.length) expect(itemsListNames.filter((item) => ['Groups', 'Teams', 'Federated users', SEARCH_TERM].includes(item)).length).toBe(0) itemsListNames.forEach((name, index) => { @@ -463,7 +454,6 @@ describe('LeftSidebar.vue', () => { const itemsListNames = prepareExpectedResults(usersResults, groupsResults, circlesResults, listedResults, 'Other sources', false, true) const itemsList = wrapper.findAll('.vue-recycle-scroller-STUB-item') - expect(itemsList.exists()).toBeTruthy() expect(itemsList).toHaveLength(itemsListNames.length) expect(itemsListNames.filter((item) => ['Teams'].includes(item)).length).toBe(0) itemsListNames.forEach((name, index) => { @@ -484,7 +474,6 @@ describe('LeftSidebar.vue', () => { const wrapper = await testSearch(searchTerm, possibleResults, listedResults, loadStateSettingsOverride) const captionsEls = wrapper.findAll('.caption') - expect(captionsEls.exists()).toBeTruthy() if (listedResults.length > 0) { expect(captionsEls.length).toBeGreaterThan(2) expect(captionsEls.at(0).text()).toBe('Conversations') @@ -601,7 +590,7 @@ describe('LeftSidebar.vue', () => { expect(newConversationbutton.exists()).toBeTruthy() }) test('does not show new conversation button if user cannot start conversations', () => { - mockCanCreateConversations = false + localCapabilities.spreed.config.conversations['can-create'] = false const wrapper = mountComponent() const newConversationbutton = findNcActionButton(wrapper, 'Create a new conversation') diff --git a/src/components/MessagesList/MessagesGroup/Message/Message.spec.js b/src/components/MessagesList/MessagesGroup/Message/Message.spec.js index c49968be284..f45abc79f2b 100644 --- a/src/components/MessagesList/MessagesGroup/Message/Message.spec.js +++ b/src/components/MessagesList/MessagesGroup/Message/Message.spec.js @@ -3,15 +3,18 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -import { flushPromises, shallowMount } from '@vue/test-utils' +import { flushPromises, mount } from '@vue/test-utils' import { cloneDeep } from 'lodash' import { createPinia, setActivePinia } from 'pinia' import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' import { createStore } from 'vuex' +import NcActions from '@nextcloud/vue/components/NcActions' import NcButton from '@nextcloud/vue/components/NcButton' +import NcRichText from '@nextcloud/vue/components/NcRichText' import IconCheck from 'vue-material-design-icons/Check.vue' import IconCheckAll from 'vue-material-design-icons/CheckAll.vue' import Quote from '../../../Quote.vue' +import CallButton from '../../../TopBar/CallButton.vue' import Message from './Message.vue' import MessageButtonsBar from './MessageButtonsBar/MessageButtonsBar.vue' import DeckCard from './MessagePart/DeckCard.vue' @@ -19,7 +22,7 @@ import DefaultParameter from './MessagePart/DefaultParameter.vue' import FilePreview from './MessagePart/FilePreview.vue' import Location from './MessagePart/Location.vue' import Mention from './MessagePart/Mention.vue' -import MessageBody from './MessagePart/MessageBody.vue' +import router from '../../../../__mocks__/router.js' import * as useIsInCallModule from '../../../../composables/useIsInCall.js' import { ATTENDEE, CONVERSATION, MESSAGE, PARTICIPANT } from '../../../../constants.ts' import { EventBus } from '../../../../services/EventBus.ts' @@ -27,19 +30,6 @@ import storeConfig from '../../../../store/storeConfig.js' import { useActorStore } from '../../../../stores/actor.ts' import { useTokenStore } from '../../../../stores/token.ts' -// needed because of https://github.com/vuejs/vue-test-utils/issues/1507 -const RichTextStub = { - props: { - text: { - type: String, - }, - arguments: { - type: Object, - }, - }, - template: '
', -} - describe('Message.vue', () => { const TOKEN = 'XXTOKENXX' let testStoreConfig @@ -100,44 +90,42 @@ describe('Message.vue', () => { vi.clearAllMocks() }) + /** + * Shared function to mount component + */ + function mountMessage(props) { + return mount(Message, { + global: { + plugins: [router, store], + provide: injected, + stubs: { + Location: true, + }, + }, + props, + }) + } + describe('message rendering', () => { beforeEach(() => { store = createStore(testStoreConfig) }) test('renders rich text message', async () => { - const wrapper = shallowMount(Message, { - global: { - plugins: [store], - stubs: { - MessageBody, - }, - }, - props: messageProps, - provide: injected, - }) + const wrapper = mountMessage(messageProps) - const message = wrapper.findComponent({ name: 'NcRichText' }) - expect(message.attributes('text')).toBe('test message') + const message = wrapper.findComponent(NcRichText) + expect(message.text()).toBe('test message') }) test('renders emoji as single plain text', async () => { messageProps.isSingleEmoji = true messageProps.message.message = '🌧️' - const wrapper = shallowMount(Message, { - global: { - plugins: [store], - stubs: { - MessageBody, - }, - }, - props: messageProps, - provide: injected, - }) + const wrapper = mountMessage(messageProps) - const message = wrapper.findComponent({ name: 'NcRichText' }) + const message = wrapper.findComponent(NcRichText) expect(message.exists()).toBeTruthy() - expect(message.attributes('text')).toBe('🌧️') + expect(message.text()).toBe('🌧️') }) describe('call button', () => { @@ -162,21 +150,12 @@ describe('Message.vue', () => { messageProps.message.message = 'message two' conversationProps.hasCall = true - const wrapper = shallowMount(Message, { - global: { - plugins: [store], - stubs: { - MessageBody, - }, - }, - props: messageProps, - provide: injected, - }) + const wrapper = mountMessage(messageProps) - const richText = wrapper.findComponent({ name: 'NcRichText' }) - expect(richText.attributes('text')).toBe('message two') + const richText = wrapper.findComponent(NcRichText) + expect(richText.text()).toBe('message two') - const callButton = wrapper.findComponent({ name: 'CallButton' }) + const callButton = wrapper.findComponent(CallButton) expect(callButton.exists()).toBe(true) }) @@ -186,18 +165,9 @@ describe('Message.vue', () => { messageProps.message.message = 'message one' conversationProps.hasCall = true - const wrapper = shallowMount(Message, { - global: { - plugins: [store], - stubs: { - MessageBody, - }, - }, - props: messageProps, - provide: injected, - }) + const wrapper = mountMessage(messageProps) - const callButton = wrapper.findComponent({ name: 'CallButton' }) + const callButton = wrapper.findComponent(CallButton) expect(callButton.exists()).toBe(false) }) @@ -207,18 +177,9 @@ describe('Message.vue', () => { messageProps.message.message = 'message two' conversationProps.hasCall = false - const wrapper = shallowMount(Message, { - global: { - plugins: [store], - stubs: { - MessageBody, - }, - }, - props: messageProps, - provide: injected, - }) + const wrapper = mountMessage(messageProps) - const callButton = wrapper.findComponent({ name: 'CallButton' }) + const callButton = wrapper.findComponent(CallButton) expect(callButton.exists()).toBe(false) }) @@ -230,18 +191,9 @@ describe('Message.vue', () => { vi.spyOn(useIsInCallModule, 'useIsInCall').mockReturnValue(() => true) - const wrapper = shallowMount(Message, { - global: { - plugins: [store], - stubs: { - MessageBody, - }, - }, - props: messageProps, - provide: injected, - }) + const wrapper = mountMessage(messageProps) - const callButton = wrapper.findComponent({ name: 'CallButton' }) + const callButton = wrapper.findComponent(CallButton) expect(callButton.exists()).toBe(false) }) }) @@ -251,32 +203,14 @@ describe('Message.vue', () => { messageProps.message.message = 'message deleted' conversationProps.hasCall = true - const wrapper = shallowMount(Message, { - global: { - plugins: [store], - stubs: { - MessageBody, - }, - }, - props: messageProps, - provide: injected, - }) + const wrapper = mountMessage(messageProps) - const richText = wrapper.findComponent({ name: 'NcRichText' }) - expect(richText.attributes('text')).toBe('message deleted') + const richText = wrapper.findComponent(NcRichText) + expect(richText.text()).toBe('message deleted') }) test('renders date', () => { - const wrapper = shallowMount(Message, { - global: { - plugins: [store], - stubs: { - MessageBody, - }, - }, - props: messageProps, - provide: injected, - }) + const wrapper = mountMessage(messageProps) const date = wrapper.find('.date') expect(date.exists()).toBe(true) @@ -300,16 +234,7 @@ describe('Message.vue', () => { testStoreConfig.modules.messagesStore.getters.message = vi.fn(() => messageGetterMock) store = createStore(testStoreConfig) - const wrapper = shallowMount(Message, { - global: { - plugins: [store], - stubs: { - MessageBody, - }, - }, - props: messageProps, - provide: injected, - }) + const wrapper = mountMessage(messageProps) const quote = wrapper.findComponent(Quote) expect(quote.exists()).toBeTruthy() @@ -326,21 +251,11 @@ describe('Message.vue', () => { messageProps.message.message = message messageProps.message.messageParameters = messageParameters store.dispatch('processMessage', { token: TOKEN, message: messageProps.message }) - const wrapper = shallowMount(Message, { - global: { - plugins: [store], - stubs: { - MessageBody, - RichText: RichTextStub, - }, - }, - props: messageProps, - provide: injected, - }) + const wrapper = mountMessage(messageProps) - const messageEl = wrapper.findComponent({ name: 'NcRichText' }) + const messageEl = wrapper.findComponent(NcRichText) // note: indices as object keys are on purpose - expect(messageEl.props('arguments')).toMatchObject(expectedRichParameters) + expect(Object.keys(messageEl.props('arguments'))).toMatchObject(Object.keys(expectedRichParameters)) return messageEl } @@ -358,6 +273,7 @@ describe('Message.vue', () => { }, 'mention-call1': { id: 'some_call', + name: 'Some call', type: 'call', }, } @@ -389,8 +305,11 @@ describe('Message.vue', () => { type: 'user', }, file: { - path: 'some/path', + id: '123', + path: 'Talk/some-path.txt', + name: 'some-path.txt', type: 'file', + mimetype: 'txt/plain', }, } renderRichObject( @@ -418,8 +337,11 @@ describe('Message.vue', () => { type: 'user', }, file: { - path: 'some/path', + id: '123', + path: 'Talk/some-path.txt', + name: 'some-path.txt', type: 'file', + mimetype: 'txt/plain', }, } const messageEl = renderRichObject( @@ -448,6 +370,11 @@ describe('Message.vue', () => { type: 'user', }, 'deck-card': { + id: '123', + name: 'Card name', + boardname: 'Board name', + stackname: 'Stack name', + link: 'https://example.com/some/deck/card/url', metadata: '{id:123}', type: 'deck-card', }, @@ -471,6 +398,10 @@ describe('Message.vue', () => { test('renders geo locations', () => { const params = { 'geo-location': { + id: '123', + name: 'Location name', + latitude: 12.345678, + longitude: 98.765432, metadata: '{id:123}', type: 'geo-location', }, @@ -495,6 +426,8 @@ describe('Message.vue', () => { type: 'user', }, unknown: { + id: '123', + name: 'Unknown name', path: 'some/path', type: 'unknown', }, @@ -516,62 +449,11 @@ describe('Message.vue', () => { }) }) - test('displays unread message marker that marks the message seen when visible', () => { - getVisualLastReadMessageIdMock.mockReturnValue(123) - messageProps.nextMessageId = 333 - const IntersectionObserver = vi.fn() - - const wrapper = shallowMount(Message, { - global: { - plugins: [store], - stubs: { - MessageBody, - }, - }, - directives: { - IntersectionObserver, - }, - props: messageProps, - provide: injected, - }) - - const marker = wrapper.find('.message-unread-marker') - expect(marker.exists()).toBe(true) - - expect(IntersectionObserver).toHaveBeenCalled() - const directiveValue = IntersectionObserver.mock.calls[0][1] - - expect(wrapper.vm.seen).toEqual(false) - - directiveValue.value([{ isIntersecting: false }]) - expect(wrapper.vm.seen).toEqual(false) - - directiveValue.value([{ isIntersecting: true }]) - expect(wrapper.vm.seen).toEqual(true) - - // stays true if it was visible once - directiveValue.value([{ isIntersecting: false }]) - expect(wrapper.vm.seen).toEqual(true) - }) - test('does not display read marker on the very last message', () => { messageProps.lastReadMessageId = 123 messageProps.nextMessageId = null // last message - const IntersectionObserver = vi.fn() - const wrapper = shallowMount(Message, { - global: { - plugins: [store], - stubs: { - MessageBody, - }, - }, - directives: { - IntersectionObserver, - }, - props: messageProps, - provide: injected, - }) + const wrapper = mountMessage(messageProps) const marker = wrapper.find('.message-unread-marker') expect(marker.exists()).toBe(false) @@ -586,16 +468,7 @@ describe('Message.vue', () => { test('does not render actions for system messages are available', async () => { messageProps.message.systemMessage = 'this is a system message' - const wrapper = shallowMount(Message, { - global: { - plugins: [store], - stubs: { - MessageBody, - }, - }, - props: messageProps, - provide: injected, - }) + const wrapper = mountMessage(messageProps) await wrapper.find('.message').trigger('mouseover') expect(wrapper.findComponent(MessageButtonsBar).exists()).toBe(false) @@ -604,16 +477,7 @@ describe('Message.vue', () => { test('does not render actions for temporary messages', async () => { messageProps.message.timestamp = 0 - const wrapper = shallowMount(Message, { - global: { - plugins: [store], - stubs: { - MessageBody, - }, - }, - props: messageProps, - provide: injected, - }) + const wrapper = mountMessage(messageProps) await wrapper.find('.message').trigger('mouseover') expect(wrapper.findComponent(MessageButtonsBar).exists()).toBe(false) @@ -622,16 +486,7 @@ describe('Message.vue', () => { test('does not render actions for deleted messages', async () => { messageProps.message.messageType = MESSAGE.TYPE.COMMENT_DELETED - const wrapper = shallowMount(Message, { - global: { - plugins: [store], - stubs: { - MessageBody, - }, - }, - props: messageProps, - provide: injected, - }) + const wrapper = mountMessage(messageProps) await wrapper.find('.message').trigger('mouseover') expect(wrapper.findComponent(MessageButtonsBar).exists()).toBe(false) @@ -639,17 +494,7 @@ describe('Message.vue', () => { test('Buttons bar is rendered on mouse over', async () => { messageProps.message.sendingFailure = 'timeout' - const wrapper = shallowMount(Message, { - global: { - plugins: [store], - stubs: { - MessageBody, - MessageButtonsBar, - }, - }, - props: messageProps, - provide: injected, - }) + const wrapper = mountMessage(messageProps) // Initial state expect(wrapper.findComponent(MessageButtonsBar).exists()).toBe(false) @@ -659,7 +504,7 @@ describe('Message.vue', () => { expect(wrapper.findComponent(MessageButtonsBar).exists()).toBe(true) // Actions are rendered with MessageButtonsBar - expect(wrapper.findComponent({ name: 'NcActions' }).exists()).toBe(true) + expect(wrapper.findComponent(NcActions).exists()).toBe(true) // Mouseleave await wrapper.find('.message').trigger('mouseleave') @@ -677,17 +522,7 @@ describe('Message.vue', () => { // need to mock the date to be within 6h vi.useFakeTimers().setSystemTime(new Date('2020-05-07T10:00:00')) - const wrapper = shallowMount(Message, { - global: { - plugins: [store], - stubs: { - MessageBody, - MessageButtonsBar, - }, - }, - props: messageProps, - provide: injected, - }) + const wrapper = mountMessage(messageProps) // Hover the messages in order to render the MessageButtonsBar component await wrapper.find('.message').trigger('mouseover') @@ -722,16 +557,7 @@ describe('Message.vue', () => { test('lets user retry sending a timed out message', async () => { messageProps.message.sendingFailure = 'timeout' - const wrapper = shallowMount(Message, { - global: { - plugins: [store], - stubs: { - MessageBody, - }, - }, - props: messageProps, - provide: injected, - }) + const wrapper = mountMessage(messageProps) await wrapper.find('.message-body').trigger('mouseover') expect(wrapper.findComponent(MessageButtonsBar).exists()).toBe(true) @@ -754,34 +580,16 @@ describe('Message.vue', () => { test('displays the message already with a spinner while sending it', () => { messageProps.message.timestamp = 0 - const wrapper = shallowMount(Message, { - global: { - plugins: [store], - stubs: { - MessageBody, - }, - }, - props: messageProps, - provide: injected, - }) - const message = wrapper.findComponent({ name: 'NcRichText' }) - expect(message.attributes('text')).toBe('test message') + const wrapper = mountMessage(messageProps) + const message = wrapper.findComponent(NcRichText) + expect(message.text()).toBe('test message') expect(wrapper.find('.icon-loading-small').exists()).toBe(true) }) test('displays icon when message was read by everyone', () => { conversationProps.lastCommonReadMessage = 123 - const wrapper = shallowMount(Message, { - global: { - plugins: [store], - stubs: { - MessageBody, - }, - }, - props: messageProps, - provide: injected, - }) + const wrapper = mountMessage(messageProps) expect(wrapper.findComponent(IconCheck).exists()).toBe(false) expect(wrapper.findComponent(IconCheckAll).exists()).toBe(true) @@ -789,16 +597,7 @@ describe('Message.vue', () => { test('displays sent icon when own message was sent', () => { conversationProps.lastCommonReadMessage = 0 - const wrapper = shallowMount(Message, { - global: { - plugins: [store], - stubs: { - MessageBody, - }, - }, - props: messageProps, - provide: injected, - }) + const wrapper = mountMessage(messageProps) expect(wrapper.findComponent(IconCheck).exists()).toBe(true) expect(wrapper.findComponent(IconCheckAll).exists()).toBe(false) @@ -808,16 +607,7 @@ describe('Message.vue', () => { conversationProps.lastCommonReadMessage = 123 messageProps.message.actorId = 'user-id-2' messageProps.message.actorType = ATTENDEE.ACTOR_TYPE.USERS - const wrapper = shallowMount(Message, { - global: { - plugins: [store], - stubs: { - MessageBody, - }, - }, - props: messageProps, - provide: injected, - }) + const wrapper = mountMessage(messageProps) expect(wrapper.findComponent(IconCheck).exists()).toBe(false) expect(wrapper.findComponent(IconCheckAll).exists()).toBe(false) diff --git a/src/components/MessagesList/MessagesGroup/Message/MessageButtonsBar/MessageButtonsBar.spec.js b/src/components/MessagesList/MessagesGroup/Message/MessageButtonsBar/MessageButtonsBar.spec.js index 8f3515733f7..724322a05f5 100644 --- a/src/components/MessagesList/MessagesGroup/Message/MessageButtonsBar/MessageButtonsBar.spec.js +++ b/src/components/MessagesList/MessagesGroup/Message/MessageButtonsBar/MessageButtonsBar.spec.js @@ -3,15 +3,14 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -import { shallowMount } from '@vue/test-utils' +import { mount } from '@vue/test-utils' import { cloneDeep } from 'lodash' import { createPinia, setActivePinia } from 'pinia' import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' import { computed } from 'vue' import { createStore } from 'vuex' -import NcActionButton from '@nextcloud/vue/components/NcActionButton' -import NcButton from '@nextcloud/vue/components/NcButton' import MessageButtonsBar from './../MessageButtonsBar/MessageButtonsBar.vue' +import router from '../../../../../__mocks__/router.js' import * as useMessageInfoModule from '../../../../../composables/useMessageInfo.ts' import { ATTENDEE, CONVERSATION, MESSAGE, PARTICIPANT } from '../../../../../constants.ts' import storeConfig from '../../../../../store/storeConfig.js' @@ -29,12 +28,18 @@ describe('MessageButtonsBar.vue', () => { let conversationProps let actorStore let tokenStore + let useMessageInfoSpy beforeEach(() => { setActivePinia(createPinia()) actorStore = useActorStore() tokenStore = useTokenStore() + injected = { + getMessagesListScroller: vi.fn(), + } + useMessageInfoSpy = vi.spyOn(useMessageInfoModule, 'useMessageInfo') + conversationProps = { token: TOKEN, lastCommonReadMessage: 0, @@ -84,38 +89,32 @@ describe('MessageButtonsBar.vue', () => { vi.clearAllMocks() }) - describe('actions', () => { - let useMessageInfoSpy - - beforeEach(() => { - store = createStore(testStoreConfig) - - injected = { - getMessagesListScroller: vi.fn(), - } - - useMessageInfoSpy = vi.spyOn(useMessageInfoModule, 'useMessageInfo') - }) - - afterEach(() => { - useMessageInfoSpy.mockRestore() + const ComponentStub = { + template: '
', + } + + /** + * Shared function to mount component + */ + function mountMessageButtonsBar(props) { + return mount(MessageButtonsBar, { + global: { + plugins: [router, store], + stubs: { + NcPopover: ComponentStub, + }, + provide: injected, + }, + props, }) + } + describe('actions', () => { describe('reply action', () => { test('replies to message', async () => { store = createStore(testStoreConfig) - const wrapper = shallowMount(MessageButtonsBar, { - global: { - plugins: [store], - stubs: { - NcActionButton, - NcButton, - }, - }, - props: messageProps, - provide: injected, - }) + const wrapper = mountMessageButtonsBar(messageProps) const replyButton = findNcButton(wrapper, 'Reply') expect(replyButton.exists()).toBe(true) @@ -129,17 +128,7 @@ describe('MessageButtonsBar.vue', () => { messageProps.message.isReplyable = false store = createStore(testStoreConfig) - const wrapper = shallowMount(MessageButtonsBar, { - global: { - plugins: [store], - stubs: { - NcActionButton, - NcButton, - }, - }, - props: messageProps, - provide: injected, - }) + const wrapper = mountMessageButtonsBar(messageProps) const replyButton = findNcButton(wrapper, 'Reply') expect(replyButton.exists()).toBe(false) @@ -149,17 +138,7 @@ describe('MessageButtonsBar.vue', () => { conversationProps.permissions = 0 store = createStore(testStoreConfig) - const wrapper = shallowMount(MessageButtonsBar, { - global: { - plugins: [store], - stubs: { - NcActionButton, - NcButton, - }, - }, - props: messageProps, - provide: injected, - }) + const wrapper = mountMessageButtonsBar(messageProps) const replyButton = findNcButton(wrapper, 'Reply') expect(replyButton.exists()).toBe(false) @@ -168,28 +147,15 @@ describe('MessageButtonsBar.vue', () => { describe('private reply action', () => { test('creates a new conversation when replying to message privately', async () => { - const routerPushMock = vi.fn().mockResolvedValue() + vi.spyOn(router, 'push') + const createOneToOneConversation = vi.fn() testStoreConfig.modules.conversationsStore.actions.createOneToOneConversation = createOneToOneConversation store = createStore(testStoreConfig) messageProps.message.actorId = 'another-user' - const wrapper = shallowMount(MessageButtonsBar, { - global: { - plugins: [store], - stubs: { - NcActionButton, - }, - mocks: { - $router: { - push: routerPushMock, - }, - }, - }, - props: messageProps, - provide: injected, - }) + const wrapper = mountMessageButtonsBar(messageProps) const actionButton = findNcActionButton(wrapper, 'Reply privately') expect(actionButton.exists()).toBe(true) @@ -202,7 +168,7 @@ describe('MessageButtonsBar.vue', () => { expect(createOneToOneConversation).toHaveBeenCalledWith(expect.anything(), 'another-user') - expect(routerPushMock).toHaveBeenCalledWith({ + expect(router.push).toHaveBeenCalledWith({ name: 'conversation', params: { token: 'new-token', @@ -216,16 +182,7 @@ describe('MessageButtonsBar.vue', () => { function testPrivateReplyActionVisible(visible) { store = createStore(testStoreConfig) - const wrapper = shallowMount(MessageButtonsBar, { - global: { - plugins: [store], - stubs: { - NcActionButton, - }, - }, - props: messageProps, - provide: injected, - }) + const wrapper = mountMessageButtonsBar(messageProps) const actionButton = findNcActionButton(wrapper, 'Reply privately') expect(actionButton.exists()).toBe(visible) @@ -267,16 +224,7 @@ describe('MessageButtonsBar.vue', () => { useMessageInfoSpy.mockReturnValue({ isDeleteable: computed(() => true), }) - const wrapper = shallowMount(MessageButtonsBar, { - global: { - plugins: [store], - stubs: { - NcActionButton, - }, - }, - props: messageProps, - provide: injected, - }) + const wrapper = mountMessageButtonsBar(messageProps) const actionButton = findNcActionButton(wrapper, 'Delete') expect(actionButton.exists()).toBe(true) @@ -292,16 +240,7 @@ describe('MessageButtonsBar.vue', () => { * @param {boolean} visible Whether or not the delete action is visible */ function testDeleteMessageVisible(visible) { - const wrapper = shallowMount(MessageButtonsBar, { - global: { - plugins: [store], - stubs: { - NcActionButton, - }, - }, - props: messageProps, - provide: injected, - }) + const wrapper = mountMessageButtonsBar(messageProps) const actionButton = findNcActionButton(wrapper, 'Delete') expect(actionButton.exists()).toBe(visible) @@ -335,16 +274,7 @@ describe('MessageButtonsBar.vue', () => { conversationProps.readOnly = CONVERSATION.STATE.READ_ONLY messageProps.message.actorId = 'another-user' - const wrapper = shallowMount(MessageButtonsBar, { - global: { - plugins: [store], - stubs: { - NcActionButton, - }, - }, - props: messageProps, - provide: injected, - }) + const wrapper = mountMessageButtonsBar(messageProps) const actionButton = findNcActionButton(wrapper, 'Mark as unread') expect(actionButton.exists()).toBe(true) @@ -368,16 +298,7 @@ describe('MessageButtonsBar.vue', () => { conversationProps.readOnly = CONVERSATION.STATE.READ_ONLY messageProps.message.actorId = 'another-user' - const wrapper = shallowMount(MessageButtonsBar, { - global: { - plugins: [store], - stubs: { - NcActionButton, - }, - }, - props: messageProps, - provide: injected, - }) + const wrapper = mountMessageButtonsBar(messageProps) Object.assign(navigator, { clipboard: { @@ -404,16 +325,7 @@ describe('MessageButtonsBar.vue', () => { actionsGetterMock.forEach((action) => integrationsStore.addMessageAction(action)) testStoreConfig.modules.messagesStore.getters.message = vi.fn(() => () => messageProps) store = createStore(testStoreConfig) - const wrapper = shallowMount(MessageButtonsBar, { - global: { - plugins: [store], - stubs: { - NcActionButton, - }, - }, - props: messageProps, - provide: injected, - }) + const wrapper = mountMessageButtonsBar(messageProps) const actionButton = findNcActionButton(wrapper, 'first action') expect(actionButton.exists()).toBeTruthy() diff --git a/src/components/MessagesList/MessagesGroup/Message/MessagePart/FilePreview.spec.js b/src/components/MessagesList/MessagesGroup/Message/MessagePart/FilePreview.spec.js index 876352729e9..9cf58949c2f 100644 --- a/src/components/MessagesList/MessagesGroup/Message/MessagePart/FilePreview.spec.js +++ b/src/components/MessagesList/MessagesGroup/Message/MessagePart/FilePreview.spec.js @@ -5,14 +5,17 @@ import { generateRemoteUrl, imagePath } from '@nextcloud/router' import { getUploader } from '@nextcloud/upload' -import { shallowMount } from '@vue/test-utils' +import { mount } from '@vue/test-utils' import { cloneDeep } from 'lodash' import { createPinia, setActivePinia } from 'pinia' import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' import { createStore } from 'vuex' import NcButton from '@nextcloud/vue/components/NcButton' +import NcLoadingIcon from '@nextcloud/vue/components/NcLoadingIcon' +import NcProgressBar from '@nextcloud/vue/components/NcProgressBar' import IconPlayCircleOutline from 'vue-material-design-icons/PlayCircleOutline.vue' import FilePreview from './FilePreview.vue' +import router from '../../../../../__mocks__/router.js' import storeConfig from '../../../../../store/storeConfig.js' import { useActorStore } from '../../../../../stores/actor.ts' @@ -54,6 +57,18 @@ describe('FilePreview.vue', () => { window.devicePixelRatio = oldPixelRatio }) + /** + * Shared function to mount component + */ + function mountFilePreview() { + return mount(FilePreview, { + global: { + plugins: [router, store], + }, + props, + }) + } + /** * @param {string} url Relative URL to parse (starting with / ) */ @@ -63,10 +78,7 @@ describe('FilePreview.vue', () => { describe('file preview rendering', () => { test('renders file preview', async () => { - const wrapper = shallowMount(FilePreview, { - global: { plugins: [store] }, - props, - }) + const wrapper = mountFilePreview() await wrapper.find('img').trigger('load') @@ -85,10 +97,7 @@ describe('FilePreview.vue', () => { props.file.link = 'https://localhost/nc-webroot/s/xtokenx' actorStore.userId = null - const wrapper = shallowMount(FilePreview, { - global: { plugins: [store] }, - props, - }) + const wrapper = mountFilePreview() await wrapper.find('img').trigger('load') @@ -106,10 +115,7 @@ describe('FilePreview.vue', () => { test('calculates preview size based on window pixel ratio', async () => { window.devicePixelRatio = 1.5 - const wrapper = shallowMount(FilePreview, { - global: { plugins: [store] }, - props, - }) + const wrapper = mountFilePreview() await wrapper.find('img').trigger('load') @@ -121,16 +127,13 @@ describe('FilePreview.vue', () => { test('renders small previews when requested', async () => { props.smallPreview = true - const wrapper = shallowMount(FilePreview, { - global: { plugins: [store] }, - props, - }) + const wrapper = mountFilePreview() await wrapper.find('img').trigger('load') expect(wrapper.element.tagName).toBe('A') const imageUrl = parseRelativeUrl(wrapper.find('img').attributes('src')) - expect(imageUrl.searchParams.get('y')).toBe('32') + expect(imageUrl.searchParams.get('y')).toBe('24') }) describe('uploading', () => { @@ -146,31 +149,28 @@ describe('FilePreview.vue', () => { store = createStore(testStoreConfig) }) - test.skip('renders progress bar while uploading', async () => { - /* getUploader.mockImplementation(() => ({ + test('renders progress bar while uploading', async () => { + getUploader.mockImplementation(() => ({ queue: [{ _source: path, _uploaded: 85, _size: 100, }], - })) */ + })) props.file.id = 'temp-123' props.file.index = 'index-1' props.file.uploadId = 1000 props.file.localUrl = 'blob:XYZ' - const wrapper = shallowMount(FilePreview, { - global: { plugins: [store] }, - props, - }) + const wrapper = mountFilePreview() await wrapper.find('img').trigger('load') expect(wrapper.element.tagName).toBe('DIV') expect(wrapper.find('img').attributes('src')).toBe('blob:XYZ') - const progressEl = wrapper.findComponent({ name: 'NcProgressBar' }) + const progressEl = wrapper.findComponent(NcProgressBar) expect(progressEl.exists()).toBe(true) expect(progressEl.props('value')).toBe(85) @@ -179,22 +179,16 @@ describe('FilePreview.vue', () => { }) test('renders spinner while loading', () => { - const wrapper = shallowMount(FilePreview, { - global: { plugins: [store] }, - props, - }) + const wrapper = mountFilePreview() expect(wrapper.element.tagName).toBe('A') - const spinner = wrapper.findComponent({ name: 'NcLoadingIcon' }) + const spinner = wrapper.findComponent(NcLoadingIcon) expect(spinner.exists()).toBe(true) }) test('renders default mime icon on load error', async () => { OC.MimeType.getIconUrl.mockReturnValueOnce(imagePath('core', 'image/jpeg')) - const wrapper = shallowMount(FilePreview, { - global: { plugins: [store] }, - props, - }) + const wrapper = mountFilePreview() await wrapper.find('img').trigger('error') @@ -207,10 +201,7 @@ describe('FilePreview.vue', () => { props.file['preview-available'] = 'no' OC.MimeType.getIconUrl.mockReturnValueOnce(imagePath('core', 'image/jpeg')) - const wrapper = shallowMount(FilePreview, { - global: { plugins: [store] }, - props, - }) + const wrapper = mountFilePreview() await wrapper.find('img').trigger('load') @@ -230,10 +221,7 @@ describe('FilePreview.vue', () => { test('directly renders small GIF files', async () => { props.file.size = '128' - const wrapper = shallowMount(FilePreview, { - global: { plugins: [store] }, - props, - }) + const wrapper = mountFilePreview() await wrapper.find('img').trigger('load') @@ -246,10 +234,7 @@ describe('FilePreview.vue', () => { props.file.size = '128' props.file.path = '/path/to/test %20.gif' - const wrapper = shallowMount(FilePreview, { - global: { plugins: [store] }, - props, - }) + const wrapper = mountFilePreview() await wrapper.find('img').trigger('load') @@ -263,10 +248,7 @@ describe('FilePreview.vue', () => { props.file.link = 'https://localhost/nc-webroot/s/xtokenx' actorStore.userId = null - const wrapper = shallowMount(FilePreview, { - global: { plugins: [store] }, - props, - }) + const wrapper = mountFilePreview() await wrapper.find('img').trigger('load') @@ -279,10 +261,7 @@ describe('FilePreview.vue', () => { // 4 MB, bigger than max from capability (3 MB) props.file.size = '4194304' - const wrapper = shallowMount(FilePreview, { - global: { plugins: [store] }, - props, - }) + const wrapper = mountFilePreview() await wrapper.find('img').trigger('load') @@ -335,10 +314,7 @@ describe('FilePreview.vue', () => { mimetypes: ['image/png', 'image/jpeg'], } - const wrapper = shallowMount(FilePreview, { - global: { plugins: [store] }, - props, - }) + const wrapper = mountFilePreview() await wrapper.find('img').trigger('load') @@ -368,10 +344,7 @@ describe('FilePreview.vue', () => { }], } - const wrapper = shallowMount(FilePreview, { - global: { plugins: [store] }, - props, - }) + const wrapper = mountFilePreview() await wrapper.find('img').trigger('load') @@ -382,10 +355,7 @@ describe('FilePreview.vue', () => { test('does not open viewer when clicking if viewer is not available', async () => { delete OCA.Viewer - const wrapper = shallowMount(FilePreview, { - global: { plugins: [store] }, - props, - }) + const wrapper = mountFilePreview() await wrapper.find('img').trigger('load') @@ -413,10 +383,7 @@ describe('FilePreview.vue', () => { * @param {boolean} visible Whether or not the play button is visible */ async function testPlayButtonVisible(visible) { - const wrapper = shallowMount(FilePreview, { - global: { plugins: [store] }, - props, - }) + const wrapper = mountFilePreview() await wrapper.find('img').trigger('load') @@ -453,10 +420,7 @@ describe('FilePreview.vue', () => { }) test('does not render play icon for failed videos', async () => { - const wrapper = shallowMount(FilePreview, { - global: { plugins: [store] }, - props, - }) + const wrapper = mountFilePreview() await wrapper.find('img').trigger('error') @@ -485,16 +449,13 @@ describe('FilePreview.vue', () => { props.isUploadEditor = true }) test('emits event when clicking remove button when inside upload editor', async () => { - const wrapper = shallowMount(FilePreview, { - global: { plugins: [store] }, - props, - }) + const wrapper = mountFilePreview() await wrapper.find('img').trigger('load') expect(wrapper.element.tagName).toBe('DIV') await wrapper.findComponent(NcButton).trigger('click') - expect(wrapper.emitted()['remove-file']).toStrictEqual([['123']]) + expect(wrapper.emitted().removeFile).toStrictEqual([['123']]) }) }) }) diff --git a/src/components/MessagesList/MessagesGroup/Message/MessagePart/Reactions.spec.js b/src/components/MessagesList/MessagesGroup/Message/MessagePart/Reactions.spec.js index ec341224f59..4a3adc9e974 100644 --- a/src/components/MessagesList/MessagesGroup/Message/MessagePart/Reactions.spec.js +++ b/src/components/MessagesList/MessagesGroup/Message/MessagePart/Reactions.spec.js @@ -4,7 +4,7 @@ */ import { showError } from '@nextcloud/dialogs' -import { shallowMount } from '@vue/test-utils' +import { mount } from '@vue/test-utils' import { cloneDeep } from 'lodash' import { createPinia, setActivePinia } from 'pinia' import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' @@ -13,7 +13,7 @@ import NcButton from '@nextcloud/vue/components/NcButton' import NcEmojiPicker from '@nextcloud/vue/components/NcEmojiPicker' import NcPopover from '@nextcloud/vue/components/NcPopover' import Reactions from './Reactions.vue' -import { ATTENDEE } from '../../../../../constants.ts' +import router from '../../../../../__mocks__/router.js' import { addReactionToMessage, getReactionsDetails, @@ -103,20 +103,30 @@ describe('Reactions.vue', () => { reactionsStore.resetReactions(token, messageId) }) + const ComponentTriggerStub = { + template: '
', + } + + /** + * Shared function to mount component + */ + function mountReactions(props) { + return mount(Reactions, { + global: { + plugins: [router, store], + stubs: { + NcEmojiPicker: ComponentTriggerStub, + NcPopover: ComponentTriggerStub, + }, + }, + props, + }) + } + describe('reactions buttons', () => { test('shows reaction buttons with count and emoji picker', async () => { // Arrange - const wrapper = shallowMount(Reactions, { - global: { - plugins: [store], - stubs: { - NcPopover, - }, - }, - props: reactionsProps, - - }) - + const wrapper = mountReactions(reactionsProps) // Assert const reactionButtons = wrapper.findAllComponents(NcPopover) expect(reactionButtons).toHaveLength(3) @@ -131,16 +141,7 @@ describe('Reactions.vue', () => { test('shows reaction buttons with count but without emoji picker when no react permission', () => { // Arrange reactionsProps.canReact = false - const wrapper = shallowMount(Reactions, { - global: { - plugins: [store], - stubs: { - NcPopover, - }, - }, - props: reactionsProps, - - }) + const wrapper = mountReactions(reactionsProps) const reactionButtons = wrapper.findAllComponents(NcButton) const emojiPicker = wrapper.findAllComponents(NcEmojiPicker) // Act @@ -171,24 +172,13 @@ describe('Reactions.vue', () => { }) testStoreConfig.modules.messagesStore.getters.message = () => messageMock store = createStore(testStoreConfig) - const wrapper = shallowMount(Reactions, { - props: reactionsProps, - global: { - plugins: [store], - stubs: { - NcEmojiPicker, - NcPopover, - }, - }, - - }) + const wrapper = mountReactions(reactionsProps) // Assert const reactionButtons = wrapper.findAllComponents(NcPopover) expect(reactionButtons).toHaveLength(0) const emojiPicker = wrapper.findComponent(NcEmojiPicker) expect(emojiPicker.exists()).toBeFalsy() - expect(emojiPicker.vm).toBeUndefined() }) test('dispatches store actions upon picking an emoji from the emojipicker', async () => { @@ -196,19 +186,8 @@ describe('Reactions.vue', () => { vi.spyOn(reactionsStore, 'addReactionToMessage') vuexStore.dispatch('processMessage', { token, message }) - const wrapper = shallowMount(Reactions, { - props: { - ...reactionsProps, - showControls: true, - }, - global: { - plugins: [store], - stubs: { - NcEmojiPicker, - }, - }, - - }) + reactionsProps.showControls = true + const wrapper = mountReactions(reactionsProps) const response = generateOCSResponse({ payload: Object.assign({}, reactionsStored, { '❤️': [{ actorDisplayName: 'user1', actorId: 'actorId1', actorType: 'users' }] }) }) addReactionToMessage.mockResolvedValue(response) @@ -232,17 +211,8 @@ describe('Reactions.vue', () => { vuexStore.dispatch('processMessage', { token, message }) - const wrapper = shallowMount(Reactions, { - props: reactionsProps, - global: { - plugins: [store], - stubs: { - NcEmojiPicker, - NcPopover, - }, - }, + const wrapper = mountReactions(reactionsProps) - }) const addedReaction = { ...reactionsStored, '🎄': [...reactionsStored['🎄'], { actorDisplayName: 'user3', actorId: 'admin', actorType: 'users' }], @@ -282,16 +252,8 @@ describe('Reactions.vue', () => { console.debug = vi.fn() vi.spyOn(reactionsStore, 'fetchReactions') - const wrapper = shallowMount(Reactions, { - props: reactionsProps, - global: { - plugins: [store], - stubs: { - NcPopover, - }, - }, + const wrapper = mountReactions(reactionsProps) - }) const response = generateOCSResponse({ payload: reactionsStored }) getReactionsDetails.mockResolvedValue(response) diff --git a/src/components/MessagesList/MessagesList.spec.js b/src/components/MessagesList/MessagesList.spec.js index cbca670b6c7..e7495e75f4b 100644 --- a/src/components/MessagesList/MessagesList.spec.js +++ b/src/components/MessagesList/MessagesList.spec.js @@ -3,21 +3,57 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -import { shallowMount } from '@vue/test-utils' +import { mount } from '@vue/test-utils' import { cloneDeep } from 'lodash' import { createPinia, setActivePinia } from 'pinia' import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' -import { createStore } from 'vuex' -import StaticDateTime from '../UIShared/StaticDateTime.vue' +import { ref } from 'vue' +import { createStore, useStore } from 'vuex' +import NcEmptyContent from '@nextcloud/vue/components/NcEmptyContent' +import LoadingPlaceholder from '../UIShared/LoadingPlaceholder.vue' +import MessagesGroup from './MessagesGroup/MessagesGroup.vue' +import MessagesSystemGroup from './MessagesGroup/MessagesSystemGroup.vue' import MessagesList from './MessagesList.vue' +import router from '../../__mocks__/router.js' import { ATTENDEE, MESSAGE } from '../../constants.ts' import storeConfig from '../../store/storeConfig.js' +import { useChatStore } from '../../stores/chat.ts' + +vi.mock('vuex', async () => { + const vuex = await vi.importActual('vuex') + return { + ...vuex, + useStore: vi.fn(), + } +}) + +const contextMessageId = ref(0) +const loadingOldMessages = ref(0) +const loadingNewMessages = ref(0) +const isInitialisingMessages = ref(true) +const isChatBeginningReached = ref(0) +const isChatEndReached = ref(0) + +vi.mock('../../composables/useGetMessages.ts', async () => ({ + useGetMessages: vi.fn(() => ({ + contextMessageId, + loadingOldMessages, + loadingNewMessages, + isInitialisingMessages, + isChatBeginningReached, + isChatEndReached, + + getOldMessages: vi.fn(), + getNewMessages: vi.fn(), + })), +})) const fakeTimestamp = (value) => new Date(value).getTime() / 1000 describe('MessagesList.vue', () => { const TOKEN = 'XXTOKENXX' let store + let chatStore let testStoreConfig const getVisualLastReadMessageIdMock = vi.fn() @@ -27,6 +63,9 @@ describe('MessagesList.vue', () => { testStoreConfig.modules.messagesStore.getters.getVisualLastReadMessageId = vi.fn().mockReturnValue(getVisualLastReadMessageIdMock) store = createStore(testStoreConfig) + useStore.mockReturnValue(store) + + chatStore = useChatStore() // scrollTo isn't implemented in JSDOM Element.prototype.scrollTo = () => {} @@ -34,6 +73,13 @@ describe('MessagesList.vue', () => { afterEach(() => { vi.clearAllMocks() + + contextMessageId.value = 0 + loadingOldMessages.value = 0 + loadingNewMessages.value = 0 + isInitialisingMessages.value = true + isChatBeginningReached.value = 0 + isChatEndReached.value = 0 }) const messagesGroup1 = [{ @@ -48,6 +94,7 @@ describe('MessagesList.vue', () => { systemMessage: '', timestamp: fakeTimestamp('2024-05-01T12:05:00'), isReplyable: true, + reactions: {}, }, { id: 110, token: TOKEN, @@ -60,6 +107,7 @@ describe('MessagesList.vue', () => { systemMessage: '', timestamp: fakeTimestamp('2024-05-01T12:06:00'), isReplyable: true, + reactions: {}, }] const messagesGroup1OldMessage = { @@ -74,6 +122,7 @@ describe('MessagesList.vue', () => { systemMessage: '', timestamp: fakeTimestamp('2024-05-01T12:04:00'), isReplyable: true, + reactions: {}, } const messagesGroup1WithOld = [messagesGroup1OldMessage].concat(messagesGroup1) @@ -89,6 +138,7 @@ describe('MessagesList.vue', () => { systemMessage: '', timestamp: fakeTimestamp('2024-05-01T12:30:00'), isReplyable: true, + reactions: {}, }, { id: 210, token: TOKEN, @@ -101,6 +151,7 @@ describe('MessagesList.vue', () => { systemMessage: '', timestamp: fakeTimestamp('2024-05-01T12:31:00'), isReplyable: true, + reactions: {}, }] const messagesGroup2NewMessage = { @@ -115,6 +166,7 @@ describe('MessagesList.vue', () => { systemMessage: '', timestamp: fakeTimestamp('2024-05-01T12:32:00'), isReplyable: true, + reactions: {}, } const messagesGroup2WithNew = messagesGroup2.concat([messagesGroup2NewMessage]) @@ -130,27 +182,38 @@ describe('MessagesList.vue', () => { systemMessage: '', timestamp: 0, // temporary isReplyable: true, + reactions: {}, }] + function mountMessagesList() { + return mount(MessagesList, { + global: { + plugins: [router, store], + }, + props: { + token: TOKEN, + isChatScrolledToBottom: true, + }, + }) + } + describe('message grouping', () => { /** * @param {Array} messagesGroups List of messages that should be grouped */ function testGrouped(...messagesGroups) { - messagesGroups.flat().forEach((message) => store.commit('addMessage', { token: TOKEN, message })) - const wrapper = shallowMount(MessagesList, { - global: { plugins: [store] }, - props: { - token: TOKEN, - isChatScrolledToBottom: true, - }, + store.commit('addConversation', { + token: TOKEN, + hasCall: false, }) + messagesGroups.flat().forEach((message) => store.commit('addMessage', { token: TOKEN, message })) + chatStore.processChatBlocks(TOKEN, messagesGroups.flat()) + isInitialisingMessages.value = false - const groups = wrapper.findAll('.messages-group') - - expect(groups.exists()).toBeTruthy() + const wrapper = mountMessagesList() - groups.wrappers.forEach((group, index) => { + const groups = wrapper.findAllComponents('li.wrapper') + groups.forEach((group, index) => { expect(group.props('messages')).toStrictEqual(messagesGroups[index]) }) @@ -161,26 +224,18 @@ describe('MessagesList.vue', () => { * @param {Array} messages List of messages that should not be grouped */ function testNotGrouped(messages) { + store.commit('addConversation', { + token: TOKEN, + hasCall: false, + }) messages.forEach((message) => store.commit('addMessage', { token: TOKEN, message })) + chatStore.processChatBlocks(TOKEN, messages) + isInitialisingMessages.value = false - const wrapper = shallowMount(MessagesList, { - global: { - plugins: [store], - stubs: { - StaticDateTime, - }, - }, - props: { - token: TOKEN, - isChatScrolledToBottom: true, - }, - }) + const wrapper = mountMessagesList() const groups = wrapper.findAll('.messages-group') - - expect(groups.exists()).toBeTruthy() - - groups.wrappers.forEach((group, index) => { + groups.forEach((group, index) => { expect(group.props('messages')).toStrictEqual([messages[index]]) }) @@ -215,6 +270,7 @@ describe('MessagesList.vue', () => { systemMessage: '', timestamp: fakeTimestamp('2019-09-14T13:00:00'), isReplyable: true, + reactions: {}, }, { id: 110, token: TOKEN, @@ -227,6 +283,7 @@ describe('MessagesList.vue', () => { systemMessage: '', timestamp: fakeTimestamp('2020-05-10T13:00:00'), isReplyable: true, + reactions: {}, }, { id: 'temp-120', token: TOKEN, @@ -239,6 +296,7 @@ describe('MessagesList.vue', () => { systemMessage: '', timestamp: 0, // temporary, matches current date isReplyable: true, + reactions: {}, }]) const dateSeparators = wrapper.findAll('.messages-date') @@ -261,6 +319,7 @@ describe('MessagesList.vue', () => { systemMessage: 'call_started', timestamp: fakeTimestamp('2020-05-09T13:00:00'), isReplyable: true, + reactions: {}, }, { id: 110, token: TOKEN, @@ -273,6 +332,7 @@ describe('MessagesList.vue', () => { systemMessage: 'call_ended', timestamp: fakeTimestamp('2020-05-09T13:02:00'), isReplyable: true, + reactions: {}, }]) }) @@ -289,6 +349,7 @@ describe('MessagesList.vue', () => { systemMessage: 'call_started', timestamp: fakeTimestamp('2020-05-09T13:00:00'), isReplyable: true, + reactions: {}, }, { id: 110, token: TOKEN, @@ -301,6 +362,7 @@ describe('MessagesList.vue', () => { systemMessage: '', timestamp: fakeTimestamp('2020-05-09T13:02:00'), isReplyable: true, + reactions: {}, }]) }) @@ -317,6 +379,7 @@ describe('MessagesList.vue', () => { systemMessage: '', timestamp: fakeTimestamp('2020-05-09T13:00:00'), isReplyable: true, + reactions: {}, }, { id: 110, token: TOKEN, @@ -329,6 +392,7 @@ describe('MessagesList.vue', () => { systemMessage: '', timestamp: fakeTimestamp('2020-05-09T13:02:00'), isReplyable: true, + reactions: {}, }]) }) @@ -345,6 +409,7 @@ describe('MessagesList.vue', () => { systemMessage: 'call_started', timestamp: fakeTimestamp('2020-05-09T13:00:00'), isReplyable: true, + reactions: {}, }, { id: 110, token: TOKEN, @@ -357,6 +422,7 @@ describe('MessagesList.vue', () => { systemMessage: '', timestamp: fakeTimestamp('2020-05-09T13:02:00'), isReplyable: true, + reactions: {}, }]) }) @@ -373,6 +439,7 @@ describe('MessagesList.vue', () => { systemMessage: '', timestamp: fakeTimestamp('2024-05-01T12:05:00'), isReplyable: true, + reactions: {}, }, { id: 110, token: TOKEN, @@ -389,6 +456,7 @@ describe('MessagesList.vue', () => { systemMessage: '', timestamp: fakeTimestamp('2024-05-01T12:06:00'), isReplyable: true, + reactions: {}, }]) }) }) @@ -400,56 +468,45 @@ describe('MessagesList.vue', () => { * @param {Array} messagesGroups initial messages groups */ function renderMessagesList(...messagesGroups) { - messagesGroups.flat().forEach((message) => store.commit('addMessage', { token: TOKEN, message })) - return shallowMount(MessagesList, { - global: { plugins: [store] }, - props: { - token: TOKEN, - isChatScrolledToBottom: true, - }, + store.commit('addConversation', { + token: TOKEN, + hasCall: false, }) + messagesGroups.flat().forEach((message) => store.commit('addMessage', { token: TOKEN, message })) + chatStore.processChatBlocks(TOKEN, messagesGroups.flat()) + isInitialisingMessages.value = false + return mountMessagesList() } test('renders a placeholder while loading', () => { - const wrapper = shallowMount(MessagesList, { - global: { plugins: [store] }, - props: { - token: TOKEN, - isChatScrolledToBottom: true, - }, - }) + const wrapper = mountMessagesList() - const groups = wrapper.findAllComponents({ name: 'MessagesGroup' }) - expect(groups.exists()).toBe(false) + const groups = wrapper.findAllComponents(MessagesGroup) + expect(groups).toHaveLength(0) - const placeholder = wrapper.findAllComponents({ name: 'LoadingPlaceholder' }) + const placeholder = wrapper.findComponent(LoadingPlaceholder) expect(placeholder.exists()).toBe(true) }) test('renders an empty content after loading', () => { store.commit('loadedMessagesOfConversation', { token: TOKEN }) - const wrapper = shallowMount(MessagesList, { - global: { plugins: [store] }, - props: { - token: TOKEN, - isChatScrolledToBottom: true, - }, - }) + isInitialisingMessages.value = false - const groups = wrapper.findAllComponents({ name: 'MessagesGroup' }) - expect(groups.exists()).toBe(false) + const wrapper = mountMessagesList() - const placeholder = wrapper.findAllComponents({ name: 'NcEmptyContent' }) + const groups = wrapper.findAllComponents(MessagesGroup) + expect(groups).toHaveLength(0) + + const placeholder = wrapper.findComponent(NcEmptyContent) expect(placeholder.exists()).toBe(true) }) test('renders initial group of messages', () => { // Act const wrapper = renderMessagesList(messagesGroup1) - const groups = wrapper.findAllComponents({ name: 'MessagesGroup' }) + const groups = wrapper.findAllComponents(MessagesGroup) // Assert: groups are rendered - expect(groups.exists()).toBe(true) expect(groups.at(0).props()).toMatchObject({ token: TOKEN, messages: messagesGroup1, @@ -464,10 +521,12 @@ describe('MessagesList.vue', () => { // Act: add new group to the store messagesGroup2.forEach((message) => store.commit('addMessage', { token: TOKEN, message })) + chatStore.processChatBlocks(TOKEN, messagesGroup2, { mergeBy: 100 }) + isInitialisingMessages.value = false await wrapper.vm.$nextTick() // Assert: old group nextMessageId is updated, new group is added - const groups = wrapper.findAllComponents({ name: 'MessagesGroup' }) + const groups = wrapper.findAllComponents(MessagesGroup) expect(groups.at(0).props()).toMatchObject({ token: TOKEN, messages: messagesGroup1, @@ -490,11 +549,13 @@ describe('MessagesList.vue', () => { // Act: add new messages to the store store.commit('addMessage', { token: TOKEN, message: messagesGroup1OldMessage }) store.commit('addMessage', { token: TOKEN, message: messagesGroup2NewMessage }) + chatStore.processChatBlocks(TOKEN, [messagesGroup1OldMessage], { mergeBy: 100 }) + chatStore.processChatBlocks(TOKEN, [messagesGroup2NewMessage], { mergeBy: 100 }) + isInitialisingMessages.value = false await wrapper.vm.$nextTick() // Assert: both groups are updated - const groups = wrapper.findAllComponents({ name: 'MessagesGroup' }) - expect(groups.exists()).toBe(true) + const groups = wrapper.findAllComponents(MessagesGroup) expect(groups.length).toBe(2) expect(groups.at(0).props()).toMatchObject({ token: TOKEN, @@ -523,11 +584,11 @@ describe('MessagesList.vue', () => { } store.commit('deleteMessage', { token: TOKEN, id: messagesGroup3[0].id }) store.commit('addMessage', { token: TOKEN, message }) + chatStore.processChatBlocks(TOKEN, [message], { mergeBy: 100 }) await wrapper.vm.$nextTick() // Assert: old group nextMessageId is updated, new group is added - const groups = wrapper.findAllComponents({ name: 'MessagesGroup' }) - expect(groups.exists()).toBe(true) + const groups = wrapper.findAllComponents(MessagesGroup) expect(groups.length).toBe(2) expect(groups.at(0).props()).toMatchObject({ token: TOKEN, @@ -556,11 +617,12 @@ describe('MessagesList.vue', () => { // Act: replace temporary message with returned from server store.commit('deleteMessage', { token: TOKEN, id: 'temp-210' }) store.commit('addMessage', { token: TOKEN, message: messagesGroup2[1] }) + chatStore.processChatBlocks(TOKEN, [messagesGroup2[1]], { mergeBy: 100 }) + await wrapper.vm.$nextTick() // Assert: old group nextMessageId is updated, new group is added - const groups = wrapper.findAllComponents({ name: 'MessagesGroup' }) - expect(groups.exists()).toBe(true) + const groups = wrapper.findAllComponents(MessagesGroup) expect(groups.length).toBe(2) expect(groups.at(1).props()).toMatchObject({ @@ -588,15 +650,18 @@ describe('MessagesList.vue', () => { systemMessage: 'history_cleared', timestamp: fakeTimestamp('2024-05-01T13:00:00'), isReplyable: false, + reactions: {}, } store.commit('purgeMessagesStore', TOKEN) store.commit('addMessage', { token: TOKEN, message }) + chatStore.processChatBlocks(TOKEN, [message], { mergeBy: 100 }) + await wrapper.vm.$nextTick() // Assert: old messages are removed, system message is added - const groups = wrapper.findAllComponents({ name: 'MessagesGroup' }) - expect(groups.exists()).toBe(false) - const groupsSystem = wrapper.findAllComponents({ name: 'MessagesSystemGroup' }) + const groups = wrapper.findAllComponents(MessagesGroup) + expect(groups).toHaveLength(0) + const groupsSystem = wrapper.findAllComponents(MessagesSystemGroup) expect(groupsSystem.length).toBe(1) expect(groupsSystem.at(0).props()).toMatchObject({ token: TOKEN, @@ -615,7 +680,7 @@ describe('MessagesList.vue', () => { store.commit('deleteMessage', { token: TOKEN, id: messagesGroup2NewMessage.id }) await wrapper.vm.$nextTick() - const groups = wrapper.findAllComponents({ name: 'MessagesGroup' }) + const groups = wrapper.findAllComponents(MessagesGroup) expect(groups.length).toBe(2) expect(groups.at(0).props()).toMatchObject({ token: TOKEN, diff --git a/src/components/RightSidebar/Participants/Participant.spec.js b/src/components/RightSidebar/Participants/Participant.spec.js index 0a6e1ba64e1..2ff7cd41131 100644 --- a/src/components/RightSidebar/Participants/Participant.spec.js +++ b/src/components/RightSidebar/Participants/Participant.spec.js @@ -3,30 +3,26 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -import { flushPromises, shallowMount } from '@vue/test-utils' +import { flushPromises, mount } from '@vue/test-utils' import { cloneDeep } from 'lodash' import { createPinia, setActivePinia } from 'pinia' import { afterEach, beforeEach, describe, expect, it, test, vi } from 'vitest' import { createStore } from 'vuex' -import NcActionButton from '@nextcloud/vue/components/NcActionButton' -import NcActionText from '@nextcloud/vue/components/NcActionText' -import NcButton from '@nextcloud/vue/components/NcButton' import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch' import NcDialog from '@nextcloud/vue/components/NcDialog' -import NcInputField from '@nextcloud/vue/components/NcInputField' -import NcListItem from '@nextcloud/vue/components/NcListItem' import NcTextArea from '@nextcloud/vue/components/NcTextArea' -import IconHandBackLeftOutline from 'vue-material-design-icons/HandBackLeftOutline.vue' +import IconHandBackLeft from 'vue-material-design-icons/HandBackLeft.vue' import IconMicrophoneOutline from 'vue-material-design-icons/MicrophoneOutline.vue' -import IconPhoneOutline from 'vue-material-design-icons/PhoneOutline.vue' +import IconPhoneDialOutline from 'vue-material-design-icons/PhoneDialOutline.vue' import IconVideoOutline from 'vue-material-design-icons/VideoOutline.vue' import AvatarWrapper from '../../AvatarWrapper/AvatarWrapper.vue' import Participant from './Participant.vue' +import router from '../../../__mocks__/router.js' import { ATTENDEE, PARTICIPANT, WEBINAR } from '../../../constants.ts' import storeConfig from '../../../store/storeConfig.js' import { useActorStore } from '../../../stores/actor.ts' import { useTokenStore } from '../../../stores/token.ts' -import { findNcActionButton, findNcButton } from '../../../test-helpers.js' +import { findNcActionButton, findNcActionText, findNcButton } from '../../../test-helpers.js' describe('Participant.vue', () => { const TOKEN = 'XXTOKENXX' @@ -76,43 +72,35 @@ describe('Participant.vue', () => { testStoreConfig = cloneDeep(storeConfig) testStoreConfig.modules.conversationsStore.getters.conversation = () => conversationGetterMock store = createStore(testStoreConfig) + + router.push({ name: 'conversation', params: { token: TOKEN } }) }) afterEach(() => { vi.clearAllMocks() }) + const ComponentStub = { + template: '
', + } + /** * @param {object} participant Participant with optional user status data * @param {boolean} showUserStatus Whether or not the user status should be shown */ function mountParticipant(participant, showUserStatus = false) { - return shallowMount(Participant, { + return mount(Participant, { global: { - plugins: [store], + plugins: [router, store], stubs: { - NcActionButton, - NcButton, - NcCheckboxRadioSwitch, - NcDialog, - NcInputField, - NcListItem, - NcTextArea, + NcModal: ComponentStub, + NcPopover: ComponentStub, }, }, props: { participant, showUserStatus, }, - mixins: [{ - // force tooltip display for testing - methods: { - forceEnableTooltips() { - this.isUserNameTooltipVisible = true - this.isStatusTooltipVisible = true - }, - }, - }], }) } @@ -120,12 +108,12 @@ describe('Participant.vue', () => { test('renders avatar', () => { const wrapper = mountParticipant(participant) const avatarEl = wrapper.findComponent(AvatarWrapper) - expect(avatarEl.exists()).toBe(true) + expect(avatarEl.exists()).toBeTruthy() expect(avatarEl.props('id')).toBe('alice-actor-id') - expect(avatarEl.props('disableTooltip')).toBe(true) - expect(avatarEl.props('disableMenu')).toBe(false) - expect(avatarEl.props('showUserStatus')).toBe(false) + expect(avatarEl.props('disableTooltip')).toBeTruthy() + expect(avatarEl.props('disableMenu')).toBeFalsy() + expect(avatarEl.props('showUserStatus')).toBeFalsy() expect(avatarEl.props('preloadedUserStatus')).toStrictEqual({ icon: '🌧️', message: 'rainy', @@ -133,15 +121,15 @@ describe('Participant.vue', () => { }) expect(avatarEl.props('name')).toBe('Alice') expect(avatarEl.props('source')).toBe(ATTENDEE.ACTOR_TYPE.USERS) - expect(avatarEl.props('offline')).toBe(false) + expect(avatarEl.props('offline')).toBeFalsy() }) test('renders avatar with enabled status', () => { const wrapper = mountParticipant(participant, true) const avatarEl = wrapper.findComponent(AvatarWrapper) - expect(avatarEl.exists()).toBe(true) + expect(avatarEl.exists()).toBeTruthy() - expect(avatarEl.props('showUserStatus')).toBe(true) + expect(avatarEl.props('showUserStatus')).toBeTruthy() }) test('renders avatar with guest name when empty', () => { @@ -149,7 +137,7 @@ describe('Participant.vue', () => { participant.actorType = ATTENDEE.ACTOR_TYPE.GUESTS const wrapper = mountParticipant(participant) const avatarEl = wrapper.findComponent(AvatarWrapper) - expect(avatarEl.exists()).toBe(true) + expect(avatarEl.exists()).toBeTruthy() expect(avatarEl.props('name')).toBe('') }) @@ -158,7 +146,7 @@ describe('Participant.vue', () => { participant.displayName = '' const wrapper = mountParticipant(participant, true) const avatarEl = wrapper.findComponent(AvatarWrapper) - expect(avatarEl.exists()).toBe(true) + expect(avatarEl.exists()).toBeTruthy() expect(avatarEl.props('name')).toBe('') }) @@ -167,15 +155,16 @@ describe('Participant.vue', () => { participant.sessionIds = [] const wrapper = mountParticipant(participant, true) const avatarEl = wrapper.findComponent(AvatarWrapper) - expect(avatarEl.exists()).toBe(true) + expect(avatarEl.exists()).toBeTruthy() - expect(avatarEl.props('offline')).toBe(true) + expect(avatarEl.props('offline')).toBeTruthy() }) }) describe('user name', () => { /** * Check which text is currently rendered as a name + * (text in user badges has a padding to separate words visually) * @param {object} participant participant object * @param {RegExp} regexp regex pattern which expected to be rendered */ @@ -187,22 +176,22 @@ describe('Participant.vue', () => { } const testCases = [ - ['Alice', 'alice', ATTENDEE.ACTOR_TYPE.USERS, PARTICIPANT.TYPE.USER, /^Alice$/], - ['Alice', 'guest-id', ATTENDEE.ACTOR_TYPE.GUESTS, PARTICIPANT.TYPE.GUEST, /^Alice\s+\(guest\)$/], - ['Alice', 'guest-id', ATTENDEE.ACTOR_TYPE.EMAILS, PARTICIPANT.TYPE.GUEST, /^Alice\s+\(guest\)$/], - ['', 'guest-id', ATTENDEE.ACTOR_TYPE.GUESTS, PARTICIPANT.TYPE.GUEST, /^Guest\s+\(guest\)$/], - ['Alice', 'alice', ATTENDEE.ACTOR_TYPE.USERS, PARTICIPANT.TYPE.MODERATOR, /^Alice\s+\(moderator\)$/], - ['Alice', 'guest-id', ATTENDEE.ACTOR_TYPE.GUESTS, PARTICIPANT.TYPE.GUEST_MODERATOR, /^Alice\s+\(moderator\)\s+\(guest\)$/], - ['Bot', ATTENDEE.BRIDGE_BOT_ID, ATTENDEE.ACTOR_TYPE.USERS, PARTICIPANT.TYPE.USER, /^Bot\s+\(bot\)$/], + ['Alice', 'alice', ATTENDEE.ACTOR_TYPE.USERS, PARTICIPANT.TYPE.USER, 'Alice'], + ['Alice', 'guest-id', ATTENDEE.ACTOR_TYPE.GUESTS, PARTICIPANT.TYPE.GUEST, 'Alice(guest)'], + ['Alice', 'guest-id', ATTENDEE.ACTOR_TYPE.EMAILS, PARTICIPANT.TYPE.GUEST, 'Alice(guest)'], + ['', 'guest-id', ATTENDEE.ACTOR_TYPE.GUESTS, PARTICIPANT.TYPE.GUEST, 'Guest(guest)'], + ['Alice', 'alice', ATTENDEE.ACTOR_TYPE.USERS, PARTICIPANT.TYPE.MODERATOR, 'Alice(moderator)'], + ['Alice', 'guest-id', ATTENDEE.ACTOR_TYPE.GUESTS, PARTICIPANT.TYPE.GUEST_MODERATOR, 'Alice(moderator)(guest)'], + ['Bot', ATTENDEE.BRIDGE_BOT_ID, ATTENDEE.ACTOR_TYPE.USERS, PARTICIPANT.TYPE.USER, 'Bot(bot)'], ] const testLobbyCases = [ - ['Alice', 'alice', ATTENDEE.ACTOR_TYPE.USERS, PARTICIPANT.TYPE.USER, /^Alice\s+\(in the lobby\)$/], - ['Alice', 'guest-id', ATTENDEE.ACTOR_TYPE.GUESTS, PARTICIPANT.TYPE.GUEST, /^Alice\s+\(guest\)\s+\(in the lobby\)$/], - ['Alice', 'guest-id', ATTENDEE.ACTOR_TYPE.EMAILS, PARTICIPANT.TYPE.GUEST, /^Alice\s+\(guest\)\s+\(in the lobby\)$/], - ['', 'guest-id', ATTENDEE.ACTOR_TYPE.GUESTS, PARTICIPANT.TYPE.GUEST, /^Guest\s+\(guest\)\s+\(in the lobby\)$/], - ['Alice', 'alice', ATTENDEE.ACTOR_TYPE.USERS, PARTICIPANT.TYPE.MODERATOR, /^Alice\s+\(moderator\)$/], - ['Alice', 'guest-id', ATTENDEE.ACTOR_TYPE.GUESTS, PARTICIPANT.TYPE.GUEST_MODERATOR, /^Alice\s+\(moderator\)\s+\(guest\)$/], + ['Alice', 'alice', ATTENDEE.ACTOR_TYPE.USERS, PARTICIPANT.TYPE.USER, 'Alice(in the lobby)'], + ['Alice', 'guest-id', ATTENDEE.ACTOR_TYPE.GUESTS, PARTICIPANT.TYPE.GUEST, 'Alice(guest)(in the lobby)'], + ['Alice', 'guest-id', ATTENDEE.ACTOR_TYPE.EMAILS, PARTICIPANT.TYPE.GUEST, 'Alice(guest)(in the lobby)'], + ['', 'guest-id', ATTENDEE.ACTOR_TYPE.GUESTS, PARTICIPANT.TYPE.GUEST, 'Guest(guest)(in the lobby)'], + ['Alice', 'alice', ATTENDEE.ACTOR_TYPE.USERS, PARTICIPANT.TYPE.MODERATOR, 'Alice(moderator)'], + ['Alice', 'guest-id', ATTENDEE.ACTOR_TYPE.GUESTS, PARTICIPANT.TYPE.GUEST_MODERATOR, 'Alice(moderator)(guest)'], ] it.each(testCases)( @@ -281,7 +270,7 @@ describe('Participant.vue', () => { describe('call icons', () => { let getParticipantRaisedHandMock - const components = [IconVideoOutline, IconPhoneOutline, IconMicrophoneOutline, IconHandBackLeftOutline] + const components = [IconVideoOutline, IconPhoneDialOutline, IconMicrophoneOutline, IconHandBackLeft] /** * Check which icons are currently rendered @@ -324,13 +313,13 @@ describe('Participant.vue', () => { }) test('renders phone call icon', async () => { participant.inCall = PARTICIPANT.CALL_FLAG.WITH_PHONE - checkStateIconsRendered(participant, IconPhoneOutline) + checkStateIconsRendered(participant, IconPhoneDialOutline) }) test('renders hand raised icon', async () => { participant.inCall = PARTICIPANT.CALL_FLAG.WITH_VIDEO getParticipantRaisedHandMock = vi.fn().mockReturnValue({ state: true }) - checkStateIconsRendered(participant, IconHandBackLeftOutline) + checkStateIconsRendered(participant, IconHandBackLeft) expect(getParticipantRaisedHandMock).toHaveBeenCalledWith(['session-id-alice']) }) test('renders video call icon when joined with multiple', async () => { @@ -356,7 +345,7 @@ describe('Participant.vue', () => { async function testCanDemote() { const wrapper = mountParticipant(participant) const actionButton = findNcActionButton(wrapper, 'Demote from moderator') - expect(actionButton.exists()).toBe(true) + expect(actionButton.exists()).toBeTruthy() await actionButton.find('button').trigger('click') @@ -372,7 +361,7 @@ describe('Participant.vue', () => { async function testCannotDemote() { const wrapper = mountParticipant(participant) const actionButton = findNcActionButton(wrapper, 'Demote to moderator') - expect(actionButton.exists()).toBe(false) + expect(actionButton.exists()).toBeFalsy() } test('allows a moderator to demote a moderator', async () => { @@ -448,7 +437,7 @@ describe('Participant.vue', () => { async function testCanPromote() { const wrapper = mountParticipant(participant) const actionButton = findNcActionButton(wrapper, 'Promote to moderator') - expect(actionButton.exists()).toBe(true) + expect(actionButton.exists()).toBeTruthy() await actionButton.find('button').trigger('click') @@ -464,7 +453,7 @@ describe('Participant.vue', () => { async function testCannotPromote() { const wrapper = mountParticipant(participant) const actionButton = findNcActionButton(wrapper, 'Promote to moderator') - expect(actionButton.exists()).toBe(false) + expect(actionButton.exists()).toBeFalsy() } test('allows a moderator to promote a user to moderator', async () => { @@ -541,7 +530,7 @@ describe('Participant.vue', () => { participant.invitedActorId = 'alice@mail.com' const wrapper = mountParticipant(participant) const actionButton = findNcActionButton(wrapper, 'Resend invitation') - expect(actionButton.exists()).toBe(true) + expect(actionButton.exists()).toBeTruthy() await actionButton.find('button').trigger('click') @@ -556,14 +545,14 @@ describe('Participant.vue', () => { participant.actorType = ATTENDEE.ACTOR_TYPE.EMAILS const wrapper = mountParticipant(participant) const actionButton = findNcActionButton(wrapper, 'Resend invitation') - expect(actionButton.exists()).toBe(false) + expect(actionButton.exists()).toBeFalsy() }) test('does not display resend invitations action when not an email actor', async () => { participant.actorType = ATTENDEE.ACTOR_TYPE.USERS const wrapper = mountParticipant(participant) const actionButton = findNcActionButton(wrapper, 'Resend invitation') - expect(actionButton.exists()).toBe(false) + expect(actionButton.exists()).toBeFalsy() }) }) describe('removing participant', () => { @@ -582,7 +571,7 @@ describe('Participant.vue', () => { async function testCanRemove(buttonText = 'Remove participant') { const wrapper = mountParticipant(participant) const actionButton = findNcActionButton(wrapper, buttonText) - expect(actionButton.exists()).toBe(true) + expect(actionButton.exists()).toBeTruthy() await actionButton.find('button').trigger('click') @@ -590,6 +579,7 @@ describe('Participant.vue', () => { expect(dialog.exists()).toBeTruthy() const button = findNcButton(dialog, 'Remove') + expect(button.exists()).toBeTruthy() await button.find('button').trigger('click') expect(removeAction).toHaveBeenCalledWith(expect.anything(), { @@ -606,7 +596,7 @@ describe('Participant.vue', () => { async function testCannotRemove() { const wrapper = mountParticipant(participant) const actionButton = findNcActionButton(wrapper, 'Remove participant') - expect(actionButton.exists()).toBe(false) + expect(actionButton.exists()).toBeFalsy() } /** @@ -616,7 +606,7 @@ describe('Participant.vue', () => { async function testCanBan(buttonText = 'Remove participant', internalNote = 'test note') { const wrapper = mountParticipant(participant) const actionButton = findNcActionButton(wrapper, buttonText) - expect(actionButton.exists()).toBe(true) + expect(actionButton.exists()).toBeTruthy() await actionButton.find('button').trigger('click') @@ -648,7 +638,7 @@ describe('Participant.vue', () => { async function testCannotBan(buttonText = 'Remove participant') { const wrapper = mountParticipant(participant) const actionButton = findNcActionButton(wrapper, buttonText) - expect(actionButton.exists()).toBe(true) + expect(actionButton.exists()).toBeTruthy() await actionButton.find('button').trigger('click') @@ -753,13 +743,9 @@ describe('Participant.vue', () => { */ function testPinVisible() { const wrapper = mountParticipant(participant) - let actionTexts = wrapper.findAllComponents(NcActionText) - actionTexts = actionTexts.filter((actionText) => { - return actionText.props('name').includes('PIN') - }) - - expect(actionTexts.exists()).toBe(true) - expect(actionTexts.at(0).text()).toBe('123 456 78') + const actionText = findNcActionText(wrapper, 'Dial-in PIN') + expect(actionText.exists()).toBeTruthy() + expect(actionText.text()).toContain('123 456 78') } test('allows moderators to see dial-in PIN when available', () => { @@ -778,24 +764,16 @@ describe('Participant.vue', () => { conversation.participantType = PARTICIPANT.TYPE.USER participant.attendeePin = '12345678' const wrapper = mountParticipant(participant) - let actionTexts = wrapper.findAllComponents(NcActionText) - actionTexts = actionTexts.filter((actionText) => { - return actionText.props('title').includes('PIN') - }) - - expect(actionTexts.exists()).toBe(false) + const actionText = findNcActionText(wrapper, 'Dial-in PIN') + expect(actionText.exists()).toBeFalsy() }) test('does not show PIN field when not set', () => { conversation.participantType = PARTICIPANT.TYPE.MODERATOR participant.attendeePin = '' const wrapper = mountParticipant(participant) - let actionTexts = wrapper.findAllComponents(NcActionText) - actionTexts = actionTexts.filter((actionText) => { - return actionText.props('title').includes('PIN') - }) - - expect(actionTexts.exists()).toBe(false) + const actionText = findNcActionText(wrapper, 'Dial-in PIN') + expect(actionText.exists()).toBeFalsy() }) }) }) diff --git a/src/components/RoomSelector.spec.js b/src/components/RoomSelector.spec.js index 83a1ed97e8a..6f438d38527 100644 --- a/src/components/RoomSelector.spec.js +++ b/src/components/RoomSelector.spec.js @@ -5,22 +5,37 @@ import axios from '@nextcloud/axios' import { generateOcsUrl } from '@nextcloud/router' -import { flushPromises, shallowMount } from '@vue/test-utils' +import { flushPromises, mount } from '@vue/test-utils' +import { cloneDeep } from 'lodash' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { createStore, useStore } from 'vuex' import NcButton from '@nextcloud/vue/components/NcButton' import NcDialog from '@nextcloud/vue/components/NcDialog' import ConversationSearchResult from './LeftSidebar/ConversationsList/ConversationSearchResult.vue' import RoomSelector from './RoomSelector.vue' +import router from '../__mocks__/router.js' import { CONVERSATION } from '../constants.ts' +import storeConfig from '../store/storeConfig.js' import { useTokenStore } from '../stores/token.ts' import { generateOCSResponse } from '../test-helpers.js' +vi.mock('vuex', async () => { + const vuex = await vi.importActual('vuex') + return { + ...vuex, + useStore: vi.fn(), + } +}) + vi.mock('@nextcloud/axios', () => ({ default: { get: vi.fn(), }, })) +const ComponentStub = { + template: '
', +} const ConversationsSearchListVirtualStub = { props: { conversations: Array, @@ -41,7 +56,14 @@ describe('RoomSelector', () => { let conversations let tokenStore + let testStoreConfig + let store = null + beforeEach(() => { + testStoreConfig = cloneDeep(storeConfig) + store = createStore(testStoreConfig) + useStore.mockReturnValue(store) + tokenStore = useTokenStore() tokenStore.token = 'current-token' @@ -101,15 +123,15 @@ describe('RoomSelector', () => { axios.get.mockResolvedValue(generateOCSResponse({ payload })) - const wrapper = shallowMount(RoomSelector, { + const wrapper = mount(RoomSelector, { global: { + plugins: [router], stubs: { ConversationsSearchListVirtual: ConversationsSearchListVirtualStub, - ConversationSearchResult, - NcDialog, + NcModal: ComponentStub, }, }, - props: props, + props, }) // need to wait for re-render, otherwise the list is not rendered yet await flushPromises() @@ -137,6 +159,7 @@ describe('RoomSelector', () => { it('excludes current conversation if mounted inside of Talk', async () => { // Arrange + await router.push({ name: 'conversation', params: { token: 'current-token' } }) const wrapper = await mountRoomSelector({ isPlugin: false }) expect(axios.get).toHaveBeenCalledWith( generateOcsUrl('/apps/spreed/api/v4/room'), @@ -226,7 +249,7 @@ describe('RoomSelector', () => { await input.vm.$emit('update:modelValue', 'conversation') // Act: click trailing button - await input.vm.$emit('trailing-button-click') + await input.find('button').trigger('click') // Assert const list = wrapper.findAllComponents({ name: 'NcListItem' }) @@ -236,47 +259,29 @@ describe('RoomSelector', () => { it('emits select event on select', async () => { // Arrange const wrapper = await mountRoomSelector() - const eventHandler = vi.fn() - wrapper.vm.$on('select', eventHandler) // Act: click on second item, then click 'Select conversation' - const list = wrapper.findComponent({ name: 'ConversationsSearchListVirtual' }) const items = wrapper.findAllComponents(ConversationSearchResult) await items.at(1).vm.$emit('click', items.at(1).props('item')) - expect(items.at(1).emitted('click')[0][0]).toMatchObject(conversations[0]) - expect(list.emitted('select')[0][0]).toMatchObject(conversations[0]) await wrapper.findComponent(NcButton).vm.$emit('click') // Assert - expect(eventHandler).toHaveBeenCalledWith(conversations[0]) + const emitted = wrapper.emitted('select') + expect(emitted).toBeTruthy() + expect(emitted[0][0]).toMatchObject(conversations[0]) }) it('emits close event', async () => { // Arrange const wrapper = await mountRoomSelector() - const eventHandler = vi.fn() - wrapper.vm.$on('close', eventHandler) - - // Act: close dialog - const dialog = wrapper.findComponent(NcDialog) - await dialog.vm.$emit('update:open') - - // Assert - expect(eventHandler).toHaveBeenCalled() - }) - - it('emits close event on $root as plugin', async () => { - // Arrange - const wrapper = await mountRoomSelector({ isPlugin: true }) - const eventHandler = vi.fn() - wrapper.vm.$root.$on('close', eventHandler) // Act: close dialog const dialog = wrapper.findComponent(NcDialog) await dialog.vm.$emit('update:open') // Assert - expect(eventHandler).toHaveBeenCalled() + const emitted = wrapper.emitted('close') + expect(emitted).toBeTruthy() }) }) }) diff --git a/src/store/fileUploadStore.spec.js b/src/store/fileUploadStore.spec.js index da7a0cdf4d1..b72f2c67584 100644 --- a/src/store/fileUploadStore.spec.js +++ b/src/store/fileUploadStore.spec.js @@ -8,6 +8,7 @@ import { getUploader } from '@nextcloud/upload' import { cloneDeep } from 'lodash' import { createPinia, setActivePinia } from 'pinia' import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' +import { ref } from 'vue' import { createStore } from 'vuex' import { getDavClient } from '../services/DavClient.ts' import { shareFile } from '../services/filesSharingServices.ts' @@ -16,6 +17,11 @@ import { useActorStore } from '../stores/actor.ts' import { findUniquePath } from '../utils/fileUpload.js' import fileUploadStore from './fileUploadStore.js' +const getThreadMock = ref(0) +vi.mock('../composables/useGetThreadId.ts', () => ({ + useGetThreadId: vi.fn(() => getThreadMock), +})) + vi.mock('../services/DavClient.ts', () => ({ getDavClient: vi.fn(), })) @@ -72,7 +78,7 @@ describe('fileUploadStore', () => { storeConfig.getters.getAttachmentFolder = vi.fn().mockReturnValue(() => '/Talk') store = createStore(storeConfig) getDavClient.mockReturnValue(client) - // getUploader.mockReturnValue({ upload: uploadMock }) + getUploader.mockReturnValue({ upload: uploadMock }) console.error = vi.fn() }) @@ -338,7 +344,7 @@ describe('fileUploadStore', () => { const uploads = store.getters.getInitialisedUploads('upload-id1') expect(uploads).toHaveLength(1) - expect(uploads[0][1].file).toBe(files[0]) + expect(uploads[0][1].file).toStrictEqual(files[0]) }) test('discard an entire upload', async () => { diff --git a/src/test-helpers.js b/src/test-helpers.js index 4a6444d4855..97fb2f59160 100644 --- a/src/test-helpers.js +++ b/src/test-helpers.js @@ -4,21 +4,42 @@ */ import { cloneDeep } from 'lodash' +import NcActionButton from '@nextcloud/vue/components/NcActionButton' +import NcActionText from '@nextcloud/vue/components/NcActionText' +import NcButton from '@nextcloud/vue/components/NcButton' +import NcListItem from '@nextcloud/vue/components/NcListItem' // helpers /** * * @param {import('@vue/test-utils').Wrapper} wrapper root wrapper to look for NcActionButton * @param {string | Array} text or array of possible texts to look for NcButtons - * @return {import('@vue/test-utils').Wrapper} + * @return {import('@vue/test-utils').Wrapper | import('@vue/test-utils').ErrorWrapper} */ function findNcActionButton(wrapper, text) { - const actionButtons = wrapper.findAllComponents({ name: 'NcActionButton' }) + const actionButtons = wrapper.findAllComponents(NcActionButton) const items = (Array.isArray(text)) ? actionButtons.filter((actionButton) => text.includes(actionButton.text())) : actionButtons.filter((actionButton) => actionButton.text() === text) - if (!items.exists()) { - return items + if (!items.length) { + return wrapper.findComponent({ name: 'VTU__return-error-wrapper' }) // Returns ErrorWrapper + } + return items[0] +} + +/** + * + * @param {import('@vue/test-utils').Wrapper} wrapper root wrapper to look for NcActionText + * @param {string | Array} text or array of possible texts to look for + * @return {import('@vue/test-utils').Wrapper | import('@vue/test-utils').ErrorWrapper} + */ +function findNcActionText(wrapper, text) { + const actionTexts = wrapper.findAllComponents(NcActionText) + const items = (Array.isArray(text)) + ? actionTexts.filter((actionText) => text.includes(actionText.text())) + : actionTexts.filter((actionText) => actionText.text() === text || actionText.text().includes(text)) + if (!items.length) { + return wrapper.findComponent({ name: 'VTU__return-error-wrapper' }) // Returns ErrorWrapper } return items[0] } @@ -30,12 +51,12 @@ function findNcActionButton(wrapper, text) { * @return {import('@vue/test-utils').Wrapper} */ function findNcButton(wrapper, text) { - const buttons = wrapper.findAllComponents({ name: 'NcButton' }) + const buttons = wrapper.findAllComponents(NcButton) const items = (Array.isArray(text)) ? buttons.filter((button) => text.includes(button.text()) || text.includes(button.vm.ariaLabel)) : buttons.filter((button) => button.text() === text || button.vm.ariaLabel === text) - if (!items.exists()) { - return items + if (!items.length) { + return wrapper.findComponent({ name: 'VTU__return-error-wrapper' }) // Returns ErrorWrapper } return items[0] } @@ -47,7 +68,7 @@ function findNcButton(wrapper, text) { * @return {import('@vue/test-utils').Wrapper} */ function findNcListItems(wrapper, text) { - const listItems = wrapper.findAllComponents({ name: 'NcListItem' }) + const listItems = wrapper.findAllComponents(NcListItem) return (Array.isArray(text)) ? listItems.filter((listItem) => text.includes(listItem.vm.name)) : listItems.filter((listItem) => listItem.vm.name === text) @@ -114,6 +135,7 @@ function generateOCSErrorResponse({ headers = {}, payload = {}, status }) { export { findNcActionButton, + findNcActionText, findNcButton, findNcListItems, generateOCSErrorResponse, diff --git a/src/test-setup.js b/src/test-setup.js index bac4d2683f9..f40355bbbef 100644 --- a/src/test-setup.js +++ b/src/test-setup.js @@ -26,6 +26,10 @@ vi.mock('@nextcloud/dialogs', () => ({ TOAST_PERMANENT_TIMEOUT: -1, })) +vi.mock('@nextcloud/files/dav', () => ({ + defaultRemoteURL: () => 'https://nextcloud.local/remote.php/dav', +})) + vi.mock('@nextcloud/initial-state', () => ({ loadState: vi.fn().mockImplementation((app, key, fallback) => { return fallback diff --git a/vitest.config.js b/vitest.config.js index e77d263d086..5707916236b 100644 --- a/vitest.config.js +++ b/vitest.config.js @@ -12,12 +12,6 @@ export default defineConfig({ assetsInclude: ['**/*.tflite', '**/*.wasm'], test: { include: ['src/**/*.{test,spec}.?(c|m)[jt]s?(x)'], - exclude: [ - // TODO: migrate to Vue 3 - 'src/components/**', - // FIXME: broken after Vue 3 migration - 'src/store/fileUploadStore.spec.js', - ], server: { deps: { // Allow importing CSS from dependencies