import { describe, expect, it } from "vitest"; import { parseAppPage } from "./app-page.js"; describe("APP.md frontmatter", () => { it("reads a short emoji icon and preserves body whitespace", () => { expect(parseAppPage("---\nicon: 🕵️‍♀️\n---\n\n# App guide\n")).toEqual({ icon: { type: "emoji", emoji: "🕵️‍♀️" }, content: "\n# App guide\n", }); }); it("reads a typed emoji icon", () => { expect( parseAppPage('---\nicon:\n type: emoji\n emoji: "👍🏽"\n---\n# App guide\n'), ).toEqual({ icon: { type: "emoji", emoji: "👍🏽" }, content: "# App guide\n", }); }); it("reads a colored Notion icon with a quoted description and comment", () => { expect( parseAppPage( '---\nicon:\n type: notion_icon\n description: "rocket" # A standard icon\n color: blue\n---\n# App guide\n', ), ).toEqual({ icon: { type: "notion_icon", description: "rocket", color: "blue" }, content: "# App guide\n", }); }); it("leaves the Notion icon color unset when omitted", () => { expect( parseAppPage("---\nicon: { type: notion_icon, description: rocket }\n---\n"), ).toEqual({ icon: { type: "notion_icon", description: "rocket" }, content: "", }); }); it("supports BOM and CRLF without changing body line endings", () => { expect(parseAppPage('\uFEFF---\r\nicon: "🚀"\r\n---\r\n# App guide\r\n')).toEqual({ icon: { type: "emoji", emoji: "🚀" }, content: "# App guide\r\n", }); }); it.each(["# App guide\n\n---\n\nBody\n", "", "---"])( "leaves Markdown without frontmatter unchanged: %j", (content) => { expect(parseAppPage(content)).toEqual({ content }); }, ); it.each(["---\n---\n# App guide\n", "---\n# Comment\n...\n# App guide\n"])( "omits the icon when frontmatter is empty: %j", (markdown) => { expect(parseAppPage(markdown)).toEqual({ content: "# App guide\n" }); }, ); it.each([ { name: "workspace emoji name", source: 'icon: ":team:"', message: "standard emoji" }, { name: "custom emoji object", source: "icon: { type: custom_emoji, id: team }", message: "not custom emoji or files", }, { name: "file icon", source: "icon: { type: file, resourceId: icon-file }", message: "not custom emoji or files", }, { name: "unsupported color", source: "icon: { type: notion_icon, description: rocket, color: rainbow }", message: "Notion page icon colors", }, { name: "null icon", source: "icon: null", message: "standard emoji or a Notion icon" }, { name: "list", source: "- 🚀", message: "YAML mapping" }, { name: "scalar", source: "🚀", message: "YAML mapping" }, { name: "malformed YAML", source: "icon: [", message: "Invalid APP.md frontmatter" }, { name: "duplicate icon", source: "icon: 🚀\nicon: 😀", message: "Map keys must be unique", }, { name: "custom YAML tag", source: "icon: !workspace team", message: "Unresolved tag", }, { name: "YAML alias", source: "other: &emoji 🚀\nicon: *emoji", message: "Alias", }, ])("rejects $name", ({ source, message }) => { expect(() => parseAppPage(`---\n${source}\n---\n# App guide\n`)).toThrow(message); }); it("reports an unclosed frontmatter section", () => { expect(() => parseAppPage("---\nicon: 🚀\n# App guide\n")).toThrow( "APP.md frontmatter must end", ); }); }); describe("APP.md custom page", () => { it("reads the custom block key and icon without emitting child content", () => { expect(parseAppPage("---\nicon: 📊\ncustom_block: dashboard\n---\n \n")).toEqual({ icon: { type: "emoji", emoji: "📊" }, customBlockKey: "dashboard", }); }); it.each(['""', '" "', "null", "true", "123", "[]", "{}"])( "rejects an invalid custom_block: %s", (value) => { expect(() => parseAppPage(`---\ncustom_block: ${value}\n---\n`)).toThrow( "Invalid APP.md frontmatter: custom_block must be a non-empty string.", ); }, ); it("rejects Markdown content on a custom page", () => { expect(() => parseAppPage("---\ncustom_block: dashboard\n---\n# Content")).toThrow( "APP.md cannot have body content when custom_block is set.", ); }); });