1import { beforeEach, describe, expect, it, vi } from "vite-plus/test";
2import { PROJECT_FAVICON_MAX_DATA_URL_LENGTH } from "@t3tools/client-runtime/project-favicon-cache";
3
4const native = vi.hoisted(() => ({
5 load: vi.fn(async (_url: string, options: { maxWidth: number; maxHeight: number }) => ({
6 width: options.maxWidth,
7 height: options.maxHeight,
8 release: vi.fn(),
9 })),
10 write: vi.fn(async () => {}),
11 path: vi.fn(async () => "/cache/thumbnail"),
12 read: vi.fn(),
13 remove: vi.fn(),
14}));
15vi.mock("expo-image", () => ({
16 Image: {
17 loadAsync: native.load,
18 writeToCacheAsync: native.write,
19 getCachePathAsync: native.path,
20 },
21}));
22vi.mock("expo-file-system", () => ({
23 File: class {
24 size = 24_000;
25 base64 = native.read;
26 delete = native.remove;
27 },
28}));
29
30import { downscaleProjectFavicon } from "./projectFaviconDatabaseCache";
31
32const png = "iVBORw0KGgoAAAAA";
33const image = { url: "https://remote/icon.png" };
34
35beforeEach(() => {
36 vi.clearAllMocks();
37 native.read.mockReset().mockResolvedValue(png);
38 native.load.mockReset().mockImplementation(async (_url, { maxWidth }) => ({
39 width: maxWidth,
40 height: maxWidth,
41 release: vi.fn(),
42 }));
43});
44
45describe("mobile project icon thumbnails", () => {
46 it("reduces an oversized encoding and deletes temporary thumbnail files", async () => {
47 native.read.mockResolvedValueOnce(
48 `iVBORw0KGgo${"a".repeat(PROJECT_FAVICON_MAX_DATA_URL_LENGTH)}`,
49 );
50 const thumbnail = await downscaleProjectFavicon(image, new AbortController().signal);
51 expect(thumbnail).toBe(`data:image/png;base64,${png}`);
52 expect(native.load.mock.calls.map(([, options]) => options.maxWidth)).toEqual([96, 48]);
53 expect(native.remove).toHaveBeenCalledTimes(2);
54 for (const call of native.load.mock.results)
55 expect((await call.value).release).toHaveBeenCalledOnce();
56 });
57
58 it("releases a decoded image when its request was canceled", async () => {
59 const controller = new AbortController();
60 const release = vi.fn();
61 native.load.mockImplementationOnce(async () => {
62 controller.abort();
63 return { width: 96, height: 96, release };
64 });
65 await expect(downscaleProjectFavicon(image, controller.signal)).rejects.toThrow();
66 expect(release).toHaveBeenCalledOnce();
67 expect(native.write).not.toHaveBeenCalled();
68 });
69
70 it("rejects an image the native decoder did not downsize", async () => {
71 const release = vi.fn();
72 native.load.mockResolvedValueOnce({ width: 4000, height: 3000, release });
73 await expect(downscaleProjectFavicon(image, new AbortController().signal)).rejects.toThrow(
74 "not resized",
75 );
76 expect(native.write).not.toHaveBeenCalled();
77 expect(release).toHaveBeenCalledOnce();
78 });
79});