1import { describe, expect, it } from "vite-plus/test";
2
3import {
4 BROWSER_FAVICON_MAX_ENTRIES,
5 type BrowserFaviconEntry,
6 evictExcessFavicons,
7 faviconKey,
8 faviconStorageLocation,
9 isStorableFaviconDataUrl,
10 migratePersistedBrowserFaviconState,
11} from "./browserFaviconLogic";
12
13const PNG = "data:image/png;base64,AAAA";
14
15function entry(capturedAt = 0): BrowserFaviconEntry {
16 return { dataUrl: PNG, capturedAt };
17}
18
19describe("browser favicon logic", () => {
20 it("keys valid origins canonically while keeping distinct scopes separate", () => {
21 expect(faviconKey("env:project", "http://myapp.test:3000/admin?x=1", null)).toBe(
22 "env:project http://myapp.test:3000",
23 );
24 expect(faviconKey("env:project", "http://192.168.64.2:3000/", "192.168.64.2")).toBe(
25 faviconKey("env:project", "http://localhost:3000/", "192.168.64.2"),
26 );
27 expect(faviconKey("env:project", "http://127.0.0.1:3000/", null)).toBe(
28 faviconKey("env:project", "http://0.0.0.0:3000/", null),
29 );
30 const keys = [
31 faviconKey("env:a", "http://localhost:3000/", null),
32 faviconKey("env:b", "http://localhost:3000/", null),
33 faviconKey("env:a", "http://localhost:5173/", null),
34 faviconKey("env:a", "https://localhost:3000/", null),
35 faviconKey("env:a", "http://192.168.1.50:3000/", "192.168.64.2"),
36 ];
37 expect(new Set(keys).size).toBe(keys.length);
38 expect(faviconKey("env:a", "not a url", null)).toBeNull();
39 expect(faviconKey("env:a", "ftp://example.com/", null)).toBeNull();
40 expect(faviconKey("", "http://localhost/", null)).toBeNull();
41 expect(faviconKey("env:a", "http://local:3000/", null)).not.toBe(
42 faviconKey("env:a", "http://localhost:3000/", null),
43 );
44 });
45
46 it("retains an exact environment-host alias for offline lookup", () => {
47 const expected = {
48 aliases: ["192.168.64.2"],
49 key: "env:project http://localhost:3000",
50 };
51 expect(
52 faviconStorageLocation("env:project", "http://localhost:3000/app", "192.168.64.2"),
53 ).toEqual(expected);
54 expect(
55 faviconStorageLocation("env:project", "http://192.168.64.2:3000/app", "192.168.64.2"),
56 ).toEqual(expected);
57 expect(
58 faviconStorageLocation("env:project", "https://example.com/app", "192.168.64.2"),
59 ).toEqual({ aliases: [], key: "env:project https://example.com:443" });
60 expect(faviconStorageLocation("env:project", "http://localhost:3000/app", "fd00::1")).toEqual({
61 aliases: ["fd00::1"],
62 key: "env:project http://localhost:3000",
63 });
64 expect(faviconKey("env:project", "http://[2001:4860:4860::8888]/", null)).toBe(
65 "env:project http://[2001:4860:4860::8888]:80",
66 );
67 });
68
69 it("accepts only bounded base64 PNG data", () => {
70 expect(isStorableFaviconDataUrl(PNG)).toBe(true);
71 expect(isStorableFaviconDataUrl("data:image/svg+xml;base64,AAAA")).toBe(false);
72 expect(isStorableFaviconDataUrl("data:image/png;base64,")).toBe(false);
73 expect(isStorableFaviconDataUrl("data:image/png;base64,%%%%")).toBe(false);
74 expect(isStorableFaviconDataUrl(`data:image/png;base64,${"A".repeat(8192)}`)).toBe(false);
75 });
76
77 it("evicts old entries and sanitizes hydrated state", () => {
78 const byKey = Object.fromEntries(
79 Array.from({ length: BROWSER_FAVICON_MAX_ENTRIES + 2 }, (_, index) => [
80 `key-${index}`,
81 entry(index),
82 ]),
83 );
84 const result = evictExcessFavicons(byKey);
85 expect(Object.keys(result)).toHaveLength(BROWSER_FAVICON_MAX_ENTRIES);
86 expect(result["key-0"]).toBeUndefined();
87 expect(result["key-1"]).toBeUndefined();
88 expect(
89 migratePersistedBrowserFaviconState({
90 byKey: {
91 "env:project http://local:3000": entry(4),
92 "env:project http://localhost:3000": entry(5),
93 "env:project http://localhost:3003": {
94 ...entry(6),
95 aliases: [
96 "192.168.64.2",
97 "192.168.64.2",
98 "192.168.64.3",
99 "192.168.64.4",
100 "192.168.64.5",
101 "192.168.64.6",
102 "Not Normalized",
103 "not a host/",
104 "x".repeat(5_000),
105 ],
106 },
107 "env:project http://localhost:3001": {
108 dataUrl: "https://example.com/icon.png",
109 capturedAt: 6,
110 },
111 "env:project http://localhost:3002": { dataUrl: PNG, capturedAt: Number.NaN },
112 },
113 }),
114 ).toEqual({
115 byKey: {
116 "env:project http://localhost:3000": entry(5),
117 "env:project http://localhost:3003": {
118 ...entry(6),
119 aliases: ["192.168.64.2", "192.168.64.3", "192.168.64.4", "192.168.64.5"],
120 },
121 },
122 });
123 });
124});