clone

tiny dependency-free nostr libraries for plain html, each under 5kb and named for an element. public domain

main
nobel / test / oganesson.test.js
oganesson.test.js 5.9K
1// Two browsers (separate localStorage, so separate helium keys) meet on demo/oganesson.html.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 pks = (page, sel) => page.evaluate((s) => [...document.querySelectorAll(s)].map((e) => e.getAttribute("nog-pk")), sel);1920const has = (page, sel, pk) => page.evaluate(([s, pk]) => !!document.querySelector(`${s} [nog-pk="${pk}"]`), [sel, pk]);21const setHidden = (page, hidden) => page.evaluate((h) => {22 Object.defineProperty(document, "hidden", { value: h, configurable: true });23 document.dispatchEvent(new Event("visibilitychange"));24}, hidden);2526Deno.test({ name: "oganesson: presence, typing, cursors, nothing stored", sanitizeResources: false, sanitizeOps: false }, async (t) => {27 const server = serve(0);28 const url = `http://localhost:${server.addr.port}/demo/oganesson.html`;29 const browser = await chromium.launch({ executablePath: await chromiumPath() });30 const errors = [];31 const open = async () => {32 const page = await (await browser.newContext()).newPage();33 page.on("pageerror", (e) => errors.push(e.message));34 await page.addInitScript(() => {35 window.__got = [];36 document.addEventListener("nog:shown", (e) => window.__got.push(e.detail.event));37 });38 await page.goto(url);39 return page;40 };41 try {42 const a = await open(), b = await open();43 const pkA = await a.evaluate(() => helium.signer.getPublicKey());44 const pkB = await b.evaluate(() => helium.signer.getPublicKey());45 assert(pkA !== pkB, "separate contexts have separate keys");46 const got = (kind) => a.evaluate((k) => window.__got.filter((e) => e.kind === k), kind);4748 await t.step("each sees the other present at once, not themselves", async () => {49 // Far sooner than the 60s heartbeat: seeing a newcomer triggers a heartbeat back.50 await a.waitForFunction((pk) => document.querySelector(`#here [nog-pk="${pk}"]`), pkB, { timeout: 3000 });51 await b.waitForFunction((pk) => document.querySelector(`#here [nog-pk="${pk}"]`), pkA, { timeout: 3000 });52 assertEquals(await pks(a, "#here li"), [pkB]);53 assertEquals(await pks(b, "#here li"), [pkA]);54 assertEquals(await a.textContent("#here li small"), "online");55 const e = (await got(20001)).at(-1);56 assertEquals([e.kind, e.tags, e.content], [20001, [["h", "oganesson-demo"]], "online"]);57 });5859 await t.step("a status change replaces the entry and fills {status}", async () => {60 await b.selectOption("#away", "away");61 await a.waitForFunction(() => document.querySelector("#here li small")?.textContent === "away");62 assertEquals(await pks(a, "#here li"), [pkB]);63 });6465 await t.step("typing shows, refreshes in place, and stops when emptied", async () => {66 await b.type("#say", "hi");67 await a.waitForFunction((pk) => document.querySelector(`#typing [nog-pk="${pk}"]`), pkB);68 assertEquals(await b.locator("#typing span").count(), 0, "own typing hidden");69 await b.waitForTimeout(3200); // past the 3s throttle70 await b.type("#say", "!");71 await a.waitForTimeout(500);72 assertEquals(await pks(a, "#typing span"), [pkB]);73 const e = (await got(20002)).at(-1);74 assertEquals([e.tags, e.content], [[["h", "oganesson-demo"]], "typing"]);75 await b.fill("#say", "");76 await a.waitForFunction(() => !document.querySelector("#typing span"), null, { timeout: 1500 });77 });7879 await t.step("typing expires after its 5s ttl", async () => {80 await b.waitForTimeout(3100);81 await b.type("#say", "x");82 await a.waitForFunction(() => document.querySelector("#typing span"));83 const t0 = Date.now();84 await a.waitForFunction(() => !document.querySelector("#typing span"), null, { timeout: 7000 });85 assert(Date.now() - t0 > 3000, "stays until its ttl");86 });8788 await t.step("cursors carry {x} and {y} on the custom kind", async () => {89 const box = await b.locator("#pad").boundingBox();90 await b.mouse.move(box.x + box.width / 4, box.y + box.height / 2);91 await a.waitForFunction(() => document.querySelector("#pad i")?.getAttribute("style").includes("left:calc(0.25 * 100%)"));92 assert((await a.locator("#pad i").getAttribute("style")).includes("top:calc(0.5 * 100%)"));93 await b.mouse.move(box.x + box.width * 0.75, box.y + box.height / 2, { steps: 5 });94 await a.waitForFunction(() => document.querySelector("#pad i").getAttribute("style").includes("left:calc(0.75 * 100%)"));95 assertEquals(await pks(a, "#pad i"), [pkB]);96 assertEquals((await got(20078)).at(-1).tags, [["h", "oganesson-demo"], ["t", "cursor"]]);97 });9899 await t.step("a hidden page goes offline at once and returns when visible", async () => {100 await setHidden(b, true);101 await a.waitForFunction((pk) => !document.querySelector(`#here [nog-pk="${pk}"]`), pkB, { timeout: 2000 });102 assert(await has(b, "#here", pkA), "hidden B keeps its list");103 await setHidden(b, false);104 await a.waitForFunction((pk) => document.querySelector(`#here [nog-pk="${pk}"]`), pkB, { timeout: 3000 });105 });106107 await t.step("argon stores none of it", async () => {108 for (const p of [a, b]) assertEquals(await p.evaluate(() => argon.query([{ kinds: [20001, 20002, 20078] }])), []);109 });110111 assertEquals(errors, []);112 } finally {113 await browser.close();114 await server.shutdown();115 }116});