| 1 | // room-reconnect.test.mjs — "Reconnecting…" only for a problem that |
| 2 | // lasts (spec §15.23, 2026-09-30), gated without a browser. |
| 3 | // |
| 4 | // node --test test/browser/room-reconnect.test.mjs |
| 5 | // |
| 6 | // reconnectStep and reconnectRecord lifted out of the shipped room.js by |
| 7 | // name, with the constants they read, as room-screen.test.mjs does. |
| 8 | import { test } from 'node:test'; |
| 9 | import assert from 'node:assert/strict'; |
| 10 | import { readFileSync } from 'node:fs'; |
| 11 | import { fileURLToPath } from 'node:url'; |
| 12 | |
| 13 | const src = readFileSync(fileURLToPath(new URL('../../internal/calls/static/room.js', import.meta.url)), 'utf8'); |
| 14 | |
| 15 | function lift(name) { |
| 16 | const start = src.indexOf('function ' + name + '('); |
| 17 | assert.notEqual(start, -1, name + ' is not in room.js any more — this test is stale, not passing'); |
| 18 | let depth = 0; |
| 19 | for (let i = src.indexOf('{', start); i < src.length; i++) { |
| 20 | if (src[i] === '{') depth += 1; |
| 21 | else if (src[i] === '}') { |
| 22 | depth -= 1; |
| 23 | if (depth === 0) return src.slice(start, i + 1); |
| 24 | } |
| 25 | } |
| 26 | assert.fail('unbalanced braces reading ' + name); |
| 27 | } |
| 28 | function liftConst(name) { |
| 29 | const m = src.match(new RegExp('const ' + name + ' = ([0-9.]+);')); |
| 30 | assert.ok(m, name + ' is not a numeric const in room.js any more'); |
| 31 | return 'const ' + name + ' = ' + m[1] + ';'; |
| 32 | } |
| 33 | const CONSTS = ['RECONNECT_STREAM_AFTER', 'RECONNECT_CONNECTION_AFTER', 'RECONNECT_AUDIO_AFTER', 'RECONNECT_HOLD'] |
| 34 | .map(liftConst).join('\n'); |
| 35 | // eslint-disable-next-line no-new-func |
| 36 | const { reconnectRecord, reconnectStep } = new Function(CONSTS + '\n' + lift('reconnectRecord') + '\n' + |
| 37 | lift('reconnectStep') + '; return { reconnectRecord, reconnectStep };')(); |
| 38 | |
| 39 | const fine = { connectionBad: false, audioMissing: false }; |
| 40 | |
| 41 | // Five looks a second from `from` to `to` seconds; the times the whole |
| 42 | // call's words showed, and the tiles' at each time. |
| 43 | function run(rec, from, to, stream, people) { |
| 44 | const call = []; |
| 45 | const peers = new Map(); |
| 46 | for (let t = from * 10; t < to * 10; t += 2) { |
| 47 | const s = t / 10; |
| 48 | reconnectStep(rec, s * 1000, stream(s), people(s)); |
| 49 | if (rec.call) call.push(s); |
| 50 | if (rec.peers.size) peers.set(s, Array.from(rec.peers).sort().join()); |
| 51 | } |
| 52 | return { call, peers }; |
| 53 | } |
| 54 | |
| 55 | test('a routine stream renewal, and a slow one, are never said', () => { |
| 56 | const rec = reconnectRecord(); |
| 57 | const seen = run(rec, 0, 60, (t) => (t >= 10 && t < 10.6) || (t >= 30 && t < 33.5), |
| 58 | () => new Map([['sam', fine]])); |
| 59 | assert.deepEqual(seen.call, []); |
| 60 | assert.equal(seen.peers.size, 0); |
| 61 | }); |
| 62 | |
| 63 | test('a stream down four seconds is said, and held a second past recovery', () => { |
| 64 | const rec = reconnectRecord(); |
| 65 | const seen = run(rec, 0, 20, (t) => t >= 5 && t < 12, () => new Map([['sam', fine]])); |
| 66 | assert.equal(seen.call[0], 9); |
| 67 | assert.equal(seen.call[seen.call.length - 1], 12.8); |
| 68 | }); |
| 69 | |
| 70 | test('short blips on one person are never said', () => { |
| 71 | const rec = reconnectRecord(); |
| 72 | const seen = run(rec, 0, 30, () => false, (t) => new Map([['sam', { |
| 73 | connectionBad: t >= 5 && t < 7.5, audioMissing: t >= 15 && t < 16.5, |
| 74 | }]])); |
| 75 | assert.deepEqual(seen.call, []); |
| 76 | assert.equal(seen.peers.size, 0); |
| 77 | }); |
| 78 | |
| 79 | test('one person\'s lasting trouble is said on their tile alone', () => { |
| 80 | const rec = reconnectRecord(); |
| 81 | const seen = run(rec, 0, 30, () => false, |
| 82 | (t) => new Map([['sam', { connectionBad: t >= 5 && t < 15, audioMissing: false }], ['ana', fine]])); |
| 83 | assert.deepEqual(seen.call, []); |
| 84 | const times = Array.from(seen.peers.keys()); |
| 85 | assert.equal(times[0], 8); |
| 86 | assert.equal(times[times.length - 1], 15.8); |
| 87 | assert.ok(Array.from(seen.peers.values()).every((v) => v === 'sam')); |
| 88 | }); |
| 89 | |
| 90 | test('audio that stops from someone unmuted is said after two seconds', () => { |
| 91 | const rec = reconnectRecord(); |
| 92 | const seen = run(rec, 0, 20, () => false, |
| 93 | (t) => new Map([['sam', { connectionBad: false, audioMissing: t >= 5 && t < 10 }], ['ana', fine]])); |
| 94 | assert.equal(Array.from(seen.peers.keys())[0], 7); |
| 95 | }); |
| 96 | |
| 97 | test('everyone\'s trouble is the whole call, not a tile each', () => { |
| 98 | const rec = reconnectRecord(); |
| 99 | const seen = run(rec, 0, 20, () => false, (t) => { |
| 100 | const bad = { connectionBad: t >= 5 && t < 12, audioMissing: false }; |
| 101 | return new Map([['sam', bad], ['ana', bad]]); |
| 102 | }); |
| 103 | assert.equal(seen.call[0], 8); |
| 104 | assert.equal(seen.peers.size, 0); |
| 105 | }); |
| 106 | |