176 lines
8.5 KiB
JavaScript
176 lines
8.5 KiB
JavaScript
// web/scripts/node-tour.mjs: the page connected to a node, in a real browser. A mock
|
|
// node (web/mesh/test/mock_ws_node.py) stands in for a MeshCore companion's WebSocket
|
|
// server; the page connects to it through Connect > Wi-Fi, exactly as to a device.
|
|
//
|
|
// node web/scripts/node-tour.mjs [--url http://localhost:8187/] [--ws localhost:18765]
|
|
// [--shots web/docs]
|
|
// Checks: the chooser, contacts / channels / waiting messages from the node, a DM that
|
|
// gets Delivered, a live message, reload reconnects (same data dir), Disconnect returns
|
|
// to the demo with the demo's own data. Screenshots: web/docs/node-*.png
|
|
import { createRequire } from 'node:module';
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
const require = createRequire(path.join(here, '..', 'package.json'));
|
|
const pw = require('playwright');
|
|
const arg = (name, dflt) => {
|
|
const i = process.argv.indexOf(name);
|
|
return i > 0 && i + 1 < process.argv.length ? process.argv[i + 1] : dflt;
|
|
};
|
|
const URL_ = arg('--url', 'http://localhost:8187/');
|
|
const WS = arg('--ws', 'localhost:18765');
|
|
const SHOTS = arg('--shots', path.join(here, '..', 'docs'));
|
|
fs.mkdirSync(SHOTS, { recursive: true });
|
|
const W = 800, H = 480;
|
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
const results = {};
|
|
let failures = 0;
|
|
const check = (name, ok, extra) => {
|
|
results[name] = extra === undefined ? !!ok : { ok: !!ok, ...extra };
|
|
if (!ok) { failures++; console.error('FAIL', name, extra || ''); } else console.log('ok ', name);
|
|
};
|
|
|
|
async function tap(page, x, y) {
|
|
const r = await page.locator('#screen').boundingBox();
|
|
await page.mouse.move(r.x + (x + 0.5) * r.width / W, r.y + (y + 0.5) * r.height / H);
|
|
await page.mouse.down(); await sleep(80); await page.mouse.up(); await sleep(150);
|
|
}
|
|
async function shot(page, name) { await page.screenshot({ path: path.join(SHOTS, `node-${name}.png`) }); }
|
|
const badge = (page) => page.locator('#mode').textContent();
|
|
async function booted(page) {
|
|
await page.waitForFunction(() => window.wadamesh && window.wadamesh.running(), null, { timeout: 60000 });
|
|
}
|
|
async function waitBadge(page, re, ms = 20000) {
|
|
const t0 = Date.now();
|
|
while (Date.now() - t0 < ms) { if (re.test(await badge(page))) return true; await sleep(250); }
|
|
return false;
|
|
}
|
|
const inStorage = (page, needle) => page.evaluate((n) => window.wadamesh.findInStorage(n), needle);
|
|
|
|
const browser = await pw.chromium.launch();
|
|
// The visitor is in Ghent and lets the page use the location (step 6: the privacy rule).
|
|
const GHENT = { latitude: 51.05, longitude: 3.72 };
|
|
const ctx = await browser.newContext({ viewport: { width: 1100, height: 760 }, geolocation: GHENT,
|
|
permissions: ['geolocation'] });
|
|
const page = await ctx.newPage();
|
|
const errors = [];
|
|
page.on('pageerror', (e) => errors.push(String(e)));
|
|
// (an http page that is not localhost: the browser notes it ignores COOP; that is the point)
|
|
page.on('console', (m) => { if (m.type() === 'error' && !/Cross-Origin-Opener-Policy header has been ignored/.test(m.text())) errors.push(m.text()); });
|
|
|
|
// 1. The demo, then the chooser.
|
|
await page.goto(URL_, { waitUntil: 'load' });
|
|
await booted(page);
|
|
await sleep(4000);
|
|
await tap(page, 60, 283); // the demo's welcome: Skip
|
|
await sleep(2500);
|
|
check('demo badge', /demo/i.test(await badge(page)), { badge: await badge(page) });
|
|
await shot(page, '01-demo');
|
|
await page.click('#btn-connect');
|
|
await page.waitForSelector('#connect-dlg[open]');
|
|
const kinds = await page.evaluate(() => [...document.querySelectorAll('#connect-dlg [data-kind]')]
|
|
.filter((b) => !b.hidden).map((b) => b.dataset.kind));
|
|
check('chooser offers the transports this browser has', kinds.includes('ws'), { kinds });
|
|
await shot(page, '02-chooser');
|
|
|
|
// 2. Connect over Wi-Fi (the mock's WebSocket server).
|
|
await page.fill('#ws-host', WS);
|
|
await page.click('#connect-dlg [data-kind="ws"]');
|
|
await page.waitForLoadState('load');
|
|
await booted(page);
|
|
check('connected', await waitBadge(page, /^Connected: Mock Node over Wi-Fi$/), { badge: await badge(page) });
|
|
check('node data dir', (await page.evaluate(() => window.wadamesh.dataDir)).startsWith('/data/node-'));
|
|
await sleep(2500);
|
|
await shot(page, '03-connected-home');
|
|
await tap(page, 80, 456); // Chats
|
|
await sleep(1500);
|
|
await shot(page, '04-chats');
|
|
await tap(page, 240, 456); // Contacts
|
|
await sleep(1500);
|
|
await shot(page, '05-contacts');
|
|
|
|
// 3. A DM to Alice: Delivered, and her reply.
|
|
await tap(page, 80, 456);
|
|
await sleep(1200);
|
|
await tap(page, 400, 152); // Alice (second row: #wadatest has the newest message)
|
|
await sleep(1500);
|
|
await tap(page, 380, 447);
|
|
await sleep(600);
|
|
await page.keyboard.type('hello from the browser', { delay: 20 });
|
|
await page.keyboard.press('Enter');
|
|
await sleep(5000);
|
|
await shot(page, '06-dm-delivered');
|
|
await page.evaluate(() => window.wadamesh.flush());
|
|
check('DM reply from the node stored', (await inStorage(page, 'mock reply: got it')).length > 0);
|
|
|
|
// 4. The live channel post the mock sends 8 s after connecting.
|
|
await page.keyboard.press('Escape');
|
|
await sleep(1500);
|
|
await shot(page, '07-live');
|
|
await page.evaluate(() => window.wadamesh.flush());
|
|
check('live message received', (await inStorage(page, 'live hello from the mock')).length > 0);
|
|
check('waiting message synced', (await inStorage(page, 'hello from the mock (waiting)')).length > 0);
|
|
const nodeDir = await page.evaluate(() => window.wadamesh.dataDir);
|
|
|
|
// 5. Reload: reconnects by itself, into the node's own data dir (now keyed by the node).
|
|
await page.reload({ waitUntil: 'load' });
|
|
await booted(page);
|
|
check('reconnects after reload', await waitBadge(page, /^Connected: Mock Node/), { badge: await badge(page) });
|
|
const dir2 = await page.evaluate(() => window.wadamesh.dataDir);
|
|
check('data dir keyed by the node', /^\/data\/node-[0-9a-f]{8}$/.test(dir2), { before: nodeDir, after: dir2 });
|
|
await sleep(2000);
|
|
await tap(page, 80, 456);
|
|
await sleep(1500);
|
|
await shot(page, '08-after-reload');
|
|
const hits = await inStorage(page, 'hello from the browser');
|
|
check('history kept across the reload', hits.some((h) => h.startsWith(dir2)), { hits });
|
|
|
|
// 6. Location privacy: with the GPS on, this computer's position reaches the node ONLY
|
|
// while the node's own "share position in adverts" is on (WEB_BUILD.md "Location").
|
|
const mockState = () => fetch(`http://${WS}/latlon`).then((r) => r.json());
|
|
if (!(await page.evaluate(() => window.isSecureContext))) {
|
|
results['location privacy'] = 'skipped (an insecure http page has no location)';
|
|
} else {
|
|
for (const t of ['tap 720 456', 'wait 1500']) if (t.startsWith('tap')) await tap(page, ...t.split(' ').slice(1).map(Number)); else await sleep(1500);
|
|
{
|
|
const r = await page.locator('#screen').boundingBox();
|
|
const at = (x, y) => [r.x + (x + 0.5) * r.width / W, r.y + (y + 0.5) * r.height / H];
|
|
await page.mouse.move(...at(200, 420)); await page.mouse.down();
|
|
for (let i = 1; i <= 24; i++) { await page.mouse.move(...at(200, 420 - 380 * i / 24)); await sleep(16); }
|
|
await sleep(400); await page.mouse.up(); await sleep(1500); // Settings, scrolled to the end
|
|
}
|
|
await tap(page, 100, 302); // GPS
|
|
await sleep(1500);
|
|
await tap(page, 745, 95); // GPS on
|
|
await sleep(9000);
|
|
await shot(page, '10-gps-on');
|
|
check('share off: the position stays in the browser', (await mockState()).latlon.length === 0, await mockState());
|
|
await fetch(`http://${WS}/share/1`); // the node now shares its position (as if set in the phone app)
|
|
await page.reload({ waitUntil: 'load' });
|
|
await booted(page);
|
|
await waitBadge(page, /^Connected: Mock Node/);
|
|
await sleep(9000);
|
|
const shared = (await mockState()).latlon;
|
|
check('share on: the node gets this computer\'s position',
|
|
shared.length >= 1 && Math.abs(shared[0][0] - GHENT.latitude) < 0.001 && Math.abs(shared[0][1] - GHENT.longitude) < 0.001,
|
|
{ latlon: shared });
|
|
await fetch(`http://${WS}/share/0`);
|
|
}
|
|
|
|
// 7. Disconnect: back to the demo, whose data never saw the node's.
|
|
await page.click('#btn-connect');
|
|
await page.waitForLoadState('load');
|
|
await booted(page);
|
|
await sleep(2500);
|
|
check('demo again', /demo/i.test(await badge(page)), { badge: await badge(page) });
|
|
const demoHits = (await inStorage(page, 'mock reply: got it')).filter((h) => h.startsWith('/data/demo'));
|
|
check('demo data has nothing from the node', demoHits.length === 0, { demoHits });
|
|
await shot(page, '09-demo-again');
|
|
|
|
check('no page errors', errors.length === 0, { errors: errors.slice(0, 5) });
|
|
console.log(JSON.stringify(results, null, 2));
|
|
await browser.close();
|
|
process.exit(failures ? 1 : 0);
|