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.
151 lines
5.1 KiB
JavaScript
151 lines
5.1 KiB
JavaScript
// @vitest-environment jsdom
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
const capacitorMocks = vi.hoisted(() => ({
|
|
isNativePlatform: vi.fn(),
|
|
checkPermissions: vi.fn(),
|
|
requestPermissions: vi.fn(),
|
|
getCurrentPosition: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("@capacitor/core", () => ({
|
|
Capacitor: {
|
|
isNativePlatform: capacitorMocks.isNativePlatform,
|
|
},
|
|
}));
|
|
|
|
vi.mock("@capacitor/geolocation", () => ({
|
|
Geolocation: {
|
|
checkPermissions: capacitorMocks.checkPermissions,
|
|
requestPermissions: capacitorMocks.requestPermissions,
|
|
getCurrentPosition: capacitorMocks.getCurrentPosition,
|
|
},
|
|
}));
|
|
|
|
const position = {
|
|
coords: {
|
|
latitude: 55.6761,
|
|
longitude: 12.5683,
|
|
},
|
|
timestamp: 1_721_234_567_890,
|
|
};
|
|
|
|
const loadService = async () => {
|
|
vi.resetModules();
|
|
return import("@/services/deviceLocation");
|
|
};
|
|
|
|
describe("device location permission flow", () => {
|
|
beforeEach(() => {
|
|
capacitorMocks.isNativePlatform.mockReset();
|
|
capacitorMocks.checkPermissions.mockReset();
|
|
capacitorMocks.requestPermissions.mockReset();
|
|
capacitorMocks.getCurrentPosition.mockReset();
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllGlobals();
|
|
});
|
|
|
|
it("does not show the native permission prompt during a silent startup check", async () => {
|
|
capacitorMocks.isNativePlatform.mockReturnValue(true);
|
|
capacitorMocks.checkPermissions.mockResolvedValue({ location: "prompt" });
|
|
|
|
const { getCurrentDeviceLocation } = await loadService();
|
|
const result = await getCurrentDeviceLocation({ promptIfNeeded: false });
|
|
|
|
expect(result.permission).toBe("prompt");
|
|
expect(result.coords).toBeNull();
|
|
expect(capacitorMocks.requestPermissions).not.toHaveBeenCalled();
|
|
expect(capacitorMocks.getCurrentPosition).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("requests native permission only after the explicit location action", async () => {
|
|
capacitorMocks.isNativePlatform.mockReturnValue(true);
|
|
capacitorMocks.checkPermissions.mockResolvedValue({ location: "prompt" });
|
|
capacitorMocks.requestPermissions.mockResolvedValue({ location: "granted" });
|
|
capacitorMocks.getCurrentPosition.mockResolvedValue(position);
|
|
|
|
const { getCurrentDeviceLocation } = await loadService();
|
|
const result = await getCurrentDeviceLocation({ promptIfNeeded: true });
|
|
|
|
expect(capacitorMocks.requestPermissions).toHaveBeenCalledTimes(1);
|
|
expect(capacitorMocks.getCurrentPosition).toHaveBeenCalledTimes(1);
|
|
expect(result).toMatchObject({
|
|
permission: "granted",
|
|
coords: position.coords,
|
|
timestamp: position.timestamp,
|
|
errorCode: null,
|
|
});
|
|
});
|
|
|
|
it("silently reuses a persistent native grant on later app starts", async () => {
|
|
capacitorMocks.isNativePlatform.mockReturnValue(true);
|
|
capacitorMocks.checkPermissions.mockResolvedValue({ location: "granted" });
|
|
capacitorMocks.getCurrentPosition.mockResolvedValue(position);
|
|
|
|
const { getCurrentDeviceLocation } = await loadService();
|
|
const result = await getCurrentDeviceLocation();
|
|
|
|
expect(capacitorMocks.requestPermissions).not.toHaveBeenCalled();
|
|
expect(capacitorMocks.getCurrentPosition).toHaveBeenCalledTimes(1);
|
|
expect(result.coords).toEqual(position.coords);
|
|
});
|
|
|
|
it("does not reprompt after native permission has been denied", async () => {
|
|
capacitorMocks.isNativePlatform.mockReturnValue(true);
|
|
capacitorMocks.checkPermissions.mockResolvedValue({ location: "denied" });
|
|
|
|
const { getCurrentDeviceLocation } = await loadService();
|
|
const result = await getCurrentDeviceLocation({ promptIfNeeded: true });
|
|
|
|
expect(result).toMatchObject({
|
|
permission: "denied",
|
|
errorCode: "permission-denied",
|
|
});
|
|
expect(capacitorMocks.requestPermissions).not.toHaveBeenCalled();
|
|
expect(capacitorMocks.getCurrentPosition).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("does not call browser geolocation while web permission is still prompt", async () => {
|
|
capacitorMocks.isNativePlatform.mockReturnValue(false);
|
|
const getCurrentPosition = vi.fn();
|
|
vi.stubGlobal("navigator", {
|
|
permissions: {
|
|
query: vi.fn().mockResolvedValue({ state: "prompt" }),
|
|
},
|
|
geolocation: {
|
|
getCurrentPosition,
|
|
},
|
|
});
|
|
|
|
const { getCurrentDeviceLocation } = await loadService();
|
|
const result = await getCurrentDeviceLocation();
|
|
|
|
expect(result.permission).toBe("prompt");
|
|
expect(getCurrentPosition).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("uses one browser position request when permission is already granted", async () => {
|
|
capacitorMocks.isNativePlatform.mockReturnValue(false);
|
|
const watchPosition = vi.fn();
|
|
const getCurrentPosition = vi.fn((success) => success(position));
|
|
vi.stubGlobal("navigator", {
|
|
permissions: {
|
|
query: vi.fn().mockResolvedValue({ state: "granted" }),
|
|
},
|
|
geolocation: {
|
|
getCurrentPosition,
|
|
watchPosition,
|
|
},
|
|
});
|
|
|
|
const { getCurrentDeviceLocation } = await loadService();
|
|
const result = await getCurrentDeviceLocation();
|
|
|
|
expect(result.coords).toEqual(position.coords);
|
|
expect(getCurrentPosition).toHaveBeenCalledTimes(1);
|
|
expect(watchPosition).not.toHaveBeenCalled();
|
|
});
|
|
});
|