openvibers/openvibe.chat Commits
Chat: 1:1 call page asks only on the Turn on microphone press (#489)
Changes
@@@ -3,7 +3,8 @@server/calls/call-server.js: the newcomer offers to everyone already in, the others answer. The room'sroles decide who talks (the server force-mutes listeners; this client never sends a microphone it maynot use). Names are set with textContent. The room's chat works without this file.-- A 1:1 call's page (/call/<channel>, cfg.call.auto) joins as it opens; while its caller waits (cfg.call.ring) it says++ A 1:1 call's page (/call/<channel>) joins only when the person presses "Turn on microphone":++ opening or refreshing the page asks for nothing. While its caller waits (cfg.call.ring) it saysringing and listens on a /ws/chat socket for the callee's vc-call-response: accepted (they are on their way),declined, busy or no-answer (then the call is left). */(function () {@@@ -162,7 +163,7 @@if (m.type !== 'vc-call-response' || !ringing || String(m.channelId) !== String(cfg.call.channel)) return;if (Number(m.fromUserId) !== Number(ringing.id)) return;var who = ringing.name, st = String(m.status || '').toLowerCase();-- if (st === 'accepted') { status(who + ' answered. Connecting…'); return; }++ if (st === 'accepted') { status(joined ? who + ' answered. Connecting…' : who + ' answered. Press "Turn on microphone" to join.'); return; }stopRinging();teardown(st === 'busy' ? who + ' is in another call.' : st === 'no-answer' ? 'No answer from ' + who + '.' : who + ' declined the call.');};@@@ -224,5 +225,5 @@});window.addEventListener('pagehide', function () { stopRinging(); if (joined) teardown(null); });if (ringing) listenForAnswer();-- if (cfg.call.auto) join();++ // The microphone opens only from the "Turn on microphone" press above — no auto-join on load.})();@@@ -12,7 +12,8 @@* POST /messages/:id/block blocks the other person of a 1:1 conversation;* POST /messages/:id/call rings them (the call-user route) and opens /call/<channel>* GET /call/:channel a 1:1 call's page (its caller and the people it invited): public/web/call.js joins-- * it, and while the caller waits it says ringing until the answer comes++ * it only when the person presses "Turn on microphone", and while the caller waits++ * it says ringing until the answer comes* GET /rooms, /r/:slug rooms: community, call (a voice/video call in the page, public/web/call.js)* and system (staff announcements); /r/:slug/settings for moderators* GET /settings your portable chat settings (Network module chat.preferences); POST saves@@@ -668,13 +669,14 @@ ${composer}`,: '<li class="oc-muted">Nobody is in the call yet.</li>';await page(req, res, 200, {actor, title, path: `/call/${ch.id}`, active: 'messages', robots: 'noindex, nofollow', callScript: true,-- page: { view: 'call', me: me.id, call: { channel: ch.id, join: true, talk: true, auto: true, ring: ringing ? { id: Number(ringing.id), name: nameOf(ringing) } : null } },++ page: { view: 'call', me: me.id, call: { channel: ch.id, join: true, talk: true, ring: ringing ? { id: Number(ringing.id), name: nameOf(ringing) } : null } },body: `<p><a href="${backLink}">← Back to the conversation</a></p><section class="oc-call oc-call-page" aria-labelledby="oc-call-h" id="oc-call"><h1 id="oc-call-h">${esc(title)} <span class="oc-muted oc-call-count" id="oc-call-count">${ch.participantCount ? `· ${ch.participantCount} in` : ''}</span></h1><p class="oc-muted" id="oc-call-how">A voice call, peer to peer: your microphone goes only to the people in it.</p><ul class="oc-call-people" id="oc-call-people">${people}</ul>--<p class="oc-call-controls"><button type="button" id="oc-call-join" hidden>Join the call</button> <button type="button" id="oc-call-mute" hidden aria-pressed="false">Mute</button> <button type="button" id="oc-call-leave" hidden>Leave the call</button></p>++<p class="oc-call-controls"><button type="button" id="oc-call-join" hidden>Turn on microphone</button> <button type="button" id="oc-call-mute" hidden aria-pressed="false">Mute</button> <button type="button" id="oc-call-leave" hidden>Leave the call</button></p>++<p class="oc-muted oc-call-join-note">Join the voice call with the people listed above. Nothing is asked until you press it.</p><p class="oc-muted oc-call-status" id="oc-call-status" role="status" aria-live="polite"></p><noscript><p class="oc-muted">A call needs JavaScript.</p></noscript></section>`,nnew file mode 100644@@@ -0,0 +1,74 @@++'use strict';++/**++ * No capture until a press: the 1:1 call page (public/web/call.js) must not ask for++ * the microphone when it opens. Only pressing "Turn on microphone" (#oc-call-join)++ * may call getUserMedia. A browser test with a getUserMedia spy in the style of++ * dm-calls.test.js runScript, plus source checks.++ */++const assert = require('assert');++const fs = require('fs');++const path = require('path');++const vm = require('vm');++++function runCallPage(page) {++ const byId = {}, created = [], sockets = [], fetches = [];++ let gumCalls = 0;++ const node = (id = null) => ({++ id, hidden: false, disabled: false, textContent: '', className: '', children: [], attrs: {}, handlers: {},++ appendChild(c) { this.children.push(c); return c; }, setAttribute(k, v) { this.attrs[k] = v; },++ removeAttribute(k) { delete this.attrs[k]; },++ addEventListener(ev, fn) { this.handlers[ev] = fn; }, remove() { this.removed = true; }, focus() {},++ querySelector: () => null,++ });++ function WebSocket(url) { this.url = url; this.readyState = 1; this.sent = []; sockets.push(this); }++ WebSocket.prototype.send = function (d) { this.sent.push(JSON.parse(d)); };++ WebSocket.prototype.close = function () { this.closed = true; };++ function RTCPeerConnection() { this.connectionState = 'new'; }++ RTCPeerConnection.prototype.close = function () {};++ const document = {++ cookie: '', hidden: false, title: 'Call',++ getElementById: (id) => (byId[id] = byId[id] || node(id)),++ createElement: (tag) => { const n = node(); n.tag = tag; created.push(n); return n; },++ createTextNode: (text) => ({ textContent: text }),++ querySelector: () => null, addEventListener() {}, body: node('body'),++ };++ const ctx = {++ window: { __OV_PAGE: { chat: page }, RTCPeerConnection, WebSocket, addEventListener() {} },++ WebSocket, RTCPeerConnection,++ navigator: { mediaDevices: { getUserMedia: async () => { gumCalls++; return { getTracks: () => [] }; } } },++ location: { protocol: 'https:', host: 'openvibe.chat', href: '' },++ document, JSON, Number, String, Map, Promise,++ fetch: async (url) => { fetches.push(url); return { ok: true, json: async () => ({}) }; },++ setTimeout: (fn) => 0, clearTimeout: () => {}, setInterval: () => 0,++ };++ vm.runInNewContext(fs.readFileSync(path.join(__dirname, '..', 'public', 'web', 'call.js'), 'utf8'), ctx);++ return { byId, sockets, fetches, gumCalls: () => gumCalls };++}++++// Loading the call page asks for nothing — even with the legacy auto flag present,++// the client must ignore it and wait for the press.++for (const page of [{ view: 'call', call: { channel: 'user-1-abc', join: true, talk: true } }, { view: 'call', call: { channel: 'user-1-abc', join: true, talk: true, auto: true } }]) {++ const s = runCallPage(page);++ assert.strictEqual(s.gumCalls(), 0, 'page load must not call getUserMedia');++ assert.ok(!s.sockets.some((x) => /\/ws\/call/.test(x.url)), 'page load must not open the call socket');++ assert.strictEqual(s.byId['oc-call-join'].hidden, false, 'the join button is shown');++ assert.ok(s.byId['oc-call-join'].handlers.click, 'the join button has a click handler');++++ // The press is what opens the microphone.++ s.byId['oc-call-join'].handlers.click();++ assert.strictEqual(s.gumCalls(), 1, 'pressing Turn on microphone calls getUserMedia once');++ console.log('OK capture-on-load: no getUserMedia until the Turn on microphone click');++}++++// Source checks: the server must not re-arm auto-join, and the button must be the press.++{++ const pages = fs.readFileSync(path.join(__dirname, '..', 'server', 'web', 'pages.js'), 'utf8');++ assert.ok(!/auto:\s*true/.test(pages), 'GET /call/:channel must not set auto:true');++ const callJs = fs.readFileSync(path.join(__dirname, '..', 'public', 'web', 'call.js'), 'utf8');++ assert.ok(!/cfg\.call\.auto/.test(callJs), 'call.js must not auto-join on load');++ assert.ok(pages.includes('>Turn on microphone</button>'), 'the call page button says Turn on microphone');++ assert.ok(pages.includes('oc-call-join-note'), 'one line under the button says what it is for');++ console.log('OK capture-on-load: no auto-join in server or client source');++}++++console.log('✅ capture on load test passed');@@@ -105,7 +105,9 @@ t('Call rings the other person (a direct row) and opens the call page with the rassert.match(mine.text, /<script src="\/web\/call\.js\?v=[^"]+" defer><\/script>/);assert.match(mine.text, new RegExp(`href="/messages/${conv}"`), 'back to the conversation');assert.match(mine.text, /<meta name="robots" content="noindex, nofollow">/);-- assert.deepStrictEqual(pageJson(mine.text).call, { channel: channelId, join: true, talk: true, auto: true, ring: { id: Number(bob.id), name: 'Bob' } });++ assert.deepStrictEqual(pageJson(mine.text).call, { channel: channelId, join: true, talk: true, ring: { id: Number(bob.id), name: 'Bob' } });++ assert.match(mine.text, /<button type="button" id="oc-call-join" hidden>Turn on microphone<\/button>/);++ assert.match(mine.text, /oc-call-join-note/);const his = await req('GET', `/call/${encodeURIComponent(channelId)}`, { headers: cookie(bob) });assert.strictEqual(his.status, 200);@@@ -166,14 +168,16 @@ t('ring.js: a ring shows Answer and Decline; answering tells the caller and opent('call.js on the caller\'s page: ringing until the answer; a decline, busy or no answer leaves the call', async () => {const signed = `ov_token=${encodeURIComponent(alice.token)}`;for (const [status, text] of [['declined', 'Bob declined the call.'], ['busy', 'Bob is in another call.'], ['no-answer', 'No answer from Bob.']]) {-- const s = runScript('call.js', { view: 'call', call: { channel: 'user-1-abc', join: true, talk: true, auto: true, ring: { id: 2, name: 'Bob' } } }, { docCookie: signed });++ const s = runScript('call.js', { view: 'call', call: { channel: 'user-1-abc', join: true, talk: true, ring: { id: 2, name: 'Bob' } } }, { docCookie: signed });const chat = s.sockets.find((x) => /\/ws\/chat$/.test(x.url));assert.ok(chat, 'the caller listens for the answer on /ws/chat');chat.onopen();assert.deepStrictEqual(chat.sent[0], { type: 'join', token: alice.token });++ assert.ok(!s.sockets.some((x) => /\/ws\/call/.test(x.url)), 'the page open asks for nothing: no call socket yet');++ s.byId['oc-call-join'].handlers.click();await tick(); await tick();const call = s.sockets.find((x) => /\/ws\/call\?channelId=user-1-abc$/.test(x.url));-- assert.ok(call, 'and joins its call as the page opens');++ assert.ok(call, 'and joins its call on the Turn on microphone press');call.onopen();call.onmessage({ data: JSON.stringify({ type: 'welcome', peerId: 'p1', participants: [{ peerId: 'p1', displayName: 'Alice' }] }) });assert.strictEqual(s.byId['oc-call-status'].textContent, 'Ringing Bob…');@@@ -184,8 +188,9 @@ t('call.js on the caller\'s page: ringing until the answer; a decline, busy or nassert.strictEqual(s.byId['oc-call-status'].textContent, text);assert.ok(chat.closed && call.closed, `${status}: the call is left and the ring socket closed`);}-- const s = runScript('call.js', { view: 'call', call: { channel: 'user-1-abc', join: true, talk: true, auto: true, ring: { id: 2, name: 'Bob' } } }, { docCookie: signed });++ const s = runScript('call.js', { view: 'call', call: { channel: 'user-1-abc', join: true, talk: true, ring: { id: 2, name: 'Bob' } } }, { docCookie: signed });const chat = s.sockets[0];++ s.byId['oc-call-join'].handlers.click();await tick(); await tick();const call = s.sockets.find((x) => /\/ws\/call/.test(x.url));call.onmessage({ data: JSON.stringify({ type: 'welcome', peerId: 'p1', participants: [{ peerId: 'p1' }] }) });
@@ -3,7 +3,8 @@
@@ -3,7 +3,8 @@
server/calls/call-server.js: the newcomer offers to everyone already in, the others answer. The room's
server/calls/call-server.js: the newcomer offers to everyone already in, the others answer. The room's
roles decide who talks (the server force-mutes listeners; this client never sends a microphone it may
roles decide who talks (the server force-mutes listeners; this client never sends a microphone it may
not use). Names are set with textContent. The room's chat works without this file.
not use). Names are set with textContent. The room's chat works without this file.
- A 1:1 call's page (/call/<channel>, cfg.call.auto) joins as it opens; while its caller waits (cfg.call.ring) it says
+ A 1:1 call's page (/call/<channel>) joins only when the person presses "Turn on microphone":
+ opening or refreshing the page asks for nothing. While its caller waits (cfg.call.ring) it says
ringing and listens on a /ws/chat socket for the callee's vc-call-response: accepted (they are on their way),
ringing and listens on a /ws/chat socket for the callee's vc-call-response: accepted (they are on their way),
declined, busy or no-answer (then the call is left). */
declined, busy or no-answer (then the call is left). */
(function () {
(function () {
@@ -162,7 +163,7 @@
@@ -162,7 +163,7 @@
if (m.type !== 'vc-call-response' || !ringing || String(m.channelId) !== String(cfg.call.channel)) return;
if (m.type !== 'vc-call-response' || !ringing || String(m.channelId) !== String(cfg.call.channel)) return;
if (Number(m.fromUserId) !== Number(ringing.id)) return;
if (Number(m.fromUserId) !== Number(ringing.id)) return;
var who = ringing.name, st = String(m.status || '').toLowerCase();
var who = ringing.name, st = String(m.status || '').toLowerCase();
- if (st === 'accepted') { status(who + ' answered. Connecting…'); return; }
+ if (st === 'accepted') { status(joined ? who + ' answered. Connecting…' : who + ' answered. Press "Turn on microphone" to join.'); return; }
stopRinging();
stopRinging();
teardown(st === 'busy' ? who + ' is in another call.' : st === 'no-answer' ? 'No answer from ' + who + '.' : who + ' declined the call.');
teardown(st === 'busy' ? who + ' is in another call.' : st === 'no-answer' ? 'No answer from ' + who + '.' : who + ' declined the call.');
};
};
@@ -224,5 +225,5 @@
@@ -224,5 +225,5 @@
});
});
window.addEventListener('pagehide', function () { stopRinging(); if (joined) teardown(null); });
window.addEventListener('pagehide', function () { stopRinging(); if (joined) teardown(null); });
if (ringing) listenForAnswer();
if (ringing) listenForAnswer();
- if (cfg.call.auto) join();
+ // The microphone opens only from the "Turn on microphone" press above — no auto-join on load.
})();
})();
@@ -12,7 +12,8 @@
@@ -12,7 +12,8 @@
* POST /messages/:id/block blocks the other person of a 1:1 conversation;
* POST /messages/:id/block blocks the other person of a 1:1 conversation;
* POST /messages/:id/call rings them (the call-user route) and opens /call/<channel>
* POST /messages/:id/call rings them (the call-user route) and opens /call/<channel>
* GET /call/:channel a 1:1 call's page (its caller and the people it invited): public/web/call.js joins
* GET /call/:channel a 1:1 call's page (its caller and the people it invited): public/web/call.js joins
- * it, and while the caller waits it says ringing until the answer comes
+ * it only when the person presses "Turn on microphone", and while the caller waits
+ * it says ringing until the answer comes
* GET /rooms, /r/:slug rooms: community, call (a voice/video call in the page, public/web/call.js)
* GET /rooms, /r/:slug rooms: community, call (a voice/video call in the page, public/web/call.js)
* and system (staff announcements); /r/:slug/settings for moderators
* and system (staff announcements); /r/:slug/settings for moderators
* GET /settings your portable chat settings (Network module chat.preferences); POST saves
* GET /settings your portable chat settings (Network module chat.preferences); POST saves
@@ -668,13 +669,14 @@ ${composer}`,
@@ -668,13 +669,14 @@ ${composer}`,
: '<li class="oc-muted">Nobody is in the call yet.</li>';
: '<li class="oc-muted">Nobody is in the call yet.</li>';
await page(req, res, 200, {
await page(req, res, 200, {
actor, title, path: `/call/${ch.id}`, active: 'messages', robots: 'noindex, nofollow', callScript: true,
actor, title, path: `/call/${ch.id}`, active: 'messages', robots: 'noindex, nofollow', callScript: true,
- page: { view: 'call', me: me.id, call: { channel: ch.id, join: true, talk: true, auto: true, ring: ringing ? { id: Number(ringing.id), name: nameOf(ringing) } : null } },
+ page: { view: 'call', me: me.id, call: { channel: ch.id, join: true, talk: true, ring: ringing ? { id: Number(ringing.id), name: nameOf(ringing) } : null } },
body: `<p><a href="${backLink}">← Back to the conversation</a></p>
body: `<p><a href="${backLink}">← Back to the conversation</a></p>
<section class="oc-call oc-call-page" aria-labelledby="oc-call-h" id="oc-call">
<section class="oc-call oc-call-page" aria-labelledby="oc-call-h" id="oc-call">
<h1 id="oc-call-h">${esc(title)} <span class="oc-muted oc-call-count" id="oc-call-count">${ch.participantCount ? `· ${ch.participantCount} in` : ''}</span></h1>
<h1 id="oc-call-h">${esc(title)} <span class="oc-muted oc-call-count" id="oc-call-count">${ch.participantCount ? `· ${ch.participantCount} in` : ''}</span></h1>
<p class="oc-muted" id="oc-call-how">A voice call, peer to peer: your microphone goes only to the people in it.</p>
<p class="oc-muted" id="oc-call-how">A voice call, peer to peer: your microphone goes only to the people in it.</p>
<ul class="oc-call-people" id="oc-call-people">${people}</ul>
<ul class="oc-call-people" id="oc-call-people">${people}</ul>
-<p class="oc-call-controls"><button type="button" id="oc-call-join" hidden>Join the call</button> <button type="button" id="oc-call-mute" hidden aria-pressed="false">Mute</button> <button type="button" id="oc-call-leave" hidden>Leave the call</button></p>
+<p class="oc-call-controls"><button type="button" id="oc-call-join" hidden>Turn on microphone</button> <button type="button" id="oc-call-mute" hidden aria-pressed="false">Mute</button> <button type="button" id="oc-call-leave" hidden>Leave the call</button></p>
+<p class="oc-muted oc-call-join-note">Join the voice call with the people listed above. Nothing is asked until you press it.</p>
<p class="oc-muted oc-call-status" id="oc-call-status" role="status" aria-live="polite"></p>
<p class="oc-muted oc-call-status" id="oc-call-status" role="status" aria-live="polite"></p>
<noscript><p class="oc-muted">A call needs JavaScript.</p></noscript>
<noscript><p class="oc-muted">A call needs JavaScript.</p></noscript>
</section>`,
</section>`,
new file mode 100644
new file mode 100644
@@ -0,0 +1,74 @@
@@ -0,0 +1,74 @@
+'use strict';
+/**
+ * No capture until a press: the 1:1 call page (public/web/call.js) must not ask for
+ * the microphone when it opens. Only pressing "Turn on microphone" (#oc-call-join)
+ * may call getUserMedia. A browser test with a getUserMedia spy in the style of
+ * dm-calls.test.js runScript, plus source checks.
+ */
+const assert = require('assert');
+const fs = require('fs');
+const path = require('path');
+const vm = require('vm');
+
+function runCallPage(page) {
+ const byId = {}, created = [], sockets = [], fetches = [];
+ let gumCalls = 0;
+ const node = (id = null) => ({
+ id, hidden: false, disabled: false, textContent: '', className: '', children: [], attrs: {}, handlers: {},
+ appendChild(c) { this.children.push(c); return c; }, setAttribute(k, v) { this.attrs[k] = v; },
+ removeAttribute(k) { delete this.attrs[k]; },
+ addEventListener(ev, fn) { this.handlers[ev] = fn; }, remove() { this.removed = true; }, focus() {},
+ querySelector: () => null,
+ });
+ function WebSocket(url) { this.url = url; this.readyState = 1; this.sent = []; sockets.push(this); }
+ WebSocket.prototype.send = function (d) { this.sent.push(JSON.parse(d)); };
+ WebSocket.prototype.close = function () { this.closed = true; };
+ function RTCPeerConnection() { this.connectionState = 'new'; }
+ RTCPeerConnection.prototype.close = function () {};
+ const document = {
+ cookie: '', hidden: false, title: 'Call',
+ getElementById: (id) => (byId[id] = byId[id] || node(id)),
+ createElement: (tag) => { const n = node(); n.tag = tag; created.push(n); return n; },
+ createTextNode: (text) => ({ textContent: text }),
+ querySelector: () => null, addEventListener() {}, body: node('body'),
+ };
+ const ctx = {
+ window: { __OV_PAGE: { chat: page }, RTCPeerConnection, WebSocket, addEventListener() {} },
+ WebSocket, RTCPeerConnection,
+ navigator: { mediaDevices: { getUserMedia: async () => { gumCalls++; return { getTracks: () => [] }; } } },
+ location: { protocol: 'https:', host: 'openvibe.chat', href: '' },
+ document, JSON, Number, String, Map, Promise,
+ fetch: async (url) => { fetches.push(url); return { ok: true, json: async () => ({}) }; },
+ setTimeout: (fn) => 0, clearTimeout: () => {}, setInterval: () => 0,
+ };
+ vm.runInNewContext(fs.readFileSync(path.join(__dirname, '..', 'public', 'web', 'call.js'), 'utf8'), ctx);
+ return { byId, sockets, fetches, gumCalls: () => gumCalls };
+}
+
+// Loading the call page asks for nothing — even with the legacy auto flag present,
+// the client must ignore it and wait for the press.
+for (const page of [{ view: 'call', call: { channel: 'user-1-abc', join: true, talk: true } }, { view: 'call', call: { channel: 'user-1-abc', join: true, talk: true, auto: true } }]) {
+ const s = runCallPage(page);
+ assert.strictEqual(s.gumCalls(), 0, 'page load must not call getUserMedia');
+ assert.ok(!s.sockets.some((x) => /\/ws\/call/.test(x.url)), 'page load must not open the call socket');
+ assert.strictEqual(s.byId['oc-call-join'].hidden, false, 'the join button is shown');
+ assert.ok(s.byId['oc-call-join'].handlers.click, 'the join button has a click handler');
+
+ // The press is what opens the microphone.
+ s.byId['oc-call-join'].handlers.click();
+ assert.strictEqual(s.gumCalls(), 1, 'pressing Turn on microphone calls getUserMedia once');
+ console.log('OK capture-on-load: no getUserMedia until the Turn on microphone click');
+}
+
+// Source checks: the server must not re-arm auto-join, and the button must be the press.
+{
+ const pages = fs.readFileSync(path.join(__dirname, '..', 'server', 'web', 'pages.js'), 'utf8');
+ assert.ok(!/auto:\s*true/.test(pages), 'GET /call/:channel must not set auto:true');
+ const callJs = fs.readFileSync(path.join(__dirname, '..', 'public', 'web', 'call.js'), 'utf8');
+ assert.ok(!/cfg\.call\.auto/.test(callJs), 'call.js must not auto-join on load');
+ assert.ok(pages.includes('>Turn on microphone</button>'), 'the call page button says Turn on microphone');
+ assert.ok(pages.includes('oc-call-join-note'), 'one line under the button says what it is for');
+ console.log('OK capture-on-load: no auto-join in server or client source');
+}
+
+console.log('✅ capture on load test passed');
@@ -105,7 +105,9 @@ t('Call rings the other person (a direct row) and opens the call page with the r
@@ -105,7 +105,9 @@ t('Call rings the other person (a direct row) and opens the call page with the r
assert.match(mine.text, /<script src="\/web\/call\.js\?v=[^"]+" defer><\/script>/);
assert.match(mine.text, /<script src="\/web\/call\.js\?v=[^"]+" defer><\/script>/);
assert.match(mine.text, new RegExp(`href="/messages/${conv}"`), 'back to the conversation');
assert.match(mine.text, new RegExp(`href="/messages/${conv}"`), 'back to the conversation');
assert.match(mine.text, /<meta name="robots" content="noindex, nofollow">/);
assert.match(mine.text, /<meta name="robots" content="noindex, nofollow">/);
- assert.deepStrictEqual(pageJson(mine.text).call, { channel: channelId, join: true, talk: true, auto: true, ring: { id: Number(bob.id), name: 'Bob' } });
+ assert.deepStrictEqual(pageJson(mine.text).call, { channel: channelId, join: true, talk: true, ring: { id: Number(bob.id), name: 'Bob' } });
+ assert.match(mine.text, /<button type="button" id="oc-call-join" hidden>Turn on microphone<\/button>/);
+ assert.match(mine.text, /oc-call-join-note/);
const his = await req('GET', `/call/${encodeURIComponent(channelId)}`, { headers: cookie(bob) });
const his = await req('GET', `/call/${encodeURIComponent(channelId)}`, { headers: cookie(bob) });
assert.strictEqual(his.status, 200);
assert.strictEqual(his.status, 200);
@@ -166,14 +168,16 @@ t('ring.js: a ring shows Answer and Decline; answering tells the caller and open
@@ -166,14 +168,16 @@ t('ring.js: a ring shows Answer and Decline; answering tells the caller and open
t('call.js on the caller\'s page: ringing until the answer; a decline, busy or no answer leaves the call', async () => {
t('call.js on the caller\'s page: ringing until the answer; a decline, busy or no answer leaves the call', async () => {
const signed = `ov_token=${encodeURIComponent(alice.token)}`;
const signed = `ov_token=${encodeURIComponent(alice.token)}`;
for (const [status, text] of [['declined', 'Bob declined the call.'], ['busy', 'Bob is in another call.'], ['no-answer', 'No answer from Bob.']]) {
for (const [status, text] of [['declined', 'Bob declined the call.'], ['busy', 'Bob is in another call.'], ['no-answer', 'No answer from Bob.']]) {
- const s = runScript('call.js', { view: 'call', call: { channel: 'user-1-abc', join: true, talk: true, auto: true, ring: { id: 2, name: 'Bob' } } }, { docCookie: signed });
+ const s = runScript('call.js', { view: 'call', call: { channel: 'user-1-abc', join: true, talk: true, ring: { id: 2, name: 'Bob' } } }, { docCookie: signed });
const chat = s.sockets.find((x) => /\/ws\/chat$/.test(x.url));
const chat = s.sockets.find((x) => /\/ws\/chat$/.test(x.url));
assert.ok(chat, 'the caller listens for the answer on /ws/chat');
assert.ok(chat, 'the caller listens for the answer on /ws/chat');
chat.onopen();
chat.onopen();
assert.deepStrictEqual(chat.sent[0], { type: 'join', token: alice.token });
assert.deepStrictEqual(chat.sent[0], { type: 'join', token: alice.token });
+ assert.ok(!s.sockets.some((x) => /\/ws\/call/.test(x.url)), 'the page open asks for nothing: no call socket yet');
+ s.byId['oc-call-join'].handlers.click();
await tick(); await tick();
await tick(); await tick();
const call = s.sockets.find((x) => /\/ws\/call\?channelId=user-1-abc$/.test(x.url));
const call = s.sockets.find((x) => /\/ws\/call\?channelId=user-1-abc$/.test(x.url));
- assert.ok(call, 'and joins its call as the page opens');
+ assert.ok(call, 'and joins its call on the Turn on microphone press');
call.onopen();
call.onopen();
call.onmessage({ data: JSON.stringify({ type: 'welcome', peerId: 'p1', participants: [{ peerId: 'p1', displayName: 'Alice' }] }) });
call.onmessage({ data: JSON.stringify({ type: 'welcome', peerId: 'p1', participants: [{ peerId: 'p1', displayName: 'Alice' }] }) });
assert.strictEqual(s.byId['oc-call-status'].textContent, 'Ringing Bob…');
assert.strictEqual(s.byId['oc-call-status'].textContent, 'Ringing Bob…');
@@ -184,8 +188,9 @@ t('call.js on the caller\'s page: ringing until the answer; a decline, busy or n
@@ -184,8 +188,9 @@ t('call.js on the caller\'s page: ringing until the answer; a decline, busy or n
assert.strictEqual(s.byId['oc-call-status'].textContent, text);
assert.strictEqual(s.byId['oc-call-status'].textContent, text);
assert.ok(chat.closed && call.closed, `${status}: the call is left and the ring socket closed`);
assert.ok(chat.closed && call.closed, `${status}: the call is left and the ring socket closed`);
}
}
- const s = runScript('call.js', { view: 'call', call: { channel: 'user-1-abc', join: true, talk: true, auto: true, ring: { id: 2, name: 'Bob' } } }, { docCookie: signed });
+ const s = runScript('call.js', { view: 'call', call: { channel: 'user-1-abc', join: true, talk: true, ring: { id: 2, name: 'Bob' } } }, { docCookie: signed });
const chat = s.sockets[0];
const chat = s.sockets[0];
+ s.byId['oc-call-join'].handlers.click();
await tick(); await tick();
await tick(); await tick();
const call = s.sockets.find((x) => /\/ws\/call/.test(x.url));
const call = s.sockets.find((x) => /\/ws\/call/.test(x.url));
call.onmessage({ data: JSON.stringify({ type: 'welcome', peerId: 'p1', participants: [{ peerId: 'p1' }] }) });
call.onmessage({ data: JSON.stringify({ type: 'welcome', peerId: 'p1', participants: [{ peerId: 'p1' }] }) });