diff --git a/src/components/LeftMenu.vue b/src/components/LeftMenu.vue index 644f831d4..9a9949499 100644 --- a/src/components/LeftMenu.vue +++ b/src/components/LeftMenu.vue @@ -41,7 +41,6 @@ const filteredItems = computed(() => { return locale.includes(normalizedSearch); }); }); - const filteredPinnedModules = computed(() => { if (!search.value) return pinnedModules.value; const normalizedSearch = search.value @@ -72,7 +71,7 @@ watch( items.value = []; getRoutes(); }, - { deep: true } + { deep: true }, ); function findMatches(search, item) { @@ -104,33 +103,40 @@ function addChildren(module, route, parent) { } function getRoutes() { - if (props.source === 'main') { - const modules = Object.assign([], navigation.getModules().value); - - for (const item of modules) { - const moduleDef = routes.find( - (route) => toLowerCamel(route.name) === item.module - ); - if (!moduleDef) continue; - item.children = []; - - addChildren(item.module, moduleDef, item.children); - } - - items.value = modules; + const handleRoutes = { + main: getMainRoutes, + card: getCardRoutes, + }; + try { + handleRoutes[props.source](); + } catch (error) { + throw new Error(`Method is not defined`); } +} +function getMainRoutes() { + const modules = Object.assign([], navigation.getModules().value); - if (props.source === 'card') { - const currentRoute = route.matched[1]; - const currentModule = toLowerCamel(currentRoute.name); - let moduleDef = routes.find( - (route) => toLowerCamel(route.name) === currentModule + for (const item of modules) { + const moduleDef = routes.find( + (route) => toLowerCamel(route.name) === item.module, ); + if (!moduleDef) continue; + item.children = []; - if (!moduleDef) return; - if (!moduleDef?.menus) moduleDef = betaGetRoutes(); - addChildren(currentModule, moduleDef, items.value); + addChildren(item.module, moduleDef, item.children); } + + items.value = modules; +} + +function getCardRoutes() { + const currentRoute = route.matched[1]; + const currentModule = toLowerCamel(currentRoute.name); + let moduleDef = routes.find((route) => toLowerCamel(route.name) === currentModule); + + if (!moduleDef) return; + if (!moduleDef?.menus) moduleDef = betaGetRoutes(); + addChildren(currentModule, moduleDef, items.value); } function betaGetRoutes() { @@ -223,9 +229,16 @@ const searchModule = () => { </template> <template v-for="(item, index) in filteredItems" :key="item.name"> <template - v-if="search ||item.children && !filteredPinnedModules.has(item.name)" + v-if=" + search || + (item.children && !filteredPinnedModules.has(item.name)) + " > - <LeftMenuItem :item="item" group="modules" :class="search && index === 0 ? 'searched' : ''"> + <LeftMenuItem + :item="item" + group="modules" + :class="search && index === 0 ? 'searched' : ''" + > <template #side> <QBtn v-if="item.isPinned === true" @@ -342,7 +355,7 @@ const searchModule = () => { .header { color: var(--vn-label-color); } -.searched{ +.searched { background-color: var(--vn-section-hover-color); } </style> diff --git a/src/components/__tests__/Leftmenu.spec.js b/src/components/__tests__/Leftmenu.spec.js index 10d9d66fb..4ab8b527f 100644 --- a/src/components/__tests__/Leftmenu.spec.js +++ b/src/components/__tests__/Leftmenu.spec.js @@ -1,9 +1,12 @@ -import { vi, describe, expect, it, beforeAll } from 'vitest'; +import { vi, describe, expect, it, beforeAll, beforeEach, afterEach } from 'vitest'; import { createWrapper, axios } from 'app/test/vitest/helper'; import Leftmenu from 'components/LeftMenu.vue'; - +import * as vueRouter from 'vue-router'; import { useNavigationStore } from 'src/stores/useNavigationStore'; +let vm; +let navigation; + vi.mock('src/router/modules', () => ({ default: [ { @@ -21,6 +24,16 @@ vi.mock('src/router/modules', () => ({ { path: '', name: 'CustomerMain', + meta: { + menu: 'Customer', + menuChildren: [ + { + name: 'CustomerCreditContracts', + title: 'creditContracts', + icon: 'vn:solunion', + }, + ], + }, children: [ { path: 'list', @@ -28,6 +41,13 @@ vi.mock('src/router/modules', () => ({ meta: { title: 'list', icon: 'view_list', + menuChildren: [ + { + name: 'CustomerCreditContracts', + title: 'creditContracts', + icon: 'vn:solunion', + }, + ], }, }, { @@ -44,51 +64,325 @@ vi.mock('src/router/modules', () => ({ }, ], })); - -describe('Leftmenu', () => { - let vm; - let navigation; - beforeAll(() => { - vi.spyOn(axios, 'get').mockResolvedValue({ - data: [], - }); - - vm = createWrapper(Leftmenu, { - propsData: { - source: 'main', +vi.spyOn(vueRouter, 'useRoute').mockReturnValue({ + matched: [ + { + path: '/', + redirect: { + name: 'Dashboard', }, - }).vm; - - navigation = useNavigationStore(); - navigation.fetchPinned = vi.fn().mockReturnValue(Promise.resolve(true)); - navigation.getModules = vi.fn().mockReturnValue({ - value: [ + name: 'Main', + meta: {}, + props: { + default: false, + }, + children: [ { - name: 'customer', - title: 'customer.pageTitles.customers', - icon: 'vn:customer', - module: 'customer', + path: '/dashboard', + name: 'Dashboard', + meta: { + title: 'dashboard', + icon: 'dashboard', + }, }, ], + }, + { + path: '/customer', + redirect: { + name: 'CustomerMain', + }, + name: 'Customer', + meta: { + title: 'customers', + icon: 'vn:client', + moduleName: 'Customer', + keyBinding: 'c', + menu: 'customer', + }, + }, + ], + query: {}, + params: {}, + meta: { moduleName: 'mockName' }, + path: 'mockName/1', + name: 'Customer', +}); +function mount(source = 'main') { + vi.spyOn(axios, 'get').mockResolvedValue({ + data: [], + }); + const wrapper = createWrapper(Leftmenu, { + propsData: { + source, + }, + }); + + navigation = useNavigationStore(); + navigation.fetchPinned = vi.fn().mockReturnValue(Promise.resolve(true)); + navigation.getModules = vi.fn().mockReturnValue({ + value: [ + { + name: 'customer', + title: 'customer.pageTitles.customers', + icon: 'vn:customer', + module: 'customer', + }, + ], + }); + return wrapper; +} + +describe('getRoutes', () => { + afterEach(() => vi.clearAllMocks()); + const getRoutes = vi.fn().mockImplementation((props, getMethodA, getMethodB) => { + const handleRoutes = { + methodA: getMethodA, + methodB: getMethodB, + }; + try { + handleRoutes[props.source](); + } catch (error) { + throw Error('Method not defined'); + } + }); + + const getMethodA = vi.fn(); + const getMethodB = vi.fn(); + const fn = (props) => getRoutes(props, getMethodA, getMethodB); + + it('should call getMethodB when source is card', () => { + let props = { source: 'methodB' }; + fn(props); + + expect(getMethodB).toHaveBeenCalled(); + expect(getMethodA).not.toHaveBeenCalled(); + }); + it('should call getMethodA when source is main', () => { + let props = { source: 'methodA' }; + fn(props); + + expect(getMethodA).toHaveBeenCalled(); + expect(getMethodB).not.toHaveBeenCalled(); + }); + + it('should call getMethodA when source is not exists or undefined', () => { + let props = { source: 'methodC' }; + expect(() => fn(props)).toThrowError('Method not defined'); + + expect(getMethodA).not.toHaveBeenCalled(); + expect(getMethodB).not.toHaveBeenCalled(); + }); +}); + +describe('Leftmenu as card', () => { + beforeAll(() => { + vm = mount('card').vm; + }); + + it('should get routes for card source', async () => { + vm.getRoutes(); + }); +}); +describe('Leftmenu as main', () => { + beforeEach(() => { + vm = mount().vm; + }); + + it('should initialize with default props', () => { + expect(vm.source).toBe('main'); + }); + + it('should filter items based on search input', async () => { + vm.search = 'cust'; + await vm.$nextTick(); + expect(vm.filteredItems[0].name).toEqual('customer'); + expect(vm.filteredItems[0].module).toEqual('customer'); + }); + it('should filter items based on search input', async () => { + vm.search = 'Rou'; + await vm.$nextTick(); + expect(vm.filteredItems).toEqual([]); + }); + + it('should return pinned items', () => { + vm.items = [ + { name: 'Item 1', isPinned: false }, + { name: 'Item 2', isPinned: true }, + ]; + expect(vm.pinnedModules).toEqual( + new Map([['Item 2', { name: 'Item 2', isPinned: true }]]), + ); + }); + + it('should find matches in routes', () => { + const search = 'child1'; + const item = { + children: [ + { name: 'child1', children: [] }, + { name: 'child2', children: [] }, + ], + }; + const matches = vm.findMatches(search, item); + expect(matches).toEqual([{ name: 'child1', children: [] }]); + }); + it('should not proceed if event is already prevented', async () => { + const item = { module: 'testModule', isPinned: false }; + const event = { + preventDefault: vi.fn(), + stopPropagation: vi.fn(), + defaultPrevented: true, + }; + + await vm.togglePinned(item, event); + + expect(event.preventDefault).not.toHaveBeenCalled(); + expect(event.stopPropagation).not.toHaveBeenCalled(); + }); + + it('should call quasar.notify with success message', async () => { + const item = { module: 'testModule', isPinned: false }; + const event = { + preventDefault: vi.fn(), + stopPropagation: vi.fn(), + defaultPrevented: false, + }; + const response = { data: { id: 1 } }; + + vi.spyOn(axios, 'post').mockResolvedValue(response); + vi.spyOn(vm.quasar, 'notify'); + + await vm.togglePinned(item, event); + + expect(vm.quasar.notify).toHaveBeenCalledWith({ + message: 'Data saved', + type: 'positive', }); }); - it('should return a proper formated object with two child items', async () => { - const expectedMenuItem = [ - { - children: null, - name: 'CustomerList', - title: 'globals.pageTitles.list', - icon: 'view_list', - }, - { - children: null, - name: 'CustomerCreate', - title: 'globals.pageTitles.createCustomer', - icon: 'vn:addperson', - }, - ]; - const firstMenuItem = vm.items[0]; - expect(firstMenuItem.children).toEqual(expect.arrayContaining(expectedMenuItem)); + it('should handle a single matched route with a menu', () => { + const route = { + matched: [{ meta: { menu: 'customer' } }], + }; + + const result = vm.betaGetRoutes(); + + expect(result.meta.menu).toEqual(route.matched[0].meta.menu); + }); + it('should get routes for main source', () => { + vm.props.source = 'main'; + vm.getRoutes(); + expect(navigation.getModules).toHaveBeenCalled(); + }); + + it('should find direct child matches', () => { + const search = 'child1'; + const item = { + children: [{ name: 'child1' }, { name: 'child2' }], + }; + const result = vm.findMatches(search, item); + expect(result).toEqual([{ name: 'child1' }]); + }); + + it('should find nested child matches', () => { + const search = 'child3'; + const item = { + children: [ + { name: 'child1' }, + { + name: 'child2', + children: [{ name: 'child3' }], + }, + ], + }; + const result = vm.findMatches(search, item); + expect(result).toEqual([{ name: 'child3' }]); + }); +}); + +describe('normalize', () => { + beforeAll(() => { + vm = mount('card').vm; + }); + it('should normalize and lowercase text', () => { + const input = 'ÁÉÍÓÚáéíóú'; + const expected = 'aeiouaeiou'; + expect(vm.normalize(input)).toBe(expected); + }); + + it('should handle empty string', () => { + const input = ''; + const expected = ''; + expect(vm.normalize(input)).toBe(expected); + }); + + it('should handle text without diacritics', () => { + const input = 'hello'; + const expected = 'hello'; + expect(vm.normalize(input)).toBe(expected); + }); + + it('should handle mixed text', () => { + const input = 'Héllo Wórld!'; + const expected = 'hello world!'; + expect(vm.normalize(input)).toBe(expected); + }); +}); + +describe('addChildren', () => { + const module = 'testModule'; + beforeEach(() => { + vm = mount().vm; + vi.clearAllMocks(); + }); + + it('should add menu items to parent if matches are found', () => { + const parent = 'testParent'; + const route = { + meta: { + menu: 'testMenu', + }, + children: [{ name: 'child1' }, { name: 'child2' }], + }; + vm.addChildren(module, route, parent); + + expect(navigation.addMenuItem).toHaveBeenCalled(); + }); + + it('should handle routes with no meta menu', () => { + const route = { + meta: {}, + menus: {}, + }; + + const parent = []; + + vm.addChildren(module, route, parent); + expect(navigation.addMenuItem).toHaveBeenCalled(); + }); + + it('should handle empty parent array', () => { + const parent = []; + const route = { + meta: { + menu: 'child11', + }, + children: [ + { + name: 'child1', + meta: { + menuChildren: [ + { + name: 'CustomerCreditContracts', + title: 'creditContracts', + icon: 'vn:solunion', + }, + ], + }, + }, + ], + }; + vm.addChildren(module, route, parent); + expect(navigation.addMenuItem).toHaveBeenCalled(); }); }); diff --git a/src/stores/__tests__/useNavigationStore.spec.js b/src/stores/__tests__/useNavigationStore.spec.js new file mode 100644 index 000000000..c5df6157e --- /dev/null +++ b/src/stores/__tests__/useNavigationStore.spec.js @@ -0,0 +1,153 @@ +import { setActivePinia, createPinia } from 'pinia'; +import { describe, beforeEach, afterEach, it, expect, vi, beforeAll } from 'vitest'; +import { useNavigationStore } from '../useNavigationStore'; +import axios from 'axios'; + +let store; + +vi.mock('src/router/modules', () => [ + { name: 'Item', meta: {} }, + { name: 'Shelving', meta: {} }, + { name: 'Order', meta: {} }, +]); + +vi.mock('src/filters', () => ({ + toLowerCamel: vi.fn((name) => name.toLowerCase()), +})); + +const modulesMock = [ + { + name: 'Item', + children: null, + title: 'globals.pageTitles.undefined', + icon: undefined, + module: 'item', + isPinned: true, + }, + { + name: 'Shelving', + children: null, + title: 'globals.pageTitles.undefined', + icon: undefined, + module: 'shelving', + isPinned: false, + }, + { + name: 'Order', + children: null, + title: 'globals.pageTitles.undefined', + icon: undefined, + module: 'order', + isPinned: false, + }, +]; + +const pinnedModulesMock = [ + { + name: 'Item', + children: null, + title: 'globals.pageTitles.undefined', + icon: undefined, + module: 'item', + isPinned: true, + }, +]; + +describe('useNavigationStore', () => { + beforeEach(() => { + setActivePinia(createPinia()); + vi.spyOn(axios, 'get').mockResolvedValue({ data: true }); + store = useNavigationStore(); + store.getModules = vi.fn().mockReturnValue({ + value: modulesMock, + }); + store.getPinnedModules = vi.fn().mockReturnValue({ + value: pinnedModulesMock, + }); + }); + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should return modules with correct structure', () => { + const store = useNavigationStore(); + const modules = store.getModules(); + + expect(modules.value).toEqual(modulesMock); + }); + + it('should return pinned modules', () => { + const store = useNavigationStore(); + const pinnedModules = store.getPinnedModules(); + + expect(pinnedModules.value).toEqual(pinnedModulesMock); + }); + + it('should toggle pinned modules', () => { + const store = useNavigationStore(); + + store.togglePinned('item'); + store.togglePinned('shelving'); + expect(store.pinnedModules).toEqual(['item', 'shelving']); + + store.togglePinned('item'); + expect(store.pinnedModules).toEqual(['shelving']); + }); + + it('should fetch pinned modules', async () => { + vi.spyOn(axios, 'get').mockResolvedValue({ + data: [{ id: 1, workerFk: 9, moduleFk: 'order', position: 1 }], + }); + const store = useNavigationStore(); + await store.fetchPinned(); + + expect(store.pinnedModules).toEqual(['order']); + }); + + it('should add menu item correctly', () => { + const store = useNavigationStore(); + const module = 'customer'; + const parent = []; + const route = { + name: 'customer', + title: 'Customer', + icon: 'customer', + meta: { + keyBinding: 'ctrl+shift+c', + name: 'customer', + title: 'Customer', + icon: 'customer', + menu: 'customer', + menuChildren: [{ name: 'customer', title: 'Customer', icon: 'customer' }], + }, + }; + + const result = store.addMenuItem(module, route, parent); + const expectedItem = { + children: [ + { + icon: 'customer', + name: 'customer', + title: 'globals.pageTitles.Customer', + }, + ], + icon: 'customer', + keyBinding: 'ctrl+shift+c', + name: 'customer', + title: 'globals.pageTitles.Customer', + }; + expect(result).toEqual(expectedItem); + expect(parent.length).toBe(1); + expect(parent).toEqual([expectedItem]); + }); + + it('should not add menu item if condition is not met', () => { + const store = useNavigationStore(); + const module = 'testModule'; + const route = { meta: { hidden: true, menuchildren: {} } }; + const parent = []; + const result = store.addMenuItem(module, route, parent); + expect(result).toBeUndefined(); + expect(parent.length).toBe(0); + }); +});