chat / client / e2e / smoke-v2.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
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
/**
 * Phase-1 smoke: two browsers join a room over the libp2p transport
 * (dev relay on :9090, `node deploy/relay/relay.mjs --dev`) and exchange chat.
 * Full suite: mesh.e2e.mjs. Run: node e2e/smoke-v2.e2e.mjs
 */
import { chromium } from "playwright";
import { fileURLToPath } from "node:url";
import { buildForE2E } from "./build-for-e2e.mjs";
import { startDevRelay } from "./dev-relay.mjs";

const BASE = process.env.BASE_URL ?? "http://localhost:5173";

// A plain `npm run build` points the app at the PRODUCTION relay, so anything
// this script spawns would be bypassed. Build the dist these tests need.
if (!process.env.E2E_NO_BUILD) buildForE2E();

// Spawn the dev relay rather than requiring one to be running. This script used
// to pass with no relay at all: the release dist silently used the PRODUCTION
// relay, so "connected to the dev relay" was true of a relay this test neither
// started nor controlled. Now that the build points at 127.0.0.1, the relay has
// to actually be here — which is the point.
const RELAY_PATH = fileURLToPath(new URL("../../deploy/relay/relay.mjs", import.meta.url));
const { proc: relayProc } = await startDevRelay(RELAY_PATH, { banner: "sueta relay up" });
// Retire the relay on EVERY exit path, not just the happy one. `fail()` and any
// uncaught error call process.exit, which used to leave the relay listening on
// :9090 — and the NEXT run then spawned its own, watched it die on the busy
// port, and read that death as "an external relay is already running". Two runs
// back to back therefore tested whatever the orphan was, and a run after a
// failed one could test with no relay at all.
process.on("exit", () => relayProc?.kill("SIGKILL"));
const HEADED = !!process.env.HEADED;

const fail = (msg) => {
  console.error("❌ " + msg);
  process.exit(1);
};
const ok = (msg) => console.log("✅ " + msg);

const browser = await chromium.launch({
  headless: !HEADED,
  args: ["--disable-features=WebRtcHideLocalIpsWithMdns"],
});

async function openPeer(name, url) {
  const ctx = await browser.newContext({ permissions: [] });
  const page = await ctx.newPage();
  page.on("pageerror", (e) => console.log(`[${name}] pageerror:`, e.message));
  if (process.env.VERBOSE) page.on("console", (m) => console.log(`[${name}]`, m.text()));
  await page.goto(url);
  await page.fill("#name-in", name);
  await page.click("#name-ok");
  return page;
}

const first = await (async () => {
  const ctx = await browser.newContext();
  const page = await ctx.newPage();
  if (process.env.VERBOSE) page.on("console", (m) => console.log(`[alice]`, m.text()));
  page.on("pageerror", (e) => console.log(`[alice] pageerror:`, e.message));
  await page.goto(BASE);
  await page.click("#new-room");
  await page.waitForFunction(() => location.hash.length > 40);
  const url = await page.evaluate(() => location.href);
  await page.fill("#name-in", "alice");
  await page.click("#name-ok");
  return { page, url };
})();

const alice = first.page;
console.log("room:", first.url);
const bob = await openPeer("bob", first.url);

// relay connectivity
for (const [who, page] of [["alice", alice], ["bob", bob]]) {
  await page
    .waitForFunction(() => window.__sueta?.net?.relayUp === true, { timeout: 20000 })
    .catch(() => fail(`${who}: never connected to the dev relay`));
}
ok("both peers connected to relay");

// membership-verified peer, ready for directed streams
for (const [who, page] of [["alice", alice], ["bob", bob]]) {
  await page
    .waitForFunction(() => window.__sueta?.net?.debugState().some((p) => p.ready), { timeout: 40000 })
    .catch(async () => {
      console.log(`${who} debug:`, JSON.stringify(await page.evaluate(() => window.__sueta?.net?.debugState())));
      fail(`${who}: no peer became ready`);
    });
}
ok("peers discovered each other and verified membership");

// direct webrtc upgrade
await alice
  .waitForFunction(
    () => window.__sueta?.net?.debugState().some((p) => p.conns?.some((a) => a.includes("/webrtc"))),
    { timeout: 30000 },
  )
  .then(() => ok("webrtc direct connection established"))
  .catch(async () => {
    console.log("alice conns:", JSON.stringify(await alice.evaluate(() => window.__sueta.net.debugState())));
    console.log("⚠️ no webrtc upgrade (circuit only) — continuing, chat must still work");
  });

// text alice → bob
await alice.fill("#input", "hello bob, e2e over libp2p");
await alice.click("#send-btn");
await bob
  .waitForFunction(
    () => [...document.querySelectorAll(".msg-bubble")].some((b) => b.textContent.includes("hello bob")),
    { timeout: 10000 },
  )
  .catch(() => fail("bob never received alice's message"));
ok("text message delivered");

// text bob → alice
await bob.fill("#input", "hi alice");
await bob.click("#send-btn");
await alice
  .waitForFunction(
    () => [...document.querySelectorAll(".msg-bubble")].some((b) => b.textContent.includes("hi alice")),
    { timeout: 10000 },
  )
  .catch(() => fail("alice never received bob's reply"));
ok("reply delivered");

console.log("smoke-v2 passed");
await browser.close();
process.exit(0);

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