// @vitest-environment jsdom import { defineComponent, h, nextTick } from "vue"; import { mount } from "@vue/test-utils"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; vi.mock("vue-router", () => { const push = vi.fn(); return { useRouter: () => ({ push }), __routerPush: push, }; }); vi.mock("@/components/global/BrandedLoadingScreen.vue", () => { return { default: defineComponent({ name: "MockBrandedLoadingScreen", props: { title: { type: String, default: "", }, subtitle: { type: String, default: "", }, testId: { type: String, default: "branded-loading-screen", }, statusTestId: { type: String, default: "", }, ariaLabel: { type: String, default: "", }, }, setup(props) { return () => h( "div", { "data-testid": props.testId, "data-aria-label": props.ariaLabel, }, [ h("span", { "data-testid": props.statusTestId || undefined }, props.title), props.subtitle ? h("small", props.subtitle) : null, ] ); }, }), }; }); vi.mock("@/components/displays/department/pos/steps/mobile/elements/PosDepartmentStepMobile1Location.vue", () => { return { default: defineComponent({ name: "MockPosDepartmentStepMobile1Location", props: { enableHighAccuracy: { type: Boolean, default: true, }, maximumAge: { type: Number, default: 30000, }, timeout: { type: Number, default: 27000, }, }, setup(props) { return () => h("div", { "data-testid": "location-probe", "data-enable-high-accuracy": String(props.enableHighAccuracy), "data-maximum-age": String(props.maximumAge), "data-timeout": String(props.timeout), }); }, }), }; }); vi.mock("@/components/viewport/conditions/ViewportTypes.vue", async () => { const { ref } = await import("vue"); const isMobile = ref(false); return { isMobile, __isMobileRef: isMobile, }; }); vi.mock("@/components/pagination/departmentTabs.vue", async () => { const { ref } = await import("vue"); const departments = ref([]); const getDepartments = vi.fn(() => Promise.resolve()); return { departments, getDepartments, __departmentsRef: departments, __getDepartmentsMock: getDepartments, }; }); vi.mock("@/components/displays/department/pos/steps/mobile/objects/PosDepartmentStepMobileFlow.vue", async () => { const { ref } = await import("vue"); const location = ref(null); const defaultTimeout = ref(500); const locations = { location, defaultTimeout, normalizeCoordinatePair: (coordinates, { allowZeroPair = true } = {}) => { const latitude = Number(coordinates?.latitude); const longitude = Number(coordinates?.longitude); if (!Number.isFinite(latitude) || !Number.isFinite(longitude)) { return null; } if (latitude < -90 || latitude > 90 || longitude < -180 || longitude > 180) { return null; } if (!allowZeroPair && latitude === 0 && longitude === 0) { return null; } return { latitude, longitude }; }, set: (newLocation) => { location.value = newLocation; }, get: () => location.value, clear: () => { location.value = null; }, getDistance: (from, to) => { return Math.abs(from.latitude - to.latitude) + Math.abs(from.longitude - to.longitude); }, }; return { locations, __locationRef: location, __defaultTimeoutRef: defaultTimeout, }; }); vi.mock("@/components/session/token/SessionUser.vue", async () => { const { ref } = await import("vue"); const initiated = ref(false); const permissions = ref([]); const isSubuser = ref(false); const token = ref(null); const hasPermission = (permission) => permissions.value.includes(permission); const SessionUser = { initiated, permissions, isSubuser, token, isInitiated: () => initiated.value, hasToken: () => token.value !== null, canAccessSuperUser: () => hasPermission("superuser"), canAccessAdmin: () => hasPermission("admin") || hasPermission("superuser"), canAccessUser: () => hasPermission("user"), canAccessGuest: () => true, canAccessAssignedDepartment: (id) => hasPermission(`department_access_${Number.parseInt(id, 10)}`), }; return { SessionUser, __sessionState: { initiated, permissions, isSubuser, token, }, }; }); import DefaultPage from "@/views/DefaultPage.vue"; import { __routerPush } from "vue-router"; import { __isMobileRef } from "@/components/viewport/conditions/ViewportTypes.vue"; import { __departmentsRef, __getDepartmentsMock } from "@/components/pagination/departmentTabs.vue"; import { __defaultTimeoutRef, __locationRef, } from "@/components/displays/department/pos/steps/mobile/objects/PosDepartmentStepMobileFlow.vue"; import { __sessionState } from "@/components/session/token/SessionUser.vue"; let wrapper = null; const mountDefaultPage = () => { wrapper = mount(DefaultPage, { global: { mocks: { $t: (key, params = {}) => (params.name ? `${key}:${params.name}` : key), }, }, }); return wrapper; }; const setAdminSession = (permissions = ["admin"]) => { __sessionState.initiated.value = true; __sessionState.permissions.value = permissions; __sessionState.isSubuser.value = false; __sessionState.token.value = "unit-token"; }; const setPendingSession = () => { __sessionState.initiated.value = false; __sessionState.permissions.value = []; __sessionState.isSubuser.value = false; __sessionState.token.value = "unit-token"; }; const setLocation = ({ latitude, longitude, timestamp = Date.now() }) => { __locationRef.value = { coords: { latitude, longitude, }, timestamp: new Date(timestamp), locatedAt: timestamp, }; }; describe("DefaultPage mobile department redirect", () => { beforeEach(() => { vi.useFakeTimers(); vi.setSystemTime(new Date("2026-06-11T10:00:00.000Z")); __routerPush.mockReset(); __getDepartmentsMock.mockReset(); __getDepartmentsMock.mockResolvedValue(undefined); __isMobileRef.value = true; __departmentsRef.value = []; __locationRef.value = null; __defaultTimeoutRef.value = 500; setAdminSession(["admin", "department_access_1", "department_access_2"]); }); afterEach(() => { wrapper?.unmount(); wrapper = null; vi.clearAllTimers(); vi.useRealTimers(); }); it("shows the branded redirect loader while session access is still unresolved", async () => { setPendingSession(); const mounted = mountDefaultPage(); await nextTick(); expect(mounted.get('[data-testid="redirect-loading-status"]').text()).toBe("Omdirigerer..."); expect(mounted.text()).toContain("Vi finder den rigtige side for dig"); expect(mounted.find('[data-testid="redirect-nearest-department-loading"]').exists()).toBe(false); }); it("shows the branded nearest-department loader during mobile auto-selection", async () => { const mounted = mountDefaultPage(); await nextTick(); expect(mounted.get('[data-testid="redirect-nearest-department-loading-status"]').text()).toBe( "Finder din nærmeste afdeling..." ); expect(mounted.text()).toContain("Vi bruger din placering til at vælge afdeling"); expect(mounted.find('[data-testid="redirect-loading"]').exists()).toBe(false); }); it("routes when geolocation arrives before departments finish loading", async () => { mountDefaultPage(); await nextTick(); setLocation({ latitude: 55.5, longitude: 12.5, }); await nextTick(); expect(__routerPush).not.toHaveBeenCalledWith({ name: "pos", params: { departmentId: 2 } }); __departmentsRef.value = [ { id: 1, name: "Far", latitude: 55.0, longitude: 12.0 }, { id: 2, name: "Near", latitude: 55.51, longitude: 12.51 }, ]; await nextTick(); expect(__getDepartmentsMock).toHaveBeenCalledTimes(1); expect(__routerPush).toHaveBeenCalledWith({ name: "pos", params: { departmentId: 2 } }); }); it("requests a fresh geolocation fix for mobile department auto-selection", async () => { const mounted = mountDefaultPage(); await nextTick(); const probe = mounted.find('[data-testid="location-probe"]'); expect(probe.attributes("data-enable-high-accuracy")).toBe("true"); expect(probe.attributes("data-maximum-age")).toBe("0"); expect(probe.attributes("data-timeout")).toBe("27000"); }); it("shows manual department selection after five seconds", async () => { const mounted = mountDefaultPage(); await nextTick(); expect(mounted.find('[data-testid="redirect-manual-department-button"]').exists()).toBe(false); await vi.advanceTimersByTimeAsync(4999); await nextTick(); expect(mounted.find('[data-testid="redirect-manual-department-button"]').exists()).toBe(false); await vi.advanceTimersByTimeAsync(1); await nextTick(); expect(mounted.find('[data-testid="redirect-manual-department-button"]').exists()).toBe(true); }); it("lets the user manually select a department after GPS wait", async () => { setAdminSession(["admin", "department_access_4", "department_access_6"]); __departmentsRef.value = [ { id: 4, name: "Køge", latitude: 55.458, longitude: 12.182 }, { id: 6, name: "Roskilde", latitude: 55.642, longitude: 12.08 }, ]; const mounted = mountDefaultPage(); await nextTick(); await vi.advanceTimersByTimeAsync(5000); await nextTick(); await mounted.get('[data-testid="redirect-manual-department-button"]').trigger("click"); await nextTick(); expect(mounted.find('[data-testid="redirect-manual-department-options"]').exists()).toBe(true); setLocation({ latitude: 55.458, longitude: 12.182, }); await nextTick(); expect(__routerPush).not.toHaveBeenCalledWith({ name: "pos", params: { departmentId: 4 } }); await vi.advanceTimersByTimeAsync(27000); await nextTick(); expect(__routerPush).not.toHaveBeenCalledWith({ name: "admin" }); await mounted.get('[data-testid="redirect-manual-department-option-6"]').trigger("click"); await nextTick(); expect(__routerPush).toHaveBeenCalledWith({ name: "pos", params: { departmentId: 6 } }); }); it("ignores stale cached location and waits for a fresh Roskilde location", async () => { setAdminSession(["admin", "department_access_4", "department_access_6"]); __departmentsRef.value = [ { id: 4, name: "Køge", latitude: 55.458, longitude: 12.182 }, { id: 6, name: "Roskilde", latitude: 55.642, longitude: 12.08 }, ]; setLocation({ latitude: 55.458, longitude: 12.182, timestamp: Date.now() - 30000, }); mountDefaultPage(); await nextTick(); expect(__locationRef.value).toBeNull(); expect(__routerPush).not.toHaveBeenCalledWith({ name: "pos", params: { departmentId: 4 } }); await vi.advanceTimersByTimeAsync(100); setLocation({ latitude: 55.642, longitude: 12.08, }); await nextTick(); expect(__routerPush).toHaveBeenCalledWith({ name: "pos", params: { departmentId: 6 } }); }); it("does not redirect from a location timestamp older than the auto-select attempt", async () => { setAdminSession(["admin", "department_access_4", "department_access_6"]); __departmentsRef.value = [ { id: 4, name: "Køge", latitude: 55.458, longitude: 12.182 }, { id: 6, name: "Roskilde", latitude: 55.642, longitude: 12.08 }, ]; const staleTimestamp = Date.now() - 1000; mountDefaultPage(); await nextTick(); setLocation({ latitude: 55.458, longitude: 12.182, timestamp: staleTimestamp, }); await nextTick(); expect(__routerPush).not.toHaveBeenCalledWith({ name: "pos", params: { departmentId: 4 } }); await vi.advanceTimersByTimeAsync(27000); await nextTick(); expect(__routerPush).toHaveBeenCalledWith({ name: "admin" }); }); it("routes immediately when only one accessible department exists", async () => { setAdminSession(["admin", "department_access_6"]); __departmentsRef.value = [ { id: 4, name: "Køge", latitude: 55.458, longitude: 12.182 }, { id: 6, name: "Roskilde", latitude: 55.642, longitude: 12.08 }, ]; mountDefaultPage(); await nextTick(); expect(__routerPush).toHaveBeenCalledWith({ name: "pos", params: { departmentId: 6 } }); }); it("falls back after timeout even when location exists but nearest cannot be resolved", async () => { __departmentsRef.value = [ { id: 1, name: "Missing coordinates" }, { id: 2, name: "Also missing coordinates" }, ]; mountDefaultPage(); await nextTick(); setLocation({ latitude: 55.5, longitude: 12.5, }); await nextTick(); expect(__routerPush).not.toHaveBeenCalled(); await vi.advanceTimersByTimeAsync(27000); await nextTick(); expect(__routerPush).toHaveBeenCalledWith({ name: "admin" }); }); it("does not auto-select departments for desktop admin users", async () => { __isMobileRef.value = false; mountDefaultPage(); await nextTick(); expect(__getDepartmentsMock).not.toHaveBeenCalled(); expect(__routerPush).toHaveBeenCalledWith({ name: "admin" }); }); });