authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-09-05 16:15:32-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-05 16:15:32-07:00
log748fe0f8b68b7951df6797d4c22d2ca01314a3dc
treec2778dafb04d24f7a4ae4284ea7335d218fd3be5
parent88fc41c1b90ff21a7d30d69e0ebffbc0c3511b9c
signature Signed by PGP key B5690EEEBB952194

refactor(web): test favicon fallback through the component (#10220)


2 files changed, 38 insertions(+), 53 deletions(-)

apps/web/src/components/preview/PreviewFaviconIcon.test.tsx+37-45
......@@ -1,51 +1,43 @@
1import { EnvironmentId, ThreadId } from "@t3tools/contracts";
2import { renderToStaticMarkup } from "react-dom/server";
3import { describe, expect, it, vi } from "vite-plus/test";
4
5const mocks = vi.hoisted(() => ({ favicon: null as string | null }));
6
7vi.mock("~/browserFaviconStore", () => ({
8 useFaviconForThreadUrl: () => mocks.favicon,
9}));
10
11import { FaviconImage, PreviewFaviconIcon, selectFaviconSource } from "./PreviewFaviconIcon";
12
13const threadRef = {
14 environmentId: EnvironmentId.make("env-1"),
15 threadId: ThreadId.make("thread-1"),
16};
17
18describe("preview favicon image", () => {
19 it("renders a captured source before later fallback sources", () => {
20 expect(
21 renderToStaticMarkup(
22 <FaviconImage
23 sources={["data:image/png;base64,AAAA", "https://public.example/icon"]}
24 fallback={<span>fallback</span>}
25 />,
26 ),
27 ).toContain('src="data:image/png;base64,AAAA"');
28 const captured = "data:image/png;base64,AAAA";
29 const google = "https://public.example/icon";
30 expect(selectFaviconSource([captured, google], new Set())).toBe(captured);
31 expect(selectFaviconSource([captured, google], new Set([captured]))).toBe(google);
32 expect(selectFaviconSource([captured, google], new Set([captured, google]))).toBeNull();
33 expect(selectFaviconSource(["data:image/png;base64,BBBB", google], new Set([captured]))).toBe(
34 "data:image/png;base64,BBBB",
1import { act } from "react";
2import { create, type ReactTestRenderer } from "react-test-renderer";
3import { afterEach, expect, it, vi } from "vite-plus/test";
4
5vi.mock("~/browserFaviconStore", () => ({ useFaviconForThreadUrl: () => null }));
6
7import { FaviconImage } from "./PreviewFaviconIcon";
8
9let renderer: ReactTestRenderer | undefined;
10
11afterEach(async () => {
12 await act(async () => renderer?.unmount());
13 vi.unstubAllGlobals();
14});
15
16it("falls through failed favicon sources and retries when the source list changes", async () => {
17 vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
18 const captured = "data:image/png;base64,AAAA";
19 const remote = "https://public.example/icon";
20 await act(async () => {
21 renderer = create(
22 <FaviconImage sources={[captured, remote]} fallback={<span>fallback</span>} />,
3523 );
3624 });
25 expect(renderer!.root.findByType("img").props.src).toBe(captured);
3726
38 it("uses a stored project icon or falls back to the browser mockup", () => {
39 mocks.favicon = null;
40 const html = renderToStaticMarkup(
41 <PreviewFaviconIcon threadRef={threadRef} url="http://localhost:3000/" />,
42 );
43 expect(html).not.toContain("<img");
44 expect(html).toContain("rounded-[5px]");
45 mocks.favicon = "data:image/png;base64,AAAA";
46 const faviconHtml = renderToStaticMarkup(
47 <PreviewFaviconIcon threadRef={threadRef} url="http://localhost:3000/" />,
27 await act(async () => renderer!.root.findByType("img").props.onError());
28 expect(renderer!.root.findByType("img").props.src).toBe(remote);
29
30 await act(async () => renderer!.root.findByType("img").props.onError());
31 expect(renderer!.root.findAllByType("img")).toHaveLength(0);
32 expect(renderer!.root.findByType("span").children).toEqual(["fallback"]);
33
34 await act(async () => {
35 renderer!.update(
36 <FaviconImage
37 sources={[captured, "https://public.example/new-icon"]}
38 fallback={<span>fallback</span>}
39 />,
4840 );
49 expect(faviconHtml).toContain('src="data:image/png;base64,AAAA"');
5041 });
42 expect(renderer!.root.findByType("img").props.src).toBe(captured);
5143});
apps/web/src/components/preview/PreviewFaviconIcon.tsx+1-8
......@@ -6,13 +6,6 @@ import { cn } from "~/lib/utils";
66
77import { BrowserMockup } from "./BrowserMockup";
88
9export function selectFaviconSource(
10 sources: ReadonlyArray<string>,
11 failed: ReadonlySet<string>,
12): string | null {
13 return sources.find((candidate) => !failed.has(candidate)) ?? null;
14}
15
169export function FaviconImage(props: {
1710 sources: ReadonlyArray<string | null | undefined>;
1811 fallback: ReactNode;
......@@ -35,7 +28,7 @@ function FaviconImageAttempt(props: {
3528 className?: string | undefined;
3629}) {
3730 const [failed, setFailed] = useState<ReadonlySet<string>>(() => new Set());
38 const source = selectFaviconSource(props.sources, failed);
31 const source = props.sources.find((candidate) => !failed.has(candidate));
3932 if (!source) return props.fallback;
4033 return (
4134 <img