import { test } from "node:test"; import assert from "node:assert/strict"; import { mkdtemp, readFile, readdir, rm } from "node:fs/promises"; import { readFileSync } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; import { mediaBannerSvg, mediaLockupGeometry, mediaMarkSvg, MEDIA_BANNER, MEDIA_VIDEO_KIT_PX } from "../lib/brandMedia"; import { BANNER_MAX_BYTES, MEDIA_ASSETS, VIDEO_KIT_PATH, videoKitJson, writeMediaAssets } from "./brand-media"; const PNG_MAGIC = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]; function ihdr(png: Buffer): { width: number; height: number; colourType: number } { assert.deepEqual([...png.subarray(0, 8)], PNG_MAGIC, "PNG magic"); assert.equal(png.toString("latin1", 12, 16), "IHDR"); return { width: png.readUInt32BE(16), height: png.readUInt32BE(20), colourType: png[25] }; } test("the asset list: YouTube's four sizes, each from its own SVG", () => { assert.deepEqual( MEDIA_ASSETS.map((a) => [a.png, a.svg, a.width, a.height]), [ ["avatar-800.png", "avatar.svg", 800, 800], ["watermark-150.png", "watermark.svg", 150, 150], ["banner-2560x1440.png", "banner.svg", 2560, 1440], ["banner-safe-1546x423.png", "banner-safe.svg", 1546, 423], ], ); const by = Object.fromEntries(MEDIA_ASSETS.map((a) => [a.png, a.render()])); assert.equal(by["avatar-800.png"], mediaMarkSvg("maskable")); assert.equal(by["watermark-150.png"], mediaMarkSvg("any")); assert.equal(by["banner-2560x1440.png"], mediaBannerSvg()); assert.equal(by["banner-safe-1546x423.png"], mediaBannerSvg({ view: MEDIA_BANNER.safe })); }); test("writeMediaAssets: every PNG at its IHDR size, the banner under 6 MB, sources and INDEX.html beside them", async () => { const dir = await mkdtemp(path.join(tmpdir(), "brand-media-")); try { const rows = await writeMediaAssets(dir); const names = (await readdir(dir)).sort(); assert.deepEqual(names, [ "INDEX.html", "avatar-800.png", "avatar.svg", "banner-2560x1440.png", "banner-safe-1546x423.png", "banner-safe.svg", "banner.svg", "lockup.svg", "watermark-150.png", "watermark.svg", ]); for (const r of rows) { const png = await readFile(path.join(dir, r.png)); const h = ihdr(png); assert.deepEqual([h.width, h.height], [r.width, r.height], r.png); assert.equal(png.byteLength, r.bytes); assert.equal(await readFile(path.join(dir, r.svg), "utf8"), r.render()); } const banner = await readFile(path.join(dir, "banner-2560x1440.png")); assert.ok(banner.byteLength <= BANNER_MAX_BYTES, `${banner.byteLength} B`); const html = await readFile(path.join(dir, "INDEX.html"), "utf8"); for (const r of rows) assert.ok(html.includes(`src="${r.png}"`), r.png); // Studio's current page: Customization -> Profile (Branding was folded into it). for (const step of ["Customization", "Profile", "Picture", "Banner image", "Video watermark", "Change"]) { assert.ok(html.includes(step), step); } } finally { await rm(dir, { recursive: true, force: true }); } }); // umtool's report-to-video preset cannot import TypeScript, so it reads this // JSON. Stale means the preset draws a lockup the channel no longer uses. test("umtool/report-to-video/brands/archilyzer-media.json is what --video-kit writes", () => { assert.equal(readFileSync(VIDEO_KIT_PATH, "utf8"), videoKitJson()); const kit = JSON.parse(videoKitJson()); const g = mediaLockupGeometry(MEDIA_VIDEO_KIT_PX); assert.equal(kit.lockup.px, MEDIA_VIDEO_KIT_PX); assert.ok(Math.abs(kit.lockup.blockHeight - g.blockHeight) < 1e-3); assert.deepEqual(kit.palette, { bg: "#151b20", fg: "#e7edf1", muted: "#8496a2", accent: "#5fa8a0", amber: "#e3b15c", dim: "#586977", }); });