evOo-local/apps/mobile/lib/app-version.test.ts
cynarAI 6c7eed7580 Initial clean evOo-local from f00577b
Fresh history cut from cynarAI/evoo antigravity-handoff/project-timeline
(Voice First + project timeline). Brand docs/titles as evOo; keep @rakazo/*
packages as tech debt. Excludes node_modules, caches, secrets, voice blobs.
2026-09-20 17:15:16 +02:00

111 lines
3.9 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from "vitest";
const application = vi.hoisted(() => ({
nativeApplicationVersion: null as string | null,
nativeBuildVersion: null as string | null,
}));
const updates = vi.hoisted(() => ({
isEnabled: false,
isEmbeddedLaunch: true,
updateId: null as string | null,
channel: null as string | null,
createdAt: null as Date | null,
}));
const constants = vi.hoisted(() => ({
expoConfig: { version: "1.0.3" } as { version?: string } | null,
platform: {
ios: { buildNumber: "1" },
android: { versionCode: 10 },
} as {
ios?: { buildNumber: string | null };
android?: { versionCode: number };
},
}));
vi.mock("expo-application", () => application);
vi.mock("expo-updates", () => updates);
vi.mock("expo-constants", () => ({ default: constants }));
vi.mock("react-native", () => ({
Platform: { OS: "ios" },
}));
afterEach(() => {
application.nativeApplicationVersion = null;
application.nativeBuildVersion = null;
updates.isEnabled = false;
updates.isEmbeddedLaunch = true;
updates.updateId = null;
updates.channel = null;
updates.createdAt = null;
constants.expoConfig = { version: "1.0.3" };
constants.platform = {
ios: { buildNumber: "1" },
android: { versionCode: 10 },
};
vi.resetModules();
});
describe("app version footer helpers", () => {
it("formats native version with and without build", async () => {
const { formatNativeVersionLabel, shortUpdateId } = await import("./app-version");
expect(formatNativeVersionLabel("1.0.3", "10")).toBe("1.0.3 (10)");
expect(formatNativeVersionLabel("1.0.3", null)).toBe("1.0.3");
expect(formatNativeVersionLabel(null, "10")).toBe("10");
expect(formatNativeVersionLabel(" ", " ")).toBeNull();
expect(shortUpdateId("A1B2C3D4-E5F6-7890-ABCD-EF1234567890")).toBe("a1b2c3d4");
});
it("prefers expo-application values and labels embedded updates", async () => {
application.nativeApplicationVersion = "1.0.3";
application.nativeBuildVersion = "12";
updates.isEnabled = true;
updates.isEmbeddedLaunch = true;
const { formatUpdateLabel, getAppVersionInfo } = await import("./app-version");
const info = getAppVersionInfo();
expect(info.nativeLabel).toBe("1.0.3 (12)");
expect(info.update).toEqual({ kind: "embedded" });
expect(formatUpdateLabel(info.update, (message) => message)).toBe("Embedded");
});
it("falls back to Constants when Application is empty", async () => {
const { getAppVersionInfo } = await import("./app-version");
expect(getAppVersionInfo().nativeLabel).toBe("1.0.3 (1)");
});
it("summarizes an OTA update with channel and date", async () => {
application.nativeApplicationVersion = "1.0.3";
application.nativeBuildVersion = "12";
updates.isEnabled = true;
updates.isEmbeddedLaunch = false;
updates.updateId = "a1b2c3d4-e5f6-7890-abcd-ef1234567890";
updates.channel = "production";
updates.createdAt = new Date("2026-09-10T12:00:00.000Z");
const { formatUpdateLabel, getAppVersionInfo } = await import("./app-version");
const info = getAppVersionInfo();
expect(info.update).toEqual({
kind: "ota",
id: "a1b2c3d4",
channel: "production",
createdAt: "2026-09-10",
});
expect(
formatUpdateLabel(info.update, (message, values) => {
if (!values) return message;
return message.replace(/\{([A-Za-z0-9_]+)\}/g, (match, key: string) =>
Object.hasOwn(values, key) ? String(values[key]) : match,
);
}),
).toBe("production · a1b2c3d4 · 2026-09-10");
});
it("treats disabled updates as embedded", async () => {
updates.isEnabled = false;
updates.isEmbeddedLaunch = false;
updates.updateId = "a1b2c3d4-e5f6-7890-abcd-ef1234567890";
const { getAppVersionInfo } = await import("./app-version");
expect(getAppVersionInfo().update).toEqual({ kind: "embedded" });
});
});