Files
cinny/src/app/plugins/soundboard/utils.test.ts
T
jared a9505ca5b2
CI / Build & Quality Checks (push) Successful in 10m56s
CI / Trigger Desktop Build (push) Successful in 8s
feat(soundboard): shared room/space packs (like emoji/stickers), grid picker, management
Soundboard v2 — a near-parallel of the custom-emoji image-pack system for
in-call audio clips.

- Data model: 3-tier packs mirroring MSC2545 — room/space pack (state event
  io.lotus.soundboard, inherited by child rooms via parent-space aggregation),
  global refs (io.lotus.soundboard_rooms), and the personal pack
  (io.lotus.soundboard account data; the v1 flat-list content is migrated to the
  pack shape on read). New plugins/soundboard/ (readers, SoundboardPack, utils) +
  hooks/useSoundboardPacks (useRelevantSoundboardPacks = user U global U room,
  deduped). Unit-tested (migration + slug).
- Management: reusable SoundboardPackEditor (name + emoji + per-clip volume +
  delete + upload + batched save), power-level-gated for room packs like emoji
  packs; a Soundboard page wired into Room + Space settings.
- In-call: CallSoundboard rewritten as a Discord-style grid grouped by pack
  (emoji + name tiles), sourcing room+parent-space U personal clips; a Manage
  toggle embeds the editors; per-clip volume x master volume on playback.
- Spam guard: host gates on a playing key (fork enforces one clip at a time).
- Control bar: Mute-Screenshare moved next to the Screenshare button.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-01 23:21:50 -04:00

69 lines
2.6 KiB
TypeScript

import { describe, test } from 'node:test';
import assert from 'node:assert/strict';
import { migrateUserSoundboardContent, slugifyClipName, uniqueShortcode } from './utils';
describe('slugifyClipName', () => {
test('lowercases, replaces spaces, strips punctuation', () => {
assert.equal(slugifyClipName(' Air Horn!! '), 'air_horn');
assert.equal(slugifyClipName('Ba-Dum Tss'), 'ba-dum_tss');
});
test('falls back to "clip" when empty', () => {
assert.equal(slugifyClipName(' '), 'clip');
assert.equal(slugifyClipName('!!!'), 'clip');
});
});
describe('uniqueShortcode', () => {
test('returns the slug when free', () => {
assert.equal(uniqueShortcode('Airhorn', new Set()), 'airhorn');
});
test('suffixes on collision', () => {
assert.equal(uniqueShortcode('Airhorn', new Set(['airhorn'])), 'airhorn-2');
assert.equal(uniqueShortcode('Airhorn', new Set(['airhorn', 'airhorn-2'])), 'airhorn-3');
});
});
describe('migrateUserSoundboardContent', () => {
test('migrates the v1 flat list into a v2 pack keyed by slug', () => {
const v1 = {
clips: [
{ id: 'a', name: 'Air Horn', url: 'mxc://x/1', mimetype: 'audio/mpeg', size: 100 },
{ id: 'b', name: 'Applause', url: 'mxc://x/2' },
],
};
const out = migrateUserSoundboardContent(v1);
assert.deepEqual(Object.keys(out.clips ?? {}).sort(), ['air_horn', 'applause']);
assert.equal(out.clips?.air_horn.url, 'mxc://x/1');
assert.equal(out.clips?.air_horn.body, 'Air Horn');
assert.equal(out.clips?.air_horn.info?.mimetype, 'audio/mpeg');
assert.ok(out.pack?.display_name);
});
test('dedupes colliding v1 names', () => {
const v1 = {
clips: [
{ id: 'a', name: 'Horn', url: 'mxc://x/1' },
{ id: 'b', name: 'Horn', url: 'mxc://x/2' },
],
};
const out = migrateUserSoundboardContent(v1);
assert.deepEqual(Object.keys(out.clips ?? {}).sort(), ['horn', 'horn-2']);
});
test('skips v1 entries without a url', () => {
const out = migrateUserSoundboardContent({ clips: [{ id: 'a', name: 'Bad' } as never] });
assert.deepEqual(out.clips, {});
});
test('passes a v2 pack through unchanged', () => {
const v2 = { pack: { display_name: 'P' }, clips: { horn: { url: 'mxc://x/1', volume: 50 } } };
assert.deepEqual(migrateUserSoundboardContent(v2), v2);
});
test('handles empty / non-object input', () => {
assert.deepEqual(migrateUserSoundboardContent({}), {});
assert.deepEqual(migrateUserSoundboardContent(null), {});
assert.deepEqual(migrateUserSoundboardContent(undefined), {});
});
});