import { describe, it, expect, beforeEach, afterEach } from "vitest"; import { mkdtempSync, rmSync, writeFileSync, mkdirSync, realpathSync } from "fs"; import { join } from "path"; import { tmpdir } from "os"; import { findProjectIcon, getProjectIcon, ICON_PATTERNS, PRIORITY_DIRS, IGNORED_DIRS, MONOREPO_PACKAGE_DIRS, } from "./project-icon.js"; function createTempDir(): string { return realpathSync(mkdtempSync(join(tmpdir(), "project-icon-test-"))); } describe("findProjectIcon", () => { let tempDir: string; beforeEach(() => { tempDir = createTempDir(); }); afterEach(() => { rmSync(tempDir, { recursive: true, force: true }); }); describe("ICON_PATTERNS", () => { it("includes common favicon patterns", () => { expect(ICON_PATTERNS).toContain("favicon.ico"); expect(ICON_PATTERNS).toContain("favicon.png"); expect(ICON_PATTERNS).toContain("favicon.svg"); }); it("includes app icon patterns", () => { expect(ICON_PATTERNS).toContain("icon.png"); expect(ICON_PATTERNS).toContain("icon.svg"); expect(ICON_PATTERNS).toContain("app-icon.png"); }); it("includes logo patterns", () => { expect(ICON_PATTERNS).toContain("logo.png"); expect(ICON_PATTERNS).toContain("logo.svg"); }); }); describe("PRIORITY_DIRS", () => { it("includes common asset directories", () => { expect(PRIORITY_DIRS).toContain("public"); expect(PRIORITY_DIRS).toContain("static"); expect(PRIORITY_DIRS).toContain("assets"); }); it("includes Phoenix static assets directory", () => { expect(PRIORITY_DIRS).toContain("priv/static"); }); }); describe("IGNORED_DIRS", () => { it("includes common ignored directories", () => { expect(IGNORED_DIRS).toContain(".git"); expect(IGNORED_DIRS).toContain("node_modules"); expect(IGNORED_DIRS).toContain("dist"); expect(IGNORED_DIRS).toContain("build"); }); }); describe("MONOREPO_PACKAGE_DIRS", () => { it("includes common monorepo package directories", () => { expect(MONOREPO_PACKAGE_DIRS).toContain("packages"); expect(MONOREPO_PACKAGE_DIRS).toContain("apps"); }); }); it("returns null when no icon is found", async () => { const result = await findProjectIcon(tempDir); expect(result).toBeNull(); }); it("finds favicon.ico in root directory", async () => { writeFileSync(join(tempDir, "favicon.ico"), "icon content"); const result = await findProjectIcon(tempDir); expect(result).toBe(join(tempDir, "favicon.ico")); }); it("finds favicon.png in root directory", async () => { writeFileSync(join(tempDir, "favicon.png"), "icon content"); const result = await findProjectIcon(tempDir); expect(result).toBe(join(tempDir, "favicon.png")); }); it("finds icon in public directory (priority dir)", async () => { mkdirSync(join(tempDir, "public")); writeFileSync(join(tempDir, "public", "favicon.ico"), "icon content"); const result = await findProjectIcon(tempDir); expect(result).toBe(join(tempDir, "public", "favicon.ico")); }); it("finds icon in static directory (priority dir)", async () => { mkdirSync(join(tempDir, "static")); writeFileSync(join(tempDir, "static", "favicon.svg"), "icon content"); const result = await findProjectIcon(tempDir); expect(result).toBe(join(tempDir, "static", "favicon.svg")); }); it("finds icon in Phoenix priv/static directory", async () => { mkdirSync(join(tempDir, "priv", "static"), { recursive: true }); writeFileSync(join(tempDir, "priv", "static", "favicon.ico"), "icon content"); const result = await findProjectIcon(tempDir); expect(result).toBe(join(tempDir, "priv", "static", "favicon.ico")); }); it("finds icon in assets directory (priority dir)", async () => { mkdirSync(join(tempDir, "assets")); writeFileSync(join(tempDir, "assets", "logo.png"), "icon content"); const result = await findProjectIcon(tempDir); expect(result).toBe(join(tempDir, "assets", "logo.png")); }); it("prioritizes favicon over logo", async () => { writeFileSync(join(tempDir, "favicon.ico"), "favicon"); writeFileSync(join(tempDir, "logo.png"), "logo"); const result = await findProjectIcon(tempDir); expect(result).toBe(join(tempDir, "favicon.ico")); }); it("prioritizes priority dirs over root", async () => { writeFileSync(join(tempDir, "logo.png"), "root logo"); mkdirSync(join(tempDir, "public")); writeFileSync(join(tempDir, "public", "favicon.ico"), "public favicon"); const result = await findProjectIcon(tempDir); expect(result).toBe(join(tempDir, "public", "favicon.ico")); }); it("ignores files in .git directory", async () => { mkdirSync(join(tempDir, ".git")); writeFileSync(join(tempDir, ".git", "favicon.ico"), "git icon"); const result = await findProjectIcon(tempDir); expect(result).toBeNull(); }); it("ignores files in node_modules directory", async () => { mkdirSync(join(tempDir, "node_modules")); writeFileSync(join(tempDir, "node_modules", "favicon.ico"), "node icon"); const result = await findProjectIcon(tempDir); expect(result).toBeNull(); }); it("ignores files in dist directory", async () => { mkdirSync(join(tempDir, "dist")); writeFileSync(join(tempDir, "dist", "favicon.ico"), "dist icon"); const result = await findProjectIcon(tempDir); expect(result).toBeNull(); }); it("finds icon in nested priority directory", async () => { mkdirSync(join(tempDir, "public", "images"), { recursive: true }); writeFileSync(join(tempDir, "public", "images", "favicon.png"), "nested icon"); const result = await findProjectIcon(tempDir); expect(result).toBe(join(tempDir, "public", "images", "favicon.png")); }); it("finds apple-touch-icon.png", async () => { writeFileSync(join(tempDir, "apple-touch-icon.png"), "apple icon"); const result = await findProjectIcon(tempDir); expect(result).toBe(join(tempDir, "apple-touch-icon.png")); }); it("finds icon-*.png patterns", async () => { writeFileSync(join(tempDir, "icon-192.png"), "192 icon"); const result = await findProjectIcon(tempDir); expect(result).toBe(join(tempDir, "icon-192.png")); }); it("handles non-existent directory gracefully", async () => { const result = await findProjectIcon(join(tempDir, "nonexistent")); expect(result).toBeNull(); }); it("returns the first match when multiple icons exist in same location", async () => { writeFileSync(join(tempDir, "favicon.ico"), "ico"); writeFileSync(join(tempDir, "favicon.png"), "png"); writeFileSync(join(tempDir, "favicon.svg"), "svg"); const result = await findProjectIcon(tempDir); // Should return the first one based on pattern order (favicon.ico comes first) expect(result).toBe(join(tempDir, "favicon.ico")); }); describe("monorepo package directories", () => { it("finds icon in packages/*/public directory", async () => { mkdirSync(join(tempDir, "packages", "app", "public"), { recursive: true }); writeFileSync(join(tempDir, "packages", "app", "public", "favicon.ico"), "icon"); const result = await findProjectIcon(tempDir); expect(result).toBe(join(tempDir, "packages", "app", "public", "favicon.ico")); }); it("finds icon in apps/*/public directory", async () => { mkdirSync(join(tempDir, "apps", "web", "public"), { recursive: true }); writeFileSync(join(tempDir, "apps", "web", "public", "favicon.png"), "icon"); const result = await findProjectIcon(tempDir); expect(result).toBe(join(tempDir, "apps", "web", "public", "favicon.png")); }); it("finds icon in packages/* root", async () => { mkdirSync(join(tempDir, "packages", "ui"), { recursive: true }); writeFileSync(join(tempDir, "packages", "ui", "logo.svg"), "icon"); const result = await findProjectIcon(tempDir); expect(result).toBe(join(tempDir, "packages", "ui", "logo.svg")); }); it("finds icon in Phoenix app priv/static directory inside monorepo", async () => { mkdirSync(join(tempDir, "apps", "api", "priv", "static"), { recursive: true }); writeFileSync(join(tempDir, "apps", "api", "priv", "static", "favicon.ico"), "icon"); const result = await findProjectIcon(tempDir); expect(result).toBe(join(tempDir, "apps", "api", "priv", "static", "favicon.ico")); }); it("prioritizes root priority dirs over monorepo dirs", async () => { mkdirSync(join(tempDir, "public"), { recursive: true }); mkdirSync(join(tempDir, "packages", "app", "public"), { recursive: true }); writeFileSync(join(tempDir, "public", "favicon.ico"), "root icon"); writeFileSync(join(tempDir, "packages", "app", "public", "favicon.ico"), "package icon"); const result = await findProjectIcon(tempDir); expect(result).toBe(join(tempDir, "public", "favicon.ico")); }); it("prioritizes monorepo dirs over root dir (non-priority)", async () => { mkdirSync(join(tempDir, "packages", "app", "public"), { recursive: true }); writeFileSync(join(tempDir, "logo.png"), "root icon"); writeFileSync(join(tempDir, "packages", "app", "public", "favicon.ico"), "package icon"); const result = await findProjectIcon(tempDir); expect(result).toBe(join(tempDir, "packages", "app", "public", "favicon.ico")); }); }); }); describe("getProjectIcon", () => { let tempDir: string; beforeEach(() => { tempDir = createTempDir(); }); afterEach(() => { rmSync(tempDir, { recursive: true, force: true }); }); // Valid 1x1 PNG (square) const squarePng = Buffer.from([ 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, // PNG signature 0x00, 0x00, 0x00, 0x0d, // IHDR chunk length 0x49, 0x48, 0x44, 0x52, // IHDR 0x00, 0x00, 0x00, 0x01, // width: 1 0x00, 0x00, 0x00, 0x01, // height: 1 0x08, 0x02, // bit depth, color type 0x00, 0x00, 0x00, // compression, filter, interlace 0x90, 0x77, 0x53, 0xde, // CRC 0x00, 0x00, 0x00, 0x0c, // IDAT chunk length 0x49, 0x44, 0x41, 0x54, // IDAT 0x08, 0xd7, 0x63, 0xf8, 0xff, 0xff, 0xff, 0x00, 0x05, 0xfe, 0x02, 0xfe, // data 0xa3, 0x6c, 0x47, 0x9f, // CRC 0x00, 0x00, 0x00, 0x00, // IEND chunk length 0x49, 0x45, 0x4e, 0x44, // IEND 0xae, 0x42, 0x60, 0x82, // CRC ]); // Valid 2x1 PNG (non-square) const nonSquarePng = Buffer.from([ 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, // PNG signature 0x00, 0x00, 0x00, 0x0d, // IHDR chunk length 0x49, 0x48, 0x44, 0x52, // IHDR 0x00, 0x00, 0x00, 0x02, // width: 2 0x00, 0x00, 0x00, 0x01, // height: 1 0x08, 0x02, // bit depth, color type 0x00, 0x00, 0x00, // compression, filter, interlace 0x00, 0x00, 0x00, 0x00, // CRC (not validated) ]); it("returns icon data for square PNG", async () => { writeFileSync(join(tempDir, "favicon.png"), squarePng); const result = await getProjectIcon(tempDir); expect(result).not.toBeNull(); expect(result?.mimeType).toBe("image/png"); expect(result?.data).toBe(squarePng.toString("base64")); }); it("returns null for non-square PNG", async () => { writeFileSync(join(tempDir, "favicon.png"), nonSquarePng); const result = await getProjectIcon(tempDir); expect(result).toBeNull(); }); it("returns icon data for ICO files (assumed square)", async () => { writeFileSync(join(tempDir, "favicon.ico"), "ico content"); const result = await getProjectIcon(tempDir); expect(result).not.toBeNull(); expect(result?.mimeType).toBe("image/x-icon"); }); it("returns icon data for SVG files (assumed square)", async () => { writeFileSync(join(tempDir, "favicon.svg"), ""); const result = await getProjectIcon(tempDir); expect(result).not.toBeNull(); expect(result?.mimeType).toBe("image/svg+xml"); }); it("returns null for files over 32KB", async () => { const largeContent = Buffer.alloc(33 * 1024, 0); writeFileSync(join(tempDir, "favicon.ico"), largeContent); const result = await getProjectIcon(tempDir); expect(result).toBeNull(); }); it("returns null when no icon is found", async () => { const result = await getProjectIcon(tempDir); expect(result).toBeNull(); }); });