T-HUB/apps/web/scripts/mobile-phase2-audit.cjs
glass beadian aa5e262f3a Tactical Media Broadcast Hub — initial release
Self-hosted multi-platform broadcast newsroom for activist collectives:
publishing pipeline with group approval and DST-proof scheduling, TweetDeck-style
column wall with hostile-content-safe rendering, live control room with Auto
Director, bridged chat governance with polls and quorum, distributed sandboxed
transcode workers, encrypted content-addressed archive, monthly analytics.

MIT licensed. See README.md and docs/.
2026-10-10 11:56:42 +00:00

121 lines
8.4 KiB
JavaScript

// The repo's Node 18 runtime can run this Chromium audit; Playwright 1.63's
// bootstrap refuses it before launch, so bypass only that version gate here.
if (Number(process.versions.node.split(".")[0]) < 20) Object.defineProperty(process.versions, "node", { value: "20.0.0" });
const { chromium } = require("playwright");
const fs = require("node:fs");
const path = require("node:path");
const origin = "http://audit.local";
const routes = ["/streams", "/search", "/inbox", "/inbox/1", "/approvals", "/connections", "/compose", "/audit"];
const mime = { ".js": "application/javascript", ".css": "text/css", ".svg": "image/svg+xml", ".png": "image/png", ".html": "text/html", ".rsc": "text/x-component", ".json": "application/json" };
async function serveBuild(context) {
await context.route(`${origin}/**`, async (route) => {
const request = route.request();
const url = new URL(request.url());
let file;
if (url.pathname.startsWith("/_next/")) file = path.join(process.cwd(), ".next", url.pathname.slice(7));
else if (url.pathname.startsWith("/brand/")) file = path.join(process.cwd(), "public", url.pathname);
else {
const name = url.pathname === "/" ? "index" : url.pathname.slice(1);
const isRsc = request.headers().rsc === "1" || url.searchParams.has("_rsc");
file = path.join(process.cwd(), ".next/server/app", `${name}.${isRsc ? "rsc" : "html"}`);
}
if (!fs.existsSync(file)) return route.fulfill({ status: 404, body: "Not found" });
return route.fulfill({ status: 200, contentType: mime[path.extname(file)] || "application/octet-stream", body: fs.readFileSync(file) });
});
}
async function inspect(page) {
return page.evaluate(() => {
const width = document.documentElement.clientWidth;
const undersized = Array.from(document.querySelectorAll("button, a[href], input, select, textarea, [role='tab']")).filter((element) => {
const rect = element.getBoundingClientRect();
const style = getComputedStyle(element);
return rect.width > 0 && rect.height > 0 && rect.left < innerWidth && rect.right > 0 && rect.top < innerHeight && rect.bottom > 0 && style.visibility !== "hidden" && style.display !== "none" && style.opacity !== "0" && !element.classList.contains("sr-only") && (rect.width < 43.5 || rect.height < 43.5);
}).map((element) => ({ text: (element.getAttribute("aria-label") || element.textContent || "").trim().slice(0, 36), width: Math.round(element.getBoundingClientRect().width), height: Math.round(element.getBoundingClientRect().height) }));
return { width, scrollWidth: document.documentElement.scrollWidth, bodyScrollWidth: document.body.scrollWidth, undersized };
});
}
async function routeAudit(browser) {
const context = await browser.newContext({ viewport: { width: 390, height: 844 }, isMobile: true, hasTouch: true, deviceScaleFactor: 1 });
await serveBuild(context);
await context.route("http://localhost:3000/**", (route) => route.abort());
const page = await context.newPage();
const results = [];
for (const route of routes) {
const parent = route.startsWith("/inbox/") || route === "/approvals" ? "/inbox" : "/more";
await page.goto(`${origin}${parent}`, { waitUntil: "networkidle" });
await page.goto(`${origin}${route}`, { waitUntil: "networkidle" });
await page.waitForTimeout(150);
const sizes = await inspect(page);
const back = page.getByRole("button", { name: "Go back" });
if (await back.count()) await back.click();
else await page.goBack({ waitUntil: "domcontentloaded" });
await page.waitForTimeout(100);
results.push({ route, ...sizes, backResolved: new URL(page.url()).pathname === parent });
}
await page.goto(`${origin}/streams`, { waitUntil: "networkidle" });
await page.getByRole("tab", { name: /Mastodon/ }).click();
await page.getByRole("button", { name: /Amplify @/ }).first().click();
const mastodonLabels = await page.getByRole("dialog").innerText();
await page.getByRole("button", { name: "Close", exact: true }).click();
await page.getByRole("tab", { name: /Telegram/ }).click();
await page.getByRole("button", { name: /Amplify @/ }).first().click();
const telegramLabels = await page.getByRole("dialog").innerText();
await context.close();
return { routes: results, capabilityLabels: { mastodonBoost: mastodonLabels.includes("Boost"), mastodonQuote: mastodonLabels.includes("Quote"), telegramForward: telegramLabels.includes("Forward"), telegramNoQuote: !telegramLabels.includes("Quote") } };
}
async function approvalAudit(browser) {
const context = await browser.newContext({ viewport: { width: 390, height: 844 }, isMobile: true, hasTouch: true });
await serveBuild(context);
await context.addInitScript(() => localStorage.setItem("tmbh.session", "audit-token"));
const drafts = ["approve", "reject"].map((id) => ({ id, workspaceId: "w", authorUserId: "contributor-1", state: "in_review", createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), post: { text: `${id} this draft`, mediaAssetIds: [], visibility: "public", providerMetadata: { targetConnectionIds: [] } } }));
const audit = [];
const calls = [];
await context.route("http://localhost:3000/**", async (route) => {
const request = route.request();
const url = new URL(request.url());
const headers = { "access-control-allow-origin": origin, "access-control-allow-methods": "GET,POST,OPTIONS", "access-control-allow-headers": "authorization,content-type", "content-type": "application/json" };
if (request.method() === "OPTIONS") return route.fulfill({ status: 204, headers, body: "" });
let body;
if (url.pathname === "/auth/me") body = { user: { id: "editor-1", email: "editor@example.test", displayName: "Editor" }, workspace: { id: "w", name: "Test", slug: "test" }, role: "Editor", permissions: ["approval.grant", "draft.view"], expiresAt: Date.now() + 60000 };
else if (url.pathname === "/publishing/drafts" && request.method() === "GET") body = drafts.filter((draft) => draft.state === "in_review");
else if (url.pathname.match(/^\/publishing\/drafts\/(approve|reject)\/(approve|reject)$/) && request.method() === "POST") {
const [, id, decision] = url.pathname.match(/^\/publishing\/drafts\/(approve|reject)\/(approve|reject)$/);
const draft = drafts.find((item) => item.id === id);
draft.state = decision === "approve" ? "approved" : "rejected";
calls.push({ id, decision, path: url.pathname });
audit.unshift({ id: `audit-${audit.length}`, seq: String(audit.length + 1), at: new Date().toISOString(), actorUserId: "editor-1", actorEmail: "editor@example.test", actorRole: "Editor", action: `draft.${decision === "approve" ? "approved" : "rejected"}`, targetType: "draft", targetId: id, diff: null });
body = draft;
} else if (url.pathname === "/audit") body = { entries: audit, nextCursor: null, facets: { actions: [...new Set(audit.map((item) => item.action))], actors: [{ id: "editor-1", email: "editor@example.test" }] } };
else if (url.pathname === "/connections") body = [];
else body = [];
return route.fulfill({ status: 200, headers, body: JSON.stringify(body) });
});
const page = await context.newPage();
await page.goto(`${origin}/approvals`, { waitUntil: "networkidle" });
await page.getByRole("button", { name: "Approve", exact: true }).first().click();
await page.getByRole("button", { name: "Reject", exact: true }).first().click();
await page.getByRole("button", { name: "Needs edits" }).click();
await page.getByRole("button", { name: "Reject with reason" }).click();
await page.goto(`${origin}/audit`, { waitUntil: "networkidle" });
const text = await page.locator("[data-mobile-main]").innerText();
await context.close();
return { calls, auditActions: audit.map((item) => item.action), visibleInAudit: text.includes("draft.approved") && text.includes("draft.rejected") };
}
async function main() {
let browser;
try {
browser = await chromium.launch({ headless: true });
const routes = await routeAudit(browser);
const approvals = await approvalAudit(browser);
const result = { ...routes, approvals };
console.log(JSON.stringify(result, null, 2));
if (routes.routes.some((row) => row.scrollWidth > row.width || row.bodyScrollWidth > row.width || row.undersized.length > 0 || !row.backResolved) || Object.values(routes.capabilityLabels).some((value) => !value) || approvals.calls.length !== 2 || !approvals.visibleInAudit) process.exitCode = 1;
} finally { await browser?.close(); }
}
main().catch((error) => { console.error(error); process.exitCode = 1; });