diff --git a/app/javascript/widget/App.vue b/app/javascript/widget/App.vue index 22b7954da..892046f37 100755 --- a/app/javascript/widget/App.vue +++ b/app/javascript/widget/App.vue @@ -8,7 +8,7 @@ import availabilityMixin from 'widget/mixins/availability'; import { getLocale } from './helpers/urlParamsHelper'; import { isEmptyObject } from 'widget/helpers/utils'; import Spinner from 'shared/components/Spinner.vue'; -import { useReplaceRoute } from 'widget/composables/useReplaceRoute'; +import { useRouterHelper } from 'widget/composables/useRouterHelper'; import { getExtraSpaceToScroll, loadedEventConfig, @@ -29,7 +29,7 @@ export default { }, mixins: [availabilityMixin, configMixin, darkModeMixin], setup() { - const replaceRoute = useReplaceRoute(); + const { replaceRoute } = useRouterHelper(); return { replaceRoute }; }, data() { diff --git a/app/javascript/widget/components/ChatFooter.vue b/app/javascript/widget/components/ChatFooter.vue index 07774d76e..20401dbf8 100755 --- a/app/javascript/widget/components/ChatFooter.vue +++ b/app/javascript/widget/components/ChatFooter.vue @@ -6,7 +6,7 @@ import FooterReplyTo from 'widget/components/FooterReplyTo.vue'; import ChatInputWrap from 'widget/components/ChatInputWrap.vue'; import { BUS_EVENTS } from 'shared/constants/busEvents'; import { sendEmailTranscript } from 'widget/api/conversation'; -import { useReplaceRoute } from 'widget/composables/useReplaceRoute'; +import { useRouterHelper } from 'widget/composables/useRouterHelper'; import { IFrameHelper } from '../helpers/utils'; import { CHATWOOT_ON_START_CONVERSATION } from '../constants/sdkEvents'; @@ -17,7 +17,7 @@ export default { FooterReplyTo, }, setup() { - const replaceRoute = useReplaceRoute(); + const { replaceRoute } = useRouterHelper(); return { replaceRoute }; }, data() { diff --git a/app/javascript/widget/components/ChatHeader.vue b/app/javascript/widget/components/ChatHeader.vue index 69753e9ca..1bff57648 100644 --- a/app/javascript/widget/components/ChatHeader.vue +++ b/app/javascript/widget/components/ChatHeader.vue @@ -3,7 +3,7 @@ import availabilityMixin from 'widget/mixins/availability'; import nextAvailabilityTime from 'widget/mixins/nextAvailabilityTime'; import FluentIcon from 'shared/components/FluentIcon/Index.vue'; import HeaderActions from './HeaderActions.vue'; -import { useReplaceRoute } from 'widget/composables/useReplaceRoute'; +import { useRouterHelper } from 'widget/composables/useRouterHelper'; import darkMixin from 'widget/mixins/darkModeMixin.js'; export default { @@ -36,7 +36,7 @@ export default { }, }, setup() { - const replaceRoute = useReplaceRoute(); + const { replaceRoute } = useRouterHelper(); return { replaceRoute }; }, computed: { diff --git a/app/javascript/widget/components/PreChat/Form.vue b/app/javascript/widget/components/PreChat/Form.vue index fafb56de4..3a0e5e6f4 100644 --- a/app/javascript/widget/components/PreChat/Form.vue +++ b/app/javascript/widget/components/PreChat/Form.vue @@ -6,7 +6,7 @@ import { getContrastingTextColor } from '@chatwoot/utils'; import { isEmptyObject } from 'widget/helpers/utils'; import { getRegexp } from 'shared/helpers/Validators'; import messageFormatterMixin from 'shared/mixins/messageFormatterMixin'; -import { useReplaceRoute } from 'widget/composables/useReplaceRoute'; +import { useRouterHelper } from 'widget/composables/useRouterHelper'; import darkModeMixin from 'widget/mixins/darkModeMixin'; import configMixin from 'widget/mixins/configMixin'; @@ -23,7 +23,7 @@ export default { }, }, setup() { - const replaceRoute = useReplaceRoute(); + const { replaceRoute } = useRouterHelper(); return { replaceRoute }; }, data() { diff --git a/app/javascript/widget/composables/useReplaceRoute.js b/app/javascript/widget/composables/useRouterHelper.js similarity index 69% rename from app/javascript/widget/composables/useReplaceRoute.js rename to app/javascript/widget/composables/useRouterHelper.js index af044b806..eaa2d0220 100644 --- a/app/javascript/widget/composables/useReplaceRoute.js +++ b/app/javascript/widget/composables/useRouterHelper.js @@ -1,25 +1,26 @@ import { useRouter, useRoute } from 'dashboard/composables/route'; /** - * Composable for replacing the current route with a new one if it's different. - * @returns {Function} A function that replaces the current route. + * Composable for handling router-related operations. + * @returns {Object} An object containing methods for router manipulation. */ -export const useReplaceRoute = () => { +export function useRouterHelper() { const router = useRouter(); const route = useRoute(); - /** * Replaces the current route with a new one if it's different. * @param {string} name - The name of the route to replace with. * @param {Object} params - The params to pass to the new route. * @returns {Promise|undefined} A promise that resolves when the navigation is complete, or undefined if no navigation occurs. */ - return (name, params = {}) => { + const replaceRoute = async (name, params = {}) => { if (route.name !== name) { return router.replace({ name, params }); } return undefined; }; -}; -export default useReplaceRoute; + return { + replaceRoute, + }; +} diff --git a/app/javascript/widget/mixins/specs/useRouterHelper.spec.js b/app/javascript/widget/mixins/specs/useRouterHelper.spec.js new file mode 100644 index 000000000..6a2247f72 --- /dev/null +++ b/app/javascript/widget/mixins/specs/useRouterHelper.spec.js @@ -0,0 +1,59 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { useRouterHelper } from '../../composables/useRouterHelper'; + +const mockRouter = { + currentRoute: { name: 'initialRoute' }, + replace: vi.fn(), +}; + +vi.mock('../../composables/useRouterHelper', () => ({ + useRouterHelper: () => ({ + replaceRoute: async (name, params = {}) => { + if (mockRouter.currentRoute.name !== name) { + return mockRouter.replace({ name, params }); + } + return undefined; + }, + }), +})); + +describe('useRouterHelper', () => { + let routerHelper; + + beforeEach(() => { + vi.resetAllMocks(); + mockRouter.currentRoute = { name: 'initialRoute' }; + routerHelper = useRouterHelper(); + }); + + describe('replaceRoute', () => { + it('should replace route when current route is different', async () => { + mockRouter.replace.mockResolvedValue(undefined); + + await routerHelper.replaceRoute('newRoute', { id: 1 }); + + expect(mockRouter.replace).toHaveBeenCalledWith({ + name: 'newRoute', + params: { id: 1 }, + }); + }); + + it('should not replace route when current route is the same', async () => { + mockRouter.currentRoute.name = 'sameRoute'; + + const result = await routerHelper.replaceRoute('sameRoute'); + + expect(mockRouter.replace).not.toHaveBeenCalled(); + expect(result).toBeUndefined(); + }); + + it('should handle router replace rejection', async () => { + const error = new Error('Navigation aborted'); + mockRouter.replace.mockRejectedValue(error); + + await expect(routerHelper.replaceRoute('newRoute')).rejects.toThrow( + 'Navigation aborted' + ); + }); + }); +}); diff --git a/app/javascript/widget/views/Home.vue b/app/javascript/widget/views/Home.vue index f4615270b..60e4449cc 100755 --- a/app/javascript/widget/views/Home.vue +++ b/app/javascript/widget/views/Home.vue @@ -5,7 +5,7 @@ import ArticleCardSkeletonLoader from 'widget/components/ArticleCardSkeletonLoad import { mapGetters } from 'vuex'; import darkModeMixin from 'widget/mixins/darkModeMixin'; -import { useReplaceRoute } from 'widget/composables/useReplaceRoute'; +import { useRouterHelper } from 'widget/composables/useRouterHelper'; import configMixin from 'widget/mixins/configMixin'; export default { @@ -17,7 +17,7 @@ export default { }, mixins: [configMixin, darkModeMixin], setup() { - const replaceRoute = useReplaceRoute(); + const { replaceRoute } = useRouterHelper(); return { replaceRoute }; }, computed: { diff --git a/app/javascript/widget/views/PreChatForm.vue b/app/javascript/widget/views/PreChatForm.vue index 393214b07..43417e8c3 100644 --- a/app/javascript/widget/views/PreChatForm.vue +++ b/app/javascript/widget/views/PreChatForm.vue @@ -1,7 +1,7 @@