tests and mocks for e2e testing (#78)

* tests and mocks for e2e testing

* formatting

* fix analyze errors
This commit is contained in:
davidliu
2022-02-06 20:36:42 +09:00
committed by GitHub
parent 0d92c569df
commit 9bd546b9a2
22 changed files with 799 additions and 34 deletions
+2
View File
@@ -158,6 +158,7 @@ class _LocalParticipantWidgetState
_visible) {
return firstVideoPublication?.track;
}
return null;
}
}
@@ -180,6 +181,7 @@ class _RemoteParticipantWidgetState
return trackPublication.track;
}
}
return null;
}
@override
+8 -1
View File
@@ -240,6 +240,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.12.11"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.3"
meta:
dependency: transitive
description:
@@ -475,7 +482,7 @@ packages:
name: test_api
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.3"
version: "0.4.8"
typed_data:
dependency: transitive
description:
+17 -10
View File
@@ -3,6 +3,7 @@ import 'dart:async';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc;
import 'package:livekit_client/src/support/websocket.dart';
import 'package:meta/meta.dart';
import '../constants.dart';
@@ -28,11 +29,10 @@ class Engine extends Disposable with EventsEmittable<EngineEvent> {
static const _lossyDCLabel = '_lossy';
static const _reliableDCLabel = '_reliable';
// Reference to the Room
final Room room;
final SignalClient signalClient;
final PeerConnectionCreate _peerConnectionCreate;
@internal
PCTransport? publisher;
@@ -60,6 +60,7 @@ class Engine extends Disposable with EventsEmittable<EngineEvent> {
// remember url and token for reconnect
String? url;
String? token;
ConnectOptions? connectOptions;
bool _subscriberPrimary = false;
@@ -71,9 +72,11 @@ class Engine extends Disposable with EventsEmittable<EngineEvent> {
final delays = CancelableDelayManager();
Engine({
required this.room,
SignalClient? signalClient,
}) : signalClient = signalClient ?? SignalClient() {
PeerConnectionCreate? peerConnectionCreate,
}) : signalClient = signalClient ?? SignalClient(LiveKitWebSocket.connect),
_peerConnectionCreate =
peerConnectionCreate ?? rtc.createPeerConnection {
if (kDebugMode) {
// log all EngineEvents
events.listen((event) => logger.fine('[EngineEvent] $objectId ${event}'));
@@ -92,9 +95,11 @@ class Engine extends Disposable with EventsEmittable<EngineEvent> {
Future<void> connect(
String url,
String token,
ConnectOptions? connectOptions,
) async {
this.url = url;
this.token = token;
this.connectOptions = connectOptions ?? const ConnectOptions();
_updateConnectionState(ConnectionState.connecting);
@@ -103,7 +108,7 @@ class Engine extends Disposable with EventsEmittable<EngineEvent> {
await signalClient.connect(
url,
token,
connectOptions: room.connectOptions,
connectOptions: this.connectOptions,
);
// wait for join response
@@ -272,7 +277,7 @@ class Engine extends Disposable with EventsEmittable<EngineEvent> {
await signalClient.connect(
url!,
token!,
connectOptions: room.connectOptions,
connectOptions: connectOptions,
reconnect: true,
);
@@ -331,7 +336,7 @@ class Engine extends Disposable with EventsEmittable<EngineEvent> {
// RTCConfiguration? config;
// use server-provided iceServers if not provided by user
final connectOptions = room.connectOptions ?? const ConnectOptions();
final connectOptions = this.connectOptions ?? const ConnectOptions();
final serverIceServers =
_serverProvidedIceServers.map((e) => e.toSDKType()).toList();
@@ -342,8 +347,10 @@ class Engine extends Disposable with EventsEmittable<EngineEvent> {
.copyWith(iceServers: serverIceServers);
}
publisher = await PCTransport.create(rtcConfiguration);
subscriber = await PCTransport.create(rtcConfiguration);
publisher =
await PCTransport.create(_peerConnectionCreate, rtcConfiguration);
subscriber =
await PCTransport.create(_peerConnectionCreate, rtcConfiguration);
publisher?.pc.onIceCandidate = (rtc.RTCIceCandidate candidate) {
logger.fine('publisher onIceCandidate');
+7 -12
View File
@@ -62,16 +62,14 @@ class Room extends DisposableChangeNotifier with EventsEmittable<RoomEvent> {
UnmodifiableListView<Participant> get activeSpeakers =>
UnmodifiableListView<Participant>(_activeSpeakers);
late final engine = Engine(room: this);
final Engine engine;
// suppport for multiple event listeners
late final _engineListener = engine.createListener();
late final EventsListener<EngineEvent> _engineListener;
Room({
this.connectOptions,
this.roomOptions,
}) {
//
Room({this.connectOptions, this.roomOptions, Engine? engine})
: engine = engine ?? Engine() {
_engineListener = this.engine.createListener();
_setUpListeners();
// Any event emitted will trigger ChangeNotifier
@@ -88,7 +86,7 @@ class Room extends DisposableChangeNotifier with EventsEmittable<RoomEvent> {
// dispose all listeners for RTCEngine
await _engineListener.dispose();
// dispose the engine
await engine.dispose();
await this.engine.dispose();
});
}
@@ -102,10 +100,7 @@ class Room extends DisposableChangeNotifier with EventsEmittable<RoomEvent> {
this.connectOptions = connectOptions ?? this.connectOptions;
this.roomOptions = roomOptions ?? this.roomOptions;
return engine.connect(
url,
token,
);
return engine.connect(url, token, this.connectOptions);
}
void _setUpListeners() => _engineListener
+9 -4
View File
@@ -22,10 +22,11 @@ class SignalClient extends Disposable with EventsEmittable<SignalEvent> {
// Connection state of the socket conection.
ConnectionState _connectionState = ConnectionState.disconnected;
final WebSocketConnector _wsConnector;
LiveKitWebSocket? _ws;
@internal
SignalClient() {
SignalClient(WebSocketConnector wsConnector) : _wsConnector = wsConnector {
events.listen((event) {
logger.fine('[SignalEvent] $event');
});
@@ -59,7 +60,7 @@ class SignalClient extends Disposable with EventsEmittable<SignalEvent> {
// Clean up existing socket
await _cleanUp();
// Attempt to connect
_ws = await LiveKitWebSocket.connect(
_ws = await _wsConnector(
rtcUri,
WebSocketEventHandlers(
onData: _onSocketData,
@@ -174,6 +175,9 @@ class SignalClient extends Disposable with EventsEmittable<SignalEvent> {
events.emit(
SignalSpeakersChangedEvent(speakers: msg.speakersChanged.speakers));
break;
case lk_rtc.SignalResponse_Message.roomUpdate:
// TODO
break;
case lk_rtc.SignalResponse_Message.connectionQuality:
events.emit(SignalConnectionQualityUpdateEvent(
updates: msg.connectionQuality.updates,
@@ -209,8 +213,9 @@ class SignalClient extends Disposable with EventsEmittable<SignalEvent> {
case lk_rtc.SignalResponse_Message.refreshToken:
events.emit(SignalTokenUpdatedEvent(token: msg.refreshToken));
break;
default:
logger.warning('skipping unsupported signal message');
case lk_rtc.SignalResponse_Message.notSet:
logger.info('signal message not set');
break;
}
}
+7 -2
View File
@@ -11,6 +11,9 @@ import '../types.dart';
import '../utils.dart';
typedef PCTransportOnOffer = void Function(rtc.RTCSessionDescription offer);
typedef PeerConnectionCreate = Future<rtc.RTCPeerConnection> Function(
Map<String, dynamic> configuration,
[Map<String, dynamic> constraints]);
/// a wrapper around PeerConnection
class PCTransport extends Disposable {
@@ -55,10 +58,11 @@ class PCTransport extends Disposable {
});
}
static Future<PCTransport> create([RTCConfiguration? rtcConfig]) async {
static Future<PCTransport> create(PeerConnectionCreate peerConnectionCreate,
[RTCConfiguration? rtcConfig]) async {
rtcConfig ??= const RTCConfiguration();
logger.fine('[PCTransport] creating ${rtcConfig.toMap()}');
final _ = await rtc.createPeerConnection(rtcConfig.toMap());
final _ = await peerConnectionCreate(rtcConfig.toMap());
return PCTransport._(_);
}
@@ -159,5 +163,6 @@ class PCTransport extends Disposable {
} catch (_) {
logger.warning('pc.getRemoteDescription failed with error: $_');
}
return null;
}
}
+1
View File
@@ -305,6 +305,7 @@ class LocalParticipant extends Participant<LocalTrackPublication> {
return await publishVideoTrack(track);
}
}
return null;
}
/// Control who can subscribe to LocalParticipant's published tracks.
+1
View File
@@ -65,6 +65,7 @@ class RemoteParticipant extends Participant<RemoteTrackPublication> {
RemoteTrackPublication? getTrackPublication(String sid) {
final pub = trackPublications[sid];
if (pub is RemoteTrackPublication) return pub;
return null;
}
/// for internal use
+1
View File
@@ -36,5 +36,6 @@ class Native {
} catch (error) {
logger.warning('appleOSVersionString did throw error: ${error}');
}
return null;
}
}
+8
View File
@@ -1,9 +1,17 @@
import 'dart:io';
import 'package:meta/meta.dart';
import 'platform/io.dart' if (dart.library.html) 'platform/web.dart';
// Returns the current platform which works for both web and devices.
PlatformType lkPlatform() => lkPlatformImplementation();
bool lkPlatformIs(PlatformType type) => lkPlatform() == type;
@internal
bool lkPlatformIsTest() => Platform.environment.containsKey('FLUTTER_TEST');
enum PlatformType {
web,
windows,
+6 -4
View File
@@ -23,6 +23,7 @@ class WebSocketEventHandlers {
final WebSocketOnData? onData;
final WebSocketOnError? onError;
final WebSocketOnDispose? onDispose;
const WebSocketEventHandlers({
this.onData,
this.onError,
@@ -30,12 +31,13 @@ class WebSocketEventHandlers {
});
}
typedef WebSocketConnector = Future<LiveKitWebSocket> Function(Uri uri,
[WebSocketEventHandlers? options]);
abstract class LiveKitWebSocket extends Disposable {
void send(List<int> data);
static Future<LiveKitWebSocket> connect(
Uri uri, [
WebSocketEventHandlers? options,
]) =>
static Future<LiveKitWebSocket> connect(Uri uri,
[WebSocketEventHandlers? options]) =>
lkWebSocketConnect(uri, options);
}
+6
View File
@@ -65,6 +65,11 @@ class Utils {
static final _deviceInfoPlugin = DeviceInfoPlugin();
static Future<lk_models.ClientInfo?> _clientInfo() async {
if (lkPlatformIsTest()) {
return lk_models.ClientInfo(
os: 'test',
);
}
switch (lkPlatform()) {
case PlatformType.web:
return lk_models.ClientInfo(
@@ -123,6 +128,7 @@ class Utils {
default:
// case PlatformType.fuchsia:
}
return null;
}
@internal
+113 -1
View File
@@ -1,6 +1,20 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
_fe_analyzer_shared:
dependency: transitive
description:
name: _fe_analyzer_shared
url: "https://pub.dartlang.org"
source: hosted
version: "34.0.0"
analyzer:
dependency: transitive
description:
name: analyzer
url: "https://pub.dartlang.org"
source: hosted
version: "3.2.0"
args:
dependency: transitive
description:
@@ -22,6 +36,27 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
build:
dependency: transitive
description:
name: build
url: "https://pub.dartlang.org"
source: hosted
version: "2.2.1"
built_collection:
dependency: transitive
description:
name: built_collection
url: "https://pub.dartlang.org"
source: hosted
version: "5.1.1"
built_value:
dependency: transitive
description:
name: built_value
url: "https://pub.dartlang.org"
source: hosted
version: "8.1.4"
characters:
dependency: transitive
description:
@@ -36,6 +71,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.3.1"
cli_util:
dependency: transitive
description:
name: cli_util
url: "https://pub.dartlang.org"
source: hosted
version: "0.3.5"
clock:
dependency: transitive
description:
@@ -43,6 +85,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.0"
code_builder:
dependency: transitive
description:
name: code_builder
url: "https://pub.dartlang.org"
source: hosted
version: "4.1.0"
collection:
dependency: "direct main"
description:
@@ -50,6 +99,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.15.0"
convert:
dependency: transitive
description:
name: convert
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.1"
crypto:
dependency: transitive
description:
@@ -57,6 +113,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "3.0.1"
dart_style:
dependency: transitive
description:
name: dart_style
url: "https://pub.dartlang.org"
source: hosted
version: "2.2.1"
dart_webrtc:
dependency: "direct main"
description:
@@ -163,6 +226,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.8.1"
glob:
dependency: transitive
description:
name: glob
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.2"
http:
dependency: "direct main"
description:
@@ -212,6 +282,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "0.12.11"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.3"
meta:
dependency: "direct main"
description:
@@ -219,6 +296,20 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.7.0"
mockito:
dependency: "direct dev"
description:
name: mockito
url: "https://pub.dartlang.org"
source: hosted
version: "5.0.17"
package_config:
dependency: transitive
description:
name: package_config
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.2"
path:
dependency: transitive
description:
@@ -303,11 +394,25 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.0.1"
pub_semver:
dependency: transitive
description:
name: pub_semver
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.0"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.99"
source_gen:
dependency: transitive
description:
name: source_gen
url: "https://pub.dartlang.org"
source: hosted
version: "1.2.1"
source_span:
dependency: transitive
description:
@@ -356,7 +461,7 @@ packages:
name: test_api
url: "https://pub.dartlang.org"
source: hosted
version: "0.4.3"
version: "0.4.8"
tint:
dependency: transitive
description:
@@ -385,6 +490,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.1.1"
watcher:
dependency: transitive
description:
name: watcher
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.1"
webrtc_interface:
dependency: transitive
description:
+1
View File
@@ -30,6 +30,7 @@ dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^1.0.4
mockito: ^5.0.17
import_sorter: ^4.6.0
flutter:
+125
View File
@@ -0,0 +1,125 @@
@Timeout(Duration(seconds: 5))
import 'package:flutter_test/flutter_test.dart';
import 'package:livekit_client/livekit_client.dart';
import '../mock/e2e_container.dart';
import '../mock/test_data.dart';
import '../mock/websocket_mock.dart';
import 'signal_client_test.dart';
void main() {
late E2EContainer container;
late Room room;
late MockWebSocketConnector ws;
setUp(() async {
container = E2EContainer();
room = container.room;
ws = container.wsConnector;
await container.connectRoom();
});
tearDown(() async {
await container.dispose();
});
group('connection', () {
test('disconnect and connect', () async {
await room.disconnect();
await container.connectRoom();
expect(room.connectionState, ConnectionState.connected);
expect(room.localParticipant?.sid, joinResponse.join.participant.sid);
}, skip: 'todo');
});
group('room updates', () {
test('participant join', () async {
expect(
room.events.streamCtrl.stream,
emits(predicate<ParticipantConnectedEvent>(
(event) => event.participant.sid == remoteParticipantData.sid,
)),
);
ws.onData(participantJoinResponse.writeToBuffer());
await room.events.waitFor<ParticipantConnectedEvent>(
duration: const Duration(seconds: 1));
expect(room.participants.length, 1);
});
test('participant disconnect', () async {
ws.onData(participantJoinResponse.writeToBuffer());
await room.events.waitFor<ParticipantConnectedEvent>(
duration: const Duration(seconds: 1));
ws.onData(participantDisconnectResponse.writeToBuffer());
expect(
room.events.streamCtrl.stream,
emitsInOrder(<Matcher>[
predicate<TrackUnpublishedEvent>(
(event) => event.participant.sid == remoteParticipantData.sid),
predicate<ParticipantDisconnectedEvent>(
(event) => event.participant.sid == remoteParticipantData.sid),
]),
);
await room.events.waitFor<ParticipantDisconnectedEvent>(
duration: const Duration(seconds: 1));
expect(room.participants.length, 0);
});
test('participant metadata changed', () async {
ws.onData(participantJoinResponse.writeToBuffer());
await room.events.waitFor<ParticipantConnectedEvent>(
duration: const Duration(seconds: 1));
ws.onData(participantMetadataChangedResponse.writeToBuffer());
expect(
room.events.streamCtrl.stream,
emits(
predicate<ParticipantMetadataUpdatedEvent>((event) =>
event.participant.metadata ==
participantMetadataChangedResponse
.update.participants[0].metadata),
),
);
});
test('room update', () async {
ws.onData(roomUpdateResponse.writeToBuffer());
// TODO: room update event and handling
}, skip: 'todo');
test('connection quality', () async {
expect(
room.events.streamCtrl.stream,
emits(predicate<ParticipantConnectionQualityUpdatedEvent>((event) =>
event.participant.sid == localParticipantData.sid &&
event.connectionQuality == ConnectionQuality.excellent)),
);
ws.onData(connectionQualityResponse.writeToBuffer());
});
test('active speakers changed', () async {
ws.onData(participantJoinResponse.writeToBuffer());
await room.events.waitFor<ParticipantConnectedEvent>(
duration: const Duration(seconds: 1));
expect(
room.events.streamCtrl.stream,
emits(
predicate<ActiveSpeakersChangedEvent>(
(event) => event.speakers[0].sid == remoteParticipantData.sid),
),
);
ws.onData(activeSpeakerResponse.writeToBuffer());
});
test('leave', () async {
expect(
room.events.streamCtrl.stream,
emits(predicate<RoomDisconnectedEvent>((event) =>
room.connectionState == ConnectionState.disconnected)));
ws.onData(leaveResponse.writeToBuffer());
});
});
}
+124
View File
@@ -0,0 +1,124 @@
@Timeout(Duration(seconds: 5))
import 'package:flutter_test/flutter_test.dart';
import 'package:livekit_client/livekit_client.dart';
import 'package:livekit_client/src/core/signal_client.dart';
import 'package:livekit_client/src/internal/events.dart';
import 'package:livekit_client/src/proto/livekit_models.pb.dart' as lk_models;
import 'package:livekit_client/src/proto/livekit_rtc.pb.dart' as lk_rtc;
import 'package:protobuf/protobuf.dart';
import '../mock/test_data.dart';
import '../mock/websocket_mock.dart';
void main() {
late SignalClient client;
late MockWebSocketConnector connector;
setUp(() async {
connector = MockWebSocketConnector();
client = SignalClient(connector.connect);
});
group('connection', () {
test('connect', () async {
expect(
client.events.streamCtrl.stream,
emitsInOrder(<Matcher>[
predicate<SignalConnectionStateUpdatedEvent>(
(event) => event.connectionState == ConnectionState.connecting),
predicate<SignalConnectionStateUpdatedEvent>(
(event) => event.connectionState == ConnectionState.connected),
]));
await client.connect(exampleUri, token);
});
test('reconnect', () async {
expect(
client.events.streamCtrl.stream,
emitsInOrder(<Matcher>[
predicate<SignalConnectionStateUpdatedEvent>((event) =>
event.connectionState == ConnectionState.reconnecting),
predicate<SignalConnectionStateUpdatedEvent>((event) =>
event.connectionState == ConnectionState.connected &&
event.didReconnect == true),
]));
await client.connect(exampleUri, token, reconnect: true);
});
});
group('messaging', () {
test('join', () async {
await client.connect(exampleUri, token);
expect(client.events.streamCtrl.stream,
emits(isA<SignalJoinResponseEvent>()));
connector.handlers?.onData!(joinResponse.writeToBuffer());
});
});
}
final lk_rtc.SignalResponse joinResponse = lk_rtc.SignalResponse(
join: lk_rtc.JoinResponse(
room: lk_models.Room(
name: 'room_name',
sid: 'room_sid',
),
participant: localParticipantData,
subscriberPrimary: true,
serverVersion: '99.999',
),
);
final lk_rtc.SignalResponse offerResponse = lk_rtc.SignalResponse(
offer: lk_rtc.SessionDescription(
sdp: 'remote_offer',
type: 'offer',
));
final lk_rtc.SignalResponse participantJoinResponse = lk_rtc.SignalResponse(
update: lk_rtc.ParticipantUpdate(
participants: [remoteParticipantData],
),
);
final lk_rtc.SignalResponse participantDisconnectResponse =
lk_rtc.SignalResponse(
update: lk_rtc.ParticipantUpdate(
participants: [
remoteParticipantData.deepCopy()
..state = lk_models.ParticipantInfo_State.DISCONNECTED,
],
),
);
final lk_rtc.SignalResponse participantMetadataChangedResponse =
lk_rtc.SignalResponse(
update: lk_rtc.ParticipantUpdate(
participants: [
remoteParticipantData.deepCopy()..metadata = 'metadata_changed',
],
),
);
final lk_rtc.SignalResponse roomUpdateResponse = lk_rtc.SignalResponse(
roomUpdate: lk_rtc.RoomUpdate(
room: lk_models.Room(
metadata: 'changed_metadata',
),
),
);
final lk_rtc.SignalResponse connectionQualityResponse = lk_rtc.SignalResponse(
connectionQuality: lk_rtc.ConnectionQualityUpdate(updates: [
lk_rtc.ConnectionQualityInfo(
participantSid: localParticipantData.sid,
quality: lk_models.ConnectionQuality.EXCELLENT,
)
]),
);
final lk_rtc.SignalResponse activeSpeakerResponse = lk_rtc.SignalResponse(
speakersChanged: lk_rtc.SpeakersChanged(
speakers: [remoteSpeakerInfo],
),
);
final lk_rtc.SignalResponse leaveResponse =
lk_rtc.SignalResponse(leave: lk_rtc.LeaveRequest());
const exampleUri = 'ws://www.example.com';
const token = 'token';
+23
View File
@@ -0,0 +1,23 @@
import 'package:flutter_webrtc/flutter_webrtc.dart';
class MockDataChannel extends RTCDataChannel {
final String? _label;
RTCDataChannelState? _state = RTCDataChannelState.RTCDataChannelOpen;
MockDataChannel(this._label);
@override
String? get label => _label;
@override
Future<void> send(RTCDataChannelMessage message) async {}
@override
RTCDataChannelState? get state => _state;
@override
Future<void> close() async {
_state = RTCDataChannelState.RTCDataChannelClosing;
_state = RTCDataChannelState.RTCDataChannelClosed;
}
}
+35
View File
@@ -0,0 +1,35 @@
import 'package:livekit_client/livekit_client.dart';
import 'package:livekit_client/src/core/engine.dart';
import 'package:livekit_client/src/core/signal_client.dart';
import '../core/signal_client_test.dart';
import 'peerconnection_mock.dart';
import 'websocket_mock.dart';
class E2EContainer {
late MockWebSocketConnector wsConnector;
late SignalClient client;
late Room room;
late Engine engine;
E2EContainer() {
wsConnector = MockWebSocketConnector();
client = SignalClient(wsConnector.connect);
engine = Engine(
signalClient: client, peerConnectionCreate: MockPeerConnection.create);
room = Room(engine: engine);
}
Future<void> dispose() async {
await room.dispose();
}
Future<void> connectRoom() async {
final connectFuture = room.connect(exampleUri, token);
Future.delayed(const Duration(milliseconds: 1), () {
wsConnector.onData(joinResponse.writeToBuffer());
wsConnector.onData(offerResponse.writeToBuffer());
});
await connectFuture;
}
}
+25
View File
@@ -0,0 +1,25 @@
@Timeout(Duration(seconds: 5))
import 'package:flutter_test/flutter_test.dart';
import 'package:livekit_client/livekit_client.dart';
import '../core/signal_client_test.dart';
import '../mock/e2e_container.dart';
void main() {
late E2EContainer container;
late Room room;
setUp(() async {
container = E2EContainer();
room = container.room;
});
tearDown(() async {
await container.dispose();
});
test('connect', () async {
await container.connectRoom();
expect(room.connectionState, ConnectionState.connected);
expect(room.localParticipant?.sid, joinResponse.join.participant.sid);
});
}
+227
View File
@@ -0,0 +1,227 @@
import 'package:flutter_webrtc/flutter_webrtc.dart';
import 'datachannel_mock.dart';
class MockPeerConnection extends RTCPeerConnection {
static const _offerType = 'offer';
static const _answerType = 'answer';
bool closed = false;
RTCSessionDescription? _localDescription;
RTCSessionDescription? _remoteDescription;
RTCPeerConnectionState _connectionState =
RTCPeerConnectionState.RTCPeerConnectionStateNew;
RTCIceConnectionState _iceConnectionState =
RTCIceConnectionState.RTCIceConnectionStateNew;
RTCIceGatheringState _iceGatheringState =
RTCIceGatheringState.RTCIceGatheringStateNew;
@override
Future<RTCSessionDescription?> getLocalDescription() async =>
_localDescription;
@override
Future<void> setLocalDescription(RTCSessionDescription description) async {
_localDescription = description;
_handleIceConnection();
}
@override
Future<RTCSessionDescription?> getRemoteDescription() async =>
_remoteDescription;
@override
Future<void> setRemoteDescription(RTCSessionDescription description) async {
_remoteDescription = description;
_handleIceConnection();
}
void _handleIceConnection() {
if ((_localDescription?.type == _offerType &&
_remoteDescription?.type == _answerType) ||
(_localDescription?.type == _answerType &&
_remoteDescription?.type == _offerType)) {
iceConnectionState = RTCIceConnectionState.RTCIceConnectionStateCompleted;
}
}
@override
RTCPeerConnectionState get connectionState => _connectionState;
set connectionState(RTCPeerConnectionState newState) {
if (newState != _connectionState) {
_connectionState = newState;
onConnectionState?.call(newState);
}
}
@override
RTCIceConnectionState get iceConnectionState => _iceConnectionState;
set iceConnectionState(RTCIceConnectionState newState) {
if (newState != _iceConnectionState) {
_iceConnectionState = newState;
onIceConnectionState?.call(newState);
switch (newState) {
case RTCIceConnectionState.RTCIceConnectionStateNew:
connectionState = RTCPeerConnectionState.RTCPeerConnectionStateNew;
break;
case RTCIceConnectionState.RTCIceConnectionStateChecking:
connectionState =
RTCPeerConnectionState.RTCPeerConnectionStateConnecting;
break;
case RTCIceConnectionState.RTCIceConnectionStateConnected:
case RTCIceConnectionState.RTCIceConnectionStateCompleted:
connectionState =
RTCPeerConnectionState.RTCPeerConnectionStateConnected;
break;
case RTCIceConnectionState.RTCIceConnectionStateFailed:
connectionState = RTCPeerConnectionState.RTCPeerConnectionStateFailed;
break;
case RTCIceConnectionState.RTCIceConnectionStateDisconnected:
connectionState =
RTCPeerConnectionState.RTCPeerConnectionStateDisconnected;
break;
case RTCIceConnectionState.RTCIceConnectionStateClosed:
connectionState = RTCPeerConnectionState.RTCPeerConnectionStateClosed;
break;
case RTCIceConnectionState.RTCIceConnectionStateCount:
throw Exception("This state shouldn't exist (not in RFC).");
}
}
}
@override
RTCIceGatheringState get iceGatheringState => _iceGatheringState;
set iceGatheringState(RTCIceGatheringState newState) {
if (newState != _iceGatheringState) {
_iceGatheringState = newState;
onIceGatheringState?.call(newState);
}
}
@override
Future<void> addCandidate(RTCIceCandidate candidate) async {}
@override
Future<void> addStream(MediaStream stream) async {}
@override
Future<RTCRtpSender> addTrack(MediaStreamTrack track,
[MediaStream? stream]) async {
// TODO: implement addTrack
throw UnimplementedError();
}
@override
Future<RTCRtpTransceiver> addTransceiver(
{MediaStreamTrack? track,
RTCRtpMediaType? kind,
RTCRtpTransceiverInit? init}) {
// TODO: implement addTransceiver
throw UnimplementedError();
}
@override
Future<void> close() async {
closed = true;
}
@override
Future<RTCSessionDescription> createAnswer(
[Map<String, dynamic>? constraints]) async {
return RTCSessionDescription('local_answer', 'answer');
}
@override
Future<RTCSessionDescription> createOffer(
[Map<String, dynamic>? constraints]) async {
return RTCSessionDescription('local_offer', 'offer');
}
@override
RTCSignalingState? get signalingState {
if (closed) {
return RTCSignalingState.RTCSignalingStateClosed;
}
if ((_localDescription?.type == null && _remoteDescription?.type == null) ||
(_localDescription?.type == _offerType &&
_remoteDescription?.type == _answerType) ||
(_localDescription?.type == _answerType &&
_remoteDescription?.type == _offerType)) {
return RTCSignalingState.RTCSignalingStateStable;
}
if (_localDescription?.type == _offerType &&
_remoteDescription?.type == null) {
return RTCSignalingState.RTCSignalingStateHaveLocalOffer;
}
if (_remoteDescription?.type == _offerType &&
_localDescription?.type == null) {
return RTCSignalingState.RTCSignalingStateHaveRemoteOffer;
}
throw Exception(
'Illegal signalling state? localDesc: $_localDescription, remoteDesc: $_remoteDescription');
}
@override
Future<RTCDataChannel> createDataChannel(
String label, RTCDataChannelInit dataChannelDict) async {
return MockDataChannel(label);
}
@override
RTCDTMFSender createDtmfSender(MediaStreamTrack track) {
// TODO: implement createDtmfSender
throw UnimplementedError();
}
@override
Future<void> dispose() async {
await close();
}
@override
// TODO: implement getConfiguration
Map<String, dynamic> get getConfiguration => throw UnimplementedError();
@override
List<MediaStream?> getLocalStreams() => List.empty();
@override
Future<List<RTCRtpReceiver>> getReceivers() async => List.empty();
@override
List<MediaStream?> getRemoteStreams() => List.empty();
@override
Future<List<RTCRtpSender>> getSenders() async => List.empty();
@override
Future<List<StatsReport>> getStats([MediaStreamTrack? track]) async =>
List.empty();
@override
Future<List<RTCRtpTransceiver>> getTransceivers() async => List.empty();
@override
Future<void> removeStream(MediaStream stream) async {}
@override
Future<bool> removeTrack(RTCRtpSender sender) async => true;
@override
Future<void> setConfiguration(Map<String, dynamic> configuration) {
// TODO: implement setConfiguration
throw UnimplementedError();
}
static Future<RTCPeerConnection> create(Map<String, dynamic> configuration,
[Map<String, dynamic>? constraints]) async =>
MockPeerConnection();
}
+31
View File
@@ -0,0 +1,31 @@
import 'package:livekit_client/livekit_client.dart';
import 'package:livekit_client/src/proto/livekit_models.pb.dart' as lk_models;
final localAudioTrack = lk_models.TrackInfo(
sid: 'local_audio_track_sid',
type: TrackType.AUDIO,
);
final remoteAudioTrack = lk_models.TrackInfo(
sid: 'remote_audio_track_sid',
type: TrackType.AUDIO,
);
final localParticipantData = lk_models.ParticipantInfo(
sid: 'local_participant_sid',
identity: 'local_participant_identity',
state: lk_models.ParticipantInfo_State.ACTIVE,
);
final remoteParticipantData = lk_models.ParticipantInfo(
sid: 'remote_participant_sid',
identity: 'remote_participant_identity',
state: lk_models.ParticipantInfo_State.ACTIVE,
tracks: [remoteAudioTrack],
);
final remoteSpeakerInfo = lk_models.SpeakerInfo(
sid: remoteParticipantData.sid,
level: 1.0,
active: true,
);
+22
View File
@@ -0,0 +1,22 @@
import 'package:livekit_client/src/support/websocket.dart';
class MockWebSocket extends LiveKitWebSocket {
@override
void send(List<int> data) {}
}
class MockWebSocketConnector {
WebSocketEventHandlers? handlers;
WebSocketOnData get onData => handlers!.onData!;
WebSocketOnDispose get onDispose => handlers!.onDispose!;
WebSocketOnError get onError => handlers!.onError!;
Future<LiveKitWebSocket> connect(Uri uri,
[WebSocketEventHandlers? options]) async {
handlers = options;
return MockWebSocket();
}
}