feat(video_calls): first impl for main video calls screen

This commit is contained in:
misha
2026-05-05 08:09:24 +03:00
parent 50aefde842
commit 91ad51d340
49 changed files with 2179 additions and 6 deletions

View File

@@ -0,0 +1,115 @@
import 'package:eatvio_app/features/video_calls/models/video_call_models.dart';
import 'package:flutter_test/flutter_test.dart';
import '../../../fixtures/fixture_reader.dart';
void main() {
group('VideoCallTokenResponse', () {
test('создаёт модель с правильными значениями', () {
const model = VideoCallTokenResponse(
token: 'ey-participant-token',
roomName: 'conv-uuid-123',
wsUrl: 'wss://livekit.eatvio.test/conv-uuid-123',
);
expect(model.token, 'ey-participant-token');
expect(model.roomName, 'conv-uuid-123');
expect(model.wsUrl, 'wss://livekit.eatvio.test/conv-uuid-123');
});
test('fromJson корректно десериализует ответ бэкенда', () {
final json = FixtureReader.readAsMap('video_calls/video_call_token_response.json');
final model = VideoCallTokenResponse.fromJson(json);
expect(model.token, 'ey-participant-token');
expect(model.roomName, 'conv-uuid-123');
expect(model.wsUrl, 'wss://livekit.eatvio.test/conv-uuid-123');
});
test('toJson сериализует модель в исходный JSON', () {
final json = FixtureReader.readAsMap('video_calls/video_call_token_response.json');
final model = VideoCallTokenResponse.fromJson(json);
final result = model.toJson();
expect(result['token'], 'ey-participant-token');
expect(result['roomName'], 'conv-uuid-123');
expect(result['wsUrl'], 'wss://livekit.eatvio.test/conv-uuid-123');
});
test('round-trip JSON сохраняет значения', () {
final json = FixtureReader.readAsMap('video_calls/video_call_token_response.json');
final model = VideoCallTokenResponse.fromJson(json);
final restored = VideoCallTokenResponse.fromJson(model.toJson());
expect(restored, model);
});
});
group('VideoCallAdminTokenResponse', () {
test('создаёт модель с правильными значениями', () {
const model = VideoCallAdminTokenResponse(
adminToken: 'ey-admin-token',
roomName: 'conv-uuid-123',
apiUrl: 'https://livekit.eatvio.test',
);
expect(model.adminToken, 'ey-admin-token');
expect(model.roomName, 'conv-uuid-123');
expect(model.apiUrl, 'https://livekit.eatvio.test');
});
test('fromJson корректно десериализует ответ бэкенда', () {
final json = FixtureReader.readAsMap('video_calls/video_call_admin_token_response.json');
final model = VideoCallAdminTokenResponse.fromJson(json);
expect(model.adminToken, 'ey-admin-token');
expect(model.roomName, 'conv-uuid-123');
expect(model.apiUrl, 'https://livekit.eatvio.test');
});
test('round-trip JSON сохраняет значения', () {
final json = FixtureReader.readAsMap('video_calls/video_call_admin_token_response.json');
final model = VideoCallAdminTokenResponse.fromJson(json);
final restored = VideoCallAdminTokenResponse.fromJson(model.toJson());
expect(restored, model);
});
});
group('VideoCallEndedResponse', () {
test('fromJson десериализует sent=true', () {
final json = FixtureReader.readAsMap('video_calls/video_call_ended_response.json');
final model = VideoCallEndedResponse.fromJson(json);
expect(model.sent, isTrue);
});
test('fromJson десериализует sent=false', () {
final model = VideoCallEndedResponse.fromJson(const {'sent': false});
expect(model.sent, isFalse);
});
test('toJson сериализует модель', () {
const model = VideoCallEndedResponse(sent: true);
final json = model.toJson();
expect(json['sent'], isTrue);
});
test('round-trip JSON сохраняет значения', () {
const model = VideoCallEndedResponse(sent: true);
final restored = VideoCallEndedResponse.fromJson(model.toJson());
expect(restored, model);
});
});
}

