chat / client / e2e / relay.e2e.mjs
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
// 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);

static mirror of HEAD · about · clone: git clone https://git.ardegazu.ro/chat.git