import type { ComponentType, Dispatch, ReactElement, SetStateAction } from "react"; import { beforeEach, describe, expect, it, vi } from "vite-plus/test"; import type { EnvironmentId } from "@t3tools/contracts"; import { PROJECT_FAVICON_FALLBACK_MARKER } from "@t3tools/shared/projectFavicon"; const testState = vi.hoisted(() => ({ faviconUrl: "https://environment.test/api/assets/token-a/v1-20-favicon.svg", lastTarget: null as unknown, })); const hooks = vi.hoisted(() => { let cursor = 0; let slots: unknown[] = []; const nextIndex = () => cursor++; return { beginRender() { cursor = 0; }, reset() { cursor = 0; slots = []; }, useMemoCache(size: number): unknown[] { const index = nextIndex(); if (!slots[index]) { slots[index] = Array.from({ length: size }, () => Symbol.for("react.memo_cache_sentinel")); } return slots[index] as unknown[]; }, useState(initialValue: T | (() => T)): [T, Dispatch>] { const index = nextIndex(); if (index >= slots.length) { slots[index] = typeof initialValue === "function" ? (initialValue as () => T)() : initialValue; } const setValue: Dispatch> = (nextValue) => { const previous = slots[index] as T; slots[index] = typeof nextValue === "function" ? (nextValue as (value: T) => T)(previous) : nextValue; }; return [slots[index] as T, setValue]; }, }; }); vi.mock("react", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, useState: hooks.useState, }; }); vi.mock("react/compiler-runtime", () => ({ c: hooks.useMemoCache })); vi.mock("lucide-react/dynamic", () => ({ DynamicIcon: "dynamic-icon", iconNames: ["alarm-clock", "folder-code"], })); vi.mock("@effect/atom-react", () => ({ useAtomValue: () => testState.faviconUrl, })); vi.mock("../state/assets", () => ({ projectFaviconUrlAtom: (input: unknown) => { testState.lastTarget = input; }, })); import { ProjectFavicon, type ProjectFaviconProject } from "./ProjectFavicon"; function makeProject( overrides: Partial & Pick, ): ProjectFaviconProject { return { environmentId: "environment-test" as EnvironmentId, ...overrides }; } type ProjectFaviconImageProps = { readonly cacheKey: string; readonly src: string; readonly className?: string | undefined; readonly fallbackIcon: ComponentType<{ className?: string }>; }; type ImageElement = ReactElement<{ readonly src: string; readonly onLoad?: () => void; readonly onError?: () => void; }>; type ProjectFaviconImageElement = ReactElement<{ readonly children: [ReactElement | null, ImageElement | null, ImageElement | null]; }>; function resolveImageComponent(): { readonly Component: (props: ProjectFaviconImageProps) => ProjectFaviconImageElement; readonly props: ProjectFaviconImageProps; } { hooks.beginRender(); const element = ProjectFavicon({ project: makeProject({ workspaceRoot: "/workspace-test", title: "workspace-test" }), }) as ReactElement; hooks.reset(); return { Component: element.type as (props: ProjectFaviconImageProps) => ProjectFaviconImageElement, props: element.props, }; } function renderImage( Component: (props: ProjectFaviconImageProps) => ProjectFaviconImageElement, props: ProjectFaviconImageProps, ): ProjectFaviconImageElement { hooks.beginRender(); return Component(props); } describe("ProjectFavicon", () => { beforeEach(() => { hooks.reset(); testState.faviconUrl = "https://environment.test/api/assets/token-a/v1-20-favicon.svg"; }); it("shows a project-name icon when no favicon exists", () => { testState.faviconUrl = `https://environment.test/api/assets/token/${PROJECT_FAVICON_FALLBACK_MARKER}`; const element = ProjectFavicon({ project: makeProject({ workspaceRoot: "/workspace/analytics-db", title: "analytics-db" }), }) as ReactElement<{ readonly colorClassName?: string; readonly emoji?: string; readonly icon?: ComponentType<{ className?: string }>; }>; expect(element.props.icon).toBeDefined(); expect(element.props.emoji).toBeUndefined(); expect(element.props.colorClassName).toContain("text-cyan-600"); }); it("chooses a deterministic semantic icon", () => { testState.faviconUrl = `https://environment.test/api/assets/token/${PROJECT_FAVICON_FALLBACK_MARKER}`; const element = ProjectFavicon({ project: makeProject({ workspaceRoot: "/workspace/agent-runtime", title: "agent-runtime" }), }) as ReactElement<{ readonly colorClassName?: string; readonly emoji?: string; readonly icon?: ComponentType<{ className?: string }>; }>; expect(element.props.icon).toBeDefined(); expect(element.props.emoji).toBeUndefined(); expect(element.props.colorClassName).toContain("text-violet-600"); }); it("renders a saved Lucide icon and color ahead of an uploaded favicon", () => { const element = ProjectFavicon({ project: makeProject({ workspaceRoot: "/workspace/test", title: "test", faviconPath: "brand/icon.svg", projectIcon: { kind: "lucide", name: "alarm-clock", color: "violet" }, }), }) as ReactElement<{ readonly children: ReactElement<{ readonly children: ReactElement<{ readonly name: string; readonly className: string }>; }>; readonly className: string; }>; expect(element.props.children.props.children.props.name).toBe("alarm-clock"); expect(element.props.className).toContain("text-violet-600"); expect(element.props.children.props.children.props.className).toContain("text-violet-600"); }); it("renders a saved emoji ahead of an uploaded favicon", () => { const element = ProjectFavicon({ project: makeProject({ workspaceRoot: "/workspace/test", title: "test", faviconPath: "brand/icon.svg", projectIcon: { kind: "emoji", emoji: "🦄" }, }), }) as ReactElement<{ readonly emoji: string }>; expect(element.props.emoji).toBe("🦄"); }); it("falls back when the displayed favicon fails without discarding a valid older image early", () => { const { Component, props } = resolveImageComponent(); const initialLoadingImage = renderImage(Component, props).props.children[2]; initialLoadingImage?.props.onLoad?.(); const refreshedProps = { ...props, src: "https://environment.test/api/assets/token-b/v1-20-favicon.svg", }; const refreshing = renderImage(Component, refreshedProps).props.children; expect(refreshing[1]?.props.src).toBe(props.src); refreshing[2]?.props.onError?.(); const afterRefreshError = renderImage(Component, refreshedProps).props.children; expect(afterRefreshError[1]?.props.src).toBe(props.src); afterRefreshError[1]?.props.onError?.(); const afterDisplayedError = renderImage(Component, refreshedProps).props.children; expect(afterDisplayedError[0]).not.toBeNull(); expect(afterDisplayedError[1]).toBeNull(); }); it("requests a saved favicon path when one is set", () => { ProjectFavicon({ project: makeProject({ workspaceRoot: "/workspace-test", title: "workspace-test", faviconPath: "brand/icon.svg", }), }); expect(testState.lastTarget).toMatchObject({ environmentId: "environment-test", cwd: "/workspace-test", faviconPath: "brand/icon.svg", }); }); });