View File

@@ -0,0 +1,108 @@
import 'package:eatvio_app/features/video_calls/models/video_call_models.dart';
import 'package:eatvio_app/features/video_calls/repositories/video_call_repository.dart';
import 'package:eatvio_app/features/video_calls/services/video_call_service.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import '../../../fixtures/fixture_reader.dart';
import 'video_call_repository_test.mocks.dart';
@GenerateNiceMocks([MockSpec<VideoCallService>()])
void main() {
group('VideoCallRepository', () {
late VideoCallRepository repository;
late MockVideoCallService mockService;
setUp(() {
mockService = MockVideoCallService();
repository = VideoCallRepository(videoCallService: mockService);
});
test('getParticipantToken - делегирует вызов сервису и возвращает токен', () async {
const conversationUuid = 'conv-uuid-123';
final json = FixtureReader.readAsMap('video_calls/video_call_token_response.json');
final expected = VideoCallTokenResponse.fromJson(json);
when(mockService.getParticipantToken(conversationUuid: conversationUuid))
.thenAnswer((_) async => expected);
final result = await repository.getParticipantToken(conversationUuid);
expect(result, isA<VideoCallTokenResponse>());
expect(result.token, expected.token);
expect(result.wsUrl, expected.wsUrl);
expect(result.roomName, expected.roomName);
verify(mockService.getParticipantToken(conversationUuid: conversationUuid)).called(1);
});
test('getParticipantToken - пробрасывает ошибку сервиса', () async {
const conversationUuid = 'conv-uuid-broken';
when(mockService.getParticipantToken(conversationUuid: conversationUuid))
.thenThrow(Exception('No token'));
expect(
() => repository.getParticipantToken(conversationUuid),
throwsA(isA<Exception>()),
);
verify(mockService.getParticipantToken(conversationUuid: conversationUuid)).called(1);
});
test('getAdminToken - делегирует вызов сервису и возвращает админский токен', () async {
const conversationUuid = 'conv-uuid-123';
final json = FixtureReader.readAsMap('video_calls/video_call_admin_token_response.json');
final expected = VideoCallAdminTokenResponse.fromJson(json);
when(mockService.getAdminToken(conversationUuid: conversationUuid))
.thenAnswer((_) async => expected);
final result = await repository.getAdminToken(conversationUuid);
expect(result, isA<VideoCallAdminTokenResponse>());
expect(result.adminToken, expected.adminToken);
expect(result.apiUrl, expected.apiUrl);
verify(mockService.getAdminToken(conversationUuid: conversationUuid)).called(1);
});
test('notifyCallEnded - вызывает сервис с пустым JSON-телом', () async {
const conversationUuid = 'conv-uuid-123';
final json = FixtureReader.readAsMap('video_calls/video_call_ended_response.json');
final expected = VideoCallEndedResponse.fromJson(json);
when(mockService.notifyCallEnded(
conversationUuid: conversationUuid,
body: anyNamed('body'),
)).thenAnswer((_) async => expected);
final result = await repository.notifyCallEnded(conversationUuid);
expect(result, isA<VideoCallEndedResponse>());
expect(result.sent, isTrue);
final captured = verify(
mockService.notifyCallEnded(
conversationUuid: conversationUuid,
body: captureAnyNamed('body'),
),
).captured;
expect(captured, hasLength(1));
expect(captured.single, isA<Map<String, dynamic>>());
expect(captured.single as Map<String, dynamic>, isEmpty);
});
test('notifyCallEnded - пробрасывает ошибку сервиса', () async {
const conversationUuid = 'conv-uuid-broken';
when(mockService.notifyCallEnded(
conversationUuid: conversationUuid,
body: anyNamed('body'),
)).thenThrow(Exception('Backend error'));
expect(
() => repository.notifyCallEnded(conversationUuid),
throwsA(isA<Exception>()),
);
});
});
}

View File

