Release IKAR 1.0.8: fix incoming calls, cancellation and call history
Build Android (FCM) / build-android-fcm (push) Canceled after 0s
Build Android / build-android (push) Canceled after 0s
Build iOS / build-ios (push) Canceled after 0s
Build Linux / build-linux (push) Canceled after 0s
Build macOS / build-macos (push) Canceled after 0s
Build Windows / build-windows (push) Canceled after 0s
Native crypto / native-crypto (push) Canceled after 0s
Release (main) / android (oneme) (push) Canceled after 0s
Release (main) / android (qlyra) (push) Canceled after 0s
Release (main) / windows (push) Canceled after 0s
Release (main) / linux (push) Canceled after 0s
Release (main) / macos (push) Canceled after 0s
Release (main) / ios (push) Canceled after 0s
Tests / test (push) Canceled after 0s
Release (main) / release (push) Canceled after 0s
Build Android (FCM) / build-android-fcm (push) Canceled after 0s
Build Android / build-android (push) Canceled after 0s
Build iOS / build-ios (push) Canceled after 0s
Build Linux / build-linux (push) Canceled after 0s
Build macOS / build-macos (push) Canceled after 0s
Build Windows / build-windows (push) Canceled after 0s
Native crypto / native-crypto (push) Canceled after 0s
Release (main) / android (oneme) (push) Canceled after 0s
Release (main) / android (qlyra) (push) Canceled after 0s
Release (main) / windows (push) Canceled after 0s
Release (main) / linux (push) Canceled after 0s
Release (main) / macos (push) Canceled after 0s
Release (main) / ios (push) Canceled after 0s
Tests / test (push) Canceled after 0s
Release (main) / release (push) Canceled after 0s
This commit is contained in:
@@ -3,6 +3,7 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:ikar/core/security/app_lock.dart';
|
||||
import 'package:ikar/frontend/screens/lock/app_lock_layer.dart';
|
||||
import 'package:ikar/frontend/screens/lock/passcode_pad.dart';
|
||||
import 'package:ikar/l10n/app_localizations.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
@@ -23,9 +24,18 @@ Future<void> _enter(WidgetTester tester, String pin) async {
|
||||
await tester.tap(find.text(digit));
|
||||
await tester.pump();
|
||||
}
|
||||
await tester.runAsync(
|
||||
() => Future<void>.delayed(const Duration(milliseconds: 400)),
|
||||
);
|
||||
// Wait for password verification, which can take longer under build load.
|
||||
for (var attempt = 0; attempt < 100; attempt++) {
|
||||
final keypad = find.byType(PasscodeKeypad);
|
||||
if (keypad.evaluate().isEmpty ||
|
||||
tester.widget<PasscodeKeypad>(keypad).enabled) {
|
||||
break;
|
||||
}
|
||||
await tester.runAsync(
|
||||
() => Future<void>.delayed(const Duration(milliseconds: 50)),
|
||||
);
|
||||
await tester.pump();
|
||||
}
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
|
||||
@@ -34,6 +34,20 @@ class BrokenSignaling extends Ws2Signaling {
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
test('hangup reaches IKAR even before WS2 connects', () async {
|
||||
final reasons = <String>[];
|
||||
final session = CallSession(
|
||||
ws2Config: config,
|
||||
role: CallRole.caller,
|
||||
endOnServer: (reason) async {
|
||||
reasons.add(reason);
|
||||
},
|
||||
);
|
||||
await session.hangup();
|
||||
await session.hangup();
|
||||
expect(reasons, ['CANCELED']);
|
||||
expect(session.currentState, CallSessionState.ended);
|
||||
});
|
||||
test(
|
||||
'ICE send failure is handled and late candidates are ignored after hangup',
|
||||
() async {
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:ikar/backend/api.dart';
|
||||
import 'package:ikar/core/calls/call_controller.dart';
|
||||
import 'package:ikar/core/protocol/packet.dart';
|
||||
import 'package:kusoft/src/rust/api.dart' show CallParams;
|
||||
import 'package:kusoft/src/rust/frb_generated.dart' show RustLib, RustLibApi;
|
||||
|
||||
class _PushApi extends Api {
|
||||
final packets = StreamController<Packet>.broadcast();
|
||||
@override
|
||||
Stream<Packet> get pushStream => packets.stream;
|
||||
}
|
||||
|
||||
class _CallDecoder implements RustLibApi {
|
||||
@override
|
||||
CallParams? crateApiDecodeVcp({
|
||||
required String vcp,
|
||||
required String conversationId,
|
||||
}) => const CallParams(
|
||||
token: 'synthetic',
|
||||
wsEndpoint: 'wss://calls.invalid/ws2',
|
||||
turn: [],
|
||||
isVideo: false,
|
||||
);
|
||||
@override
|
||||
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
||||
}
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
setUpAll(() => RustLib.initMock(api: _CallDecoder()));
|
||||
|
||||
late _PushApi api;
|
||||
late CallController controller;
|
||||
setUp(() {
|
||||
api = _PushApi();
|
||||
controller = CallController.forTesting()..appResumed = true;
|
||||
controller.init(api);
|
||||
});
|
||||
tearDown(() async {
|
||||
controller.dispose();
|
||||
await api.packets.close();
|
||||
await api.dispose();
|
||||
});
|
||||
Future<void> push(Map<String, dynamic> data) async {
|
||||
api.packets.add(Packet(opcode: 137, payload: data));
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
}
|
||||
|
||||
Map<String, dynamic> invite(String id) => {
|
||||
'conversationId': id,
|
||||
'callerId': 2,
|
||||
'vcp': 'synthetic-incoming',
|
||||
'type': 'AUDIO',
|
||||
};
|
||||
|
||||
test(
|
||||
'caller cancellation dismisses a ringing recipient without WS2',
|
||||
() async {
|
||||
var canceled = 0;
|
||||
final sub = controller.incomingCanceled.listen((_) => canceled++);
|
||||
await push(invite('first'));
|
||||
expect(controller.pendingIncoming?.conversationId, 'first');
|
||||
await push({'conversationId': 'first', 'event': 'ended'});
|
||||
expect(controller.pendingIncoming, isNull);
|
||||
expect(canceled, 1);
|
||||
await sub.cancel();
|
||||
},
|
||||
);
|
||||
test('late invite after cancellation does not start ringing', () async {
|
||||
await push({'conversationId': 'first', 'event': 'ended'});
|
||||
await push(invite('first'));
|
||||
controller.injectFromNative(invite('first'), autoAccept: true);
|
||||
expect(controller.pendingIncoming, isNull);
|
||||
});
|
||||
test('old cancellation cannot dismiss a newer incoming call', () async {
|
||||
await push(invite('new'));
|
||||
await push({'conversationId': 'old', 'event': 'ended'});
|
||||
expect(controller.pendingIncoming?.conversationId, 'new');
|
||||
});
|
||||
test(
|
||||
'expired invite is ignored and committed history refreshes listeners',
|
||||
() async {
|
||||
await push({
|
||||
...invite('expired'),
|
||||
'expiresAt': DateTime.now().millisecondsSinceEpoch - 1000,
|
||||
});
|
||||
expect(controller.pendingIncoming, isNull);
|
||||
var refreshed = 0;
|
||||
final sub = controller.callEnded.listen((_) => refreshed++);
|
||||
await push({'conversationId': 'expired', 'event': 'history'});
|
||||
expect(refreshed, 1);
|
||||
await sub.cancel();
|
||||
},
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user