| 1 | import { describe, expect, it } from "vite-plus/test"; |
| 2 | |
| 3 | import { |
| 4 | BROWSER_FAVICON_MAX_ENTRIES, |
| 5 | type BrowserFaviconEntry, |
| 6 | evictExcessFavicons, |
| 7 | faviconKey, |
| 8 | faviconStorageLocation, |
| 9 | isStorableFaviconDataUrl, |
| 10 | migratePersistedBrowserFaviconState, |
| 11 | } from "./browserFaviconLogic"; |
| 12 | |
| 13 | const PNG = "data:image/png;base64,AAAA"; |
| 14 | |
| 15 | function entry(capturedAt = 0): BrowserFaviconEntry { |
| 16 | return { dataUrl: PNG, capturedAt }; |
| 17 | } |
| 18 | |
| 19 | describe("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 | }); |