Cause: the monochrome-first treatment from #351 flattened the emoji artwork through an alpha mask (transparent fill + zero-offset text-shadow), dropping internal details (the ghost lost its tongue and eyes), and the watermark box did not occupy the full space available above the chat composer. Fix: new .project-cell-illustration-area container (container-type: size) wrapping the scroll zone so the absolute watermark spans the whole region between the agent toolbar and the composer; glyph keeps the OS full-colour emoji artwork as source and is neutralised with a non-destructive CSS filter (grayscale(1) contrast(0.92)) so internal tones and details stay visible in black and white; glyph sized to min(100cqw - 8px, 100cqh - 12px); text-variation selector U+FE0E stripping removed (full emoji preserved) and project-theme tint plumbing (accessibleProjectAccent/onProjectThemeChanged) dropped as superseded by grayscale; empty label and overlay z-layering untouched. QA verte reconfirmée par Git: npm test -- CustomAgentChatView.test.tsx projectIcons.test.tsx 96/96, npm run typecheck OK, npm run build OK, npm test 1396/1396, git diff --check OK. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
106 lines
4.4 KiB
TypeScript
106 lines
4.4 KiB
TypeScript
import { fireEvent, render, screen } from "@testing-library/react";
|
|
import { describe, expect, it, vi } from "vitest";
|
|
|
|
import { ProjectIconPicker } from "./ProjectIconPicker";
|
|
import {
|
|
PROJECT_ICON_CATALOG,
|
|
ProjectIcon,
|
|
findProjectIcon,
|
|
resolveProjectIcon,
|
|
toEmojiIconRef,
|
|
} from "./projectIcons";
|
|
|
|
describe("project emoji catalogue", () => {
|
|
it("contains only stable Unicode emoji references and no bundled artwork metadata", () => {
|
|
expect(PROJECT_ICON_CATALOG.length).toBeGreaterThan(90);
|
|
for (const entry of PROJECT_ICON_CATALOG) {
|
|
expect(entry.set).toBe("emoji");
|
|
expect(entry.name).toMatch(/^[a-z0-9]+(?:-[a-z0-9]+)*$/);
|
|
expect(entry.glyph).toBeTruthy();
|
|
expect(entry).not.toHaveProperty("assetSrc");
|
|
expect(entry).not.toHaveProperty("legal");
|
|
}
|
|
});
|
|
|
|
it("resolves known legacy references to emoji and unknown references to an initial", () => {
|
|
expect(resolveProjectIcon({ set: "devicon", name: "python" })?.glyph).toBe("🐍");
|
|
expect(toEmojiIconRef({ set: "lucide", name: "star" })).toEqual({ set: "emoji", name: "star" });
|
|
expect(findProjectIcon({ set: "devicon", name: "python" })).toBeNull();
|
|
|
|
const { getByRole } = render(<ProjectIcon icon={{ set: "devicon", name: "unknown-tool" }} />);
|
|
expect(getByRole("img", { name: "U" }).textContent).toBe("U");
|
|
expect(getByRole("img").dataset.resolvedProjectIcon).toBe("initial");
|
|
});
|
|
|
|
it("keeps legacy alphabet initials readable", () => {
|
|
const { getByRole } = render(<ProjectIcon icon={{ set: "idea", name: "letter-z" }} />);
|
|
expect(getByRole("img", { name: "Z" }).textContent).toBe("Z");
|
|
});
|
|
|
|
it("uses the project initial when no icon is configured", () => {
|
|
const { getByRole } = render(<ProjectIcon icon={null} fallbackLabel="Alpha project" />);
|
|
expect(getByRole("img", { name: "A" }).textContent).toBe("A");
|
|
expect(getByRole("img").dataset.projectIcon).toBe("none");
|
|
});
|
|
|
|
it("keeps the full emoji artwork available for grayscale watermark rendering", () => {
|
|
const { getByTestId } = render(
|
|
<>
|
|
<ProjectIcon icon={{ set: "emoji", name: "star" }} data-testid="product-icon" />
|
|
<ProjectIcon
|
|
icon={{ set: "emoji", name: "star" }}
|
|
surface="watermark"
|
|
decorative
|
|
data-testid="watermark-icon"
|
|
/>
|
|
</>,
|
|
);
|
|
|
|
expect(getByTestId("product-icon").textContent).toBe("⭐");
|
|
expect(getByTestId("watermark-icon").textContent).toBe("⭐");
|
|
expect((getByTestId("watermark-icon") as HTMLElement).style.fontFamily).toContain("Color Emoji");
|
|
});
|
|
});
|
|
|
|
describe("ProjectIconPicker", () => {
|
|
function renderPicker(value = null as Parameters<typeof ProjectIconPicker>[0]["value"]) {
|
|
render(<ProjectIconPicker value={value} onApply={vi.fn()} onCancel={vi.fn()} />);
|
|
}
|
|
|
|
it("shows phone-like emoji categories only", () => {
|
|
renderPicker();
|
|
for (const label of ["Smileys", "Animaux et nature", "Nourriture et boissons", "Activités", "Voyages et lieux", "Objets", "Symboles"]) {
|
|
expect(screen.getByText(label)).toBeTruthy();
|
|
}
|
|
expect(screen.queryByText("Technologies / Frameworks")).toBeNull();
|
|
expect(screen.queryByText("Lettres")).toBeNull();
|
|
});
|
|
|
|
it.each([
|
|
["python", "Snake"],
|
|
["docker", "Package"],
|
|
["space launch", "Rocket"],
|
|
["database", "Floppy disk"],
|
|
])("finds %s using the English search metadata", (query, label) => {
|
|
renderPicker();
|
|
fireEvent.change(screen.getByLabelText("Search icons in English"), { target: { value: query } });
|
|
expect(screen.getByRole("option", { name: label })).toBeTruthy();
|
|
});
|
|
|
|
it("selects and persists an emoji reference", () => {
|
|
const onApply = vi.fn();
|
|
render(<ProjectIconPicker value={null} onApply={onApply} onCancel={vi.fn()} />);
|
|
fireEvent.change(screen.getByLabelText("Search icons in English"), { target: { value: "rocket" } });
|
|
fireEvent.click(screen.getByRole("option", { name: "Rocket" }));
|
|
fireEvent.click(screen.getByRole("button", { name: "Appliquer" }));
|
|
expect(onApply).toHaveBeenCalledWith({ set: "emoji", name: "rocket" });
|
|
});
|
|
|
|
it("normalizes a known legacy selection before applying it", () => {
|
|
const onApply = vi.fn();
|
|
render(<ProjectIconPicker value={{ set: "devicon", name: "python" }} onApply={onApply} onCancel={vi.fn()} />);
|
|
fireEvent.click(screen.getByRole("button", { name: "Appliquer" }));
|
|
expect(onApply).toHaveBeenCalledWith({ set: "emoji", name: "snake" });
|
|
});
|
|
});
|