1import { beforeEach, describe, expect, it, vi } from "vite-plus/test";
2
3const mocks = vi.hoisted(() => ({
4 os: "android",
5 version: 36,
6 openSettings: vi.fn(),
7 native: null as {
8 configure?: ReturnType<typeof vi.fn>;
9 clear?: ReturnType<typeof vi.fn>;
10 openLiveUpdateSettings?: ReturnType<typeof vi.fn>;
11 } | null,
12 config: { scheme: ["t3code-preview"], extra: { iosPersonalTeamBuild: false } },
13 requireModule: vi.fn(),
14}));
15
16vi.mock("expo", () => ({ requireOptionalNativeModule: mocks.requireModule }));
17vi.mock("expo-constants", () => ({ default: { expoConfig: mocks.config } }));
18vi.mock("react-native", () => ({
19 Linking: { openSettings: mocks.openSettings },
20 Platform: {
21 get Version() {
22 return mocks.version;
23 },
24 get OS() {
25 return mocks.os;
26 },
27 },
28}));
29
30beforeEach(() => {
31 vi.resetModules();
32 mocks.os = "android";
33 mocks.version = 36;
34 mocks.openSettings.mockReset().mockResolvedValue(undefined);
35 mocks.native = { configure: vi.fn(), clear: vi.fn() };
36 mocks.config.extra.iosPersonalTeamBuild = false;
37 mocks.requireModule.mockReset().mockImplementation(() => mocks.native);
38});
39
40describe("Android native notification capability", () => {
41 it("opens the Live Update controls on supported Android builds", async () => {
42 mocks.native!.openLiveUpdateSettings = vi.fn(() => true);
43 const { openAndroidLiveUpdateSettings, supportsAndroidLiveUpdateSettings } =
44 await import("./androidNotifications");
45 expect(supportsAndroidLiveUpdateSettings()).toBe(true);
46 await openAndroidLiveUpdateSettings();
47 expect(mocks.native!.openLiveUpdateSettings).toHaveBeenCalledOnce();
48 expect(mocks.openSettings).not.toHaveBeenCalled();
49 mocks.version = 35;
50 expect(supportsAndroidLiveUpdateSettings()).toBe(false);
51 mocks.os = "ios";
52 mocks.version = 36;
53 expect(supportsAndroidLiveUpdateSettings()).toBe(false);
54 });
55
56 it.each([undefined, vi.fn(() => false)])(
57 "falls back to app settings for older binaries or missing system activities (%j)",
58 async (openLiveUpdateSettings) => {
59 if (openLiveUpdateSettings) mocks.native!.openLiveUpdateSettings = openLiveUpdateSettings;
60 const { openAndroidLiveUpdateSettings } = await import("./androidNotifications");
61 await openAndroidLiveUpdateSettings();
62 expect(mocks.openSettings).toHaveBeenCalledOnce();
63 },
64 );
65
66 it("uses the installed module and the build variant's deep-link scheme", async () => {
67 const { configureAndroidAgentNotifications, clearAndroidAgentNotifications } =
68 await import("./androidNotifications");
69 const { supportsAgentAwarenessPush } = await import("./capabilities");
70 // An iOS-only signing restriction must not disable Android notifications.
71 mocks.config.extra.iosPersonalTeamBuild = true;
72 expect(supportsAgentAwarenessPush()).toBe(true);
73 configureAndroidAgentNotifications("device", "user", false);
74 expect(mocks.native?.configure).toHaveBeenCalledWith("device", "user", "t3code-preview", false);
75 clearAndroidAgentNotifications();
76 expect(mocks.native?.clear).toHaveBeenCalledOnce();
77 });
78
79 it.each([null, { clear: vi.fn() }, { configure: vi.fn() }])(
80 "disables push when the native binary is missing required methods (%j)",
81 async (native) => {
82 mocks.native = native;
83 const { configureAndroidAgentNotifications, clearAndroidAgentNotifications } =
84 await import("./androidNotifications");
85 const { supportsAgentAwarenessPush } = await import("./capabilities");
86 expect(supportsAgentAwarenessPush()).toBe(false);
87 expect(() => configureAndroidAgentNotifications("device", "user", true)).not.toThrow();
88 expect(() => clearAndroidAgentNotifications()).not.toThrow();
89 },
90 );
91
92 it("preserves the iOS personal-team restriction without loading Android code", async () => {
93 mocks.os = "ios";
94 const { supportsAgentAwarenessPush } = await import("./capabilities");
95 expect(supportsAgentAwarenessPush()).toBe(true);
96 mocks.config.extra.iosPersonalTeamBuild = true;
97 expect(supportsAgentAwarenessPush()).toBe(false);
98 expect(mocks.requireModule).not.toHaveBeenCalled();
99 });
100});