forked from eatvio/eatvio_app
Merge remote-tracking branch 'upstream/main' into personalized_feed
This commit is contained in:
@@ -1,9 +1,14 @@
|
||||
import 'package:eatvio_app/core/app_config.dart';
|
||||
import 'package:eatvio_app/core/app_initializer.dart';
|
||||
import 'package:eatvio_app/core/env_config.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
import 'package:eatvio_app/features/video_calls/presentation/bloc/video_call_session/video_call_session_cubit.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
|
||||
class _MockVideoCallSessionCubit extends Mock implements VideoCallSessionCubit {}
|
||||
|
||||
void main() {
|
||||
setUpAll(() async {
|
||||
@@ -11,6 +16,17 @@ void main() {
|
||||
dotenv.loadFromString(envString: 'DEV_BASE_URL=http://localhost\nBASE_URL=http://localhost');
|
||||
});
|
||||
|
||||
setUp(() async {
|
||||
await sl.reset();
|
||||
sl.registerLazySingleton<VideoCallSessionCubit>(
|
||||
_MockVideoCallSessionCubit.new,
|
||||
);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await sl.reset();
|
||||
});
|
||||
|
||||
group('AppConfig', () {
|
||||
test('creates config with prod build type', () {
|
||||
final config = AppConfig(buildType: AppBuildType.prod);
|
||||
|
||||
@@ -33,6 +33,7 @@ void main() {
|
||||
expect(newMessagesChannelName, 'private-new-chat-messages');
|
||||
expect(newNotificationsChannelName, 'private-message-events');
|
||||
expect(directChatChannelName, 'private-direct-chat');
|
||||
expect(presenceDirectChatChannelName, 'presence-direct-chat');
|
||||
});
|
||||
|
||||
test('thresholdForPagination has correct value', () {
|
||||
|
||||
@@ -32,6 +32,7 @@ void main() {
|
||||
DioExceptionType.connectionTimeout,
|
||||
DioExceptionType.sendTimeout,
|
||||
DioExceptionType.receiveTimeout,
|
||||
DioExceptionType.transformTimeout,
|
||||
]) {
|
||||
final e = DioException(
|
||||
requestOptions: RequestOptions(path: '/x'),
|
||||
|
||||
@@ -54,7 +54,7 @@ void main() {
|
||||
isRead: false,
|
||||
isCurrentUserAuthor: true,
|
||||
roomId: 'room-1',
|
||||
type: 'text',
|
||||
type: ChatMessageType.text,
|
||||
);
|
||||
|
||||
final json = model.toJson();
|
||||
|
||||
@@ -48,7 +48,7 @@ void main() {
|
||||
isRead: false,
|
||||
isCurrentUserAuthor: true,
|
||||
roomId: 'room-1',
|
||||
type: 'text',
|
||||
type: ChatMessageType.text,
|
||||
);
|
||||
|
||||
const meta = ChatMetaModel(lastPage: 1);
|
||||
|
||||
@@ -28,12 +28,14 @@ void main() {
|
||||
expect(model.profilePhotoUrl, isNull);
|
||||
});
|
||||
|
||||
test('fromJson создает ChatUserModel из JSON', () {
|
||||
test('fromJson creates ChatUserModel from JSON', () {
|
||||
final json = {
|
||||
'id': 1,
|
||||
'name': 'User Name',
|
||||
'slug': 'user-slug',
|
||||
'profile_photo_url': 'https://example.com/photo.jpg',
|
||||
'is_online': true,
|
||||
'last_seen_text': 'был(а) недавно',
|
||||
};
|
||||
|
||||
final model = ChatUserModel.fromJson(json);
|
||||
@@ -42,6 +44,8 @@ void main() {
|
||||
expect(model.name, 'User Name');
|
||||
expect(model.slug, 'user-slug');
|
||||
expect(model.profilePhotoUrl, 'https://example.com/photo.jpg');
|
||||
expect(model.isOnline, isTrue);
|
||||
expect(model.lastSeenText, 'был(а) недавно');
|
||||
});
|
||||
|
||||
test('fromJson создает ChatUserModel из JSON без фото', () {
|
||||
|
||||
@@ -336,7 +336,7 @@ void main() {
|
||||
isRead: false,
|
||||
isCurrentUserAuthor: true,
|
||||
roomId: chatId,
|
||||
type: 'text',
|
||||
type: ChatMessageType.text,
|
||||
);
|
||||
|
||||
// Используем рефлексию для вызова приватного метода через тестовый доступ
|
||||
@@ -390,7 +390,7 @@ void main() {
|
||||
isRead: false,
|
||||
isCurrentUserAuthor: true,
|
||||
roomId: chatId,
|
||||
type: 'text',
|
||||
type: ChatMessageType.text,
|
||||
);
|
||||
|
||||
final streamController = StreamController<ChatMessageModel>();
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:eatvio_app/core/app_config.dart';
|
||||
import 'package:eatvio_app/core/app_initializer.dart';
|
||||
import 'package:eatvio_app/core/locator_service.dart';
|
||||
import 'package:eatvio_app/core/theme/theme_helper.dart';
|
||||
import 'package:eatvio_app/core/websockets/ws_manager.dart';
|
||||
import 'package:eatvio_app/features/chat/models/chat_message_model.dart';
|
||||
import 'package:eatvio_app/features/chat/models/chat_room_model.dart';
|
||||
import 'package:eatvio_app/features/chat/models/chat_room_settings_model.dart';
|
||||
@@ -37,6 +38,7 @@ void main() {
|
||||
setUp(() async {
|
||||
await sl.reset();
|
||||
sl.registerLazySingleton<AppConfig>(() => AppConfig(buildType: AppBuildType.tests)..setBaseUrl('http://localhost'));
|
||||
sl.registerLazySingleton<WsManager>(() => _FakeWsManager());
|
||||
});
|
||||
|
||||
const fakeSvg = '''
|
||||
@@ -175,6 +177,11 @@ void main() {
|
||||
});
|
||||
}
|
||||
|
||||
class _FakeWsManager extends Fake implements WsManager {
|
||||
@override
|
||||
int? userId = 1;
|
||||
}
|
||||
|
||||
class _FakeAssetBundle extends CachingAssetBundle {
|
||||
_FakeAssetBundle(this.assets);
|
||||
final Map<String, String> assets;
|
||||
|
||||
@@ -112,4 +112,24 @@ void main() {
|
||||
expect(restored, model);
|
||||
});
|
||||
});
|
||||
|
||||
group('VideoCallAcceptResponse', () {
|
||||
test('fromJson десериализует sent и devices_notified', () {
|
||||
final json = FixtureReader.readAsMap('video_calls/video_call_accept_response.json');
|
||||
|
||||
final model = VideoCallAcceptResponse.fromJson(json);
|
||||
|
||||
expect(model.sent, isTrue);
|
||||
expect(model.devicesNotified, 2);
|
||||
});
|
||||
|
||||
test('round-trip JSON сохраняет значения', () {
|
||||
final json = FixtureReader.readAsMap('video_calls/video_call_accept_response.json');
|
||||
|
||||
final model = VideoCallAcceptResponse.fromJson(json);
|
||||
final restored = VideoCallAcceptResponse.fromJson(model.toJson());
|
||||
|
||||
expect(restored, model);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import 'package:eatvio_app/features/video_calls/managers/video_call_manager.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';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:livekit_client/livekit_client.dart' as lk;
|
||||
import 'package:mockito/annotations.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
|
||||
import 'video_call_manager_lifecycle_test.mocks.dart';
|
||||
|
||||
class _RecordingForeground extends VideoCallForegroundCoordinator {
|
||||
int startCount = 0;
|
||||
int stopCount = 0;
|
||||
|
||||
@override
|
||||
Future<void> startOngoingCall({
|
||||
required String conversationUuid,
|
||||
required String notificationTitle,
|
||||
required String notificationBody,
|
||||
required String endCallButtonLabel,
|
||||
}) async {
|
||||
startCount++;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> stopOngoingCall() async {
|
||||
stopCount++;
|
||||
}
|
||||
}
|
||||
|
||||
@GenerateNiceMocks([MockSpec<VideoCallRepository>(), MockSpec<lk.Room>()])
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('VideoCallManager lifecycle', () {
|
||||
late MockVideoCallRepository mockRepository;
|
||||
late _RecordingForeground foreground;
|
||||
late VideoCallLifecycleBinding lifecycleBinding;
|
||||
late VideoCallManager manager;
|
||||
late MockRoom mockRoom;
|
||||
|
||||
setUp(() {
|
||||
mockRepository = MockVideoCallRepository();
|
||||
foreground = _RecordingForeground();
|
||||
lifecycleBinding = VideoCallLifecycleBinding();
|
||||
manager = VideoCallManager(
|
||||
videoCallRepository: mockRepository,
|
||||
foregroundCoordinator: foreground,
|
||||
lifecycleBinding: lifecycleBinding,
|
||||
);
|
||||
mockRoom = MockRoom();
|
||||
when(mockRoom.connectionState).thenReturn(lk.ConnectionState.connected);
|
||||
when(mockRoom.localParticipant).thenReturn(null);
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
manager.dispose();
|
||||
});
|
||||
|
||||
test('paused starts foreground and does not disconnect room', () async {
|
||||
manager.markActiveCallForTesting(mockRoom);
|
||||
|
||||
lifecycleBinding.simulateLifecycle(AppLifecycleState.paused);
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
|
||||
expect(foreground.startCount, 1);
|
||||
expect(manager.liveKitDisposedForTesting, isFalse);
|
||||
verifyNever(mockRepository.notifyCallEnded(any));
|
||||
});
|
||||
|
||||
test('paused without active call does not start foreground', () async {
|
||||
lifecycleBinding.simulateLifecycle(AppLifecycleState.paused);
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
|
||||
expect(foreground.startCount, 0);
|
||||
});
|
||||
|
||||
test('endCall stops foreground service', () async {
|
||||
manager.markActiveCallForTesting(mockRoom);
|
||||
|
||||
await manager.endCall();
|
||||
|
||||
expect(foreground.stopCount, 1);
|
||||
expect(manager.liveKitDisposedForTesting, isTrue);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,390 @@
|
||||
import 'package:eatvio_app/features/video_calls/managers/video_call_manager.dart';
|
||||
import 'package:eatvio_app/features/video_calls/managers/video_call_manager_snapshot.dart';
|
||||
import 'package:eatvio_app/features/video_calls/presentation/bloc/video_call_session/video_call_session_cubit.dart';
|
||||
import 'package:eatvio_app/features/video_calls/presentation/bloc/video_call_session/video_call_session_state.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:livekit_client/livekit_client.dart' as lk;
|
||||
import 'package:mockito/annotations.dart';
|
||||
import 'package:mockito/mockito.dart';
|
||||
|
||||
import 'video_call_session_cubit_test.mocks.dart';
|
||||
|
||||
@GenerateNiceMocks([MockSpec<VideoCallManager>(), MockSpec<lk.Room>()])
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('VideoCallSessionCubit', () {
|
||||
late MockVideoCallManager mockManager;
|
||||
late _TestVideoCallSessionCubit cubit;
|
||||
|
||||
late VideoCallManagerListener managerListener;
|
||||
|
||||
setUp(() {
|
||||
mockManager = MockVideoCallManager();
|
||||
when(mockManager.snapshot).thenReturn(VideoCallManagerSnapshot.initial);
|
||||
when(mockManager.room).thenReturn(null);
|
||||
when(mockManager.addListener(any)).thenAnswer((invocation) {
|
||||
managerListener =
|
||||
invocation.positionalArguments[0] as VideoCallManagerListener;
|
||||
});
|
||||
when(
|
||||
mockManager.notifyIncomingCallAccepted(any),
|
||||
).thenAnswer((_) async {});
|
||||
cubit = _TestVideoCallSessionCubit(videoCallManager: mockManager);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
when(mockManager.endCall()).thenAnswer((_) async {});
|
||||
await cubit.close();
|
||||
});
|
||||
|
||||
test('onCallScreenDisposed minimizes to pip when in call stage', () async {
|
||||
cubit.emit(
|
||||
const VideoCallSessionState(
|
||||
conversationUuid: 'uuid',
|
||||
connectPhase: VideoCallConnectPhase.connected,
|
||||
callStageEntered: true,
|
||||
uiMode: VideoCallUiMode.fullscreen,
|
||||
),
|
||||
);
|
||||
|
||||
cubit.onCallScreenDisposed();
|
||||
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
|
||||
expect(cubit.state.uiMode, VideoCallUiMode.pip);
|
||||
verifyNever(mockManager.endCall());
|
||||
});
|
||||
|
||||
test('onCallScreenDisposed ends call when not in call stage', () async {
|
||||
cubit.emit(
|
||||
const VideoCallSessionState(
|
||||
conversationUuid: 'uuid',
|
||||
connectPhase: VideoCallConnectPhase.connected,
|
||||
callStageEntered: false,
|
||||
uiMode: VideoCallUiMode.fullscreen,
|
||||
),
|
||||
);
|
||||
|
||||
when(mockManager.endCall()).thenAnswer((_) async {});
|
||||
|
||||
cubit.onCallScreenDisposed();
|
||||
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
|
||||
verify(mockManager.endCall()).called(1);
|
||||
});
|
||||
|
||||
test(
|
||||
'manager idle snapshot resets callStageEntered and uiMode',
|
||||
() async {
|
||||
cubit.emit(
|
||||
const VideoCallSessionState(
|
||||
conversationUuid: 'uuid',
|
||||
connectPhase: VideoCallConnectPhase.connected,
|
||||
callStageEntered: true,
|
||||
uiMode: VideoCallUiMode.pip,
|
||||
),
|
||||
);
|
||||
|
||||
managerListener(VideoCallManagerSnapshot.initial);
|
||||
|
||||
expect(cubit.state.callStageEntered, isFalse);
|
||||
expect(cubit.state.uiMode, VideoCallUiMode.hidden);
|
||||
expect(cubit.state.connectPhase, VideoCallConnectPhase.idle);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'manager connecting snapshot resets callStageEntered for new call',
|
||||
() async {
|
||||
cubit.emit(
|
||||
const VideoCallSessionState(
|
||||
conversationUuid: 'old',
|
||||
connectPhase: VideoCallConnectPhase.connected,
|
||||
callStageEntered: true,
|
||||
uiMode: VideoCallUiMode.fullscreen,
|
||||
),
|
||||
);
|
||||
|
||||
managerListener(
|
||||
const VideoCallManagerSnapshot(
|
||||
conversationUuid: 'new-uuid',
|
||||
micEnabled: true,
|
||||
videoEnabled: true,
|
||||
connectPhase: VideoCallConnectPhase.connecting,
|
||||
linkIssue: VideoCallLinkIssue.none,
|
||||
),
|
||||
);
|
||||
|
||||
expect(cubit.state.conversationUuid, 'new-uuid');
|
||||
expect(cubit.state.callStageEntered, isFalse);
|
||||
expect(cubit.state.uiMode, VideoCallUiMode.fullscreen);
|
||||
expect(cubit.state.connectPhase, VideoCallConnectPhase.connecting);
|
||||
},
|
||||
);
|
||||
|
||||
test('isMicActiveForUi mirrors micEnabled', () {
|
||||
const state = VideoCallSessionState(micEnabled: true);
|
||||
expect(state.isMicActiveForUi, isTrue);
|
||||
});
|
||||
|
||||
test('onCallEndedExternally resets session state', () async {
|
||||
cubit.emit(
|
||||
const VideoCallSessionState(
|
||||
conversationUuid: 'uuid',
|
||||
connectPhase: VideoCallConnectPhase.connected,
|
||||
callStageEntered: true,
|
||||
uiMode: VideoCallUiMode.pip,
|
||||
),
|
||||
);
|
||||
|
||||
await cubit.onCallEndedExternally();
|
||||
|
||||
expect(cubit.state, const VideoCallSessionState());
|
||||
});
|
||||
|
||||
test('syncPipUiStateFromSystem skips overlay when call screen is visible', () async {
|
||||
cubit.emit(
|
||||
const VideoCallSessionState(
|
||||
conversationUuid: 'uuid',
|
||||
connectPhase: VideoCallConnectPhase.connected,
|
||||
callStageEntered: true,
|
||||
uiMode: VideoCallUiMode.fullscreen,
|
||||
),
|
||||
);
|
||||
|
||||
await cubit.onCallScreenVisible(conversationUuid: 'uuid');
|
||||
await cubit.syncPipUiStateFromSystem();
|
||||
|
||||
expect(cubit.state.uiMode, VideoCallUiMode.fullscreen);
|
||||
});
|
||||
|
||||
test('manager idle snapshot ignored while call screen is visible', () async {
|
||||
when(
|
||||
mockManager.startCall(
|
||||
conversationUuid: anyNamed('conversationUuid'),
|
||||
counterpartName: anyNamed('counterpartName'),
|
||||
localParticipantName: anyNamed('localParticipantName'),
|
||||
micEnabled: anyNamed('micEnabled'),
|
||||
videoEnabled: anyNamed('videoEnabled'),
|
||||
),
|
||||
).thenAnswer((_) async {});
|
||||
|
||||
await cubit.onCallScreenVisible(
|
||||
conversationUuid: 'uuid',
|
||||
entryMode: VideoCallEntryMode.incomingAccepted,
|
||||
);
|
||||
|
||||
managerListener(VideoCallManagerSnapshot.initial);
|
||||
|
||||
expect(cubit.state.entryMode, VideoCallEntryMode.incomingAccepted);
|
||||
expect(cubit.state.conversationUuid, 'uuid');
|
||||
});
|
||||
|
||||
test('incoming accept auto-enters call stage after connect', () async {
|
||||
when(
|
||||
mockManager.startCall(
|
||||
conversationUuid: anyNamed('conversationUuid'),
|
||||
counterpartName: anyNamed('counterpartName'),
|
||||
localParticipantName: anyNamed('localParticipantName'),
|
||||
micEnabled: anyNamed('micEnabled'),
|
||||
videoEnabled: anyNamed('videoEnabled'),
|
||||
),
|
||||
).thenAnswer((_) async {
|
||||
managerListener(
|
||||
const VideoCallManagerSnapshot(
|
||||
conversationUuid: 'uuid',
|
||||
micEnabled: false,
|
||||
videoEnabled: false,
|
||||
connectPhase: VideoCallConnectPhase.connected,
|
||||
linkIssue: VideoCallLinkIssue.none,
|
||||
),
|
||||
);
|
||||
});
|
||||
when(mockManager.onEnterCallStage()).thenAnswer((_) async => true);
|
||||
|
||||
await cubit.onCallScreenVisible(
|
||||
conversationUuid: 'uuid',
|
||||
entryMode: VideoCallEntryMode.incomingAccepted,
|
||||
);
|
||||
|
||||
verify(
|
||||
mockManager.startCall(
|
||||
conversationUuid: 'uuid',
|
||||
counterpartName: anyNamed('counterpartName'),
|
||||
localParticipantName: anyNamed('localParticipantName'),
|
||||
micEnabled: false,
|
||||
videoEnabled: false,
|
||||
),
|
||||
).called(1);
|
||||
verify(mockManager.onEnterCallStage()).called(1);
|
||||
expect(cubit.state.entryMode, VideoCallEntryMode.incomingAccepted);
|
||||
expect(cubit.state.callStageEntered, isTrue);
|
||||
});
|
||||
|
||||
test('outgoing call stays on connecting lobby until callee joins', () async {
|
||||
when(
|
||||
mockManager.startCall(
|
||||
conversationUuid: anyNamed('conversationUuid'),
|
||||
counterpartName: anyNamed('counterpartName'),
|
||||
localParticipantName: anyNamed('localParticipantName'),
|
||||
micEnabled: anyNamed('micEnabled'),
|
||||
videoEnabled: anyNamed('videoEnabled'),
|
||||
),
|
||||
).thenAnswer((_) async {});
|
||||
when(mockManager.ensureLobbyRoomJoined()).thenAnswer((_) async {});
|
||||
when(mockManager.onEnterCallStage()).thenAnswer((_) async => true);
|
||||
|
||||
await cubit.onCallScreenVisible(conversationUuid: 'uuid');
|
||||
|
||||
verifyNever(mockManager.onEnterCallStage());
|
||||
expect(cubit.state.entryMode, VideoCallEntryMode.outgoing);
|
||||
expect(cubit.state.callStageEntered, isFalse);
|
||||
expect(cubit.state.micEnabled, isFalse);
|
||||
|
||||
managerListener(
|
||||
const VideoCallManagerSnapshot(
|
||||
conversationUuid: 'uuid',
|
||||
micEnabled: false,
|
||||
videoEnabled: false,
|
||||
connectPhase: VideoCallConnectPhase.connected,
|
||||
linkIssue: VideoCallLinkIssue.none,
|
||||
),
|
||||
);
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
|
||||
verify(mockManager.ensureLobbyRoomJoined()).called(1);
|
||||
verifyNever(mockManager.onEnterCallStage());
|
||||
});
|
||||
|
||||
test('outgoing call auto-enters when callee joins lobby room', () async {
|
||||
when(
|
||||
mockManager.startCall(
|
||||
conversationUuid: anyNamed('conversationUuid'),
|
||||
counterpartName: anyNamed('counterpartName'),
|
||||
localParticipantName: anyNamed('localParticipantName'),
|
||||
micEnabled: anyNamed('micEnabled'),
|
||||
videoEnabled: anyNamed('videoEnabled'),
|
||||
),
|
||||
).thenAnswer((_) async {});
|
||||
when(mockManager.ensureLobbyRoomJoined()).thenAnswer((_) async {});
|
||||
when(mockManager.onEnterCallStage()).thenAnswer((_) async => true);
|
||||
|
||||
await cubit.onCallScreenVisible(conversationUuid: 'uuid');
|
||||
|
||||
managerListener(
|
||||
const VideoCallManagerSnapshot(
|
||||
conversationUuid: 'uuid',
|
||||
micEnabled: false,
|
||||
videoEnabled: false,
|
||||
connectPhase: VideoCallConnectPhase.connected,
|
||||
linkIssue: VideoCallLinkIssue.none,
|
||||
counterpartPresentInLobby: true,
|
||||
),
|
||||
);
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
|
||||
verify(mockManager.onEnterCallStage()).called(1);
|
||||
expect(cubit.state.callStageEntered, isTrue);
|
||||
});
|
||||
|
||||
test('duplicate onCallScreenVisible does not restart connecting call', () async {
|
||||
when(
|
||||
mockManager.startCall(
|
||||
conversationUuid: anyNamed('conversationUuid'),
|
||||
counterpartName: anyNamed('counterpartName'),
|
||||
localParticipantName: anyNamed('localParticipantName'),
|
||||
micEnabled: anyNamed('micEnabled'),
|
||||
videoEnabled: anyNamed('videoEnabled'),
|
||||
),
|
||||
).thenAnswer((_) async {});
|
||||
|
||||
await cubit.onCallScreenVisible(
|
||||
conversationUuid: 'uuid',
|
||||
entryMode: VideoCallEntryMode.incomingAccepted,
|
||||
);
|
||||
await cubit.onCallScreenVisible(
|
||||
conversationUuid: 'uuid',
|
||||
entryMode: VideoCallEntryMode.outgoing,
|
||||
);
|
||||
|
||||
verify(
|
||||
mockManager.startCall(
|
||||
conversationUuid: 'uuid',
|
||||
counterpartName: anyNamed('counterpartName'),
|
||||
localParticipantName: anyNamed('localParticipantName'),
|
||||
micEnabled: false,
|
||||
videoEnabled: false,
|
||||
),
|
||||
).called(1);
|
||||
expect(cubit.state.entryMode, VideoCallEntryMode.incomingAccepted);
|
||||
});
|
||||
|
||||
test('leaveCallRoute minimizes to pip and marks call screen not visible', () async {
|
||||
final mockRoom = MockRoom();
|
||||
when(mockManager.room).thenReturn(mockRoom);
|
||||
when(
|
||||
mockManager.onCallScreenVisible(
|
||||
conversationUuid: anyNamed('conversationUuid'),
|
||||
counterpartName: anyNamed('counterpartName'),
|
||||
localParticipantName: anyNamed('localParticipantName'),
|
||||
micEnabled: anyNamed('micEnabled'),
|
||||
videoEnabled: anyNamed('videoEnabled'),
|
||||
),
|
||||
).thenAnswer((_) async {});
|
||||
|
||||
cubit.emit(
|
||||
const VideoCallSessionState(
|
||||
conversationUuid: 'uuid',
|
||||
connectPhase: VideoCallConnectPhase.connected,
|
||||
callStageEntered: true,
|
||||
uiMode: VideoCallUiMode.fullscreen,
|
||||
),
|
||||
);
|
||||
await cubit.onCallScreenVisible(conversationUuid: 'uuid');
|
||||
expect(cubit.isCallScreenRouteVisible, isTrue);
|
||||
|
||||
await cubit.leaveCallRoute();
|
||||
|
||||
expect(cubit.state.uiMode, VideoCallUiMode.pip);
|
||||
expect(cubit.isCallScreenRouteVisible, isFalse);
|
||||
});
|
||||
|
||||
test('tracks whether call screen route is visible', () async {
|
||||
when(
|
||||
mockManager.startCall(
|
||||
conversationUuid: anyNamed('conversationUuid'),
|
||||
counterpartName: anyNamed('counterpartName'),
|
||||
localParticipantName: anyNamed('localParticipantName'),
|
||||
micEnabled: anyNamed('micEnabled'),
|
||||
videoEnabled: anyNamed('videoEnabled'),
|
||||
),
|
||||
).thenAnswer((_) async {});
|
||||
|
||||
await cubit.onCallScreenVisible(conversationUuid: 'uuid');
|
||||
expect(cubit.isCallScreenRouteVisible, isTrue);
|
||||
|
||||
cubit.emit(
|
||||
const VideoCallSessionState(
|
||||
conversationUuid: 'uuid',
|
||||
connectPhase: VideoCallConnectPhase.connected,
|
||||
callStageEntered: true,
|
||||
uiMode: VideoCallUiMode.pip,
|
||||
),
|
||||
);
|
||||
|
||||
cubit.onCallScreenDisposed();
|
||||
expect(cubit.isCallScreenRouteVisible, isFalse);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
class _TestVideoCallSessionCubit extends VideoCallSessionCubit {
|
||||
_TestVideoCallSessionCubit({required super.videoCallManager});
|
||||
|
||||
bool grantMicOnRequest = false;
|
||||
|
||||
@override
|
||||
Future<bool> requestMicPermissionForCall() async => grantMicOnRequest;
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import 'package:eatvio_app/features/video_calls/services/video_call_ended_push_payload.dart';
|
||||
import 'package:eatvio_app/features/video_calls/services/video_call_push_payload.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
group('VideoCallPushPayload', () {
|
||||
test('parses video_call type with conversation uuid', () {
|
||||
final payload = VideoCallPushPayload.tryParse({
|
||||
'type': 'video_call_incoming',
|
||||
'conversation_uuid': 'abc-123',
|
||||
'caller_name': 'Anna',
|
||||
'avatar': 'https://cdn.example/avatar.jpg',
|
||||
});
|
||||
|
||||
expect(payload, isNotNull);
|
||||
expect(payload!.conversationUuid, 'abc-123');
|
||||
expect(payload.counterpartName, 'Anna');
|
||||
expect(payload.counterpartAvatarUrl, 'https://cdn.example/avatar.jpg');
|
||||
});
|
||||
|
||||
test('parses is_consultant flag from push payload', () {
|
||||
final payload = VideoCallPushPayload.tryParse({
|
||||
'type': 'video_call_incoming',
|
||||
'conversation_uuid': 'abc-123',
|
||||
'caller_name': 'Anna',
|
||||
'is_consultant': 'true',
|
||||
});
|
||||
|
||||
expect(payload, isNotNull);
|
||||
expect(payload!.counterpartIsConsultant, isTrue);
|
||||
});
|
||||
|
||||
test('parses action_url with video-call path', () {
|
||||
final payload = VideoCallPushPayload.tryParse({
|
||||
'action_url': 'https://eatvio.ru/video-call-screen?uuid=room-42',
|
||||
'counterpartName': 'Ivan',
|
||||
});
|
||||
|
||||
expect(payload, isNotNull);
|
||||
expect(payload!.conversationUuid, 'room-42');
|
||||
expect(payload.counterpartName, 'Ivan');
|
||||
});
|
||||
|
||||
test('parses ended video call push with type only', () {
|
||||
final payload = VideoCallEndedPushPayload.tryParse({
|
||||
'type': 'video_call_ended',
|
||||
});
|
||||
|
||||
expect(payload, isNotNull);
|
||||
});
|
||||
|
||||
test('parses ended video call push with optional conversation uuid', () {
|
||||
final payload = VideoCallEndedPushPayload.tryParse({
|
||||
'type': 'video_call_ended',
|
||||
'conversation_uuid': 'abc-123',
|
||||
});
|
||||
|
||||
expect(payload, isNotNull);
|
||||
});
|
||||
|
||||
test('ignores regular chat push', () {
|
||||
expect(
|
||||
VideoCallPushPayload.tryParse({
|
||||
'type': 'chat_message',
|
||||
'action_url': 'https://eatvio.ru/chat/123',
|
||||
}),
|
||||
isNull,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -90,5 +90,25 @@ void main() {
|
||||
throwsA(isA<Exception>()),
|
||||
);
|
||||
});
|
||||
|
||||
test('notifyCallAccepted - отправляет hw_id и получает ответ', () async {
|
||||
const conversationUuid = 'conv-uuid-123';
|
||||
const body = <String, dynamic>{'hw_id': 'device-android-001'};
|
||||
final json = FixtureReader.readAsMap('video_calls/video_call_accept_response.json');
|
||||
final expected = VideoCallAcceptResponse.fromJson(json);
|
||||
|
||||
when(mockService.notifyCallAccepted(conversationUuid: conversationUuid, body: body))
|
||||
.thenAnswer((_) async => expected);
|
||||
|
||||
final result = await mockService.notifyCallAccepted(
|
||||
conversationUuid: conversationUuid,
|
||||
body: body,
|
||||
);
|
||||
|
||||
expect(result, isA<VideoCallAcceptResponse>());
|
||||
expect(result.sent, isTrue);
|
||||
expect(result.devicesNotified, 2);
|
||||
verify(mockService.notifyCallAccepted(conversationUuid: conversationUuid, body: body)).called(1);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ import 'video_call_livekit_track_utils_test.mocks.dart';
|
||||
MockSpec<lk.Room>(),
|
||||
MockSpec<lk.LocalParticipant>(),
|
||||
MockSpec<lk.RemoteParticipant>(),
|
||||
MockSpec<lk.RemoteTrackPublication>(),
|
||||
MockSpec<lk.RemoteVideoTrack>(),
|
||||
MockSpec<lk.TrackPublication>(),
|
||||
MockSpec<lk.VideoTrack>(),
|
||||
])
|
||||
@@ -67,6 +69,33 @@ void main() {
|
||||
expect(VideoCallLiveKitTrackUtils.visibleVideoFromPublication(pub), isNull);
|
||||
});
|
||||
|
||||
test('visibleVideoFromPublication возвращает remote track при paused stream', () {
|
||||
final pub = MockRemoteTrackPublication();
|
||||
final track = MockRemoteVideoTrack();
|
||||
when(pub.muted).thenReturn(false);
|
||||
when(pub.subscribed).thenReturn(true);
|
||||
when(pub.streamState).thenReturn(lk.StreamState.paused);
|
||||
when(pub.track).thenReturn(track);
|
||||
when(track.muted).thenReturn(false);
|
||||
|
||||
expect(VideoCallLiveKitTrackUtils.visibleVideoFromPublication(pub), equals(track));
|
||||
});
|
||||
|
||||
test('visibleVideoFromPublication возвращает null для unsubscribed remote', () {
|
||||
final pub = MockRemoteTrackPublication();
|
||||
when(pub.muted).thenReturn(false);
|
||||
when(pub.subscribed).thenReturn(false);
|
||||
|
||||
expect(VideoCallLiveKitTrackUtils.visibleVideoFromPublication(pub), isNull);
|
||||
});
|
||||
|
||||
test('isSoloInRoom is true when remote participants are empty', () {
|
||||
final room = MockRoom();
|
||||
when(room.remoteParticipants).thenReturn(UnmodifiableMapView(<String, lk.RemoteParticipant>{}));
|
||||
|
||||
expect(VideoCallLiveKitTrackUtils.isSoloInRoom(room), isTrue);
|
||||
});
|
||||
|
||||
test('localCameraVideoTrack возвращает null если localParticipant отсутствует', () {
|
||||
final room = MockRoom();
|
||||
when(room.localParticipant).thenReturn(null);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import 'package:eatvio_app/features/video_calls/presentation/widgets/video_call_name_chips.dart';
|
||||
import 'package:eatvio_app/generated/l10n.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
@@ -18,4 +20,30 @@ void main() {
|
||||
|
||||
expect(find.text('Alex'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('VideoCallParticipantNameLabel renders consultant role above name', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
localizationsDelegates: const [
|
||||
S.delegate,
|
||||
...GlobalMaterialLocalizations.delegates,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
],
|
||||
supportedLocales: S.delegate.supportedLocales,
|
||||
home: const Scaffold(
|
||||
body: Stack(
|
||||
children: [
|
||||
VideoCallParticipantNameLabel(
|
||||
name: 'Alex',
|
||||
isConsultant: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.text('Alex'), findsOneWidget);
|
||||
expect(find.text('Eatvio consultant'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -25,7 +25,9 @@ void main() {
|
||||
);
|
||||
}
|
||||
|
||||
testWidgets('shows error text when connectError is provided', (tester) async {
|
||||
testWidgets('hides connectError text when room is connected without remote video', (
|
||||
tester,
|
||||
) async {
|
||||
final room = buildRoom(state: lk.ConnectionState.connected);
|
||||
|
||||
await tester.pumpWidget(
|
||||
@@ -37,10 +39,27 @@ void main() {
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.text('Connection failed'), findsOneWidget);
|
||||
expect(find.text('Connection failed'), findsNothing);
|
||||
expect(find.byType(CircularProgressIndicator), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('shows error text when connectError and room is not connected', (
|
||||
tester,
|
||||
) async {
|
||||
final room = buildRoom(state: lk.ConnectionState.disconnected);
|
||||
|
||||
await tester.pumpWidget(
|
||||
wrap(
|
||||
VideoCallRemoteVideoStage(
|
||||
room: room,
|
||||
connectError: 'Connection failed',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.text('Connection failed'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('shows progress indicator when not connected and no error', (tester) async {
|
||||
final room = buildRoom(state: lk.ConnectionState.disconnected);
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import 'package:mockito/mockito.dart';
|
||||
|
||||
import 'video_call_stage_layout_test.mocks.dart';
|
||||
|
||||
@GenerateNiceMocks([MockSpec<lk.Room>()])
|
||||
@GenerateNiceMocks([MockSpec<lk.Room>(), MockSpec<lk.RemoteParticipant>()])
|
||||
void main() {
|
||||
Widget wrap(Widget child) {
|
||||
return MaterialApp(
|
||||
@@ -27,8 +27,19 @@ void main() {
|
||||
return room;
|
||||
}
|
||||
|
||||
MockRoom buildRoomWithRemote() {
|
||||
final room = MockRoom();
|
||||
final remote = MockRemoteParticipant();
|
||||
when(room.remoteParticipants).thenReturn(
|
||||
UnmodifiableMapView(<String, lk.RemoteParticipant>{'remote': remote}),
|
||||
);
|
||||
when(room.localParticipant).thenReturn(null);
|
||||
when(room.connectionState).thenReturn(lk.ConnectionState.connected);
|
||||
return room;
|
||||
}
|
||||
|
||||
testWidgets('renders split mode participant names and controls', (tester) async {
|
||||
final room = buildRoom();
|
||||
final room = buildRoomWithRemote();
|
||||
var localExpanded = 0;
|
||||
var remoteExpanded = 0;
|
||||
|
||||
@@ -113,8 +124,8 @@ void main() {
|
||||
expect(shrinkPressed, greaterThanOrEqualTo(0));
|
||||
});
|
||||
|
||||
testWidgets('split layout shows connectError on remote pane and uses bottomInset', (tester) async {
|
||||
final room = buildRoom();
|
||||
testWidgets('split layout hides connectError on remote pane when connected', (tester) async {
|
||||
final room = buildRoomWithRemote();
|
||||
|
||||
await tester.pumpWidget(
|
||||
wrap(
|
||||
@@ -145,12 +156,12 @@ void main() {
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.text('No connection'), findsOneWidget);
|
||||
expect(find.text('No connection'), findsNothing);
|
||||
expect(find.text('Doctor'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('fullscreen layout shows remote name when main stage is remote', (tester) async {
|
||||
final room = buildRoom();
|
||||
final room = buildRoomWithRemote();
|
||||
|
||||
await tester.pumpWidget(
|
||||
wrap(
|
||||
@@ -186,7 +197,7 @@ void main() {
|
||||
});
|
||||
|
||||
testWidgets('split layout with video on shows camera switch on local pane', (tester) async {
|
||||
final room = buildRoom();
|
||||
final room = buildRoomWithRemote();
|
||||
|
||||
await tester.pumpWidget(
|
||||
wrap(
|
||||
@@ -222,7 +233,7 @@ void main() {
|
||||
});
|
||||
|
||||
testWidgets('split layout with rear camera and remote main stage variant', (tester) async {
|
||||
final room = buildRoom();
|
||||
final room = buildRoomWithRemote();
|
||||
var stageSwapped = 0;
|
||||
|
||||
await tester.pumpWidget(
|
||||
@@ -299,8 +310,131 @@ void main() {
|
||||
expect(find.text('Leave'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('fullscreen layout with connect error shows it on stage', (tester) async {
|
||||
testWidgets('fullscreen layout shows local PIP when camera is off', (tester) async {
|
||||
final room = buildRoomWithRemote();
|
||||
|
||||
await tester.pumpWidget(
|
||||
wrap(
|
||||
VideoCallStageLayout(
|
||||
room: room,
|
||||
connectError: null,
|
||||
localDisplayName: 'You',
|
||||
remoteDisplayName: 'Doctor',
|
||||
micEnabled: true,
|
||||
videoEnabled: false,
|
||||
splitParticipantView: false,
|
||||
mainStageShowsLocalVideo: false,
|
||||
useFrontCamera: true,
|
||||
onExpandLocal: () {},
|
||||
onExpandRemote: () {},
|
||||
onShrinkFromFullscreen: () {},
|
||||
onToggleStageSwap: () {},
|
||||
onChat: () {},
|
||||
onToggleMic: () {},
|
||||
onToggleVideo: () {},
|
||||
onHangUp: () {},
|
||||
onSwitchCamera: () {},
|
||||
controlLabelStyle: const TextStyle(fontSize: 14),
|
||||
labels: (chat: 'Chat', sound: 'Sound', video: 'Video', leave: 'Leave'),
|
||||
topInset: 0,
|
||||
bottomInset: 0,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pump();
|
||||
expect(find.byType(AnimatedContainer), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('fullscreen layout shows only local participant when solo in room', (tester) async {
|
||||
final room = buildRoom();
|
||||
var stageSwapped = 0;
|
||||
|
||||
await tester.pumpWidget(
|
||||
wrap(
|
||||
VideoCallStageLayout(
|
||||
room: room,
|
||||
connectError: null,
|
||||
localDisplayName: 'You',
|
||||
remoteDisplayName: 'Doctor',
|
||||
micEnabled: true,
|
||||
videoEnabled: false,
|
||||
splitParticipantView: false,
|
||||
mainStageShowsLocalVideo: false,
|
||||
useFrontCamera: true,
|
||||
onExpandLocal: () {},
|
||||
onExpandRemote: () {},
|
||||
onShrinkFromFullscreen: () {},
|
||||
onToggleStageSwap: () => stageSwapped++,
|
||||
onChat: () {},
|
||||
onToggleMic: () {},
|
||||
onToggleVideo: () {},
|
||||
onHangUp: () {},
|
||||
onSwitchCamera: () {},
|
||||
controlLabelStyle: const TextStyle(fontSize: 14),
|
||||
labels: (chat: 'Chat', sound: 'Sound', video: 'Video', leave: 'Leave'),
|
||||
topInset: 8,
|
||||
bottomInset: 32,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pump();
|
||||
expect(find.text('You'), findsOneWidget);
|
||||
expect(find.text('Doctor'), findsNothing);
|
||||
expect(find.byType(AnimatedContainer), findsNothing);
|
||||
expect(stageSwapped, 0);
|
||||
});
|
||||
|
||||
testWidgets('split layout places remote above local with 70/30 height ratio', (tester) async {
|
||||
final room = buildRoomWithRemote();
|
||||
|
||||
await tester.pumpWidget(
|
||||
wrap(
|
||||
VideoCallStageLayout(
|
||||
room: room,
|
||||
connectError: null,
|
||||
localDisplayName: 'You',
|
||||
remoteDisplayName: 'Doctor',
|
||||
micEnabled: true,
|
||||
videoEnabled: false,
|
||||
splitParticipantView: true,
|
||||
mainStageShowsLocalVideo: false,
|
||||
useFrontCamera: true,
|
||||
onExpandLocal: () {},
|
||||
onExpandRemote: () {},
|
||||
onShrinkFromFullscreen: () {},
|
||||
onToggleStageSwap: () {},
|
||||
onChat: () {},
|
||||
onToggleMic: () {},
|
||||
onToggleVideo: () {},
|
||||
onHangUp: () {},
|
||||
onSwitchCamera: () {},
|
||||
controlLabelStyle: const TextStyle(fontSize: 14),
|
||||
labels: (chat: 'Chat', sound: 'Sound', video: 'Video', leave: 'Leave'),
|
||||
topInset: 0,
|
||||
bottomInset: 0,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.pump();
|
||||
|
||||
final remoteTop = tester.getTopLeft(find.text('Doctor')).dy;
|
||||
final localTop = tester.getTopLeft(find.text('You')).dy;
|
||||
expect(remoteTop, lessThan(localTop));
|
||||
|
||||
final remotePane = tester.getSize(
|
||||
find.ancestor(of: find.text('Doctor'), matching: find.byType(Material)).first,
|
||||
);
|
||||
final localPane = tester.getSize(
|
||||
find.ancestor(of: find.text('You'), matching: find.byType(Material)).first,
|
||||
);
|
||||
expect(remotePane.height / localPane.height, closeTo(7 / 3, 0.05));
|
||||
});
|
||||
|
||||
testWidgets('split layout with connect error shows it on stage', (tester) async {
|
||||
final room = buildRoomWithRemote();
|
||||
when(room.connectionState).thenReturn(lk.ConnectionState.disconnected);
|
||||
|
||||
await tester.pumpWidget(
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import 'package:eatvio_app/features/video_calls/presentation/widgets/video_call_top_overlay.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('insertBanner shows message and can be removed manually', (tester) async {
|
||||
await tester.pumpWidget(const MaterialApp(home: Scaffold(body: SizedBox.shrink())));
|
||||
testWidgets('insertBanner shows message and can be removed manually', (
|
||||
tester,
|
||||
) async {
|
||||
await tester.pumpWidget(
|
||||
const MaterialApp(home: Scaffold(body: SizedBox.shrink())),
|
||||
);
|
||||
|
||||
final context = tester.element(find.byType(SizedBox));
|
||||
final entry = VideoCallTopOverlay.insertBanner(
|
||||
@@ -23,7 +28,9 @@ void main() {
|
||||
});
|
||||
|
||||
testWidgets('insertBanner auto removes entry', (tester) async {
|
||||
await tester.pumpWidget(const MaterialApp(home: Scaffold(body: SizedBox.shrink())));
|
||||
await tester.pumpWidget(
|
||||
const MaterialApp(home: Scaffold(body: SizedBox.shrink())),
|
||||
);
|
||||
|
||||
final context = tester.element(find.byType(SizedBox));
|
||||
VideoCallTopOverlay.insertBanner(
|
||||
@@ -40,4 +47,57 @@ void main() {
|
||||
await tester.pump(const Duration(milliseconds: 40));
|
||||
expect(find.text('Muted mic'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('insertBanner does not throw without an overlay', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
const Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: SizedBox.shrink(),
|
||||
),
|
||||
);
|
||||
|
||||
final context = tester.element(find.byType(SizedBox));
|
||||
final entry = VideoCallTopOverlay.insertBanner(
|
||||
context,
|
||||
message: 'Recording notice',
|
||||
backgroundColor: Colors.grey,
|
||||
svgAsset: VideoCallTopOverlay.warningSvg,
|
||||
);
|
||||
|
||||
expect(entry.mounted, isFalse);
|
||||
await tester.pump();
|
||||
expect(find.text('Recording notice'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('showPoorConnectionIcon renders compact icon chip', (tester) async {
|
||||
await tester.pumpWidget(
|
||||
const MaterialApp(home: Scaffold(body: SizedBox.shrink())),
|
||||
);
|
||||
|
||||
final context = tester.element(find.byType(SizedBox));
|
||||
VideoCallTopOverlay.showPoorConnectionIcon(context);
|
||||
|
||||
await tester.pump();
|
||||
expect(find.byType(SvgPicture), findsOneWidget);
|
||||
expect(find.text('Poor connection'), findsNothing);
|
||||
expect(VideoCallTopOverlay.isPoorConnectionIconVisible, isTrue);
|
||||
});
|
||||
|
||||
testWidgets('hidePoorConnectionIcon removes poor connection chip', (
|
||||
tester,
|
||||
) async {
|
||||
await tester.pumpWidget(
|
||||
const MaterialApp(home: Scaffold(body: SizedBox.shrink())),
|
||||
);
|
||||
|
||||
final context = tester.element(find.byType(SizedBox));
|
||||
VideoCallTopOverlay.showPoorConnectionIcon(context);
|
||||
await tester.pump();
|
||||
expect(VideoCallTopOverlay.isPoorConnectionIconVisible, isTrue);
|
||||
|
||||
VideoCallTopOverlay.hidePoorConnectionIcon();
|
||||
await tester.pump();
|
||||
expect(VideoCallTopOverlay.isPoorConnectionIconVisible, isFalse);
|
||||
expect(find.byType(SvgPicture), findsNothing);
|
||||
});
|
||||
}
|
||||
|
||||
4
test/fixtures/video_calls/video_call_accept_response.json
vendored
Normal file
4
test/fixtures/video_calls/video_call_accept_response.json
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"sent": true,
|
||||
"devices_notified": 2
|
||||
}
|
||||
Reference in New Issue
Block a user