Files
ChatApp/apps/desktop/src/lib/conversationFeatures.test.ts
T
byGalax 825160ee46 feat(desktop): port v0.11.4-v0.15.2 from Tauri to Electron + Discord-parity audio (v0.16.0)
Chronological port of every Tauri release commit (v0.11.4 -> v0.15.2)
into the Electron rebuild, plus Discord-style audio handling that goes
beyond the Tauri original.

Highlights:
  - All 17 Tauri release commits ported (audio fixes, custom notification
    sound, Discord-style chat UX, profile banner, changelog page,
    Discord-parity call UX, screen-share echo + re-watch UX, audio-loop
    fix).
  - Native napi-rs audio-loopback addon with WASAPI process-loopback:
      * EXCLUDE_TARGET_PROCESS_TREE for full-screen shares -> peers
        never hear themselves echoed back through the capture.
      * INCLUDE_TARGET_PROCESS_TREE for window shares -> only the
        picked window's audio is captured, not the whole OS mixer
        (Discord parity).
      * HWND -> PID resolution via Win32 GetWindowThreadProcessId.
  - Discord-style screen-source picker (thumbnail grid, screens vs
    apps tabs, live-refreshing thumbnails).
  - Hash routing fix for packaged builds (file:// can't resolve
    BrowserRouter paths).
  - Tauri sources removed (apps/desktop/src-tauri).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 23:35:01 +02:00

98 lines
3.1 KiB
TypeScript

import {
parseMessagePayload,
type AttachmentHandle,
type DecryptedMessage,
} from '@chat-app/shared/chat';
import { describe, expect, it } from 'vitest';
import {
collectConversationAttachments,
createPollPayload,
summarizePollVotes,
} from './conversationFeatures';
function handle(id: string, mimeType: string): AttachmentHandle {
return {
id,
storagePath: 'conversation/' + id + '.bin',
mimeType,
sizeBytes: 2048,
keyB64: 'key',
nonceB64: 'nonce',
};
}
function message(id: string, createdAt: string, attachments: AttachmentHandle[]): DecryptedMessage {
return {
id,
conversationId: 'conv-1',
senderId: 'sender-1',
senderDeviceId: 'device-1',
replyToId: null,
editedAt: null,
deletedAt: null,
createdAt,
plaintext: JSON.stringify({ v: 1, type: 'text', text: '', attachments }),
};
}
describe('collectConversationAttachments', () => {
it('indexes media, audio, and files newest first', () => {
const first = message('m1', '2026-04-24T09:00:00.000Z', [
handle('img-1', 'image/png'),
handle('file-1', 'application/pdf'),
]);
const second = message('m2', '2026-04-24T10:00:00.000Z', [
handle('audio-1', 'audio/webm'),
handle('video-1', 'video/mp4'),
]);
const index = collectConversationAttachments([first, second]);
expect(index.media.map((item) => item.handle.id)).toEqual(['video-1', 'img-1']);
expect(index.audio.map((item) => item.handle.id)).toEqual(['audio-1']);
expect(index.files.map((item) => item.handle.id)).toEqual(['file-1']);
expect(index.all.map((item) => item.messageId)).toEqual(['m2', 'm2', 'm1', 'm1']);
});
});
describe('createPollPayload', () => {
it('creates a parseable encrypted-message poll payload', () => {
const payload = createPollPayload(' Lieblings Feature? ', [' Media Drawer ', '', 'Polls']);
const parsed = parseMessagePayload(payload);
expect(parsed.kind).toBe('poll');
if (parsed.kind !== 'poll') throw new Error('expected poll payload');
expect(parsed.question).toBe('Lieblings Feature?');
expect(parsed.options).toEqual([
{ id: 'option-1', emoji: '1️⃣', text: 'Media Drawer' },
{ id: 'option-2', emoji: '2️⃣', text: 'Polls' },
]);
});
it('requires a question and at least two non-empty options', () => {
expect(() => createPollPayload('', ['A', 'B'])).toThrow('question');
expect(() => createPollPayload('Feature?', ['A', ''])).toThrow('options');
});
});
describe('summarizePollVotes', () => {
it('counts only configured poll options and marks the current user vote', () => {
const summary = summarizePollVotes(
[
{ id: 'option-1', emoji: '1️⃣', text: 'Media Drawer' },
{ id: 'option-2', emoji: '2️⃣', text: 'Polls' },
],
[
{ emoji: '1️⃣', count: 3, mine: false },
{ emoji: '2️⃣', count: 1, mine: true },
{ emoji: '🔥', count: 99, mine: true },
],
);
expect(summary.totalVotes).toBe(4);
expect(summary.options.map((option) => option.percent)).toEqual([75, 25]);
expect(summary.options.map((option) => option.mine)).toEqual([false, true]);
});
});