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);
|