chore: Fix app
This commit is contained in:
+92
-4
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user