Files
pleno-vue/tests/unit/default-page-mobile-redirect.spec.js
T

460 lines
14 KiB
JavaScript

// @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" });
});
});