mirror of
https://github.com/ancsemi/Haven.git
synced 2026-10-27 09:05:01 +00:00
173 lines
6.9 KiB
JavaScript
173 lines
6.9 KiB
JavaScript
'use strict';
|
|
|
|
// Push to mute: hold a key in Haven Desktop and the mic is muted until it
|
|
// comes up. The rules are a small pure module; the app mixin is run here
|
|
// against a stand-in voice manager to check what reaches the mic.
|
|
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const test = require('node:test');
|
|
const { pathToFileURL } = require('node:url');
|
|
|
|
const read = (p) => fs.readFileSync(path.join(__dirname, '..', p), 'utf8');
|
|
const load = async (p) => import(pathToFileURL(path.join(__dirname, '..', p)).href);
|
|
|
|
let createPushToMute;
|
|
let Methods;
|
|
test.before(async () => {
|
|
({ createPushToMute } = await load('public/js/modules/push-to-mute-state.js'));
|
|
Methods = (await load('public/js/modules/app-push-to-mute.js')).default;
|
|
});
|
|
|
|
test('hold and release puts a live mic back live', () => {
|
|
const ptm = createPushToMute();
|
|
assert.equal(ptm.press(false), true, 'the press mutes a live mic');
|
|
assert.equal(ptm.isHeld(), true);
|
|
assert.equal(ptm.release({ isMuted: true }), 'unmute');
|
|
assert.equal(ptm.isHeld(), false);
|
|
});
|
|
|
|
test('already muted stays muted', () => {
|
|
const ptm = createPushToMute();
|
|
assert.equal(ptm.press(true), false, 'nothing to mute');
|
|
assert.equal(ptm.release({ isMuted: true }), null, 'the release does not unmute');
|
|
});
|
|
|
|
test('a second press from another path while held changes nothing', () => {
|
|
const ptm = createPushToMute();
|
|
ptm.press(false);
|
|
assert.equal(ptm.press(true), false);
|
|
assert.equal(ptm.release({ isMuted: true }), 'unmute', 'the first press decides');
|
|
assert.equal(ptm.release({ isMuted: false }), null, 'a second release is ignored');
|
|
});
|
|
|
|
test('push to talk while push to mute is held: mute wins, and the release follows push to talk', () => {
|
|
// Haven in push to talk mode: muted, then the talk key goes down while
|
|
// push to mute is held.
|
|
const ptm = createPushToMute();
|
|
ptm.press(true);
|
|
assert.equal(ptm.toggle(), true, 'the talk press is taken while held');
|
|
assert.equal(ptm.userMuted(true), false, 'push to talk wants the mic open');
|
|
assert.equal(ptm.release({ isMuted: true }), 'unmute', 'still talking when push to mute comes up');
|
|
|
|
// Talk key released before push to mute.
|
|
const again = createPushToMute();
|
|
again.press(true);
|
|
again.toggle();
|
|
again.toggle();
|
|
assert.equal(again.release({ isMuted: true }), null, 'talk key already up: stays muted');
|
|
|
|
// Talking first, then push to mute.
|
|
const first = createPushToMute();
|
|
assert.equal(first.press(false), true, 'mute wins over a held talk key');
|
|
first.toggle(); // talk key comes up while held
|
|
assert.equal(first.release({ isMuted: true }), null);
|
|
});
|
|
|
|
test('a toggle with nothing held is left to the caller', () => {
|
|
const ptm = createPushToMute();
|
|
assert.equal(ptm.toggle(), false);
|
|
assert.equal(ptm.userMuted(true), true);
|
|
assert.equal(ptm.userMuted(false), false);
|
|
});
|
|
|
|
test('deafened while held: the mic stays muted with the deafen', () => {
|
|
const ptm = createPushToMute();
|
|
ptm.press(false);
|
|
assert.equal(ptm.release({ isMuted: true, isDeafened: true }), 'stay-deafened');
|
|
});
|
|
|
|
// ── The app mixin against a stand-in voice manager ──
|
|
|
|
function makeApp({ isMuted = false, inVoice = true } = {}) {
|
|
const emitted = [];
|
|
const sounds = [];
|
|
const track = { enabled: !isMuted };
|
|
const voice = {
|
|
isMuted, isDeafened: false, inVoice, currentChannel: inVoice ? 'abc' : null,
|
|
isListenerOnly: false, _voiceSessionGeneration: 1,
|
|
toggleMute() { this.isMuted = !this.isMuted; track.enabled = !this.isMuted; return this.isMuted; },
|
|
};
|
|
const app = Object.assign(Object.create(null), Methods, {
|
|
voice, track, emitted, sounds,
|
|
socket: { emit: (name, data) => emitted.push([name, data]) },
|
|
notifications: { playDirect: (tone) => sounds.push(tone) },
|
|
_syncMuteDeafenButtons() {},
|
|
_updateVoiceBar() {},
|
|
});
|
|
// The mute button, as app-voice.js has it (the deafen branch left out).
|
|
app._toggleMute = function () {
|
|
if (this._pushToMuteTakesToggle()) return;
|
|
const muted = this.voice.toggleMute();
|
|
this.socket.emit('voice-mute-state', { code: this.voice.currentChannel, muted });
|
|
};
|
|
return app;
|
|
}
|
|
|
|
test('the app silences the outgoing track while held and restores it after', () => {
|
|
const app = makeApp();
|
|
app._pushToMute(true);
|
|
assert.equal(app.track.enabled, false);
|
|
assert.equal(app._pushToMuteHeld(), true);
|
|
app._pushToMute(true); // the hook and the page both saw the press
|
|
app._pushToMute(false);
|
|
assert.equal(app.track.enabled, true);
|
|
assert.equal(app.voice.isMuted, false);
|
|
assert.deepEqual(app.emitted.map(e => e[1].muted), [true, false], 'one mute state each way, as push to talk sends');
|
|
assert.deepEqual(app.sounds, ['mute_on', 'mute_off']);
|
|
});
|
|
|
|
test('the app leaves a muted mic muted', () => {
|
|
const app = makeApp({ isMuted: true });
|
|
app._pushToMute(true);
|
|
app._pushToMute(false);
|
|
assert.equal(app.voice.isMuted, true);
|
|
assert.equal(app.track.enabled, false);
|
|
assert.equal(app.emitted.length, 0);
|
|
});
|
|
|
|
test('push to talk through the mute toggle while held never opens the mic', () => {
|
|
const app = makeApp({ isMuted: true });
|
|
app._pushToMute(true);
|
|
// What Haven Desktop does on a talk press: reads the user's own mute and
|
|
// toggles when it has to change.
|
|
assert.equal(app._micUserMuted(), true);
|
|
app._toggleMute();
|
|
assert.equal(app.voice.isMuted, true, 'mute wins');
|
|
assert.equal(app.track.enabled, false);
|
|
assert.equal(app._micUserMuted(), false);
|
|
app._toggleMute(); // talk key up
|
|
app._pushToMute(false);
|
|
assert.equal(app.voice.isMuted, true, 'back to push to talk at rest');
|
|
});
|
|
|
|
test('out of voice the key does nothing, and a session change drops a held key', () => {
|
|
const out = makeApp({ inVoice: false });
|
|
out._pushToMute(true);
|
|
out._pushToMute(false);
|
|
assert.equal(out.voice.isMuted, false);
|
|
assert.equal(out.emitted.length, 0);
|
|
|
|
const app = makeApp();
|
|
app._pushToMute(true);
|
|
app.voice._voiceSessionGeneration = 2; // left and joined again, key still down
|
|
app._pushToMute(false);
|
|
assert.equal(app.voice.isMuted, true, 'a release never unmutes a session it did not mute');
|
|
});
|
|
|
|
test('the app loads the mixin and the mute paths ask it first', () => {
|
|
const app = read('public/js/app.js');
|
|
assert.match(app, /import PushToMuteMethods from '\.\/modules\/app-push-to-mute\.js\?v=[0-9.]+';/);
|
|
assert.match(app, /^\s*PushToMuteMethods,$/m);
|
|
const voice = read('public/js/modules/app-voice.js');
|
|
assert.match(voice, /_toggleMute\(\) \{\n[^\n]*\n[^\n]*\n\s*if \(this\._pushToMuteTakesToggle\?\.\(\)\) return;/);
|
|
assert.match(voice, /btn\.classList\.toggle\('ptm-held', ptmHeld\)/);
|
|
const html = read('public/app.html');
|
|
assert.match(html, /id="shortcut-row-ptm"/);
|
|
assert.match(html, /data-action="ptm"/);
|
|
const en = JSON.parse(read('public/locales/en.json'));
|
|
for (const key of ['ptm', 'ptm_hint']) assert.ok(en.settings.desktop_shortcuts_section[key], key);
|
|
for (const key of ['push_to_mute_held', 'status_push_to_mute']) assert.ok(en.voice[key], key);
|
|
});
|