463 lines
17 KiB
HTML
463 lines
17 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="de">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>randomp2p – Pairing-Test</title>
|
||
<style>
|
||
* { box-sizing: border-box; }
|
||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', monospace; background: #0d1117; color: #c9d1d9; padding: 24px; max-width: 800px; margin: 0 auto; }
|
||
h1 { color: #f0c040; font-size: 1.4rem; margin-bottom: 4px; }
|
||
.sub { color: #8b949e; font-size: 0.85rem; margin-bottom: 20px; }
|
||
button { padding: 10px 24px; background: #238636; color: #fff; border: none; border-radius: 8px; cursor: pointer; font-size: 1rem; font-weight: 600; }
|
||
button:hover { background: #2ea043; }
|
||
button:disabled { opacity: 0.5; cursor: wait; }
|
||
.test { background: #161b22; border: 1px solid #30363d; border-radius: 8px; padding: 14px; margin: 12px 0; }
|
||
.test h3 { margin: 0 0 6px; font-size: 1rem; }
|
||
.test .status { font-weight: 600; }
|
||
.test .detail { font-size: 0.85rem; color: #8b949e; white-space: pre-wrap; margin-top: 4px; max-height: 400px; overflow-y: auto; }
|
||
.pass { color: #2ea043; }
|
||
.fail { color: #f85149; }
|
||
.skip { color: #8b949e; }
|
||
.running { color: #f0c040; }
|
||
.summary { font-size: 1.1rem; margin: 16px 0; padding: 12px; border-radius: 8px; background: #161b22; border: 1px solid #30363d; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<h1>randomp2p Pairing-Test</h1>
|
||
<p class="sub">Mock-SimplePeer statt WebRTC – testet QR-Pairing, Mesh-Aufbau und Chat</p>
|
||
<button id="run">Tests ausführen</button>
|
||
<div id="summary" class="summary" style="display:none"></div>
|
||
<div id="output"></div>
|
||
|
||
<!-- Load dependencies first, with SimplePeer mock injected before p2p.js -->
|
||
<script src="crypto.js"></script>
|
||
<script>
|
||
/* ─── Mock SimplePeer (injected before p2p.js) ─── */
|
||
const mockPeers = [];
|
||
|
||
function clearMockPeers() {
|
||
for (const p of mockPeers) p._destroyed = true;
|
||
mockPeers.length = 0;
|
||
window.SimplePeer = MockSimplePeer;
|
||
}
|
||
|
||
class MockSimplePeer {
|
||
constructor(opts) {
|
||
this._initiator = !!opts.initiator;
|
||
this._destroyed = false;
|
||
this._connected = false;
|
||
this._pairKey = null;
|
||
this._remotePeer = null;
|
||
this._callbacks = { signal: [], connect: [], data: [], close: [], error: [] };
|
||
|
||
mockPeers.push(this);
|
||
this._mockId = mockPeers.length;
|
||
|
||
if (this._initiator) {
|
||
setTimeout(() => {
|
||
if (this._destroyed) return;
|
||
const sdp = 'mock-offer-' + Date.now() + '-' + Math.random().toString(36).slice(2, 8);
|
||
this._pairKey = sdp;
|
||
this._emit('signal', { type: 'offer', sdp });
|
||
}, 0);
|
||
}
|
||
}
|
||
|
||
_emit(event, arg) {
|
||
for (const cb of this._callbacks[event]) cb(arg);
|
||
}
|
||
|
||
on(event, cb) {
|
||
this._callbacks[event].push(cb);
|
||
return this;
|
||
}
|
||
|
||
signal(data) {
|
||
if (this._destroyed) return;
|
||
|
||
if (data.type === 'offer' && !this._initiator) {
|
||
this._pairKey = data.sdp;
|
||
setTimeout(() => {
|
||
if (this._destroyed) return;
|
||
this._emit('signal', { type: 'answer', sdp: 'mock-answer-' + Date.now() + '-' + Math.random().toString(36).slice(2, 8) });
|
||
}, 0);
|
||
|
||
} else if (data.type === 'answer' && this._initiator) {
|
||
const nonInitiator = mockPeers.find(p =>
|
||
p._pairKey === this._pairKey && p !== this && !p._destroyed
|
||
);
|
||
if (nonInitiator) {
|
||
this._remotePeer = nonInitiator;
|
||
nonInitiator._remotePeer = this;
|
||
this._connected = true;
|
||
nonInitiator._connected = true;
|
||
setTimeout(() => {
|
||
if (this._destroyed) return;
|
||
this._emit('connect');
|
||
if (!nonInitiator._destroyed) nonInitiator._emit('connect');
|
||
}, 0);
|
||
}
|
||
}
|
||
}
|
||
|
||
send(data) {
|
||
if (this._destroyed || !this._connected || !this._remotePeer) return;
|
||
const remote = this._remotePeer;
|
||
setTimeout(() => {
|
||
if (!remote._destroyed) remote._emit('data', data);
|
||
}, 0);
|
||
}
|
||
|
||
destroy() {
|
||
if (this._destroyed) return;
|
||
this._destroyed = true;
|
||
if (this._remotePeer) {
|
||
this._remotePeer._connected = false;
|
||
this._remotePeer._remotePeer = null;
|
||
}
|
||
this._emit('close');
|
||
const idx = mockPeers.indexOf(this);
|
||
if (idx >= 0) mockPeers.splice(idx, 1);
|
||
}
|
||
}
|
||
|
||
clearMockPeers();
|
||
</script>
|
||
<script src="p2p.js"></script>
|
||
<script>
|
||
(function() {
|
||
'use strict';
|
||
|
||
const $ = id => document.getElementById(id);
|
||
|
||
/* ─── MeshNet wrapper with event recording ─── */
|
||
function createTestMesh(name) {
|
||
const mesh = new MeshNet();
|
||
mesh.playerName = name;
|
||
mesh.names.set(mesh.myPeerId, name);
|
||
mesh._testName = name;
|
||
mesh._events = [];
|
||
mesh._chatReceived = [];
|
||
mesh._qrData = null;
|
||
mesh._answerData = null;
|
||
|
||
mesh.onqrupdate = d => { mesh._qrData = d; };
|
||
mesh.onanswerready = d => { mesh._answerData = d; };
|
||
mesh.onchat = (peerId, text) => {
|
||
mesh._chatReceived.push({ from: peerId, text });
|
||
};
|
||
mesh.onpeerconnect = pid => {
|
||
mesh._events.push({ t: 'connect', pid });
|
||
};
|
||
mesh.onpeerdisconnect = pid => {
|
||
mesh._events.push({ t: 'disconnect', pid });
|
||
};
|
||
|
||
return mesh;
|
||
}
|
||
|
||
function clearLogs(meshes) {
|
||
for (const m of meshes) {
|
||
m._chatReceived = [];
|
||
m._events = [];
|
||
}
|
||
}
|
||
|
||
function delay(ms) {
|
||
return new Promise(r => setTimeout(r, ms));
|
||
}
|
||
|
||
/* ─── Test: QR-Daten format (encode45/decode45 roundtrip) ─── */
|
||
async function testQRRoundtrip() {
|
||
const testData = { v: 1, id: 'aabbccdd', s: { type: 'offer', sdp: 'mock-sdp' } };
|
||
const packed = await pack(testData);
|
||
const encoded = encode45(packed);
|
||
const decoded = decode45(encoded);
|
||
const unpacked = await unpack(decoded);
|
||
|
||
const pass = unpacked.v === 1 && unpacked.id === 'aabbccdd' && unpacked.s.type === 'offer';
|
||
return {
|
||
pass,
|
||
detail: pass
|
||
? `pack → encode45 → decode45 → unpack OK\nEncoded: ${encoded.slice(0, 40)}…`
|
||
: `Roundtrip fehlgeschlagen: ${JSON.stringify(unpacked)}`
|
||
};
|
||
}
|
||
|
||
/* ─── Test: QR-Pairing Host ↔ Peer ─── */
|
||
async function testBasicPairing() {
|
||
clearMockPeers();
|
||
const host = createTestMesh('Host');
|
||
const peer = createTestMesh('Peer');
|
||
|
||
// 1. Host creates room → generates QR
|
||
host.createRoom();
|
||
await delay(50);
|
||
if (!host._qrData) return { pass: false, detail: 'Host QR wurde nicht generiert' };
|
||
|
||
let parsed;
|
||
try {
|
||
parsed = await unpack(decode45(host._qrData));
|
||
} catch (e) {
|
||
return { pass: false, detail: 'Host QR parsing failed: ' + e.message };
|
||
}
|
||
if (!parsed.s || parsed.s.type !== 'offer') return { pass: false, detail: 'Host QR enthält kein Offer-Signal' };
|
||
if (parsed.id !== host.myPeerId) return { pass: false, detail: 'Host QR enthält falsche ID' };
|
||
|
||
// 2. Peer joins from QR → generates answer
|
||
peer.joinFromQR(host._qrData);
|
||
await delay(50);
|
||
if (!peer._answerData) return { pass: false, detail: 'Peer Answer QR wurde nicht generiert' };
|
||
|
||
try {
|
||
parsed = await unpack(decode45(peer._answerData));
|
||
} catch (e) {
|
||
return { pass: false, detail: 'Peer Answer QR parsing failed: ' + e.message };
|
||
}
|
||
if (parsed.s.type !== 'answer') return { pass: false, detail: 'Peer QR ist kein Answer' };
|
||
if (parsed.to !== host.myPeerId) return { pass: false, detail: 'Peer Answer QR target falsch' };
|
||
|
||
// 3. Host feeds answer → connection established
|
||
await host.feedAnswer(peer._answerData);
|
||
await delay(50);
|
||
|
||
if (host.getPeerIds().length < 1) return { pass: false, detail: 'Host hat keine Verbindung' };
|
||
|
||
const pid = Array.from(host.connections.keys())[0];
|
||
if (host.getPeerName(pid).length === 0) return { pass: false, detail: 'Host kennt Peer-Namen nicht' };
|
||
if (peer.getPeerName(host.myPeerId).length === 0) return { pass: false, detail: 'Peer kennt Host-Namen nicht' };
|
||
|
||
return { pass: true, detail: `Host ↔ Peer: ${host.myPeerId} ↔ ${pid}\nHost peers: ${host.getPeerIds().length}\nPeer peers: ${peer.getPeerIds().length}\nNames: ${host.getPeerName(pid)}, ${peer.getPeerName(host.myPeerId)}` };
|
||
}
|
||
|
||
/* ─── Test: Chat über Pairing-Verbindung ─── */
|
||
async function testChatAfterPairing() {
|
||
clearMockPeers();
|
||
const host = createTestMesh('Host');
|
||
const peer = createTestMesh('Peer');
|
||
|
||
host.createRoom();
|
||
await delay(30);
|
||
peer.joinFromQR(host._qrData);
|
||
await delay(30);
|
||
await host.feedAnswer(peer._answerData);
|
||
await delay(60);
|
||
clearLogs([host, peer]);
|
||
|
||
// Host sends chat → peer receives
|
||
host.broadcast({ type: 'chat', text: 'Hallo von Host' });
|
||
await delay(20);
|
||
if (peer._chatReceived.length !== 1) return { pass: false, detail: `Peer empfing ${peer._chatReceived.length} Chats (erwartet: 1)` };
|
||
if (peer._chatReceived[0].text !== 'Hallo von Host') return { pass: false, detail: 'Falscher Chat-Text bei Peer' };
|
||
|
||
// Peer sends chat → host receives
|
||
clearLogs([host, peer]);
|
||
peer.broadcast({ type: 'chat', text: 'Hallo von Peer' });
|
||
await delay(20);
|
||
if (host._chatReceived.length !== 1) return { pass: false, detail: `Host empfing ${host._chatReceived.length} Chats (erwartet: 1)` };
|
||
if (host._chatReceived[0].text !== 'Hallo von Peer') return { pass: false, detail: 'Falscher Chat-Text bei Host' };
|
||
|
||
return { pass: true, detail: 'Host → Peer: "Hallo von Host" ✓\nPeer → Host: "Hallo von Peer" ✓' };
|
||
}
|
||
|
||
/* ─── Test: onchat Callback ─── */
|
||
async function testOnchatCallback() {
|
||
clearMockPeers();
|
||
const host = createTestMesh('Host');
|
||
const peer = createTestMesh('Peer');
|
||
|
||
host.createRoom();
|
||
await delay(30);
|
||
peer.joinFromQR(host._qrData);
|
||
await delay(30);
|
||
await host.feedAnswer(peer._answerData);
|
||
await delay(60);
|
||
clearLogs([host, peer]);
|
||
|
||
const hostId = Array.from(peer.connections.keys())[0];
|
||
|
||
peer.sendTo(hostId, { type: 'chat', text: 'Direkter Chat via sendTo' });
|
||
await delay(20);
|
||
|
||
if (host._chatReceived.length !== 1) return { pass: false, detail: `onchat nicht aufgerufen (${host._chatReceived.length} empfangen)` };
|
||
if (host._chatReceived[0].text !== 'Direkter Chat via sendTo') return { pass: false, detail: `Falscher Text: "${host._chatReceived[0].text}"` };
|
||
|
||
return { pass: true, detail: `onchat ausgelöst: "${host._chatReceived[0].text}" von ${host._chatReceived[0].from}` };
|
||
}
|
||
|
||
/* ─── Test: 3 Peers nacheinander paaren + Chat ─── */
|
||
async function testSequentialPairing() {
|
||
clearMockPeers();
|
||
const host = createTestMesh('Host');
|
||
const peers = [createTestMesh('Peer1'), createTestMesh('Peer2'), createTestMesh('Peer3')];
|
||
|
||
host.createRoom();
|
||
await delay(50);
|
||
if (!host._qrData) return { pass: false, detail: 'Host QR wurde nicht generiert' };
|
||
|
||
for (let i = 0; i < peers.length; i++) {
|
||
const p = peers[i];
|
||
p.joinFromQR(host._qrData);
|
||
await delay(50);
|
||
if (!p._answerData) return { pass: false, detail: `Peer${i + 1} hat keinen Answer generiert` };
|
||
await host.feedAnswer(p._answerData);
|
||
// Wait for identity exchange + host creates new pending peer
|
||
await delay(500);
|
||
}
|
||
|
||
if (host.getPeerIds().length !== 3) return { pass: false, detail: `Host hat ${host.getPeerIds().length} Peers (erwartet: 3)` };
|
||
|
||
let allConnected = true;
|
||
for (const p of peers) {
|
||
if (p.getPeerIds().length === 0) { allConnected = false; break; }
|
||
}
|
||
|
||
// Broadcast chat to all
|
||
clearLogs([host, ...peers]);
|
||
host.broadcast({ type: 'chat', text: 'An alle' });
|
||
await delay(30);
|
||
|
||
const detail = [
|
||
`Host Peers: ${host.getPeerIds().length}`,
|
||
...peers.map((p, i) => `Peer${i + 1} connections: ${p.getPeerIds().length}, chats: ${p._chatReceived.length}`),
|
||
].join('\n');
|
||
|
||
for (let i = 0; i < peers.length; i++) {
|
||
if (peers[i]._chatReceived.length === 0) {
|
||
return { pass: false, detail: `Peer${i + 1} hat keinen Chat empfangen\n${detail}` };
|
||
}
|
||
}
|
||
|
||
return { pass: true, detail };
|
||
}
|
||
|
||
/* ─── Test: Chat nach Verbindungsabbruch ─── */
|
||
async function testChatAfterDisconnect() {
|
||
clearMockPeers();
|
||
const host = createTestMesh('Host');
|
||
const peer = createTestMesh('Peer');
|
||
|
||
host.createRoom();
|
||
await delay(30);
|
||
peer.joinFromQR(host._qrData);
|
||
await delay(30);
|
||
await host.feedAnswer(peer._answerData);
|
||
await delay(60);
|
||
|
||
const pid = Array.from(host.connections.keys())[0];
|
||
host._cleanupPeer(host.connections.get(pid));
|
||
await delay(20);
|
||
|
||
try {
|
||
host.broadcast({ type: 'chat', text: 'Nach Verbindungsabbruch' });
|
||
} catch (e) {
|
||
return { pass: false, detail: 'Chat warf Exception: ' + e.message };
|
||
}
|
||
await delay(20);
|
||
|
||
return { pass: true, detail: `Chat nach disconnect ohne Fehler\nHost connections: ${host.getPeerIds().length}` };
|
||
}
|
||
|
||
/* ─── Test: Chat broadcast via onchat callback chain ─── */
|
||
async function testChatBroadcastFlow() {
|
||
clearMockPeers();
|
||
const host = createTestMesh('Host');
|
||
const peer1 = createTestMesh('Peer1');
|
||
const peer2 = createTestMesh('Peer2');
|
||
|
||
// Pair peer1
|
||
host.createRoom();
|
||
await delay(30);
|
||
peer1.joinFromQR(host._qrData);
|
||
await delay(30);
|
||
await host.feedAnswer(peer1._answerData);
|
||
await delay(500);
|
||
|
||
// Pair peer2
|
||
if (!host._qrData) return { pass: false, detail: 'Nach peer1 kein neues QR' };
|
||
peer2.joinFromQR(host._qrData);
|
||
await delay(30);
|
||
await host.feedAnswer(peer2._answerData);
|
||
await delay(500);
|
||
|
||
clearLogs([host, peer1, peer2]);
|
||
|
||
// Peer1 sends chat → should reach host (direct) and peer2 (via host broadcast)
|
||
peer1.broadcast({ type: 'chat', text: 'Von Peer1 an alle' });
|
||
await delay(30);
|
||
|
||
const detail = [
|
||
`Host chats: ${host._chatReceived.length} (von Peer1)` +
|
||
(host._chatReceived.length > 0 ? `: "${host._chatReceived[0].text}"` : ''),
|
||
`Peer2 chats: ${peer2._chatReceived.length}` +
|
||
(peer2._chatReceived.length > 0 ? `: "${peer2._chatReceived[0].text}"` : ''),
|
||
].join('\n');
|
||
|
||
if (host._chatReceived.length === 0) return { pass: false, detail: 'Host hat Chat von Peer1 nicht erhalten\n' + detail };
|
||
if (peer2._chatReceived.length === 0) return { pass: false, detail: 'Peer2 hat Chat von Peer1 nicht erhalten\n' + detail };
|
||
|
||
return { pass: true, detail };
|
||
}
|
||
|
||
/* ─── UI helpers ─── */
|
||
function addResult(id, label, status, detail) {
|
||
const div = document.createElement('div');
|
||
div.className = 'test';
|
||
div.id = id;
|
||
div.innerHTML = `<h3>${label}</h3><div class="status ${status}">${status === 'pass' ? 'PASS' : status === 'fail' ? 'FAIL' : status === 'skip' ? 'SKIP' : 'RUNNING'}</div><div class="detail">${detail || ''}</div>`;
|
||
$('output').appendChild(div);
|
||
}
|
||
|
||
function updateResult(id, status, detail) {
|
||
const div = document.getElementById(id);
|
||
if (div) {
|
||
div.querySelector('.status').className = 'status ' + status;
|
||
div.querySelector('.status').textContent = status === 'pass' ? 'PASS' : status === 'fail' ? 'FAIL' : 'RUNNING';
|
||
if (detail !== undefined) div.querySelector('.detail').textContent = detail;
|
||
}
|
||
}
|
||
|
||
async function runTests() {
|
||
$('run').disabled = true;
|
||
$('run').textContent = 'Läuft...';
|
||
$('output').innerHTML = '';
|
||
$('summary').style.display = 'none';
|
||
|
||
const tests = [
|
||
{ id: 't-qr-roundtrip', label: 'QR-Format: pack/encode45/decode45/unpack', fn: testQRRoundtrip },
|
||
{ id: 't-basic-pairing', label: 'Basis-Pairing: Host → QR → Peer → Answer → Feed', fn: testBasicPairing },
|
||
{ id: 't-chat-pairing', label: 'Chat via Pairing: Host ↔ Peer', fn: testChatAfterPairing },
|
||
{ id: 't-onchat', label: 'onchat Callback bei Chat-Empfang', fn: testOnchatCallback },
|
||
{ id: 't-sequential', label: '3 Peers nacheinander paaren + Chat', fn: testSequentialPairing },
|
||
{ id: 't-chat-disconnect', label: 'Chat nach Verbindungsabbruch', fn: testChatAfterDisconnect },
|
||
{ id: 't-broadcast', label: 'Chat Broadcast zu allen Peers', fn: testChatBroadcastFlow },
|
||
];
|
||
|
||
for (const t of tests) addResult(t.id, t.label, 'running', '');
|
||
|
||
let passed = 0, failed = 0;
|
||
for (const t of tests) {
|
||
try {
|
||
const { pass, detail } = await t.fn();
|
||
updateResult(t.id, pass ? 'pass' : 'fail', detail);
|
||
if (pass) passed++; else failed++;
|
||
} catch (e) {
|
||
updateResult(t.id, 'fail', `Exception: ${e.message}`);
|
||
failed++;
|
||
}
|
||
}
|
||
|
||
const summary = $('summary');
|
||
summary.style.display = 'block';
|
||
summary.textContent = `${passed}/${passed + failed} Tests bestanden`;
|
||
summary.className = 'summary ' + (failed === 0 ? 'pass' : 'fail');
|
||
$('run').disabled = false;
|
||
$('run').textContent = 'Tests ausführen';
|
||
}
|
||
|
||
$('run').addEventListener('click', runTests);
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|