Release IKAR 1.0.7: port navigation and sharing improvements, harden calls
Tests / test (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
Release (main) / release (push) Canceled after 0s
Tests / test (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
Release (main) / release (push) Canceled after 0s
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter_webrtc/flutter_webrtc.dart';
|
||||
import 'package:ikar/core/calls/call_session.dart';
|
||||
import 'package:ikar/core/calls/ws2_signaling.dart';
|
||||
|
||||
final config = Ws2Config(
|
||||
userId: 1,
|
||||
uri: Uri(scheme: 'wss', host: 'calls.invalid'),
|
||||
);
|
||||
|
||||
class BrokenSignaling extends Ws2Signaling {
|
||||
BrokenSignaling() : super(config);
|
||||
int sent = 0;
|
||||
@override
|
||||
Future<void> transmitCandidate({
|
||||
required int participantId,
|
||||
required String candidate,
|
||||
required String sdpMid,
|
||||
required int sdpMLineIndex,
|
||||
String participantType = 'USER',
|
||||
int deviceIdx = 0,
|
||||
}) async {
|
||||
sent++;
|
||||
throw const SocketException('mobile network disconnected');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> hangup({String reason = 'HUNGUP'}) async {}
|
||||
@override
|
||||
Future<void> close() async {}
|
||||
}
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
test(
|
||||
'ICE send failure is handled and late candidates are ignored after hangup',
|
||||
() async {
|
||||
final signaling = BrokenSignaling();
|
||||
final session = CallSession(ws2Config: config, role: CallRole.caller);
|
||||
session.bindCandidateSignalingForTesting(signaling, 2);
|
||||
final errors = <Object>[];
|
||||
runZonedGuarded(() {
|
||||
session.receiveLocalCandidateForTesting(
|
||||
RTCIceCandidate('candidate:mobile', '0', 0),
|
||||
);
|
||||
}, (error, stack) => errors.add(error));
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
final reconnecting = session.isReconnecting;
|
||||
await session.hangup();
|
||||
runZonedGuarded(() {
|
||||
session.receiveLocalCandidateForTesting(
|
||||
RTCIceCandidate('candidate:late', '0', 0),
|
||||
);
|
||||
}, (error, stack) => errors.add(error));
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
expect(reconnecting, isTrue);
|
||||
expect(signaling.sent, 1);
|
||||
expect(errors, isEmpty);
|
||||
expect(session.currentState, CallSessionState.ended);
|
||||
await Future<void>.delayed(const Duration(milliseconds: 1100));
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:ikar/core/config/app_bottom_navigation_style.dart';
|
||||
import 'package:ikar/frontend/widgets/compact_bottom_navigation_bar.dart';
|
||||
import 'package:ikar/frontend/widgets/bottom_navigation_style_card.dart';
|
||||
import 'package:ikar/frontend/widgets/sliding_pill_nav.dart';
|
||||
import 'package:ikar/l10n/app_localizations.dart';
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
setUp(() {
|
||||
SharedPreferences.setMockInitialValues({});
|
||||
AppBottomNavigationStyle.current.value = BottomNavigationStyle.floating;
|
||||
});
|
||||
testWidgets(
|
||||
'compact tabs retain badge, selection and account long press at large text size',
|
||||
(tester) async {
|
||||
var selected = -1;
|
||||
var held = -1;
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: MediaQuery(
|
||||
data: const MediaQueryData(
|
||||
size: Size(320, 640),
|
||||
textScaler: TextScaler.linear(2),
|
||||
),
|
||||
child: Scaffold(
|
||||
bottomNavigationBar: CompactBottomNavigationBar(
|
||||
items: const [
|
||||
PillNavItem(
|
||||
icon: Icons.chat,
|
||||
label: 'Чаты',
|
||||
unreadCount: 120,
|
||||
),
|
||||
PillNavItem(icon: Icons.call, label: 'Звонки'),
|
||||
PillNavItem(icon: Icons.people, label: 'Контакты'),
|
||||
PillNavItem(
|
||||
icon: Icons.settings,
|
||||
label: 'Настройки',
|
||||
longPressable: true,
|
||||
),
|
||||
],
|
||||
currentIndex: 0,
|
||||
onTap: (index) => selected = index,
|
||||
onItemLongPress: (index, _) => held = index,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
expect(find.text('99+'), findsOneWidget);
|
||||
await tester.tap(find.byIcon(Icons.call));
|
||||
expect(selected, 1);
|
||||
await tester.longPress(find.byIcon(Icons.settings));
|
||||
expect(held, 3);
|
||||
expect(tester.takeException(), isNull);
|
||||
},
|
||||
);
|
||||
testWidgets('appearance setting persists and restores compact layout', (
|
||||
tester,
|
||||
) async {
|
||||
await tester.pumpWidget(
|
||||
const MaterialApp(
|
||||
locale: Locale('ru'),
|
||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
home: Scaffold(body: BottomNavigationStyleCard()),
|
||||
),
|
||||
);
|
||||
await tester.tap(find.text('Компактная'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
AppBottomNavigationStyle.current.value,
|
||||
BottomNavigationStyle.compact,
|
||||
);
|
||||
AppBottomNavigationStyle.current.value = BottomNavigationStyle.floating;
|
||||
await AppBottomNavigationStyle.load();
|
||||
expect(
|
||||
AppBottomNavigationStyle.current.value,
|
||||
BottomNavigationStyle.compact,
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:ikar/backend/modules/messages.dart';
|
||||
import 'package:ikar/core/utils/download_progress.dart';
|
||||
import 'package:ikar/core/utils/file_download.dart';
|
||||
import 'package:ikar/frontend/widgets/attachment/bubbles/bubble_context.dart';
|
||||
import 'package:ikar/frontend/widgets/attachment/bubbles/file_bubble.dart';
|
||||
import 'package:ikar/l10n/app_localizations.dart';
|
||||
import 'package:ikar/models/attachment.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets(
|
||||
'whole file card cannot open twice while external opening is pending',
|
||||
(tester) async {
|
||||
var opens = 0;
|
||||
var ready = Completer<FileDownloadResult>();
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
locale: const Locale('ru'),
|
||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
home: Scaffold(
|
||||
body: Builder(
|
||||
builder: (context) {
|
||||
const file = FileAttachment(
|
||||
fileId: 765,
|
||||
name: 'report.pdf',
|
||||
size: 1024,
|
||||
);
|
||||
return FileBubble(
|
||||
file: file,
|
||||
ctx: BubbleContext(
|
||||
context: context,
|
||||
cs: Theme.of(context).colorScheme,
|
||||
text: Colors.black,
|
||||
shape: BubbleShape.singleMiddle,
|
||||
contentType: MessageType.attachment,
|
||||
hasPhotoWithCaption: false,
|
||||
hasMultiplePhotosNoCaption: false,
|
||||
message: CachedMessage(
|
||||
id: '1',
|
||||
accountId: 1,
|
||||
chatId: 2,
|
||||
senderId: 3,
|
||||
time: 1000,
|
||||
status: 'sent',
|
||||
),
|
||||
isMe: false,
|
||||
myId: 1,
|
||||
chatType: 'DIALOG',
|
||||
),
|
||||
openFile:
|
||||
(
|
||||
name,
|
||||
url, {
|
||||
onProgress,
|
||||
onReady,
|
||||
download,
|
||||
downloadOnlyIfMissing = false,
|
||||
}) {
|
||||
opens++;
|
||||
onReady?.call();
|
||||
return ready.future;
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.tap(find.text('report.pdf'));
|
||||
await tester.pump();
|
||||
await tester.tap(find.text('report.pdf'));
|
||||
await tester.pump();
|
||||
expect(opens, 1);
|
||||
expect(find.textContaining('Открытие'), findsOneWidget);
|
||||
ready.complete(const FileDownloadResult(ok: true));
|
||||
await tester.pumpAndSettle();
|
||||
ready = Completer<FileDownloadResult>();
|
||||
await tester.tap(find.text('report.pdf'));
|
||||
expect(opens, 2);
|
||||
ready.complete(const FileDownloadResult(ok: true));
|
||||
await tester.pumpAndSettle();
|
||||
MediaDownloadProgress.set('765_report.pdf', null);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:ikar/backend/modules/pending_message_deletions.dart';
|
||||
|
||||
void main() {
|
||||
test(
|
||||
'undo retains other pending removals and isolates accounts and chats',
|
||||
() {
|
||||
final pending = PendingMessageDeletions();
|
||||
pending.hold(1, 2, ['a', 'b']);
|
||||
pending.hold(1, 2, ['c']);
|
||||
pending.hold(3, 2, ['a']);
|
||||
pending.release(1, 2, ['a', 'b']);
|
||||
expect(pending.contains(1, 2, 'a'), isFalse);
|
||||
expect(pending.contains(1, 2, 'c'), isTrue);
|
||||
expect(pending.contains(3, 2, 'a'), isTrue);
|
||||
expect(pending.contains(1, 4, 'c'), isFalse);
|
||||
pending.dispose();
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:ikar/core/share/share_inbox.dart';
|
||||
|
||||
void main() {
|
||||
test(
|
||||
'concurrent events present each share once and wait for file users',
|
||||
() async {
|
||||
final records = [
|
||||
for (var i = 0; i < 3; i++) {'id': '$i', 'text': 'share $i'},
|
||||
];
|
||||
final shown = <String>[];
|
||||
final acknowledged = <String>[];
|
||||
final released = <String>[];
|
||||
final upload = Completer<void>();
|
||||
final inbox = ShareInbox(
|
||||
consume: () async => records.isEmpty ? null : records.removeAt(0),
|
||||
acknowledge: (id) async => acknowledged.add(id),
|
||||
release: (id) async => released.add(id),
|
||||
onShare: (payload) => shown.add(payload.text!),
|
||||
);
|
||||
await Future.wait([inbox.check(), inbox.check(), inbox.check()]);
|
||||
expect(shown, ['share 0']);
|
||||
final completing = inbox.complete(waitForRelease: () => upload.future);
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
expect(acknowledged, ['0']);
|
||||
expect(released, isEmpty);
|
||||
expect(shown, ['share 0']);
|
||||
upload.complete();
|
||||
await completing;
|
||||
expect(released, ['0']);
|
||||
expect(shown, ['share 0', 'share 1']);
|
||||
await inbox.complete();
|
||||
await inbox.complete();
|
||||
expect(shown, ['share 0', 'share 1', 'share 2']);
|
||||
expect(released, ['0', '1', '2']);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'ack failure keeps active record and retries without presenting twice',
|
||||
() async {
|
||||
var failures = 1;
|
||||
var shown = 0;
|
||||
var consumed = false;
|
||||
final inbox = ShareInbox(
|
||||
consume: () async {
|
||||
if (consumed) return null;
|
||||
consumed = true;
|
||||
return {'id': 'a', 'text': 'text'};
|
||||
},
|
||||
acknowledge: (_) async {
|
||||
if (failures-- > 0) throw StateError('disk');
|
||||
},
|
||||
release: (_) async {},
|
||||
onShare: (_) => shown++,
|
||||
);
|
||||
await inbox.check();
|
||||
await expectLater(inbox.complete(), throwsStateError);
|
||||
await inbox.check();
|
||||
expect(shown, 1);
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'invalid record is discarded and next valid record is presented',
|
||||
() async {
|
||||
final records = [
|
||||
{'id': 'bad'},
|
||||
{'id': 'good', 'text': 'ok'},
|
||||
];
|
||||
final acknowledged = <String>[];
|
||||
final shown = <String>[];
|
||||
final inbox = ShareInbox(
|
||||
consume: () async => records.isEmpty ? null : records.removeAt(0),
|
||||
acknowledge: (id) async => acknowledged.add(id),
|
||||
release: (_) async {},
|
||||
onShare: (payload) => shown.add(payload.text!),
|
||||
);
|
||||
await inbox.check();
|
||||
expect(acknowledged, ['bad']);
|
||||
expect(shown, ['ok']);
|
||||
await inbox.complete();
|
||||
},
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user