import 'dart:async'; import 'dart:convert'; import 'package:eatvio_app/core/enums/base_state.dart'; import 'package:eatvio_app/core/models/pagination_model.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_pagination_model.dart'; import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart'; import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_state.dart'; import 'package:eatvio_app/features/chat/repositories/chat_repository.dart'; import 'package:eatvio_app/features/profile/repositories/profile_repository.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'package:dart_pusher_channels/dart_pusher_channels.dart'; import 'chats_cubit_test.mocks.dart'; @GenerateNiceMocks([ MockSpec(), MockSpec(), MockSpec(), MockSpec(), ]) void main() { TestWidgetsFlutterBinding.ensureInitialized(); group('ChatsCubit', () { late ChatsCubit chatsCubit; late MockChatRepository mockChatRepository; late MockProfileRepository mockProfileRepository; late MockWsManager mockWsManager; const testChatRoomsPagination = ChatRoomPaginationModel( data: [], unreadChatsCount: 0, meta: Meta( currentPage: 1, from: null, lastPage: 1, links: [], perPage: 10, to: null, total: 0, ), ); setUp(() { mockChatRepository = MockChatRepository(); mockProfileRepository = MockProfileRepository(); mockWsManager = MockWsManager(); chatsCubit = ChatsCubit( chatRepository: mockChatRepository, wsManager: mockWsManager, profileRepository: mockProfileRepository, ); }); tearDown(() async { await Future.delayed(const Duration(milliseconds: 100)); if (!chatsCubit.isClosed) { await chatsCubit.close(); } }); ChatRoom buildChatRoom(String chatId, {int unread = 0}) { return ChatRoom( id: chatId, lastMessage: null, users: const Users( fromChatUser: ChatUser( id: 1, name: 'User 1', profilePhotoPath: null, profilePhotoUrl: '', ), toChatUser: ChatUser( id: 2, name: 'User 2', profilePhotoPath: null, profilePhotoUrl: '', ), ), unreadMessagesCount: unread, chosenPlan: null, ); } ChannelReadEvent socketEvent({ required int id, required int authorId, required String roomId, String data = 'socket message', String? callId, }) { return _FakeChannelReadEvent( jsonEncode({ 'id': id, 'data': data, 'media_data': null, 'author_id': authorId, 'updated_at': DateTime.now().toIso8601String(), 'read_by_to_user': false, 'is_author_message_auth_user': false, 'room_id': roomId, 'call_id': ?callId, }), ); } Future> connectToSocketStream() async { final controller = StreamController.broadcast(); when( mockWsManager.newMessagesStream, ).thenAnswer((_) => controller.stream); await chatsCubit.connectToWebSocket(); return controller; } test('начальное состояние - initial', () { expect(chatsCubit.state, isA()); expect(chatsCubit.state.state, BaseState.initial); }); test( 'getChatRoomsWithLoading - загружает комнаты с loading состоянием', () async { when( mockChatRepository.getChatRooms(query: null), ).thenAnswer((_) async => testChatRoomsPagination); await chatsCubit.getChatRoomsWithLoading(); await Future.delayed(const Duration(milliseconds: 100)); expect(chatsCubit.state.state, BaseState.success); verify(mockChatRepository.getChatRooms(query: null)).called(1); }, ); test('getChatRoomsWithLoading - обрабатывает ошибку', () async { when( mockChatRepository.getChatRooms(query: null), ).thenThrow(Exception('Network error')); await chatsCubit.getChatRoomsWithLoading(); await Future.delayed(const Duration(milliseconds: 100)); expect(chatsCubit.state.state, BaseState.error); expect(chatsCubit.state.errorMessage, isNotNull); }); test('getChatRooms - загружает комнаты без loading состояния', () async { when( mockChatRepository.getChatRooms(query: null), ).thenAnswer((_) async => testChatRoomsPagination); await chatsCubit.getChatRooms(); await Future.delayed(const Duration(milliseconds: 100)); expect(chatsCubit.state.state, BaseState.success); verify(mockChatRepository.getChatRooms(query: null)).called(1); }); test('getChatRooms - загружает комнаты с запросом', () async { const query = 'test query'; when( mockChatRepository.getChatRooms(query: query), ).thenAnswer((_) async => testChatRoomsPagination); await chatsCubit.getChatRooms(query: query); await Future.delayed(const Duration(milliseconds: 100)); expect(chatsCubit.state.state, BaseState.success); verify(mockChatRepository.getChatRooms(query: query)).called(1); }); test('loadMoreChatRooms - загружает больше комнат', () async { // Create pagination with lastPage > 1 so loadMoreChatRooms doesn't return early const paginationPage1 = ChatRoomPaginationModel( data: [], unreadChatsCount: 0, meta: Meta( currentPage: 1, from: null, lastPage: 3, // Set to 3 so we can load more links: [], perPage: 10, to: null, total: 30, ), ); const paginationPage2 = ChatRoomPaginationModel( data: [], unreadChatsCount: 0, meta: Meta( currentPage: 2, from: null, lastPage: 3, links: [], perPage: 10, to: null, total: 30, ), ); when( mockChatRepository.getChatRooms(query: null, page: 1), ).thenAnswer((_) async => paginationPage1); // Сначала загружаем первую страницу await chatsCubit.getChatRooms(); await Future.delayed(const Duration(milliseconds: 100)); // Затем загружаем следующую страницу when( mockChatRepository.getChatRooms(query: null, page: 2), ).thenAnswer((_) async => paginationPage2); await chatsCubit.loadMoreChatRooms(); await Future.delayed(const Duration(milliseconds: 100)); verify(mockChatRepository.getChatRooms(query: null, page: 2)).called(1); }); test('loadMoreChatRooms - не загружает если уже загружается', () async { when( mockChatRepository.getChatRooms(query: null), ).thenAnswer((_) async => testChatRoomsPagination); await chatsCubit.getChatRooms(); await Future.delayed(const Duration(milliseconds: 100)); // Устанавливаем isLoadingMoreChats в true chatsCubit.emit(chatsCubit.state.copyWith(isLoadingMoreChats: true)); await chatsCubit.loadMoreChatRooms(); // Не должно вызывать репозиторий verifyNever(mockChatRepository.getChatRooms(query: null, page: 2)); }); test( 'reduceUnreadMessagesCount - уменьшает количество непрочитанных сообщений', () { const chatId = 'chat-id'; const count = 2; const chatRoom = ChatRoom( id: chatId, lastMessage: null, users: Users( fromChatUser: ChatUser( id: 1, name: 'User 1', profilePhotoPath: null, profilePhotoUrl: '', ), toChatUser: ChatUser( id: 2, name: 'User 2', profilePhotoPath: null, profilePhotoUrl: '', ), ), unreadMessagesCount: 5, chosenPlan: null, ); const pagination = ChatRoomPaginationModel( data: [chatRoom], unreadChatsCount: 1, meta: Meta( currentPage: 1, from: null, lastPage: 1, links: [], perPage: 10, to: null, total: 1, ), ); chatsCubit.emit( ChatsState( state: BaseState.success, chatRooms: pagination.data, chatsWithUnreadMessagesCount: 1, ), ); chatsCubit.reduceUnreadMessagesCount(chatId: chatId, count: count); final updatedState = chatsCubit.state; final updatedRoom = updatedState.chatRooms.firstWhere( (r) => r.id == chatId, ); expect(updatedRoom.unreadMessagesCount, 3); }, ); test('connectToWebSocket - подключается к WebSocket', () async { const mockStream = Stream.empty(); when(mockWsManager.newMessagesStream).thenAnswer((_) => mockStream); await chatsCubit.connectToWebSocket(); verify(mockWsManager.newMessagesStream).called(1); }); test( '_handleNewMessageViaSocket - собственное сообщение обновляет lastMessage без непрочитанного', () async { const chatId = 'chat-id'; chatsCubit.emit( ChatsState( state: BaseState.success, chatRooms: [buildChatRoom(chatId)], ), ); when(mockWsManager.userId).thenReturn(42); final controller = await connectToSocketStream(); controller.add(socketEvent(id: 100, authorId: 42, roomId: chatId)); await Future.delayed(const Duration(milliseconds: 100)); final room = chatsCubit.state.chatRooms.firstWhere( (r) => r.id == chatId, ); expect(room.lastMessage?.id, 100); expect(room.unreadMessagesCount, 0); expect(chatsCubit.state.chatsWithUnreadMessagesCount, 0); await controller.close(); }, ); test( '_handleNewMessageViaSocket - чужое сообщение увеличивает непрочитанное', () async { const chatId = 'chat-id'; chatsCubit.emit( ChatsState( state: BaseState.success, chatRooms: [buildChatRoom(chatId)], ), ); when(mockWsManager.userId).thenReturn(42); final controller = await connectToSocketStream(); controller.add(socketEvent(id: 200, authorId: 7, roomId: chatId)); await Future.delayed(const Duration(milliseconds: 100)); final room = chatsCubit.state.chatRooms.firstWhere( (r) => r.id == chatId, ); expect(room.lastMessage?.id, 200); expect(room.unreadMessagesCount, 1); expect(chatsCubit.state.chatsWithUnreadMessagesCount, 1); await controller.close(); }, ); test( '_handleNewMessageViaSocket - повторная доставка того же id не двоит непрочитанное', () async { const chatId = 'chat-id'; chatsCubit.emit( ChatsState( state: BaseState.success, chatRooms: [buildChatRoom(chatId)], ), ); when(mockWsManager.userId).thenReturn(42); final controller = await connectToSocketStream(); controller.add(socketEvent(id: 300, authorId: 7, roomId: chatId)); await Future.delayed(const Duration(milliseconds: 100)); controller.add(socketEvent(id: 300, authorId: 7, roomId: chatId)); await Future.delayed(const Duration(milliseconds: 100)); final room = chatsCubit.state.chatRooms.firstWhere( (r) => r.id == chatId, ); expect(room.unreadMessagesCount, 1); expect(chatsCubit.state.chatsWithUnreadMessagesCount, 1); await controller.close(); }, ); test( '_handleNewMessageViaSocket - чужое сообщение в непрочитанном чате не поднимает счётчик чатов', () async { const chatId = 'chat-id'; chatsCubit.emit( ChatsState( state: BaseState.success, chatRooms: [buildChatRoom(chatId, unread: 2)], chatsWithUnreadMessagesCount: 1, ), ); when(mockWsManager.userId).thenReturn(42); final controller = await connectToSocketStream(); controller.add(socketEvent(id: 400, authorId: 7, roomId: chatId)); await Future.delayed(const Duration(milliseconds: 100)); final room = chatsCubit.state.chatRooms.firstWhere( (r) => r.id == chatId, ); expect(room.unreadMessagesCount, 3); expect(chatsCubit.state.chatsWithUnreadMessagesCount, 1); await controller.close(); }, ); test( '_handleNewMessageViaSocket - чужой старт видеозвонка показывает входящий вызов', () async { const chatId = 'chat-id'; final presented = []; String? presentedCallId; chatsCubit.presentIncomingVideoCall = (payload) async { presented.add(payload.conversationUuid); presentedCallId = payload.callId; }; chatsCubit.emit( ChatsState( state: BaseState.success, chatRooms: [buildChatRoom(chatId)], ), ); when(mockWsManager.userId).thenReturn(42); final controller = await connectToSocketStream(); controller.add( socketEvent( id: 500, authorId: 7, roomId: chatId, data: 'Анна начал(а) видеозвонок.', callId: '6f1e8c0a-1b2c-4d3e-8f90-aabbccddeeff', ), ); await Future.delayed(const Duration(milliseconds: 100)); expect(presented, [chatId]); expect(presentedCallId, '6f1e8c0a-1b2c-4d3e-8f90-aabbccddeeff'); await controller.close(); }, ); test( '_handleNewMessageViaSocket - свой старт видеозвонка не показывает входящий вызов', () async { const chatId = 'chat-id'; final presented = []; chatsCubit.presentIncomingVideoCall = (payload) async { presented.add(payload.conversationUuid); }; chatsCubit.emit( ChatsState( state: BaseState.success, chatRooms: [buildChatRoom(chatId)], ), ); when(mockWsManager.userId).thenReturn(42); final controller = await connectToSocketStream(); controller.add( socketEvent( id: 501, authorId: 42, roomId: chatId, data: 'Анна начал(а) видеозвонок.', ), ); await Future.delayed(const Duration(milliseconds: 100)); expect(presented, isEmpty); await controller.close(); }, ); test( '_handleNewMessageViaSocket - завершение видеозвонка закрывает только этот чат', () async { const chatId = 'chat-id'; final dismissed = []; chatsCubit.dismissIncomingVideoCall = (uuid) async { dismissed.add(uuid); }; chatsCubit.emit( ChatsState( state: BaseState.success, chatRooms: [buildChatRoom(chatId)], ), ); when(mockWsManager.userId).thenReturn(42); final controller = await connectToSocketStream(); controller.add( socketEvent( id: 502, authorId: 7, roomId: chatId, data: 'Анна завершил(а) видеозвонок.', ), ); await Future.delayed(const Duration(milliseconds: 100)); expect(dismissed, [chatId]); await controller.close(); }, ); test('close - закрывает cubit и отписывается от подписок', () async { final mockSubscription1 = MockStreamSubscription(); when(mockSubscription1.cancel()).thenAnswer((_) async => Future.value()); // Set newIncomingMessagesSubscription chatsCubit.newIncomingMessagesSubscription = mockSubscription1; await chatsCubit.close(); // Verify that cancel was called on newIncomingMessagesSubscription verify(mockSubscription1.cancel()).called(1); // newLastMessageSubscription is cancelled in super.close() which we can't easily verify // but the test verifies that the method completes without error }); test( 'reduceUnreadMessagesCount - уменьшает до 0 и уменьшает chatsWithUnreadMessagesCount', () { const chatId = 'chat-id'; const count = 5; const chatRoom = ChatRoom( id: chatId, lastMessage: null, users: Users( fromChatUser: ChatUser( id: 1, name: 'User 1', profilePhotoPath: null, profilePhotoUrl: '', ), toChatUser: ChatUser( id: 2, name: 'User 2', profilePhotoPath: null, profilePhotoUrl: '', ), ), unreadMessagesCount: 5, chosenPlan: null, ); const pagination = ChatRoomPaginationModel( data: [chatRoom], unreadChatsCount: 1, meta: Meta( currentPage: 1, from: null, lastPage: 1, links: [], perPage: 10, to: null, total: 1, ), ); chatsCubit.emit( ChatsState( state: BaseState.success, chatRooms: pagination.data, chatsWithUnreadMessagesCount: 1, ), ); chatsCubit.reduceUnreadMessagesCount(chatId: chatId, count: count); final updatedState = chatsCubit.state; final updatedRoom = updatedState.chatRooms.firstWhere( (r) => r.id == chatId, ); expect(updatedRoom.unreadMessagesCount, 0); expect(updatedState.chatsWithUnreadMessagesCount, 0); }, ); test( 'reduceUnreadMessagesCount - не уменьшает chatsWithUnreadMessagesCount если unreadMessagesCount > 0', () { const chatId = 'chat-id'; const count = 2; const chatRoom = ChatRoom( id: chatId, lastMessage: null, users: Users( fromChatUser: ChatUser( id: 1, name: 'User 1', profilePhotoPath: null, profilePhotoUrl: '', ), toChatUser: ChatUser( id: 2, name: 'User 2', profilePhotoPath: null, profilePhotoUrl: '', ), ), unreadMessagesCount: 5, chosenPlan: null, ); const pagination = ChatRoomPaginationModel( data: [chatRoom], unreadChatsCount: 1, meta: Meta( currentPage: 1, from: null, lastPage: 1, links: [], perPage: 10, to: null, total: 1, ), ); chatsCubit.emit( ChatsState( state: BaseState.success, chatRooms: pagination.data, chatsWithUnreadMessagesCount: 1, ), ); chatsCubit.reduceUnreadMessagesCount(chatId: chatId, count: count); final updatedState = chatsCubit.state; expect(updatedState.chatsWithUnreadMessagesCount, 1); }, ); test('loadMoreChatRooms - не загружает если lastPage == page', () async { const paginationPage1 = ChatRoomPaginationModel( data: [], unreadChatsCount: 0, meta: Meta( currentPage: 1, from: null, lastPage: 1, links: [], perPage: 10, to: null, total: 10, ), ); when( mockChatRepository.getChatRooms(query: null, page: 1), ).thenAnswer((_) async => paginationPage1); await chatsCubit.getChatRooms(); await Future.delayed(const Duration(milliseconds: 100)); // lastPage = meta.lastPage + 1 = 2, page = 2, поэтому не должно загружать await chatsCubit.loadMoreChatRooms(); verifyNever(mockChatRepository.getChatRooms(query: null, page: 2)); }); test('loadMoreChatRooms - обрабатывает ошибку', () async { const paginationPage1 = ChatRoomPaginationModel( data: [], unreadChatsCount: 0, meta: Meta( currentPage: 1, from: null, lastPage: 3, links: [], perPage: 10, to: null, total: 30, ), ); when( mockChatRepository.getChatRooms(query: null, page: 1), ).thenAnswer((_) async => paginationPage1); when( mockChatRepository.getChatRooms(query: null, page: 2), ).thenThrow(Exception('Ошибка загрузки')); await chatsCubit.getChatRooms(); await Future.delayed(const Duration(milliseconds: 100)); await chatsCubit.loadMoreChatRooms(); await Future.delayed(const Duration(milliseconds: 100)); expect(chatsCubit.state.state, BaseState.error); expect(chatsCubit.state.errorMessage, isNotNull); expect(chatsCubit.state.isLoadingMoreChats, false); }); test('getChatRooms - обрабатывает ошибку', () async { when( mockChatRepository.getChatRooms(query: null), ).thenThrow(Exception('Network error')); await chatsCubit.getChatRooms(); await Future.delayed(const Duration(milliseconds: 100)); expect(chatsCubit.state.state, BaseState.error); expect(chatsCubit.state.errorMessage, isNotNull); }); test('_setNewLastMessage - обновляет последнее сообщение в чате', () async { const chatId = 'chat-id'; const chatRoom = ChatRoom( id: chatId, lastMessage: null, users: Users( fromChatUser: ChatUser( id: 1, name: 'User 1', profilePhotoPath: null, profilePhotoUrl: '', ), toChatUser: ChatUser( id: 2, name: 'User 2', profilePhotoPath: null, profilePhotoUrl: '', ), ), unreadMessagesCount: 0, chosenPlan: null, ); const pagination = ChatRoomPaginationModel( data: [chatRoom], unreadChatsCount: 0, meta: Meta( currentPage: 1, from: null, lastPage: 1, links: [], perPage: 10, to: null, total: 1, ), ); chatsCubit.emit( ChatsState(state: BaseState.success, chatRooms: pagination.data), ); final newMessage = ChatMessageModel( id: 1, data: 'Test message', mediaData: null, urlsMediaInfo: null, authorId: 1, updatedAt: DateTime.now(), isRead: false, isCurrentUserAuthor: true, roomId: chatId, type: ChatMessageType.text, ); // Используем рефлексию для вызова приватного метода через тестовый доступ // Или проверяем через публичный API, если есть // В данном случае, _setNewLastMessage вызывается через newUserMessagesStream final streamController = StreamController(); when( mockChatRepository.newUserMessagesStream, ).thenAnswer((_) => streamController.stream); // Пересоздаем cubit чтобы подписаться на stream await chatsCubit.close(); chatsCubit = ChatsCubit( chatRepository: mockChatRepository, wsManager: mockWsManager, profileRepository: mockProfileRepository, ); chatsCubit.emit( ChatsState(state: BaseState.success, chatRooms: pagination.data), ); streamController.add(newMessage); await Future.delayed(const Duration(milliseconds: 100)); final updatedState = chatsCubit.state; final updatedRoom = updatedState.chatRooms.firstWhere( (r) => r.id == chatId, ); expect(updatedRoom.lastMessage, isNotNull); expect(updatedRoom.lastMessage?.id, 1); await streamController.close(); }); test( '_setNewLastMessage - обновляет последнее сообщение через newUserMessagesStream', () async { const chatId = 'chat-id'; const chatRoom = ChatRoom( id: chatId, lastMessage: null, users: Users( fromChatUser: ChatUser( id: 1, name: 'User 1', profilePhotoPath: null, profilePhotoUrl: '', ), toChatUser: ChatUser( id: 2, name: 'User 2', profilePhotoPath: null, profilePhotoUrl: '', ), ), unreadMessagesCount: 0, chosenPlan: null, ); const pagination = ChatRoomPaginationModel( data: [chatRoom], unreadChatsCount: 0, meta: Meta( currentPage: 1, from: null, lastPage: 1, links: [], perPage: 10, to: null, total: 1, ), ); final newMessage = ChatMessageModel( id: 1, data: 'Test message', mediaData: null, urlsMediaInfo: null, authorId: 1, updatedAt: DateTime.now(), isRead: false, isCurrentUserAuthor: true, roomId: chatId, type: ChatMessageType.text, ); final streamController = StreamController(); when( mockChatRepository.newUserMessagesStream, ).thenAnswer((_) => streamController.stream); // Пересоздаем cubit чтобы подписаться на stream await chatsCubit.close(); chatsCubit = ChatsCubit( chatRepository: mockChatRepository, wsManager: mockWsManager, profileRepository: mockProfileRepository, ); chatsCubit.emit( ChatsState(state: BaseState.success, chatRooms: pagination.data), ); streamController.add(newMessage); await Future.delayed(const Duration(milliseconds: 100)); final updatedState = chatsCubit.state; final updatedRoom = updatedState.chatRooms.firstWhere( (r) => r.id == chatId, ); expect(updatedRoom.lastMessage, isNotNull); expect(updatedRoom.lastMessage?.id, 1); await streamController.close(); }, ); test( 'reduceUnreadMessagesCount - обрабатывает чат который не существует', () { const chatId = 'non-existent-chat'; const count = 2; const chatRoom = ChatRoom( id: 'other-chat-id', lastMessage: null, users: Users( fromChatUser: ChatUser( id: 1, name: 'User 1', profilePhotoPath: null, profilePhotoUrl: '', ), toChatUser: ChatUser( id: 2, name: 'User 2', profilePhotoPath: null, profilePhotoUrl: '', ), ), unreadMessagesCount: 5, chosenPlan: null, ); const pagination = ChatRoomPaginationModel( data: [chatRoom], unreadChatsCount: 1, meta: Meta( currentPage: 1, from: null, lastPage: 1, links: [], perPage: 10, to: null, total: 1, ), ); chatsCubit.emit( ChatsState( state: BaseState.success, chatRooms: pagination.data, chatsWithUnreadMessagesCount: 1, ), ); chatsCubit.reduceUnreadMessagesCount(chatId: chatId, count: count); final updatedState = chatsCubit.state; // Чат не должен измениться expect(updatedState.chatRooms.length, 1); expect(updatedState.chatRooms.first.unreadMessagesCount, 5); expect(updatedState.chatsWithUnreadMessagesCount, 1); }, ); test( 'reduceUnreadMessagesCount - обрабатывает уменьшение больше чем unreadMessagesCount', () { const chatId = 'chat-id'; const count = 10; // Больше чем unreadMessagesCount const chatRoom = ChatRoom( id: chatId, lastMessage: null, users: Users( fromChatUser: ChatUser( id: 1, name: 'User 1', profilePhotoPath: null, profilePhotoUrl: '', ), toChatUser: ChatUser( id: 2, name: 'User 2', profilePhotoPath: null, profilePhotoUrl: '', ), ), unreadMessagesCount: 5, chosenPlan: null, ); const pagination = ChatRoomPaginationModel( data: [chatRoom], unreadChatsCount: 1, meta: Meta( currentPage: 1, from: null, lastPage: 1, links: [], perPage: 10, to: null, total: 1, ), ); chatsCubit.emit( ChatsState( state: BaseState.success, chatRooms: pagination.data, chatsWithUnreadMessagesCount: 1, ), ); chatsCubit.reduceUnreadMessagesCount(chatId: chatId, count: count); final updatedState = chatsCubit.state; final updatedRoom = updatedState.chatRooms.firstWhere( (r) => r.id == chatId, ); expect(updatedRoom.unreadMessagesCount, 0); expect(updatedState.chatsWithUnreadMessagesCount, 0); }, ); test( 'connectToWebSocket - обновляет список чатов после реконнекта', () async { final reconnectController = StreamController.broadcast(); when( mockWsManager.reconnected, ).thenAnswer((_) => reconnectController.stream); when(mockWsManager.newMessagesStream).thenReturn(null); when( mockChatRepository.getChatRooms(query: null), ).thenAnswer((_) async => testChatRoomsPagination); await chatsCubit.connectToWebSocket(); reconnectController.add(null); await Future.delayed(const Duration(milliseconds: 100)); verify(mockChatRepository.getChatRooms(query: null)).called(1); await reconnectController.close(); }, ); test('video-call-declined closes the call on this device', () async { final declined = StreamController.broadcast(); when(mockWsManager.newMessagesStream).thenReturn(null); when(mockWsManager.reconnected).thenAnswer((_) => const Stream.empty()); when( mockWsManager.videoCallDeclinedStream, ).thenAnswer((_) => declined.stream); when(mockWsManager.videoCallEndedStream).thenReturn(null); final dismissed = []; final ended = []; chatsCubit.dismissIncomingVideoCall = (uuid) async { dismissed.add(uuid); }; chatsCubit.endActiveVideoCall = ({conversationUuid, callId}) async { ended.add(conversationUuid); expect(callId, 'call-1'); }; await chatsCubit.connectToWebSocket(); declined.add( _FakeChannelReadEvent( jsonEncode({ 'call_id': 'call-1', 'conversation_uuid': 'conv-1', 'status': 'declined', }), ), ); await Future.delayed(Duration.zero); expect(dismissed, ['conv-1']); expect(ended, ['conv-1']); await declined.close(); }); test('video-call-accepted closes ringing on the other device', () async { final accepted = StreamController.broadcast(); when(mockWsManager.newMessagesStream).thenReturn(null); when(mockWsManager.reconnected).thenAnswer((_) => const Stream.empty()); when(mockWsManager.videoCallDeclinedStream).thenReturn(null); when(mockWsManager.videoCallEndedStream).thenReturn(null); when( mockWsManager.videoCallAcceptedStream, ).thenAnswer((_) => accepted.stream); chatsCubit.currentDeviceHwId = () async => 'device-iphone'; final dismissed = []; final ended = []; chatsCubit.dismissIncomingVideoCall = (uuid) async { dismissed.add(uuid); }; chatsCubit.endActiveVideoCall = ({conversationUuid, callId}) async { ended.add(conversationUuid); }; await chatsCubit.connectToWebSocket(); accepted.add( _FakeChannelReadEvent( jsonEncode({ 'call_id': 'call-1', 'conversation_uuid': 'conv-1', 'status': 'accepted', 'accepted_device_id': 'device-android', 'accepted_client': 'android', }), ), ); await Future.delayed(Duration.zero); expect(dismissed, ['conv-1']); expect(ended, isEmpty); await accepted.close(); }); test( 'video-call-accepted keeps the call on the device that answered', () async { final accepted = StreamController.broadcast(); when(mockWsManager.newMessagesStream).thenReturn(null); when(mockWsManager.reconnected).thenAnswer((_) => const Stream.empty()); when(mockWsManager.videoCallDeclinedStream).thenReturn(null); when(mockWsManager.videoCallEndedStream).thenReturn(null); when( mockWsManager.videoCallAcceptedStream, ).thenAnswer((_) => accepted.stream); chatsCubit.currentDeviceHwId = () async => 'Device-iPhone'; final dismissed = []; chatsCubit.dismissIncomingVideoCall = (uuid) async { dismissed.add(uuid); }; chatsCubit.endActiveVideoCall = ({conversationUuid, callId}) async { fail('accepted call must stay on the answering device'); }; await chatsCubit.connectToWebSocket(); accepted.add( _FakeChannelReadEvent( jsonEncode({ 'call_id': 'call-1', 'conversation_uuid': 'conv-1', 'status': 'accepted', 'accepted_device_id': 'device-iphone', }), ), ); await Future.delayed(Duration.zero); expect(dismissed, isEmpty); await accepted.close(); }, ); }); } /// Облегчённый фейк события канала: ChatsCubit использует только event.data, /// поэтому реальный ChannelReadEvent (требует Channel) не нужен. class _FakeChannelReadEvent extends Fake implements ChannelReadEvent { _FakeChannelReadEvent(this.data); @override final dynamic data; }