Skip to content
Source
Sign inSign up

browserFaviconStore.test.ts

168 lines6.7 KB

1import { scopedThreadKey, scopeProjectRef } from "@t3tools/client-runtime/environment";
2import { EnvironmentId, ProjectId, ThreadId } from "@t3tools/contracts";
3import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test";
4​
5const metadata = vi.hoisted(() => ({
6 projectId: null as string | null,
7 httpBaseUrl: null as string | null,
8}));
9​
10vi.mock("~/state/entities", () => ({
11 useQuietThreadShell: () => null,
12 readThreadShell: () => (metadata.projectId ? { projectId: metadata.projectId } : null),
13}));
14vi.mock("~/state/session", () => ({
15 usePreparedConnection: () => ({ _tag: "None" }),
16 readPreparedConnection: () =>
17 metadata.httpBaseUrl ? { httpBaseUrl: metadata.httpBaseUrl } : null,
18}));
19​
20import { DraftId, useComposerDraftStore } from "./composerDraftStore";
21import {
22 lookupFavicon,
23 lookupFaviconForThread,
24 recordFaviconForProject,
25 recordFaviconForThread,
26 resetBrowserFaviconsForTests,
27 resolveBrowserFaviconStorage,
28 useBrowserFaviconStore,
29} from "./browserFaviconStore";
30​
31const environmentId = EnvironmentId.make("env-1");
32const projectRef = scopeProjectRef(environmentId, ProjectId.make("project-1"));
33const threadRef = { environmentId, threadId: ThreadId.make("thread-1") };
34const PNG = "data:image/png;base64,AAAA";
35const favicon = (pageUrl: string, capturedAt: number, dataUrl = PNG) => ({
36 pageUrl,
37 capturedAt,
38 dataUrl,
39});
40​
41describe("browser favicon store", () => {
42 beforeEach(() => {
43 resetBrowserFaviconsForTests();
44 metadata.projectId = null;
45 metadata.httpBaseUrl = null;
46 useComposerDraftStore.setState({ draftThreadsByThreadKey: {} });
47 });
48 afterEach(() => {
49 vi.unstubAllGlobals();
50 vi.restoreAllMocks();
51 });
52​
53 it("does not share localhost icons across environments or physical projects", () => {
54 const otherEnvironment = scopeProjectRef(
55 EnvironmentId.make("env-2"),
56 ProjectId.make("project-1"),
57 );
58 const otherProject = scopeProjectRef(environmentId, ProjectId.make("project-2"));
59 recordFaviconForProject(projectRef, favicon("http://localhost:3000/", 1), null);
60 recordFaviconForProject(otherEnvironment, favicon("http://localhost:3000/", 2), null);
61 recordFaviconForProject(otherProject, favicon("http://localhost:3000/", 3), null);
62 expect(Object.keys(useBrowserFaviconStore.getState().byKey)).toEqual([
63 "env-1:project-1 http://localhost:3000",
64 "env-2:project-1 http://localhost:3000",
65 "env-1:project-2 http://localhost:3000",
66 ]);
67 });
68​
69 it("finds a persisted environment icon after shell hydration without a live connection", () => {
70 recordFaviconForProject(projectRef, favicon("http://192.168.64.2:3000/app", 5), "192.168.64.2");
71 const byKey = useBrowserFaviconStore.getState().byKey;
72 expect(lookupFavicon(byKey, null, "http://localhost:3000/app", null)).toBeNull();
73 expect(lookupFavicon(byKey, projectRef, "http://localhost:3000/app", null)).toBe(PNG);
74 expect(lookupFavicon(byKey, projectRef, "http://192.168.64.2:3000/app", null)).toBe(PNG);
75 expect(lookupFavicon(byKey, projectRef, "http://192.168.64.3:3000/app", null)).toBeNull();
76 expect(lookupFavicon(byKey, projectRef, "https://192.168.64.2:3000/app", null)).toBeNull();
77 expect(lookupFavicon(byKey, projectRef, "http://192.168.64.2:3001/app", null)).toBeNull();
78 expect(
79 lookupFavicon(
80 byKey,
81 scopeProjectRef(environmentId, ProjectId.make("project-2")),
82 "http://192.168.64.2:3000/app",
83 null,
84 ),
85 ).toBeNull();
86 });
87​
88 it("retains origins until the thread's project is known, then lands them with the next capture", () => {
89 expect(recordFaviconForThread(threadRef, favicon("http://localhost:3000/", 1))).toBe(false);
90 expect(recordFaviconForThread(threadRef, favicon("http://localhost:5173/", 2))).toBe(false);
91 expect(
92 Object.keys(Object.values(useBrowserFaviconStore.getState().pendingByThreadKey)[0] ?? {}),
93 ).toHaveLength(2);
94​
95 metadata.projectId = "project-1";
96 metadata.httpBaseUrl = "http://192.168.64.2:3773";
97 expect(recordFaviconForThread(threadRef, favicon("http://localhost:5173/", 3))).toBe(true);
98 expect(Object.keys(useBrowserFaviconStore.getState().byKey).toSorted()).toEqual([
99 "env-1:project-1 http://localhost:3000",
100 "env-1:project-1 http://localhost:5173",
101 ]);
102 expect(useBrowserFaviconStore.getState().pendingByThreadKey).toEqual({});
103 });
104​
105 it("files a draft thread's capture under the draft's project with no view registering it", () => {
106 useComposerDraftStore
107 .getState()
108 .setLogicalProjectDraftThreadId("env-1:project-1", projectRef, DraftId.make("draft-1"), {
109 threadId: threadRef.threadId,
110 });
111 expect(recordFaviconForThread(threadRef, favicon("http://localhost:3000/", 1))).toBe(true);
112 expect(useBrowserFaviconStore.getState().byKey).toEqual({
113 "env-1:project-1 http://localhost:3000": { dataUrl: PNG, capturedAt: 1 },
114 });
115 });
116​
117 it("persists unambiguous loopback captures while the environment is offline", () => {
118 metadata.projectId = "project-1";
119 expect(recordFaviconForThread(threadRef, favicon("http://localhost:3000/", 1))).toBe(true);
120 expect(useBrowserFaviconStore.getState().byKey).toEqual({
121 "env-1:project-1 http://localhost:3000": { dataUrl: PNG, capturedAt: 1 },
122 });
123​
124 expect(recordFaviconForThread(threadRef, favicon("http://192.168.64.2:5173/", 2))).toBe(false);
125 expect(Object.values(useBrowserFaviconStore.getState().pendingByThreadKey)[0]).toBeDefined();
126 });
127​
128 it("shows a newer pending capture over the stored favicon for the same origin", () => {
129 const NEWER = "data:image/png;base64,BBBB";
130 metadata.projectId = "project-1";
131 metadata.httpBaseUrl = "http://192.168.64.2:3773";
132 expect(recordFaviconForThread(threadRef, favicon("http://192.168.64.2:5173/", 1))).toBe(true);
133​
134 metadata.httpBaseUrl = null;
135 expect(recordFaviconForThread(threadRef, favicon("http://192.168.64.2:5173/", 2, NEWER))).toBe(
136 false,
137 );
138 expect(
139 lookupFaviconForThread(
140 useBrowserFaviconStore.getState(),
141 scopedThreadKey(threadRef),
142 projectRef,
143 "http://192.168.64.2:5173/",
144 "192.168.64.2",
145 ),
146 ).toBe(NEWER);
147 });
148​
149 it("keeps the memory shadow authoritative after an asymmetric primary write failure", () => {
150 vi.stubGlobal("window", {
151 localStorage: {
152 getItem: vi.fn(() => "stale"),
153 setItem: vi.fn(() => {
154 throw new Error("quota exceeded");
155 }),
156 removeItem: vi.fn(() => {
157 throw new Error("remove blocked");
158 }),
159 },
160 });
161 const storage = resolveBrowserFaviconStorage();
162​
163 storage.setItem("key", "fresh");
164 expect(storage.getItem("key")).toBe("fresh");
165 storage.removeItem("key");
166 expect(storage.getItem("key")).toBeNull();
167 });
168});