1// Drives demo/lanthanides.html (praseodymium + cerium) in headless Chromium against the dev server's relay.2import { assert, assertEquals } from "jsr:@std/assert@1";3import { chromium } from "npm:playwright@1.58";4import { serve } from "../server.ts";56// Use CHROMIUM if set, else the newest headless shell already in the Playwright cache.7async function chromiumPath() {8 if (Deno.env.get("CHROMIUM")) return Deno.env.get("CHROMIUM");9 const cache = `${Deno.env.get("HOME")}/Library/Caches/ms-playwright`;10 try {11 const dirs = [...Deno.readDirSync(cache)].map((d) => d.name).filter((n) => n.startsWith("chromium_headless_shell-")).sort();12 const dir = `${cache}/${dirs.at(-1)}`;13 const arch = [...Deno.readDirSync(dir)][0].name;14 return `${dir}/${arch}/chrome-headless-shell`;15 } catch { return undefined; }16}1718const PIC = "data:image/svg+xml," + encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1 1"><rect width="1" height="1" fill="red"/></svg>');1920Deno.test({ name: "lanthanides: praseodymium presets, cerium styles", sanitizeResources: false, sanitizeOps: false }, async (t) => {21 const server = serve(0);22 const origin = `http://localhost:${server.addr.port}`;23 const url = `${origin}/demo/lanthanides.html`;24 const browser = await chromium.launch({ executablePath: await chromiumPath() });25 const context = await browser.newContext({ permissions: ["clipboard-read", "clipboard-write"] });26 const page = await context.newPage();27 const errors = [];28 page.on("pageerror", (e) => errors.push(e.message));29 // Record what praseodymium inserts, before radon or avatars touch it.30 await page.addInitScript(() => {31 window.__rendered = {};32 document.addEventListener("npr:render", (e) => window.__rendered[e.target.id] = e.detail.html);33 });34 try {35 await page.goto(url);36 const pk = await page.evaluate(() => pk);37 const npub = await page.evaluate(() => neon.encode("npub", pk));38 const short = npub.slice(0, 10) + "…" + npub.slice(-4);3940 await t.step("an empty npr-card gets exactly the documented markup", async () => {41 const expected = `<img npr-avatar="${pk}" npr-part="avatar" alt="">` +42 `<b nrn-profile="${pk}" nrn-field="name" npr-part="name">${short}</b>` +43 `<span nrn-profile="${pk}" nrn-field="nip05" npr-part="nip05"></span>` +44 `<p nrn-profile="${pk}" nrn-field="about" npr-part="about"></p>` +45 `<button npr-copy="${npub}" npr-part="copy">${short}</button>`;46 assertEquals(await page.evaluate(() => praseodymium.markup("card", pk)), expected);47 assertEquals(await page.evaluate(() => praseodymium.markup("card", neon.encode("npub", pk))), expected, "npub in, same markup");48 assertEquals(await page.evaluate(() => window.__rendered.card), expected, "markup() equals what was inserted");49 assertEquals(await page.evaluate(() => praseodymium.markup("chip", pk)),50 `<img npr-avatar="${pk}" npr-part="avatar" alt=""><b nrn-profile="${pk}" nrn-field="name" npr-part="name">${short}</b>`);51 assertEquals(await page.evaluate(() => window.__rendered.chip), await page.evaluate(() => praseodymium.markup("chip", pk)));52 });5354 await t.step("hand-writing markup() gives the same result as the preset", async () => {55 const same = await page.evaluate(async () => {56 let a = document.createElement("article");57 a.innerHTML = praseodymium.markup("card", pk);58 document.querySelector("#praseodymium").append(a);59 await new Promise((r) => setTimeout(r, 50));60 let out = a.innerHTML === card.innerHTML;61 a.remove();62 return out;63 });64 assert(same);65 });6667 await t.step("a card with hand-written children is left alone", async () => {68 assertEquals(await page.evaluate(() => window.__rendered.hand), undefined);69 assertEquals(await page.locator("#hand > *").count(), 1);70 assertEquals(await page.textContent("#hand b"), "my own markup");71 });7273 await t.step("npr-avatar shows the identicon before a picture", async () => {74 const src = await page.getAttribute("#avatar", "src");75 assert(src.startsWith("data:image/svg+xml,"), src);76 assertEquals(src, await page.evaluate(() => praseodymium.face(pk)));77 assertEquals(await page.getAttribute("#avatar", "nrn-profile"), pk);78 assertEquals(await page.getAttribute("#avatar", "nrn-field"), "picture");79 assert(await page.evaluate(() => avatar.complete && avatar.naturalWidth > 0), "fallback renders");80 assertEquals(await page.getAttribute("#card [npr-part=avatar]", "src"), src);81 });8283 await t.step("a published kind 0 fills cards, chips and avatars", async () => {84 await page.fill("#profile [name=picture]", PIC);85 await page.evaluate(() => new Promise((r) => {86 document.addEventListener("nxe:published", r, { once: true });87 profile.requestSubmit();88 }));89 await page.waitForFunction(() => document.querySelector("#card [npr-part=name]").textContent === "Ada");90 assertEquals(await page.textContent("#card [npr-part=about]"), "Counting engines.");91 assertEquals(await page.getAttribute("#avatar", "src"), PIC);92 assertEquals(await page.textContent("#chip [npr-part=name]"), "Ada");93 assertEquals(await page.textContent("#hand b"), "Ada", "radon still fills hand-written fields");94 });9596 await t.step("after reload radon loads the profile from the relay", async () => {97 await page.reload();98 await page.waitForFunction(() => document.querySelector("#card [npr-part=name]").textContent === "Ada");99 await page.waitForFunction((pic) => document.querySelector("#card [npr-part=avatar]").getAttribute("src") === pic, PIC);100 assertEquals(await page.textContent("#card [npr-part=about]"), "Counting engines.");101 });102103 await t.step("a broken picture falls back to the identicon", async () => {104 const src = await page.evaluate(async () => {105 avatar.src = "/nope.png";106 await new Promise((r) => setTimeout(r, 300));107 return avatar.getAttribute("src");108 });109 assertEquals(src, await page.evaluate(() => praseodymium.face(pk)));110 });111112 await t.step("npr-copy copies and toggles npr-copied", async () => {113 const copied = page.evaluate(() => new Promise((r) => document.addEventListener("npr:copied", (e) => r(e.detail.text), { once: true })));114 await page.click("#copy");115 assertEquals(await copied, "hello nobel");116 assertEquals(await page.evaluate(() => navigator.clipboard.readText()), "hello nobel");117 assert(await page.evaluate(() => copy.hasAttribute("npr-copied")));118 await page.waitForFunction(() => !copy.hasAttribute("npr-copied"), null, { timeout: 3000 });119 await page.click("#card [npr-part=copy]");120 await page.waitForFunction(() => document.querySelector("#card [npr-part=copy]").hasAttribute("npr-copied"));121 assertEquals(await page.evaluate(() => navigator.clipboard.readText()), npub);122 });123124 await t.step("cerium injects one layered, zero-specificity style", async () => {125 const styles = await page.evaluate(() => [...document.querySelectorAll("style[nce-style]")].map((s) => s.textContent));126 assertEquals(styles.length, 1);127 assert(styles[0].startsWith("@layer nobel {"));128 assertEquals(await page.evaluate(() => cerium.css === cerium.style.textContent), true);129 assertEquals(await page.evaluate(() => getComputedStyle(card).borderTopLeftRadius), "12px");130 assertEquals(await page.evaluate(() => getComputedStyle(avatar).borderTopLeftRadius), "50%");131 });132133 await t.step("page CSS overrides a themed property", async () => {134 assertEquals(await page.evaluate(() => getComputedStyle(document.querySelector("#themed button")).color), "rgb(200, 30, 60)");135 await page.addStyleTag({ content: "#card { border-radius: 2px }" });136 assertEquals(await page.evaluate(() => getComputedStyle(card).borderTopLeftRadius), "2px");137 });138139 await t.step("nce-bare skips the style but keeps cerium.css", async () => {140 const bare = await context.newPage();141 bare.on("pageerror", (e) => errors.push(e.message));142 await bare.goto(`${origin}/demo/`);143 await bare.setContent(`<html nce-bare><head><script src="${origin}/cerium/cerium.js"></script></head><body></body></html>`);144 assertEquals(await bare.evaluate(() => document.querySelectorAll("style[nce-style]").length), 0);145 assert(await bare.evaluate(() => cerium.css.includes("[npr-part=avatar]") && cerium.style === null));146 await bare.close();147 });148149 assertEquals(errors, []);150 } finally {151 await browser.close();152 await server.shutdown();153 }154});