* feat(draw-image): add SVG template render engine * feat(draw-image): add opencode plugin and tool tests * test(draw-image): add vitest unit integration e2e suite * chore(draw-image): wire CI dependabot docs and ADR * docs(handoff): set PR number * docs(handoff): fix PR number placeholder in frontmatter * fix(ci): use tempfile and trailing newline in draw-image tests --------- Co-authored-by: opencode-agent <agent@opencode.local>
75 lines
No EOL
2.9 KiB
TypeScript
75 lines
No EOL
2.9 KiB
TypeScript
import { describe, test, expect, beforeAll } from "vitest"
|
|
import { existsSync, rmSync, mkdirSync } from "node:fs"
|
|
import path from "node:path"
|
|
import { fileURLToPath } from "node:url"
|
|
import { loadBrand } from "../src/config"
|
|
import { loadTemplate, buildSvg } from "../src/render"
|
|
import { parseSlots } from "../src/slot-parser"
|
|
import { resolveSlotContent } from "../src/resolve"
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
const DRAW_IMAGE_DIR = path.resolve(__dirname, "..")
|
|
|
|
beforeAll(() => {
|
|
const tmp = "/tmp/draw-image-slot"
|
|
if (existsSync(tmp)) rmSync(tmp, { recursive: true, force: true })
|
|
mkdirSync(tmp, { recursive: true })
|
|
})
|
|
|
|
describe("integration — slot resolution", () => {
|
|
test("icon=mic resolves lucide icon and recolors to accent", () => {
|
|
const brand = loadBrand(DRAW_IMAGE_DIR)
|
|
const templateSvg = loadTemplate(DRAW_IMAGE_DIR, "cover")
|
|
const slots = parseSlots(templateSvg)
|
|
const iconSlot = slots.find((s) => s.name === "icon")!
|
|
expect(iconSlot).toBeDefined()
|
|
|
|
const resolved = resolveSlotContent(iconSlot, brand, DRAW_IMAGE_DIR, "mic")
|
|
expect(resolved).not.toBeNull()
|
|
expect(resolved!.inner).toContain("<path")
|
|
expect(resolved!.rootAttrs.stroke).toBe("#ccff00")
|
|
expect(resolved!.contentW).toBe(24)
|
|
expect(resolved!.contentH).toBe(24)
|
|
})
|
|
|
|
test("icon=play resolves lucide icon", () => {
|
|
const brand = loadBrand(DRAW_IMAGE_DIR)
|
|
const templateSvg = loadTemplate(DRAW_IMAGE_DIR, "cover")
|
|
const slots = parseSlots(templateSvg)
|
|
const iconSlot = slots.find((s) => s.name === "icon")!
|
|
const resolved = resolveSlotContent(iconSlot, brand, DRAW_IMAGE_DIR, "play")
|
|
expect(resolved).not.toBeNull()
|
|
expect(resolved!.inner).toContain("<path")
|
|
})
|
|
|
|
test("badge slot has bg=surface and border=accent", () => {
|
|
const templateSvg = loadTemplate(DRAW_IMAGE_DIR, "cover")
|
|
const slots = parseSlots(templateSvg)
|
|
const badgeSlot = slots.find((s) => s.name === "badge")!
|
|
expect(badgeSlot.bg).toBe("surface")
|
|
expect(badgeSlot.border).toBe("accent")
|
|
expect(badgeSlot.radius).toBe(0.5)
|
|
})
|
|
|
|
test("buildSvg inserts slot content into final SVG", () => {
|
|
const brand = loadBrand(DRAW_IMAGE_DIR)
|
|
const templateSvg = loadTemplate(DRAW_IMAGE_DIR, "cover")
|
|
const svg = buildSvg(templateSvg, brand, {
|
|
template: "cover",
|
|
title: "With Icon",
|
|
slots: { icon: "mic" },
|
|
}, DRAW_IMAGE_DIR)
|
|
expect(svg).toContain("<path")
|
|
expect(svg).toContain('stroke="#ccff00"')
|
|
expect(svg).not.toContain("<!-- slot:")
|
|
expect(svg).toContain("With Icon")
|
|
})
|
|
|
|
test("buildSvg without slots renders template defaults", () => {
|
|
const brand = loadBrand(DRAW_IMAGE_DIR)
|
|
const templateSvg = loadTemplate(DRAW_IMAGE_DIR, "cover")
|
|
const svg = buildSvg(templateSvg, brand, { template: "cover", title: "No Icon" }, DRAW_IMAGE_DIR)
|
|
expect(svg).toContain("No Icon")
|
|
expect(svg).not.toContain("<!-- slot:")
|
|
})
|
|
}) |