chore: Fix app

This commit is contained in:
2026-07-14 00:41:05 +02:00
parent 3e090933fd
commit edc4d935d8
98 changed files with 2878 additions and 155 deletions
@@ -477,6 +477,56 @@ describe('DirectCallService', () => {
expect(context.voiceSession.endSession).toHaveBeenCalled();
});
it('surfaces a join error when the microphone permission is denied instead of failing silently', async () => {
const context = createServiceContext({ currentUser: alice, allUsers: [alice, bob] });
const session = createSession('connected', false);
session.participants.bob.joined = true;
(context.service as DirectCallService & { upsertSession: (nextSession: DirectCallSession) => void }).upsertSession(session);
context.mobileMedia.ensureVoiceCapturePermissions.mockResolvedValue(false);
await withStubbedGetUserMedia(vi.fn(async () => new FakeMediaStream()), async () => {
await context.service.joinCall(session.callId);
});
expect(context.service.joinError()).not.toBeNull();
expect(context.voice.setLocalStream).not.toHaveBeenCalled();
expect(context.service.sessionById(session.callId)?.participants.alice.joined).not.toBe(true);
});
it('surfaces a join error when getUserMedia rejects', async () => {
const context = createServiceContext({ currentUser: alice, allUsers: [alice, bob] });
const session = createSession('connected', false);
session.participants.bob.joined = true;
(context.service as DirectCallService & { upsertSession: (nextSession: DirectCallSession) => void }).upsertSession(session);
await withStubbedGetUserMedia(vi.fn(async () => {
throw new Error('NotReadableError');
}), async () => {
await context.service.joinCall(session.callId);
});
expect(context.service.joinError()).not.toBeNull();
expect(context.voice.setLocalStream).not.toHaveBeenCalled();
});
it('clears the join error on a successful join', async () => {
const context = createServiceContext({ currentUser: alice, allUsers: [alice, bob] });
const session = createSession('connected', false);
session.participants.bob.joined = true;
(context.service as DirectCallService & { upsertSession: (nextSession: DirectCallSession) => void }).upsertSession(session);
await withStubbedGetUserMedia(vi.fn(async () => new FakeMediaStream()), async () => {
await context.service.joinCall(session.callId);
});
expect(context.service.joinError()).toBeNull();
expect(context.voice.setLocalStream).toHaveBeenCalled();
expect(context.service.sessionById(session.callId)?.participants.alice.joined).toBe(true);
});
it('starts group calls by keeping the rail-visible call session and ringing every other participant', async () => {
const context = createServiceContext({ currentUser: alice, allUsers: [
alice,
@@ -643,6 +693,10 @@ function createServiceContext(options: ServiceContextOptions): ServiceContext {
const voiceSession = {
endSession: vi.fn()
};
const mobileMedia = {
ensureVoiceCapturePermissions: vi.fn(async () => true),
setSpeakerphoneEnabled: vi.fn(async () => undefined)
};
const credentialStore = {
listValidCredentials: vi.fn(() => (options.selfActorIds ?? []).map((userId) => ({
serverUrl: `https://signal.example/${userId}`,
@@ -732,10 +786,7 @@ function createServiceContext(options: ServiceContextOptions): ServiceContext {
},
{
provide: MobileMediaService,
useValue: {
ensureVoiceCapturePermissions: vi.fn(async () => true),
setSpeakerphoneEnabled: vi.fn(async () => undefined)
}
useValue: mobileMedia
},
{
provide: RealtimeSessionFacade,
@@ -761,6 +812,7 @@ function createServiceContext(options: ServiceContextOptions): ServiceContext {
directCallEvents,
directMessages,
effectScheduler,
mobileMedia,
router,
service: runInInjectionContext(injector, () => new DirectCallService()),
voice,
@@ -768,6 +820,42 @@ function createServiceContext(options: ServiceContextOptions): ServiceContext {
};
}
class FakeMediaStream {
getTracks(): unknown[] {
return [];
}
}
/** Temporarily provide `navigator.mediaDevices.getUserMedia` for the join path under Node. */
async function withStubbedGetUserMedia(
getUserMedia: () => Promise<unknown>,
run: () => Promise<void>
): Promise<void> {
const globalWithNavigator = globalThis as { navigator?: { mediaDevices?: unknown } };
const originalNavigator = globalWithNavigator.navigator;
Object.defineProperty(globalThis, 'navigator', {
configurable: true,
value: {
...(originalNavigator ?? {}),
mediaDevices: { getUserMedia }
}
});
try {
await run();
} finally {
if (originalNavigator === undefined) {
delete globalWithNavigator.navigator;
} else {
Object.defineProperty(globalThis, 'navigator', {
configurable: true,
value: originalNavigator
});
}
}
}
function createCallEvent(action: 'leave' | 'ring', sender: User, participantIds: string[]): ChatEvent {
return {
type: 'direct-call',