Files
pleno-vue/tests/unit/default-page-mobile-redirect.spec.js
Jeppe B 9ff103d4d0 fix(ios): respect status area and persist location consent (#224)
Keep the iOS status bar outside the Capacitor web view and replace startup geolocation watching with silent permission checks plus an explicit location action.

Verified by full unit, App Store readiness, Qodana, production build, Capacitor sync, and Playwright mobile suites.
2026-07-23 19:29:54 +02:00

475 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",
emits: ["permission-state-changed"],
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("shows manual department selection immediately when location permission needs a user action", async () => {
const mounted = mountDefaultPage();
await nextTick();
expect(mounted.find('[data-testid="redirect-manual-department-button"]').exists()).toBe(false);
mounted
.getComponent({ name: "MockPosDepartmentStepMobile1Location" })
.vm.$emit("permission-state-changed", "prompt");
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" });
});
});