1// Drives demo/europium.html in headless Chromium against the dev server's in-memory 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}1718Deno.test({ name: "europium: note embeds", sanitizeResources: false, sanitizeOps: false }, async (t) => {19 const server = serve(0);20 const url = `http://localhost:${server.addr.port}/demo/europium.html`;21 const browser = await chromium.launch({ executablePath: await chromiumPath() });22 const page = await browser.newPage();23 const errors = [];24 page.on("pageerror", (e) => errors.push(e.message));25 const rendered = (sel) => page.waitForFunction((s) => document.querySelector(`${s} article [neu-part=content]`), sel);26 try {27 await page.goto(url);28 await page.waitForFunction(() => window.demo);29 const demo = await page.evaluate(() => window.demo);3031 await t.step("nevent renders the default markup with escaped, linked content", async () => {32 const a = page.locator("#a article");33 assertEquals(await a.getAttribute("nxe-filter"), JSON.stringify({ ids: [demo.note.id] }));34 assertEquals(await a.getAttribute("nhy-relay"), `ws://localhost:${server.addr.port}/relay`, "relay hints become nhy-relay");35 await rendered("#a");36 assertEquals(await a.locator("[neu-part=content] b").count(), 0, "content must be escaped");37 assert((await a.locator("[neu-part=content]").textContent()).includes("<b>europium</b>"));38 assertEquals(await a.locator("[neu-part=content] a").getAttribute("href"), "https://example.com/x");39 assertEquals(await a.locator("[neu-part=time]").textContent(), "now");40 const link = await a.locator("[neu-part=link]").getAttribute("href");41 assert(link.startsWith("https://njump.me/nevent1"));42 assertEquals(await page.evaluate((n) => neon.decode(n.slice(17)).data.id, link), demo.note.id);43 assertEquals(await a.locator("template").count(), 0, "the swap replaces the template");44 assertEquals(await a.getAttribute("neu-loading"), null, "loading clears after the swap");45 for (const p of ["author", "avatar", "name", "time", "content", "link"]) assertEquals(await a.locator(`[neu-part=${p}]`).count(), 1, p);46 });4748 await t.step("radon fills the author's name", async () => {49 await page.waitForFunction(() => document.querySelector("#a [neu-part=name]").textContent === "Ada");50 });5152 await t.step("note1 and hex ids work too", async () => {53 await rendered("#b");54 await rendered("#c");55 for (const s of ["#b", "#c"]) assert((await page.locator(`${s} [neu-part=content]`).textContent()).includes("europium"));56 });5758 await t.step("a hand-written template is used instead of the default, with {reply}", async () => {59 await rendered("#d");60 const d = page.locator("#d article");61 assertEquals(await d.locator("header").count(), 0);62 assertEquals(await d.locator("p[neu-part=content]").textContent(), "a reply");63 const href = await d.locator("[neu-part=link]").getAttribute("href");64 assertEquals(await page.evaluate((h) => neon.decode(h.slice(17)).data.id, href), demo.note.id);65 });6667 await t.step("a missing note sets neu-missing after neu-timeout", async () => {68 const e = page.locator("#e article");69 assertEquals(await e.getAttribute("neu-loading"), "");70 await page.waitForFunction(() => document.querySelector("#e article").hasAttribute("neu-missing"), null, { timeout: 4000 });71 assertEquals(await e.getAttribute("neu-loading"), null);72 assertEquals(await e.textContent(), "note not found");73 });7475 await t.step("europium.markup() and process() are exposed", async () => {76 assert(await page.evaluate(() => typeof europium.process === "function" && europium.markup().includes('neu-part="content">{content}')));77 assert(await page.evaluate(() => europium.markup("https://x/").includes('href="https://x/{nevent}"')));78 });7980 await t.step("reload renders from argon's cache with no relay", async () => {81 await page.addInitScript(() => {82 window.__from = [];83 document.addEventListener("nhy:event", (e) => e.target.id === "cached" && window.__from.push(e.detail.from));84 });85 await page.reload();86 await page.waitForFunction(() => window.demo);87 await page.evaluate((id) => {88 document.body.insertAdjacentHTML("beforeend", `<article id="cached" nhy-relay="ws://127.0.0.1:9/" neu-note="${neon.encode("note", id)}"></article>`);89 }, demo.note.id);90 await page.waitForFunction(() => document.querySelector("#cached [neu-part=content]"));91 assert((await page.locator("#cached [neu-part=content]").textContent()).includes("europium"));92 assertEquals(await page.evaluate(() => window.__from), ["argon"]);93 await page.waitForFunction(() => document.querySelector("#cached [neu-part=name]").textContent === "Ada");94 });9596 assertEquals(errors, []);97 } finally {98 await browser.close();99 await server.shutdown();100 }101});