oficina / meet Public

Clone
git clone https://amadan.net/oficina/meet

Plain git — no account needed to clone.

Download

Download this file

1// room-mute.test.mjs — room.js's media state (spec §15.19), gated
2// without a browser.
3//
4// node --test test/browser/room-mute.test.mjs
5//
6// Same trick as room-share.test.mjs: the functions are lifted out of the
7// shipped room.js by name, so a rename fails here loudly instead of
8// passing against a copy.
9//
10// mediaFrom what counts as a report: exactly two booleans, from a
11// roster entry or a `media` payload, and nothing looser —
12// an absent or malformed one is unknown, never "on" —
13// plus §15.20's two optional keys, read forgivingly
14// sameMedia whether the hub's word on us already matches ours
15// mediaKey the dedupe key a toggle is compared on
16// mediaPayload the report: two keys, and §15.20's only with bad news
17// mediaChange the sentence the live region says, and when it says
18// nothing at all
19// rosterMedia with reportMedia: when our own report goes — on the
20// roster that admits us, on one that disagrees, on a
21// toggle, never before admission, and again after a 400
22import { test } from 'node:test';
23import assert from 'node:assert/strict';
24import { readFileSync } from 'node:fs';
25import { fileURLToPath } from 'node:url';
26
27const roomJS = fileURLToPath(new URL('../../internal/calls/static/room.js', import.meta.url));
28const src = readFileSync(roomJS, 'utf8');
29
30function lift(source, name) {
31 const start = source.indexOf('function ' + name + '(');
32 assert.notEqual(start, -1, name + ' is not in room.js any more — this test is stale, not passing');
33 let depth = 0;
34 for (let i = source.indexOf('{', start); i < source.length; i++) {
35 if (source[i] === '{') depth += 1;
36 else if (source[i] === '}') {
37 depth -= 1;
38 if (depth === 0) return source.slice(start, i + 1);
39 }
40 }
41 assert.fail('unbalanced braces reading ' + name + ' out of room.js');
42}
43
44// build lifts one function, and any it calls that are named after it.
45const build = (name, ...with_) =>
46 // eslint-disable-next-line no-new-func
47 new Function([name, ...with_].map((n) => lift(src, n)).join('\n') + '; return ' + name + ';')();
48
49const mediaFrom = build('mediaFrom');
50const sameMedia = build('sameMedia', 'mediaKey');
51const mediaKey = build('mediaKey');
52const mediaChange = build('mediaChange');
53const mediaPayload = build('mediaPayload');
54
55const P1 = 'a'.repeat(32);
56const P2 = 'b'.repeat(32);
57
58test('mediaFrom takes exactly two booleans and nothing looser', () => {
59 assert.deepEqual(mediaFrom({ muted: true, cameraOff: false }), { muted: true, cameraOff: false });
60 // Extra keys are dropped rather than carried: what is drawn is what
61 // was checked.
62 assert.deepEqual(mediaFrom({ muted: false, cameraOff: true, extra: 1 }), { muted: false, cameraOff: true });
63 for (const bad of [
64 undefined, null, 'muted', 1, [],
65 {}, { muted: true }, { cameraOff: true },
66 { muted: 'true', cameraOff: false }, { muted: 1, cameraOff: 0 }, { muted: null, cameraOff: false },
67 ]) {
68 assert.equal(mediaFrom(bad), null, JSON.stringify(bad) + ' is unknown');
69 }
70});
71
72test('mediaFrom reads §15.20’s optional keys, and drops a bad one on its own', () => {
73 assert.deepEqual(
74 mediaFrom({ muted: false, cameraOff: false, audioBlocked: true, noSoundFrom: [P1, P2] }),
75 { muted: false, cameraOff: false, audioBlocked: true, noSoundFrom: [P1, P2] });
76 // A malformed optional key costs only itself: the booleans still stand.
77 for (const [bad, want] of [
78 [{ audioBlocked: 'yes' }, {}],
79 [{ audioBlocked: false }, {}],
80 [{ noSoundFrom: 'x' }, {}],
81 [{ noSoundFrom: [] }, {}],
82 [{ noSoundFrom: [P1, 'nope', 7, P1.toUpperCase(), P1] }, { noSoundFrom: [P1] }],
83 ]) {
84 assert.deepEqual(mediaFrom({ muted: true, cameraOff: false, ...bad }),
85 { muted: true, cameraOff: false, ...want }, JSON.stringify(bad));
86 }
87 // Never more than the hub's fifteen.
88 const many = Array.from({ length: 20 }, (_, i) => i.toString(16).padStart(32, '0'));
89 assert.equal(mediaFrom({ muted: false, cameraOff: false, noSoundFrom: many }).noSoundFrom.length, 15);
90 // And the required pair is still required.
91 assert.equal(mediaFrom({ muted: 'no', cameraOff: false, audioBlocked: true }), null);
92});
93
94test('sameMedia and mediaKey see a change in hearing as a change', () => {
95 const on = { muted: false, cameraOff: false };
96 assert.equal(sameMedia(on, { ...on, audioBlocked: true }), false);
97 assert.equal(sameMedia(on, { ...on, noSoundFrom: [P1] }), false);
98 assert.equal(sameMedia({ ...on, noSoundFrom: [P1, P2] }, { ...on, noSoundFrom: [P2, P1] }), true,
99 'the same peers in another order are the same report');
100 assert.notEqual(mediaKey({ ...on, noSoundFrom: [P1] }), mediaKey({ ...on, noSoundFrom: [P2] }));
101});
102
103test('mediaPayload is the two-key object unless there is bad news', () => {
104 const mine = { muted: false, cameraOff: true };
105 assert.deepEqual(mediaPayload(mine, false, [], false), { muted: false, cameraOff: true });
106 assert.deepEqual(Object.keys(mediaPayload(mine, false, null, false)), ['muted', 'cameraOff']);
107 assert.deepEqual(mediaPayload(mine, true, [P1], false),
108 { muted: false, cameraOff: true, audioBlocked: true, noSoundFrom: [P1] });
109 // A server that refused the extras once gets the two keys for good.
110 assert.deepEqual(mediaPayload(mine, true, [P1], true), { muted: false, cameraOff: true });
111});
112
113test('sameMedia never matches unknown', () => {
114 const on = { muted: false, cameraOff: false };
115 assert.equal(sameMedia(on, { muted: false, cameraOff: false }), true);
116 assert.equal(sameMedia(on, { muted: true, cameraOff: false }), false);
117 assert.equal(sameMedia(on, { muted: false, cameraOff: true }), false);
118 // The roster's silence about us is not agreement with "on": a fresh
119 // participation must be told, or a peer who joined unmuted stays
120 // unknown to everyone for the whole call.
121 assert.equal(sameMedia(null, on), false);
122 assert.equal(sameMedia(on, null), false);
123});
124
125test('mediaKey tells every state apart', () => {
126 const keys = new Set();
127 for (const muted of [false, true]) {
128 for (const cameraOff of [false, true]) keys.add(mediaKey({ muted, cameraOff }));
129 }
130 assert.equal(keys.size, 4);
131 // And never collides with the empty string mediaSent starts at.
132 assert.equal(keys.has(''), false);
133});
134
135test('mediaChange says what moved, and nothing for a first report', () => {
136 const on = { muted: false, cameraOff: false };
137 assert.equal(mediaChange('Bea', null, { muted: true, cameraOff: false }), '',
138 'a first report is somebody arriving, not news');
139 assert.equal(mediaChange('Bea', on, on), '');
140 assert.equal(mediaChange('Bea', on, { muted: true, cameraOff: false }), 'Bea muted their microphone.');
141 assert.equal(mediaChange('Bea', { muted: true, cameraOff: false }, on), 'Bea unmuted their microphone.');
142 assert.equal(mediaChange('Bea', on, { muted: false, cameraOff: true }), 'Bea turned their camera off.');
143 assert.equal(mediaChange('Bea', { muted: false, cameraOff: true }, on), 'Bea turned their camera on.');
144 assert.equal(mediaChange('Bea', on, { muted: true, cameraOff: true }),
145 'Bea muted their microphone. Bea turned their camera off.');
146});
147
148test('the dispatch routes media, and the report rides the broadcast lane', () => {
149 // Wire-level facts a lift cannot reach: the event name the hub sends,
150 // and that the report is an untargeted broadcast rather than a send()
151 // with a seq and a generation the hub would refuse.
152 assert.match(src, /case 'media': return onMedia\(msg\);/);
153 // An EventSource hears only the names it listens for: the dispatch
154 // above is dead without this, and the roster path hides it (the
155 // browser gate caught exactly that).
156 assert.match(src, /const DOWNSTREAM = \[[^\]]*'media'\]/);
157 assert.match(src, /broadcast\('media', now,/);
158});
159
160// rosterMedia and reportMedia, lifted together over real Maps with only
161// the wire and the local stream stubbed: the decision under test is when
162// a report goes, and that is the pair of them.
163function reporter({ admitted = true, local = { muted: false, cameraOff: false }, answer = true, status = 200 } = {}) {
164 const sent = [];
165 const env = { local, answer, status, blocked: false, noSound: [], heard: null };
166 const made = new Function(
167 'state', 'remoteMedia', 'localMedia', 'broadcast', 'env',
168 ['mediaFrom', 'sameMedia', 'mediaKey', 'mediaPayload', 'learnMedia', 'reportMedia', 'rosterMedia']
169 .map((n) => lift(src, n)).join('\n') +
170 '\nlet mediaSent = "";' +
171 '\nlet mediaExtras = true;' +
172 '\nlet audioBlocked = false;' +
173 '\nlet screenStalled = [];' +
174 '\nconst noSoundList = () => env.noSound;' +
175 '\nconst hearingList = () => env.heard;' +
176 '\nreturn { rosterMedia, reportMedia, memo: () => mediaSent, extras: () => mediaExtras,' +
177 '\n block: (on) => { audioBlocked = on; } };',
178 )(
179 { admitted, stopped: false, peer: 'me' },
180 new Map(),
181 () => env.local,
182 (type, payload, onStatus) => {
183 sent.push([type, payload]);
184 // The status of the answer goes to onStatus first, as pumpBroadcast
185 // does it, and then the boolean settles the promise.
186 const extra = Object.keys(payload).length > 2;
187 const status = extra ? env.status : (env.answer ? 200 : 400);
188 if (onStatus) onStatus(status);
189 return Promise.resolve(status === 200);
190 },
191 env,
192 );
193 return { ...made, sent, env };
194}
195
196test('a roster that has never heard from us is answered with a report', () => {
197 const r = reporter({ local: { muted: true, cameraOff: false } });
198 r.rosterMedia({ peers: [{ peer: 'me' }, { peer: 'bea' }] });
199 assert.deepEqual(r.sent, [['media', { muted: true, cameraOff: false }]]);
200});
201
202test('a roster that already agrees costs nothing, and a toggle then goes once', () => {
203 const r = reporter();
204 r.rosterMedia({ peers: [{ peer: 'me', media: { muted: false, cameraOff: false } }] });
205 assert.equal(r.sent.length, 0);
206 r.env.local = { muted: true, cameraOff: false };
207 r.reportMedia();
208 r.reportMedia();
209 assert.equal(r.sent.length, 1, 'a second call with nothing changed must not send again');
210});
211
212test('a roster that disagrees — a report lost while the stream was down — is corrected', () => {
213 const r = reporter({ local: { muted: true, cameraOff: true } });
214 r.reportMedia();
215 assert.equal(r.sent.length, 1);
216 r.rosterMedia({ peers: [{ peer: 'me', media: { muted: true, cameraOff: false } }] });
217 assert.equal(r.sent.length, 2);
218 assert.deepEqual(r.sent[1][1], { muted: true, cameraOff: true });
219});
220
221test('nothing is reported before admission', () => {
222 const r = reporter({ admitted: false, local: { muted: true, cameraOff: false } });
223 r.reportMedia();
224 assert.equal(r.sent.length, 0);
225});
226
227test('a refused report is forgotten, so the next chance tries again', async () => {
228 const r = reporter({ answer: false, local: { muted: true, cameraOff: false } });
229 r.reportMedia();
230 await new Promise((resolve) => setTimeout(resolve, 0));
231 assert.equal(r.memo(), '', 'an old server’s 400 must not be remembered as sent');
232 r.reportMedia();
233 assert.equal(r.sent.length, 2);
234});
235
236test('bad news about hearing is reported, and its end too', () => {
237 const r = reporter();
238 r.rosterMedia({ peers: [{ peer: 'me', media: { muted: false, cameraOff: false } }] });
239 assert.equal(r.sent.length, 0);
240 r.env.noSound = [P1];
241 r.reportMedia();
242 assert.deepEqual(r.sent.at(-1)[1], { muted: false, cameraOff: false, noSoundFrom: [P1] });
243 r.block(true);
244 r.reportMedia();
245 assert.deepEqual(r.sent.at(-1)[1], { muted: false, cameraOff: false, audioBlocked: true, noSoundFrom: [P1] });
246 r.reportMedia();
247 assert.equal(r.sent.length, 2, 'nothing changed, nothing sent');
248 r.block(false);
249 r.env.noSound = [];
250 r.reportMedia();
251 assert.deepEqual(r.sent.at(-1)[1], { muted: false, cameraOff: false });
252 assert.deepEqual(Object.keys(r.sent.at(-1)[1]), ['muted', 'cameraOff'],
253 'the ordinary report is exactly the two keys an old server takes');
254});
255
256test('a roster carrying our hearing as we would report it costs nothing', () => {
257 const r = reporter();
258 r.env.noSound = [P1];
259 r.rosterMedia({ peers: [{ peer: 'me', media: { muted: false, cameraOff: false, noSoundFrom: [P1] } }] });
260 assert.equal(r.sent.length, 0);
261 r.rosterMedia({ peers: [{ peer: 'me', media: { muted: false, cameraOff: false } }] });
262 assert.equal(r.sent.length, 1, 'a roster that lost the list is told again');
263});
264
265test('a 400 for the extras falls back to the two keys, at once and for good', async () => {
266 const r = reporter({ status: 400 });
267 r.env.noSound = [P1];
268 r.reportMedia();
269 await new Promise((resolve) => setTimeout(resolve, 0));
270 assert.equal(r.extras(), false);
271 assert.equal(r.sent.length, 2);
272 assert.deepEqual(r.sent[1][1], { muted: false, cameraOff: false },
273 'the fallback goes straight away, without waiting for the next change');
274 r.block(true);
275 r.env.noSound = [P1, P2];
276 r.reportMedia();
277 assert.equal(r.sent.length, 2, 'with the extras off, a change in hearing alone is no change');
278 r.env.local = { muted: true, cameraOff: false };
279 r.reportMedia();
280 assert.deepEqual(r.sent.at(-1)[1], { muted: true, cameraOff: false });
281});
282
283// §15.24: `hearing` is good news and travels whenever it is given, []
284// included — its presence is what tells the others this tab says who it
285// hears — and it is read back the same way: an empty list is kept, and
286// is a different report from one without the key.
287test('hearing is carried even when empty, and read back as given', () => {
288 assert.deepEqual(mediaPayload({ muted: false, cameraOff: false }, false, [], false, [], []),
289 { muted: false, cameraOff: false, hearing: [] });
290 assert.deepEqual(mediaPayload({ muted: false, cameraOff: false }, false, [], false, [], [P1, P2]),
291 { muted: false, cameraOff: false, hearing: [P1, P2] });
292 assert.deepEqual(mediaPayload({ muted: false, cameraOff: false }, false, [], true, [], [P1]),
293 { muted: false, cameraOff: false }, 'a server that refused the extras gets none');
294 assert.deepEqual(mediaFrom({ muted: false, cameraOff: false, hearing: [] }), { muted: false, cameraOff: false, hearing: [] });
295 assert.deepEqual(mediaFrom({ muted: false, cameraOff: false, hearing: [P1, 'nope', P1] }),
296 { muted: false, cameraOff: false, hearing: [P1] });
297 assert.deepEqual(mediaFrom({ muted: false, cameraOff: false, hearing: 'all' }), { muted: false, cameraOff: false });
298 assert.notEqual(mediaKey({ muted: false, cameraOff: false, hearing: [] }), mediaKey({ muted: false, cameraOff: false }));
299 assert.equal(mediaKey({ muted: false, cameraOff: false, hearing: [P2, P1] }), mediaKey({ muted: false, cameraOff: false, hearing: [P1, P2] }));
300});
301
302test('who this tab hears is reported on a change, and a roster that agrees costs nothing', () => {
303 const r = reporter();
304 r.env.heard = [];
305 r.rosterMedia({ peers: [{ peer: 'me' }] });
306 assert.deepEqual(r.sent.at(-1)[1], { muted: false, cameraOff: false, hearing: [] });
307 r.rosterMedia({ peers: [{ peer: 'me', media: { muted: false, cameraOff: false, hearing: [] } }] });
308 assert.equal(r.sent.length, 1);
309 r.env.heard = [P1];
310 r.reportMedia();
311 assert.deepEqual(r.sent.at(-1)[1], { muted: false, cameraOff: false, hearing: [P1] });
312 r.reportMedia();
313 assert.equal(r.sent.length, 2);
314});
315
316test('a 400 for a report carrying hearing falls back to the two keys', async () => {
317 const r = reporter({ status: 400 });
318 r.env.heard = [];
319 r.reportMedia();
320 await new Promise((resolve) => setTimeout(resolve, 0));
321 assert.equal(r.extras(), false);
322 assert.deepEqual(r.sent.at(-1)[1], { muted: false, cameraOff: false });
323});
324