clone

a self-hosted multitenant nostr relay with sites, file storage, git hosting, and inbox and outbox delivery

main
tinyrelay / scripts / qa / room-attachments-browser.js
room-attachments-browser.js 10.3K
1// Run with playwright-cli run-code after signing in as the fixture owner:2// ... run-code --filename scripts/qa/room-attachments-browser.js3// The fixture is created by room-attachments-fixture.mjs.4async page => {5 const results = [], errors = [];6 const check = (condition, message) => { if (!condition) throw Error(message); };7 const run = async (name, action) => { try { results.push({name, ok: true, detail: await action()}); } catch (error) { results.push({name, ok: false, error: error.message}); } };8 page.on("pageerror", error => errors.push(error.message));9 const base = await page.evaluate(() => location.origin);10 const room = "/rooms/attachments-qa";11 await page.setViewportSize({width: 1440, height: 900});1213 await run("SSR room shows image, generic file and agent message", async () => {14 await page.goto(base + room);15 await page.waitForSelector("#messages room-message");16 await page.waitForFunction(() => typeof window.tiny?.signer === "function" && typeof window.tiny.signer()?.signEvent === "function");17 const info = await page.evaluate(() => ({18 messages: document.querySelectorAll("#messages room-message").length,19 images: document.querySelectorAll("#messages img").length,20 links: [...document.querySelectorAll("#messages a")].map(a => a.textContent.trim()),21 videos: document.querySelectorAll("#messages video").length,22 agent: [...document.querySelectorAll("#messages room-message")].filter(node => node.textContent.includes("Agent published file")).length,23 live: !!document.querySelector("room-live")24 }));25 check(info.images >= 1, "initial image was not rendered inline");26 check(info.links.some(label => label.includes("fixture notes.txt")), "generic file link missing");27 check(info.agent >= 1, "agent message missing from room timeline");28 check(info.live, "room live stream missing");29 const liveText = "Live agent attachment";30 await page.evaluate(async text => {31 const url = document.querySelector("#messages img")?.src;32 const unsigned = {kind: 9, created_at: Math.floor(Date.now() / 1000), tags: [["h", "attachments-qa"], ["imeta", "url " + url, "m image/png"]], content: text + "\n\n![image](" + url + ")"};33 const signer = window.tiny.signer();34 if (!signer?.signEvent) throw Error("connected signer unavailable for live check");35 const signed = await signer.signEvent(unsigned);36 const response = await window.tiny.signedFetch("/events", "POST", JSON.stringify(signed), {contentType: "application/json"});37 if (!response.ok) throw Error("live publish failed");38 }, liveText);39 await page.waitForFunction(text => document.querySelector("#messages")?.textContent.includes(text), liveText);40 await page.screenshot({path: "output/playwright/room-attachments/room-desktop.png"});41 return {...info, liveAppend: true};42 });4344 await run("file picker previews and publishes an image", async () => {45 const picker = page.locator('room-compose input[name="attachments"]');46 check(await picker.count() === 1, "attachment picker missing");47 const attach = page.locator('room-compose [data-attach]');48 check(await attach.count() === 1, "compact attach button missing");49 check(!await picker.isVisible(), "native file picker should be hidden behind the attach button");50 // Check the native chooser separately with CLI click/upload commands;51 // its modal state is managed outside run-code by playwright-cli.52 await picker.setInputFiles("output/playwright/room-attachments/browser-upload.png");53 await page.waitForSelector("room-files li");54 check((await page.locator("room-files").textContent()).includes("browser-upload.png"), "pending file preview missing");55 check(await page.locator("room-files img").count() === 1, "image attachment thumbnail missing");56 await page.waitForFunction(() => {57 const img = document.querySelector("room-files img");58 return img?.complete && img.naturalWidth > 0;59 });60 check(await page.locator("room-files img").first().evaluate(img => img.complete && img.naturalWidth > 0), "image attachment thumbnail did not load");61 const remove = page.getByRole("button", {name: "Remove browser-upload.png", exact: true});62 check(await remove.count() === 1, "attachment remove button missing");63 await remove.click();64 check(await page.locator("room-files li").count() === 0, "attachment remove did not clear preview");65 await picker.setInputFiles("output/playwright/room-attachments/browser-upload.png");66 await page.waitForSelector("room-files li");67 await page.locator('room-compose textarea[name="content"]').fill("Browser upload");68 const request = page.waitForRequest(req => req.method() === "PUT" && req.url().includes("/rooms/attachments-qa/attachments"));69 await page.locator("room-compose").getByRole("button", {name: "Send", exact: true}).click();70 await request;71 await page.waitForFunction(() => !document.querySelector("room-files li"));72 check(await page.locator("#messages").textContent().then(text => text.includes("Browser upload")), "published upload missing from timeline");73 return {uploaded: true};74 });7576 await run("attachment-only message is accepted", async () => {77 const picker = page.locator('room-compose input[name="attachments"]');78 await picker.setInputFiles("output/playwright/room-attachments/only.txt");79 await page.waitForSelector("room-files li");80 check(await page.locator('room-compose textarea[name="content"]').getAttribute("required") === null, "content remained required with an attachment");81 await page.locator("room-compose").getByRole("button", {name: "Send", exact: true}).click();82 await page.waitForFunction(() => !document.querySelector("room-files li"));83 return {uploaded: true};84 });8586 await run("reload and narrow viewport preserve media layout", async () => {87 await page.reload();88 await page.waitForSelector("#messages room-message");89 await page.setViewportSize({width: 360, height: 800});90 const metrics = await page.evaluate(() => ({width: innerWidth, scrollWidth: document.documentElement.scrollWidth, content: document.querySelector("#content")?.scrollWidth, image: document.querySelector("#messages img")?.complete}));91 check(metrics.scrollWidth <= metrics.width && metrics.content <= metrics.width, "room overflows at narrow viewport");92 check(metrics.image, "image did not load after reload");93 await page.screenshot({path: "output/playwright/room-attachments/room-mobile.png"});94 return metrics;95 });9697 await run("composer stays compact and touch friendly", async () => {98 await page.setViewportSize({width: 360, height: 420});99 const compose = page.locator("room-compose");100 const input = compose.locator('input[name="attachments"]');101 check(await input.count() === 1, "attachment input missing");102 const metrics = await page.evaluate(() => {103 const node = document.querySelector("room-compose");104 const attach = node?.querySelector('button[aria-label*="Attach" i], label[for], label');105 const send = node?.querySelector('button[type="submit"], button:not([type])');106 const box = element => element?.getBoundingClientRect();107 const attachBox = box(attach), sendBox = box(send);108 return {109 composeHeight: box(node)?.height || 0,110 viewportHeight: innerHeight,111 scrollWidth: document.documentElement.scrollWidth,112 viewportWidth: innerWidth,113 attachWidth: attachBox?.width || 0,114 attachHeight: attachBox?.height || 0,115 sendWidth: sendBox?.width || 0,116 sendHeight: sendBox?.height || 0,117 placeholder: node?.querySelector("textarea")?.getAttribute("placeholder") || ""118 };119 });120 check(metrics.scrollWidth <= metrics.viewportWidth, "composer overflows horizontally");121 check(metrics.composeHeight <= 90, "empty composer occupies too much of the viewport");122 check(metrics.attachWidth >= 40 && metrics.attachHeight >= 40, "attach control is too small to tap");123 check(metrics.sendWidth >= 40 && metrics.sendHeight >= 40, "send control is too small to tap");124 check(!/[0-9a-f]{16,}/i.test(metrics.placeholder), "composer exposes an internal identifier in its placeholder");125 await page.screenshot({path: "output/playwright/room-attachments/room-mobile-composer.png"});126 await page.setViewportSize({width: 1440, height: 900});127 const desktop = await page.evaluate(() => ({128 width: innerWidth,129 scrollWidth: document.documentElement.scrollWidth,130 composeHeight: document.querySelector("room-compose")?.getBoundingClientRect().height || 0131 }));132 check(desktop.scrollWidth <= desktop.width, "desktop composer overflows horizontally");133 await page.screenshot({path: "output/playwright/room-attachments/room-desktop-composer.png"});134 return {mobile: metrics, desktop};135 });136137 await run("JavaScript-disabled SSR still renders media", async () => {138 const context = await page.context().browser().newContext({javaScriptEnabled: false, viewport: {width: 360, height: 800}, storageState: await page.context().storageState()});139 try {140 const noJS = await context.newPage();141 await noJS.goto(base + room);142 const info = await noJS.evaluate(() => ({images: document.querySelectorAll("#messages img").length, links: document.querySelectorAll("#messages a").length, compose: !!document.querySelector("room-compose")}));143 check(info.images >= 1 && info.links >= 1, "no-JS room omitted attachment markup");144 check(info.compose, "no-JS room omitted the attachment composer markup");145 await noJS.screenshot({path: "output/playwright/room-attachments/room-nojs.png"});146 return info;147 } finally { await context.close(); }148 });149150 await run("guest cannot fetch private media", async () => {151 const context = await page.context().browser().newContext();152 try {153 const guest = await context.newPage();154 const mediaURL = await page.locator("#messages img").first().getAttribute("src");155 check(mediaURL, "could not locate fixture media URL");156 const media = await context.request.get(mediaURL);157 check([401, 403].includes(media.status()), "guest private media returned " + media.status());158 return {status: media.status()};159 } finally { await context.close(); }160 });161 results.push({name: "uncaught browser errors", ok: errors.length === 0, errors});162 return {passed: results.filter(item => item.ok).length, failed: results.filter(item => !item.ok).length, results};163}