diff --git a/tests/unit/authenticated-request.spec.js b/tests/unit/authenticated-request.spec.js new file mode 100644 index 00000000..e6cfb76a --- /dev/null +++ b/tests/unit/authenticated-request.spec.js @@ -0,0 +1,84 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +const { axiosMock } = vi.hoisted(() => ({ + axiosMock: vi.fn() +})); + +vi.mock('axios', () => ({ + default: axiosMock +})); + +import { authenticatedRequest } from '@/components/session/authenticatedRequest.vue'; + +describe('authenticatedRequest', () => { + beforeEach(() => { + axiosMock.mockReset(); + localStorage.clear(); + localStorage.setItem('token', 'token'); + }); + + it('suppresses known scanner lpr extraction failures and resolves with response', async () => { + const response = { + status: 500, + data: { + success: false, + data: { + message: 'Internal server error: License plate extraction failed.' + }, + meta: [], + includes: [] + } + }; + + axiosMock.mockRejectedValueOnce({ response }); + const catchCallable = vi.fn(); + + const result = await authenticatedRequest('/modules/scanner/lpr', 'POST', { base64_image: 'x' }, catchCallable); + + expect(result).toBe(response); + expect(catchCallable).not.toHaveBeenCalled(); + }); + + it('continues to reject non-suppressed scanner errors', async () => { + const error = { + response: { + status: 500, + data: { + success: false, + data: { + message: 'Internal server error: Different scanner problem.' + } + } + } + }; + + axiosMock.mockRejectedValueOnce(error); + const catchCallable = vi.fn(); + + await expect( + authenticatedRequest('/modules/scanner/lpr', 'POST', { base64_image: 'x' }, catchCallable) + ).rejects.toBe(error); + + expect(catchCallable).toHaveBeenCalledWith(error); + }); + + it('continues to reject non-lpr errors', async () => { + const error = { + response: { + status: 500, + data: { + success: false, + data: { + message: 'Internal server error: License plate extraction failed.' + } + } + } + }; + + axiosMock.mockRejectedValueOnce(error); + + await expect( + authenticatedRequest('/auth/session', 'GET') + ).rejects.toBe(error); + }); +}); diff --git a/tests/unit/navigation-menu-department.spec.js b/tests/unit/navigation-menu-department.spec.js new file mode 100644 index 00000000..969e94a2 --- /dev/null +++ b/tests/unit/navigation-menu-department.spec.js @@ -0,0 +1,161 @@ +// @vitest-environment jsdom +import { defineComponent, h, nextTick } from "vue"; +import { mount } from "@vue/test-utils"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +vi.mock("vue-router", async () => { + const { reactive } = await import("vue"); + const route = reactive({ params: { departmentId: undefined } }); + return { + useRoute: () => route, + __setDepartmentId: (departmentId) => { + route.params.departmentId = departmentId; + }, + }; +}); + +vi.mock("@/components/pagination/departmentTabs.vue", async () => { + const { ref } = await import("vue"); + const departments = ref([]); + const getDepartments = vi.fn(); + return { + departments, + getDepartments, + __departmentsRef: departments, + __getDepartmentsMock: getDepartments, + }; +}); + +vi.mock("@/components/displays/pagination/PaginationDisplayIsSmall.vue", async () => { + const { ref } = await import("vue"); + return { + isSmall: ref(true), + }; +}); + +vi.mock("@/components/displays/department/pos/steps/mobile/objects/PosDepartmentStepMobileFlow.vue", async () => { + const { ref } = await import("vue"); + return { + locations: { + location: ref(null), + getDistance: () => 0, + }, + }; +}); + +vi.mock("@/components/viewport/page/headers/ViewportHeaderSettings.vue", async () => { + const { ref } = await import("vue"); + return { + toggleExpanded: vi.fn(), + isOpen: ref(false), + getDepartmentSelectionVisibility: vi.fn(() => true), + }; +}); + +vi.mock("@/components/session/token/SessionUser.vue", () => { + const selectDepartment = vi.fn(); + const canAccessDepartment = vi.fn(() => true); + const getDepartmentIdFromUrl = vi.fn(() => undefined); + + return { + SessionUser: { + canAccessDepartment: (...args) => canAccessDepartment(...args), + functions: { + selectDepartment: (...args) => selectDepartment(...args), + getDepartmentIdFromUrl: (...args) => getDepartmentIdFromUrl(...args), + }, + objects: { + global: { + language: { + select: "Select", + }, + }, + departments: { + meta: { + labels: { + single: "department", + }, + }, + }, + }, + }, + __sessionMocks: { + selectDepartment, + canAccessDepartment, + getDepartmentIdFromUrl, + }, + }; +}); + +vi.mock("buefy", () => { + const Tooltip = defineComponent({ + name: "MockTooltip", + setup(_props, { slots }) { + return () => h("div", { "data-testid": "mock-tooltip" }, slots.default ? slots.default() : []); + }, + }); + + const Button = defineComponent({ + name: "MockButton", + props: { + label: { + type: String, + default: "", + }, + }, + setup(props) { + return () => h("button", { "data-testid": "mock-button-label" }, props.label); + }, + }); + + const passthrough = defineComponent({ + name: "MockPassthrough", + setup(_props, { slots }) { + return () => h("div", slots.default ? slots.default() : []); + }, + }); + + return { + BTooltip: Tooltip, + BButton: Button, + BDropdownItem: passthrough, + BField: passthrough, + BIcon: passthrough, + BSelect: passthrough, + }; +}); + +import NavigationMenuDepartment from "@/components/viewport/page/headers/menu/NavigationMenuDepartment.vue"; +import { __setDepartmentId } from "vue-router"; +import { __departmentsRef, __getDepartmentsMock } from "@/components/pagination/departmentTabs.vue"; +import { __sessionMocks } from "@/components/session/token/SessionUser.vue"; + +describe("NavigationMenuDepartment", () => { + beforeEach(() => { + __getDepartmentsMock.mockReset(); + __sessionMocks.selectDepartment.mockReset(); + __sessionMocks.canAccessDepartment.mockReset(); + __sessionMocks.getDepartmentIdFromUrl.mockReset(); + + __sessionMocks.canAccessDepartment.mockReturnValue(true); + __sessionMocks.getDepartmentIdFromUrl.mockReturnValue(undefined); + __departmentsRef.value = [ + { id: 9, name: "Nord", description: "", latitude: 55.6, longitude: 12.5, branding: 0 }, + { id: 12, name: "Taastrup", description: "", latitude: 55.7, longitude: 12.4, branding: 0 }, + ]; + __setDepartmentId(undefined); + }); + + it("updates the selected department label when route department changes", async () => { + const wrapper = mount(NavigationMenuDepartment); + + expect(wrapper.get("[data-testid='mock-button-label']").text()).toBe("Select department"); + expect(__getDepartmentsMock).toHaveBeenCalledTimes(1); + + __setDepartmentId("12"); + await nextTick(); + + expect(wrapper.get("[data-testid='mock-button-label']").text()).toBe("Taastrup"); + }); +}); + diff --git a/tests/unit/navigation-menu-items-admin-contract.spec.js b/tests/unit/navigation-menu-items-admin-contract.spec.js new file mode 100644 index 00000000..7ce465e2 --- /dev/null +++ b/tests/unit/navigation-menu-items-admin-contract.spec.js @@ -0,0 +1,18 @@ +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { describe, expect, it } from 'vitest'; + +const root = process.cwd(); +const source = readFileSync( + join(root, 'src/components/models/navigation/items/NavigationMenuItemsAdmin.vue'), + 'utf8' +).replace(/\r\n/g, '\n'); + +describe('NavigationMenuItemsAdmin contract', () => { + it('derives department id from reactive route context', () => { + expect(source).toContain('import { useRoute } from "vue-router";'); + expect(source).toContain('const route = useRoute();'); + expect(source).toContain('route.params.departmentId'); + expect(source).toContain('route.path.match(/^\\/admin\\/(\\d+)(?:\\/|$)/)'); + }); +});