forked from eatvio/eatvio_app
feat(video_calls): android repeated call after hanging up fix and ios repeated calls after call initiation fix
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import 'package:eatvio_app/features/video_calls/managers/video_call_manager.dart';
|
||||
import 'package:eatvio_app/features/video_calls/models/video_call_models.dart';
|
||||
import 'package:eatvio_app/features/video_calls/services/video_call_foreground_coordinator.dart';
|
||||
import 'package:eatvio_app/features/video_calls/services/video_call_lifecycle_binding.dart';
|
||||
import 'package:eatvio_app/features/video_calls/repositories/video_call_repository.dart';
|
||||
@@ -82,6 +83,47 @@ void main() {
|
||||
expect(foreground.startCount, 0);
|
||||
});
|
||||
|
||||
test(
|
||||
'decline right after hang up does not notify the backend again',
|
||||
() async {
|
||||
manager.markActiveCallForTesting(mockRoom);
|
||||
|
||||
await manager.endCall();
|
||||
clearInteractions(mockRepository);
|
||||
|
||||
await manager.declineIncomingCall('test-uuid');
|
||||
|
||||
verifyNever(mockRepository.notifyCallEnded(any));
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'startCall right after hang up does not request a new token',
|
||||
() async {
|
||||
manager.markActiveCallForTesting(mockRoom);
|
||||
await manager.endCall();
|
||||
clearInteractions(mockRepository);
|
||||
|
||||
final started = await manager.startCall(conversationUuid: 'test-uuid');
|
||||
|
||||
expect(started, isFalse);
|
||||
verifyNever(mockRepository.getParticipantToken(any));
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'decline of a call that was never joined notifies the backend',
|
||||
() async {
|
||||
when(
|
||||
mockRepository.notifyCallEnded(any),
|
||||
).thenAnswer((_) async => const VideoCallEndedResponse(sent: true));
|
||||
|
||||
await manager.declineIncomingCall('other-uuid');
|
||||
|
||||
verify(mockRepository.notifyCallEnded('other-uuid')).called(1);
|
||||
},
|
||||
);
|
||||
|
||||
test('endCall stops foreground service', () async {
|
||||
manager.markActiveCallForTesting(mockRoom);
|
||||
|
||||
|
||||
@@ -199,7 +199,7 @@ void main() {
|
||||
micEnabled: anyNamed('micEnabled'),
|
||||
videoEnabled: anyNamed('videoEnabled'),
|
||||
),
|
||||
).thenAnswer((_) async {});
|
||||
).thenAnswer((_) async => true);
|
||||
|
||||
await cubit.onCallScreenVisible(
|
||||
conversationUuid: 'uuid',
|
||||
@@ -231,6 +231,7 @@ void main() {
|
||||
linkIssue: VideoCallLinkIssue.none,
|
||||
),
|
||||
);
|
||||
return true;
|
||||
});
|
||||
when(mockManager.onEnterCallStage()).thenAnswer((_) async => true);
|
||||
|
||||
@@ -262,7 +263,7 @@ void main() {
|
||||
micEnabled: anyNamed('micEnabled'),
|
||||
videoEnabled: anyNamed('videoEnabled'),
|
||||
),
|
||||
).thenAnswer((_) async {});
|
||||
).thenAnswer((_) async => true);
|
||||
when(mockManager.ensureLobbyRoomJoined()).thenAnswer((_) async {});
|
||||
when(mockManager.onEnterCallStage()).thenAnswer((_) async => true);
|
||||
|
||||
@@ -297,7 +298,7 @@ void main() {
|
||||
micEnabled: anyNamed('micEnabled'),
|
||||
videoEnabled: anyNamed('videoEnabled'),
|
||||
),
|
||||
).thenAnswer((_) async {});
|
||||
).thenAnswer((_) async => true);
|
||||
when(mockManager.ensureLobbyRoomJoined()).thenAnswer((_) async {});
|
||||
when(mockManager.onEnterCallStage()).thenAnswer((_) async => true);
|
||||
|
||||
@@ -328,7 +329,7 @@ void main() {
|
||||
micEnabled: anyNamed('micEnabled'),
|
||||
videoEnabled: anyNamed('videoEnabled'),
|
||||
),
|
||||
).thenAnswer((_) async {});
|
||||
).thenAnswer((_) async => true);
|
||||
|
||||
await cubit.onCallScreenVisible(
|
||||
conversationUuid: 'uuid',
|
||||
@@ -362,7 +363,7 @@ void main() {
|
||||
micEnabled: anyNamed('micEnabled'),
|
||||
videoEnabled: anyNamed('videoEnabled'),
|
||||
),
|
||||
).thenAnswer((_) async {});
|
||||
).thenAnswer((_) async => true);
|
||||
|
||||
cubit.emit(
|
||||
const VideoCallSessionState(
|
||||
@@ -390,7 +391,7 @@ void main() {
|
||||
micEnabled: anyNamed('micEnabled'),
|
||||
videoEnabled: anyNamed('videoEnabled'),
|
||||
),
|
||||
).thenAnswer((_) async {});
|
||||
).thenAnswer((_) async => true);
|
||||
|
||||
await cubit.onCallScreenVisible(conversationUuid: 'uuid');
|
||||
expect(cubit.isCallScreenRouteVisible, isTrue);
|
||||
|
||||
@@ -74,7 +74,7 @@ void main() {
|
||||
expect(shown.single.ios?.configureAudioSession, isFalse);
|
||||
expect(shown.single.ios?.audioSessionActive, isFalse);
|
||||
expect(shown.single.ios?.audioSessionMode, 'videoChat');
|
||||
expect(shown.single.ios?.ringtonePath, 'system_ringtone_default');
|
||||
expect(shown.single.ios?.ringtonePath, isNull);
|
||||
});
|
||||
|
||||
test('iOS stops polling when the push claims the call', () async {
|
||||
@@ -122,6 +122,25 @@ void main() {
|
||||
expect(shown, isEmpty);
|
||||
});
|
||||
|
||||
test('Android does not open a second CallKit UI', () async {
|
||||
final shown = <CallKitParams>[];
|
||||
final native = <VideoCallPushPayload>[];
|
||||
|
||||
await VideoCallIncomingPushPresenter.showIncomingCall(
|
||||
payload,
|
||||
isIos: false,
|
||||
showAndroidIncoming: (incoming) async {
|
||||
native.add(incoming);
|
||||
},
|
||||
showCallkitIncoming: (params) async {
|
||||
shown.add(params);
|
||||
},
|
||||
);
|
||||
|
||||
expect(shown, isEmpty);
|
||||
expect(native, [payload]);
|
||||
});
|
||||
|
||||
test(
|
||||
'Android shows the incoming call without waiting for a VoIP claim',
|
||||
() async {
|
||||
|
||||
@@ -96,6 +96,71 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
test('a decline just after leaving does not report the end again', () {
|
||||
final leftAt = DateTime(2026, 10, 16, 12);
|
||||
expect(
|
||||
incomingDeclineShouldNotifyBackend(
|
||||
leftAt: leftAt,
|
||||
now: leftAt.add(const Duration(seconds: 5)),
|
||||
),
|
||||
isFalse,
|
||||
);
|
||||
expect(
|
||||
incomingDeclineShouldNotifyBackend(
|
||||
leftAt: leftAt,
|
||||
now: leftAt.add(incomingDeclineNotifySuppression),
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
expect(
|
||||
incomingDeclineShouldNotifyBackend(leftAt: null, now: leftAt),
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
|
||||
test('a token is not requested again right after leaving', () {
|
||||
final leftAt = DateTime(2026, 10, 16, 12);
|
||||
expect(
|
||||
mayRequestParticipantToken(
|
||||
sessionPermitted: false,
|
||||
leftAt: leftAt,
|
||||
now: leftAt.add(const Duration(seconds: 2)),
|
||||
),
|
||||
isFalse,
|
||||
);
|
||||
expect(
|
||||
mayRequestParticipantToken(
|
||||
sessionPermitted: true,
|
||||
leftAt: leftAt,
|
||||
now: leftAt.add(const Duration(seconds: 2)),
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
expect(
|
||||
mayRequestParticipantToken(
|
||||
sessionPermitted: false,
|
||||
leftAt: null,
|
||||
now: leftAt,
|
||||
),
|
||||
isTrue,
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
'an ended VoIP push is marked so its CallKit close is not a decline',
|
||||
() {
|
||||
expect(
|
||||
callKitExtraMarksEndedPush(<String, dynamic>{'endedPush': true}),
|
||||
isTrue,
|
||||
);
|
||||
expect(
|
||||
callKitExtraMarksEndedPush(<String, dynamic>{'endedPush': 'true'}),
|
||||
isTrue,
|
||||
);
|
||||
expect(callKitExtraMarksEndedPush(<String, dynamic>{}), isFalse);
|
||||
},
|
||||
);
|
||||
|
||||
test('keeps a connection-loss screen when the link drops', () {
|
||||
expect(
|
||||
shouldForceEndCallAfterRoomDisconnect(
|
||||
|
||||
Reference in New Issue
Block a user