// Browser checks of the dashboard page (`cargo test --features dashboard // dashboard_in_a_browser -- --ignored`, which serves this the page as the node does): the // page loads without errors; the peers table has a row per peer, and an update changes // cells without making the rows anew (`morphChildren`); the map draws a node per member; // and with 300 members, laid out by round trip, the first drawing doesn't hold the page up // for long (`LAYOUT_FOR_MS`). // // Needs Node.js, Playwright's library (`playwright-core`) and its Chromium; `check.nix` // gives all three from nixpkgs. // // Usage: node check.js const { chromium } = require("playwright-core"); const page = process.argv[2]; const failures = []; const check = (ok, what) => { if (!ok) failures.push(what); console.log((ok ? "ok " : "FAIL ") + what); }; (async () => { const browser = await chromium.launch(); const tab = await browser.newPage({ viewport: { width: 1400, height: 900 } }); const errors = []; tab.on("pageerror", error => errors.push(String(error))); await tab.goto("file://" + page); await tab.waitForTimeout(300); // The peers table, and an update to it. const peers = await tab.evaluate(() => { const peer = (alias, rtt) => ({ alias, status: "active", lastSeenSecsAgo: 1, destination: { address: "sim:" + alias, paths: [{ rttMs: rtt }] }, clock: null, neighbour: true, sameEpoch: true, epoch: { generation: 1, id: "ab" }, reaches: ["a"], hopDelay: null }); const members = { a: { self: true, alias: "a" }, b: { alias: "b" }, c: { alias: "c" }, d: { alias: "d" } }; state = { node: { id: "a", alias: "a" }, stats: {}, peers: { localReaches: ["b", "c", "d"], peers: { b: peer("b", 10), c: peer("c", 20), d: peer("d", 30) } }, graph: { members, intended: {}, measured: {} } }; renderPeers(); const before = [...document.querySelectorAll("#peers tr")]; state.peers.peers.c.destination.paths[0].rttMs = 25; renderPeers(); const after = [...document.querySelectorAll("#peers tr")]; return { rows: after.length, same: before.every((row, i) => row === after[i]), rtt: after[2].textContent.includes("25.0 ms") }; }); check(peers.rows === 4, "the peers table has a header and a row per peer"); check(peers.same, "an update keeps the table's rows"); check(peers.rtt, "an update changes what changed"); // The map, small and large. for (const [n, budget] of [[13, 500], [300, 1500]]) { const map = await tab.evaluate(n => { const ids = [...Array(n).keys()].map(i => "n" + String(i).padStart(3, "0")); const members = {}; for (const id of ids) members[id] = { alias: id, self: id === "n000" }; const intended = {}, measured = {}; for (let i = 0; i < n; i++) { const neighbours = [1, 2, 7, 13].map(k => ids[(i + k) % n]).filter(id => id !== ids[i]); intended[ids[i]] = neighbours; measured[ids[i]] = { secsAgo: 1, links: neighbours.map(peer => ({ peer, rttMs: 20 + (i * 37) % 200, dormant: false, down: false, others: [] })) }; } state = { node: { id: "n000", alias: "n000" }, stats: {}, peers: { localReaches: [], peers: {} }, graph: { members, intended, measured } }; byRtt = true; relayout = true; lastShape = null; const started = performance.now(); renderGraph(); const ms = performance.now() - started; const nodes = [...drawn.keys()].filter(key => key.startsWith("node ")).length; return { ms, nodes }; }, n); check(map.nodes === n, `the map draws a node per member (${map.nodes} of ${n})`); check(map.ms < budget, `${n} members drawn in ${map.ms.toFixed(0)} ms (under ${budget})`); } await tab.waitForTimeout(500); check(errors.length === 0, "no errors on the page" + (errors.length ? ": " + errors : "")); await browser.close(); process.exit(failures.length ? 1 : 0); })().catch(error => { console.error(error); process.exit(2); });