Files
cinny/src/app/utils/callOverlay.test.ts
T
jaredandClaude Opus 5.5 01254291e5
CI / Build & Quality Checks (push) Skipped
CI / Secret scan (gitleaks) (push) Skipped
CI / Playwright smoke (e2e) (push) Skipped
CI / Secret scan (gitleaks) (pull_request) Successful in 23s
CI / Build & Quality Checks (pull_request) Successful in 5m9s
CI / Trigger Desktop Build (pull_request) Skipped
CI / Docker image build & smoke test (pull_request) Skipped
CI / Playwright smoke (e2e) (pull_request) Successful in 23m21s
feat(calls): in-game voice overlay for the desktop app (cinny-desktop #25)
Settings → Calls → "In-game overlay (beta)", desktop only, off by
default, device-local. While a call is joined it sends the call
snapshot to the desktop's new `set_overlay_state` command, which shows
a small always-on-top, click-through list while Lotus isn't focused.

- utils/callOverlay: pure snapshot builder. Everyone in the call, you
  first, one row per person across devices (speaking if any device is,
  muted only if all are), your own mute/deafen/PTT. In PTT mode you're
  not shown as Muted while the key is up.
- useTauriOverlay: live io.lotus.call_state participants plus control
  state, coalesced to at most one send per 100 ms (latest always
  lands), unchanged snapshots skipped, cleared on unmount.
- Overlay position setting (four corners).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PPmy3tPq869XDW4njjVaKA
2026-11-09 17:25:36 -05:00

125 lines
3.8 KiB
TypeScript

import { test } from 'node:test';
import assert from 'node:assert/strict';
import { buildOverlaySnapshot, inactiveOverlay, OverlayInput } from './callOverlay';
const names: Record<string, string> = { '@me:x': 'Me', '@a:x': 'Alice', '@b:x': 'Bob' };
const base = (over: Partial<OverlayInput> = {}): OverlayInput => ({
enabled: true,
joined: true,
corner: 'top-left',
myUserId: '@me:x',
participants: [
{ userId: '@me:x', speaking: false, audioEnabled: true },
{ userId: '@a:x', speaking: true, audioEnabled: true },
{ userId: '@b:x', speaking: false, audioEnabled: false },
],
nameOf: (id) => names[id] ?? id,
muted: false,
deafened: false,
pttMode: false,
...over,
});
test('inactive when the setting is off or there is no joined call', () => {
assert.deepEqual(buildOverlaySnapshot(base({ enabled: false })), inactiveOverlay('top-left'));
assert.deepEqual(
buildOverlaySnapshot(base({ joined: false, corner: 'bottom-right' })),
inactiveOverlay('bottom-right'),
);
});
test('everyone is listed, you first, speakers and muted people marked', () => {
const s = buildOverlaySnapshot(base());
assert.equal(s.active, true);
assert.deepEqual(
s.people.map((p) => [p.name, p.speaking, p.muted, p.isSelf]),
[
['Me', false, false, true],
['Alice', true, false, false],
['Bob', false, true, false],
],
);
});
test('you are listed first even when the fork reports you later', () => {
const s = buildOverlaySnapshot(
base({
participants: [
{ userId: '@a:x', speaking: false, audioEnabled: true },
{ userId: '@me:x', speaking: true, audioEnabled: true },
],
}),
);
assert.deepEqual(
s.people.map((p) => p.userId),
['@me:x', '@a:x'],
);
assert.equal(s.people[0].speaking, true);
});
test('several devices of one person become one row', () => {
const s = buildOverlaySnapshot(
base({
participants: [
{ userId: '@a:x', speaking: false, audioEnabled: false },
{ userId: '@a:x', speaking: true, audioEnabled: true },
{ userId: '@b:x', speaking: false, audioEnabled: false },
{ userId: '@b:x', speaking: false, audioEnabled: false },
],
}),
);
const alice = s.people.find((p) => p.userId === '@a:x');
const bob = s.people.find((p) => p.userId === '@b:x');
assert.equal(s.people.filter((p) => p.userId === '@a:x').length, 1);
assert.deepEqual([alice?.speaking, alice?.muted], [true, false]);
assert.deepEqual([bob?.speaking, bob?.muted], [false, true]);
});
test('your own mute and deafen state', () => {
const muted = buildOverlaySnapshot(base({ muted: true }));
assert.equal(muted.muted, true);
assert.equal(muted.people[0].muted, true);
const deaf = buildOverlaySnapshot(
base({
deafened: true,
participants: [{ userId: '@me:x', speaking: true, audioEnabled: true }],
}),
);
assert.equal(deaf.deafened, true);
assert.equal(deaf.people[0].muted, true);
assert.equal(deaf.people[0].speaking, false);
});
test('push-to-talk: not shown as muted while the key is up', () => {
const s = buildOverlaySnapshot(base({ pttMode: true, muted: true }));
assert.equal(s.ptt, true);
assert.equal(s.muted, false);
assert.equal(s.people[0].muted, false);
});
test('you are still listed before the fork reports you', () => {
const s = buildOverlaySnapshot(base({ participants: [] }));
assert.deepEqual(
s.people.map((p) => [p.name, p.isSelf]),
[['Me', true]],
);
});
test('non-user ids from the fork are ignored', () => {
const s = buildOverlaySnapshot(
base({
myUserId: null,
participants: [
{ userId: 'screen:1', speaking: true, audioEnabled: true },
{ userId: '@a:x', speaking: false, audioEnabled: true },
],
}),
);
assert.deepEqual(
s.people.map((p) => p.userId),
['@a:x'],
);
});