@@ -0,0 +1,94 @@
import 'package:eatvio_app/features/video_calls/models/video_call_models.dart';
import 'package:eatvio_app/features/video_calls/services/video_call_service.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import '../../../fixtures/fixture_reader.dart';
import 'video_call_service_test.mocks.dart';
@GenerateNiceMocks([MockSpec<VideoCallService>()])
void main() {
group('VideoCallService', () {
late MockVideoCallService mockService;
setUp(() {
mockService = MockVideoCallService();
});
test('getParticipantToken - возвращает токен и WebSocket URL', () async {
const conversationUuid = 'conv-uuid-123';
final json = FixtureReader.readAsMap('video_calls/video_call_token_response.json');
final expected = VideoCallTokenResponse.fromJson(json);
when(mockService.getParticipantToken(conversationUuid: conversationUuid))
.thenAnswer((_) async => expected);
final result = await mockService.getParticipantToken(conversationUuid: conversationUuid);
expect(result, isA<VideoCallTokenResponse>());
expect(result.token, expected.token);
expect(result.wsUrl, expected.wsUrl);
expect(result.roomName, expected.roomName);
verify(mockService.getParticipantToken(conversationUuid: conversationUuid)).called(1);
});
test('getParticipantToken - пробрасывает ошибку', () async {
const conversationUuid = 'conv-uuid-error';
when(mockService.getParticipantToken(conversationUuid: conversationUuid))
.thenThrow(Exception('Network error'));
expect(
() => mockService.getParticipantToken(conversationUuid: conversationUuid),
throwsA(isA<Exception>()),
);
});
test('getAdminToken - возвращает админский токен и API URL', () async {
const conversationUuid = 'conv-uuid-123';
final json = FixtureReader.readAsMap('video_calls/video_call_admin_token_response.json');
final expected = VideoCallAdminTokenResponse.fromJson(json);
when(mockService.getAdminToken(conversationUuid: conversationUuid))
.thenAnswer((_) async => expected);
final result = await mockService.getAdminToken(conversationUuid: conversationUuid);
expect(result, isA<VideoCallAdminTokenResponse>());
expect(result.adminToken, expected.adminToken);
expect(result.apiUrl, expected.apiUrl);
expect(result.roomName, expected.roomName);
verify(mockService.getAdminToken(conversationUuid: conversationUuid)).called(1);
});
test('notifyCallEnded - отправляет пустое тело и получает sent=true', () async {
const conversationUuid = 'conv-uuid-123';
const body = <String, dynamic>{};
final json = FixtureReader.readAsMap('video_calls/video_call_ended_response.json');
final expected = VideoCallEndedResponse.fromJson(json);
when(mockService.notifyCallEnded(conversationUuid: conversationUuid, body: body))
.thenAnswer((_) async => expected);
final result = await mockService.notifyCallEnded(conversationUuid: conversationUuid, body: body);
expect(result, isA<VideoCallEndedResponse>());
expect(result.sent, isTrue);
verify(mockService.notifyCallEnded(conversationUuid: conversationUuid, body: body)).called(1);
});
test('notifyCallEnded - пробрасывает ошибку', () async {
const conversationUuid = 'conv-uuid-error';
const body = <String, dynamic>{};
when(mockService.notifyCallEnded(conversationUuid: conversationUuid, body: body))
.thenThrow(Exception('Backend error'));
expect(
() => mockService.notifyCallEnded(conversationUuid: conversationUuid, body: body),
throwsA(isA<Exception>()),
);
});
});
}

View File

@@ -0,0 +1,14 @@
import 'package:eatvio_app/features/video_calls/presentation/widgets/video_call_livekit_track_utils.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('VideoCallLiveKitTrackUtils', () {
test('asVideoTrack возвращает null для null-трека', () {
expect(VideoCallLiveKitTrackUtils.asVideoTrack(null), isNull);
});
test('visibleVideoFromPublication возвращает null для null-публикации', () {
expect(VideoCallLiveKitTrackUtils.visibleVideoFromPublication(null), isNull);
});
});
}