Files
IKAR/test/contacts_and_accounts_test.dart
T

201 lines
6.4 KiB
Dart

import 'dart:async';
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:image/image.dart' as img;
import 'package:ikar/backend/api.dart';
import 'package:ikar/backend/modules/contacts.dart';
import 'package:ikar/core/contacts/contact_labels.dart';
import 'package:ikar/core/storage/local_contact_avatars.dart';
import 'package:ikar/core/utils/image_utils.dart';
import 'package:ikar/frontend/widgets/qlyra_avatar.dart';
import 'package:ikar/frontend/widgets/lost_account_dialog.dart';
import 'package:ikar/l10n/app_localizations.dart';
import 'package:path_provider_platform_interface/path_provider_platform_interface.dart';
import 'package:plugin_platform_interface/plugin_platform_interface.dart';
import 'package:shared_preferences/shared_preferences.dart';
class _BlockedApi extends Api {
final List<Completer<Map<dynamic, dynamic>?>> calls = [];
@override
Future<Map<dynamic, dynamic>?> sendRequestMap(
int opcode,
Map<dynamic, dynamic> payload, {
bool silent = false,
}) {
final call = Completer<Map<dynamic, dynamic>?>();
calls.add(call);
return call.future;
}
}
class _SyntheticPathProvider extends PathProviderPlatform
with MockPlatformInterfaceMixin {
_SyntheticPathProvider(this.directory);
final String directory;
@override
Future<String?> getApplicationDocumentsPath() async => directory;
}
Map<String, Object> _blockedPage(List<int> ids) => {
'contacts': [
for (final id in ids) {'id': id},
],
};
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('contactMatchesQuery', () {
test('matches names regardless of case', () {
expect(
contactMatchesQuery(
'алис',
title: 'Алиса Синтетическая',
firstName: 'Алиса',
lastName: 'Синтетическая',
phone: 70000000001,
),
isTrue,
);
});
test('matches phone digits only from three digits on', () {
bool matches(String query) => contactMatchesQuery(
query,
title: 'Синтетический контакт',
phone: 70000012345,
);
expect(matches('+7 000 001'), isTrue);
expect(matches('12'), isFalse);
expect(matches('999'), isFalse);
});
test('an empty query keeps everyone', () {
expect(contactMatchesQuery(' ', title: 'Кто угодно'), isTrue);
});
});
group('blocked contacts', () {
tearDown(ContactsModule.clearBlockedCache);
test('load once and announce the change', () async {
final api = _BlockedApi();
final revision = ContactsModule.revision.value;
final first = ContactsModule.ensureBlockedLoaded(api);
final second = ContactsModule.ensureBlockedLoaded(api);
expect(api.calls, hasLength(1));
api.calls.single.complete(_blockedPage([7, 9]));
await Future.wait([first, second]);
expect(ContactsModule.blockedIds, {7, 9});
expect(ContactsModule.revision.value, revision + 1);
await ContactsModule.ensureBlockedLoaded(api);
expect(api.calls, hasLength(1));
});
test('a list fetched for the previous account is dropped', () async {
final api = _BlockedApi();
final pending = ContactsModule.ensureBlockedLoaded(api);
ContactsModule.clearBlockedCache();
api.calls.single.complete(_blockedPage([5]));
await pending;
expect(ContactsModule.blockedIds, isEmpty);
});
});
group('local contact avatars', () {
late Directory directory;
setUp(() {
directory = Directory.systemTemp.createTempSync('synthetic_avatars');
PathProviderPlatform.instance = _SyntheticPathProvider(directory.path);
SharedPreferences.setMockInitialValues({});
});
tearDown(() async {
await LocalContactAvatars.instance.clear(42);
if (directory.existsSync()) directory.deleteSync(recursive: true);
});
test('replacing a photo removes the previous file', () async {
final store = LocalContactAvatars.instance;
final revision = store.revision.value;
await store.set(42, Uint8List.fromList([1, 2, 3]));
final first = (store.imageFor(42)! as FileImage).file;
await store.set(42, Uint8List.fromList([4, 5, 6]));
final second = (store.imageFor(42)! as FileImage).file;
expect(first.existsSync(), isFalse);
expect(second.readAsBytesSync(), [4, 5, 6]);
expect(store.revision.value, revision + 2);
await store.clear(42);
expect(store.has(42), isFalse);
expect(second.existsSync(), isFalse);
});
testWidgets('the avatar prefers the local photo', (tester) async {
await tester.runAsync(
() => LocalContactAvatars.instance.set(42, Uint8List(4)),
);
await tester.pumpWidget(
const MaterialApp(
home: QlyraAvatar(name: 'Синтетика', size: 40, userId: 42),
),
);
final image = tester.widget<Image>(find.byType(Image));
expect((image.image as ResizeImage).imageProvider, isA<FileImage>());
});
});
test('photos are cropped to a centred square', () {
final wide = img.Image(width: 300, height: 200);
final huge = img.Image(width: 1200, height: 900);
final small = img.decodeJpg(encodeSquareAvatar(img.encodePng(wide))!)!;
final big = img.decodeJpg(encodeSquareAvatar(img.encodePng(huge))!)!;
expect([small.width, small.height], [200, 200]);
expect([big.width, big.height], [512, 512]);
});
testWidgets('a lost account offers to sign in or remove it', (tester) async {
LostAccountChoice? choice;
await tester.pumpWidget(
MaterialApp(
locale: const Locale('ru'),
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: Builder(
builder: (context) => TextButton(
onPressed: () async {
choice = await showLostAccountDialog(context, name: 'Синтетика');
},
child: const Text('open'),
),
),
),
);
await tester.tap(find.text('open'));
await tester.pumpAndSettle();
expect(find.textContaining('«Синтетика»'), findsOne);
await tester.tap(find.text('Удалить с устройства'));
await tester.pumpAndSettle();
expect(choice, LostAccountChoice.remove);
});
}