// TURN relay-forced test: two peers with #<secret>&relay must exchange chat
// with iceTransportPolicy "relay" — every WebRTC candidate rides coturn — over
// the PRODUCTION ird libp2p relay. Defaults to production; to test a local
// build against prod infra: npm run build && node e2e/serve.mjs 4173,
// then BASE_URL=http://localhost:4173 node e2e/relay.e2e.mjs
// (:4173 is in the relay's origin allowlist; :5173 dev points at the dev relay.)
import { chromium } from "playwright";
const BASE = process.env.BASE_URL ?? "https://chat.ardegazu.ro";
const browser = await chromium.launch({
args: [
"--disable-features=WebRtcHideLocalIpsWithMdns",
"--use-fake-device-for-media-stream",
"--use-fake-ui-for-media-stream",
"--autoplay-policy=no-user-gesture-required",
],
});
// create a room once, then open both peers fresh on the &relay URL
const tmp = await browser.newContext().then((c) => c.newPage());
await tmp.goto(BASE);
await tmp.click("#new-room");
await tmp.waitForFunction(() => location.hash.length > 40);
const url = (await tmp.evaluate(() => location.href)) + "&relay";
await tmp.close();
async function relayPeer(name) {
const page = await browser.newContext({ permissions: ["camera", "microphone"] }).then((c) => c.newPage());
page.on("pageerror", (e) => console.log(`[${name}] pageerror:`, e.message));
await page.goto(url);
await page.fill("#name-in", name);
await page.click("#name-ok");
return page;
}
const a = await relayPeer("relay-a");
const b = await relayPeer("relay-b");
try {
await a.waitForFunction(() => window.__sueta?.net?.debugState().some((p) => p.ready), { timeout: 60000 });
} catch {
console.error(
"❌ relay-forced connection failed:",
JSON.stringify(await a.evaluate(() => window.__sueta?.net?.debugState())),
);
process.exit(1);
}
const state = await a.evaluate(() => window.__sueta.net.debugState());
console.log("peer state:", JSON.stringify(state));
await b.fill("#input", "message via TURN relay");
await b.click("#send-btn");
await a.waitForFunction(
() => [...document.querySelectorAll(".msg-bubble")].some((m) => m.textContent.includes("via TURN relay")),
{ timeout: 10000 },
);
console.log("✅ relay-forced chat delivered over the prod libp2p relay + coturn");
console.log(" (peer:", state[0]?.name, "state:", state[0]?.state, ")");
// calls, TURN-only: media pcs run iceTransportPolicy relay → every RTP packet
// traverses coturn; SDP rides sealed frames over the prod libp2p relay
await a.click("#call-btn");
await b.waitForSelector("#call-join-video:not(.hidden)", { timeout: 15000 });
await b.click("#call-join-video");
await a
.waitForFunction(
() => [...document.querySelectorAll(".tile video")].some((v) => !v.muted && v.videoWidth > 0 && v.readyState >= 2),
{ timeout: 40000 },
)
.catch(async () => {
console.error("❌ relay-forced call: no media —", JSON.stringify(await a.evaluate(() => window.__sueta.media.debugState())));
process.exit(1);
});
console.log("✅ relay-forced call: video frames delivered through coturn");
await browser.close();
process.exit(0);