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<T>(initialValue: T | (() => T)): [T, Dispatch<SetStateAction<T>>] { const index = nextIndex(); if (index >= slots.length) { slots[index] = typeof initialValue === "function" ? (initialValue as () => T)() : initialValue; } const setValue: Dispatch<SetStateAction<T>> = (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<typeof import("react")>(); 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<ProjectFaviconProject> & Pick<ProjectFaviconProject, "workspaceRoot" | "title">,): 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<ProjectFaviconImageProps>; 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", }); });});
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<T>(initialValue: T | (() => T)): [T, Dispatch<SetStateAction<T>>] {
const index = nextIndex();
if (index >= slots.length) {
slots[index] =
typeof initialValue === "function" ? (initialValue as () => T)() : initialValue;
}
const setValue: Dispatch<SetStateAction<T>> = (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<typeof import("react")>();
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<ProjectFaviconProject> &
Pick<ProjectFaviconProject, "workspaceRoot" | "title">,
): 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<ProjectFaviconImageProps>;
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",
});
});
});