feat(domain,frontend): remplace le catalogue d'icônes projet par des emojis Unicode système — #345 (QA verte)
Reprend à zéro la liste des icônes projet: catalogue d'emojis Unicode rendus par la police système, sans embarquer d'assets graphiques propriétaires. - domain: variante `emoji` ajoutée à ProjectIconSet (serde kebab-case) - ports TS: union ProjectIconSet alignée avec "emoji" - frontend: catalogue réécrit en 7 groupes (smileys, nature, food, activities, travel, objects, symbols), registres légaux et métadonnées supprimés, assets SVG projet retirés (angular.svg, NOTICE.md) - watermark CLI custom: rendu emoji direct avec libellé de repli dérivé du nom de dossier du cwd - i18n FR/EN des groupes du picker et tests mis à jour (domain settings, adapter projet, picker, panneau settings, chat custom) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -7,86 +7,81 @@ import {
|
||||
ProjectIcon,
|
||||
findProjectIcon,
|
||||
resolveProjectIcon,
|
||||
toEmojiIconRef,
|
||||
} from "./projectIcons";
|
||||
|
||||
describe("project icon catalogue", () => {
|
||||
it("carries legal policy for every entry and an attribution for the vendored Angular artwork", () => {
|
||||
expect(PROJECT_ICON_CATALOG.length).toBeGreaterThan(70);
|
||||
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.legal.owner).toBeTruthy();
|
||||
expect(entry.legal.license).toBeTruthy();
|
||||
expect(typeof entry.legal.productUiAllowed).toBe("boolean");
|
||||
expect(typeof entry.legal.watermarkAllowed).toBe("boolean");
|
||||
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");
|
||||
}
|
||||
|
||||
const angular = findProjectIcon({ set: "simple-icons", name: "angular" });
|
||||
expect(angular).toMatchObject({
|
||||
group: "technologies",
|
||||
legal: {
|
||||
license: "CC BY 4.0",
|
||||
productUiAllowed: true,
|
||||
watermarkAllowed: true,
|
||||
sourceUrl: "https://angular.dev/press-kit",
|
||||
},
|
||||
});
|
||||
expect(angular?.assetSrc).toBeTruthy();
|
||||
});
|
||||
|
||||
it.each([
|
||||
["unity", "gamepad-2"],
|
||||
["unrealengine", "gamepad-2"],
|
||||
["windows", "terminal"],
|
||||
["apple", "terminal"],
|
||||
["google", "globe-2"],
|
||||
["microsoft", "terminal"],
|
||||
])("resolves the restricted %s brand to product-owned fallback artwork", (name, fallbackName) => {
|
||||
const set = name === "windows" || name === "apple" ? "devicon" : "simple-icons";
|
||||
const restricted = findProjectIcon({ set, name });
|
||||
expect(restricted?.legal.productUiAllowed).toBe(false);
|
||||
expect(restricted?.legal.watermarkAllowed).toBe(false);
|
||||
expect(resolveProjectIcon({ set, name }, "product-ui")?.name).toBe(fallbackName);
|
||||
expect(resolveProjectIcon({ set, name }, "watermark")?.name).toBe(fallbackName);
|
||||
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("provides stable, strongly boxed A-Z monograms", () => {
|
||||
const letters = PROJECT_ICON_CATALOG.filter((entry) => entry.group === "letters");
|
||||
expect(letters.map((entry) => entry.glyph).join("")).toBe("ABCDEFGHIJKLMNOPQRSTUVWXYZ");
|
||||
it("keeps legacy alphabet initials readable", () => {
|
||||
const { getByRole } = render(<ProjectIcon icon={{ set: "idea", name: "letter-z" }} />);
|
||||
expect(getByRole("img", { name: "Z" }).style.borderTopWidth).toBe("1px");
|
||||
expect(getByRole("img", { name: "Z" }).style.borderRadius).toBe("18%");
|
||||
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");
|
||||
});
|
||||
});
|
||||
|
||||
describe("ProjectIconPicker", () => {
|
||||
function renderPicker() {
|
||||
render(<ProjectIconPicker value={null} onApply={vi.fn()} onCancel={vi.fn()} />);
|
||||
function renderPicker(value = null as Parameters<typeof ProjectIconPicker>[0]["value"]) {
|
||||
render(<ProjectIconPicker value={value} onApply={vi.fn()} onCancel={vi.fn()} />);
|
||||
}
|
||||
|
||||
it("shows the four catalogue groups", () => {
|
||||
it("shows phone-like emoji categories only", () => {
|
||||
renderPicker();
|
||||
expect(screen.getByText("Plateformes")).toBeTruthy();
|
||||
expect(screen.getByText("Technologies / Frameworks")).toBeTruthy();
|
||||
expect(screen.getByText("Symboles / Génériques")).toBeTruthy();
|
||||
expect(screen.getByText("Lettres")).toBeTruthy();
|
||||
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([
|
||||
["unity", "Unity"],
|
||||
["unreal", "Unreal Engine"],
|
||||
["angular", "Angular"],
|
||||
["windows", "Windows"],
|
||||
["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("finds and selects an alphabet monogram", () => {
|
||||
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: "letter q" } });
|
||||
fireEvent.click(screen.getByRole("option", { name: "Q" }));
|
||||
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: "idea", name: "letter-q" });
|
||||
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" });
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user