Source Code added
This commit is contained in:
parent
800376eafd
commit
9efa9bc6dd
3912 changed files with 754770 additions and 2 deletions
175
mobile/test/modules/activity/activities_page_test.dart
Normal file
175
mobile/test/modules/activity/activities_page_test.dart
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
@Skip('currently failing due to mock HTTP client to download ISAR binaries')
|
||||
@Tags(['widget'])
|
||||
library;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/domain/services/store.service.dart';
|
||||
import 'package:immich_mobile/entities/album.entity.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
|
||||
import 'package:immich_mobile/models/activities/activity.model.dart';
|
||||
import 'package:immich_mobile/pages/common/activities.page.dart';
|
||||
import 'package:immich_mobile/providers/activity.provider.dart';
|
||||
import 'package:immich_mobile/providers/album/current_album.provider.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
import 'package:immich_mobile/widgets/activities/activity_text_field.dart';
|
||||
import 'package:immich_mobile/widgets/activities/dismissible_activity.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
import '../../fixtures/album.stub.dart';
|
||||
import '../../fixtures/asset.stub.dart';
|
||||
import '../../fixtures/user.stub.dart';
|
||||
import '../../test_utils.dart';
|
||||
import '../../widget_tester_extensions.dart';
|
||||
import '../album/album_mocks.dart';
|
||||
import '../asset_viewer/asset_viewer_mocks.dart';
|
||||
import '../shared/shared_mocks.dart';
|
||||
import 'activity_mocks.dart';
|
||||
|
||||
final _activities = [
|
||||
Activity(
|
||||
id: '1',
|
||||
createdAt: DateTime(100),
|
||||
type: ActivityType.comment,
|
||||
comment: 'First Activity',
|
||||
assetId: 'asset-2',
|
||||
user: UserStub.admin,
|
||||
),
|
||||
Activity(
|
||||
id: '2',
|
||||
createdAt: DateTime(200),
|
||||
type: ActivityType.comment,
|
||||
comment: 'Second Activity',
|
||||
user: UserStub.user1,
|
||||
),
|
||||
Activity(id: '3', createdAt: DateTime(300), type: ActivityType.like, assetId: 'asset-1', user: UserStub.user2),
|
||||
Activity(id: '4', createdAt: DateTime(400), type: ActivityType.like, user: UserStub.user1),
|
||||
];
|
||||
|
||||
void main() {
|
||||
late MockAlbumActivity activityMock;
|
||||
late MockCurrentAlbumProvider mockCurrentAlbumProvider;
|
||||
late MockCurrentAssetProvider mockCurrentAssetProvider;
|
||||
late List<Override> overrides;
|
||||
late Isar db;
|
||||
|
||||
setUpAll(() async {
|
||||
TestUtils.init();
|
||||
db = await TestUtils.initIsar();
|
||||
await StoreService.init(storeRepository: IsarStoreRepository(db));
|
||||
await Store.put(StoreKey.currentUser, UserStub.admin);
|
||||
await Store.put(StoreKey.serverEndpoint, '');
|
||||
await Store.put(StoreKey.accessToken, '');
|
||||
});
|
||||
|
||||
setUp(() async {
|
||||
mockCurrentAlbumProvider = MockCurrentAlbumProvider(AlbumStub.twoAsset);
|
||||
mockCurrentAssetProvider = MockCurrentAssetProvider(AssetStub.image1);
|
||||
activityMock = MockAlbumActivity(_activities);
|
||||
overrides = [
|
||||
albumActivityProvider(AlbumStub.twoAsset.remoteId!, AssetStub.image1.remoteId!).overrideWith(() => activityMock),
|
||||
currentAlbumProvider.overrideWith(() => mockCurrentAlbumProvider),
|
||||
currentAssetProvider.overrideWith(() => mockCurrentAssetProvider),
|
||||
];
|
||||
|
||||
await db.writeTxn(() async {
|
||||
await db.clear();
|
||||
// Save all assets
|
||||
await db.users.put(User.fromDto(UserStub.admin));
|
||||
await db.assets.putAll([AssetStub.image1, AssetStub.image2]);
|
||||
await db.albums.put(AlbumStub.twoAsset);
|
||||
await AlbumStub.twoAsset.owner.save();
|
||||
await AlbumStub.twoAsset.assets.save();
|
||||
});
|
||||
expect(db.albums.countSync(), 1);
|
||||
expect(db.assets.countSync(), 2);
|
||||
expect(db.users.countSync(), 1);
|
||||
});
|
||||
|
||||
group("App bar", () {
|
||||
testWidgets("No title when currentAsset != null", (tester) async {
|
||||
await tester.pumpConsumerWidget(const ActivitiesPage(), overrides: overrides);
|
||||
|
||||
final listTile = tester.widget<AppBar>(find.byType(AppBar));
|
||||
expect(listTile.title, isNull);
|
||||
});
|
||||
|
||||
testWidgets("Album name as title when currentAsset == null", (tester) async {
|
||||
await tester.pumpConsumerWidget(const ActivitiesPage(), overrides: overrides);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
mockCurrentAssetProvider.state = null;
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text(AlbumStub.twoAsset.name), findsOneWidget);
|
||||
final listTile = tester.widget<AppBar>(find.byType(AppBar));
|
||||
expect(listTile.title, isNotNull);
|
||||
});
|
||||
});
|
||||
|
||||
group("Body", () {
|
||||
testWidgets("Contains a stack with Activity List and Activity Input", (tester) async {
|
||||
await tester.pumpConsumerWidget(const ActivitiesPage(), overrides: overrides);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.descendant(of: find.byType(Stack), matching: find.byType(ActivityTextField)), findsOneWidget);
|
||||
|
||||
expect(find.descendant(of: find.byType(Stack), matching: find.byType(ListView)), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets("List Contains all dismissible activities", (tester) async {
|
||||
await tester.pumpConsumerWidget(const ActivitiesPage(), overrides: overrides);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final listFinder = find.descendant(of: find.byType(Stack), matching: find.byType(ListView));
|
||||
final listChildren = find.descendant(of: listFinder, matching: find.byType(DismissibleActivity));
|
||||
expect(listChildren, findsNWidgets(_activities.length));
|
||||
});
|
||||
|
||||
testWidgets("Submitting text input adds a comment with the text", (tester) async {
|
||||
await tester.pumpConsumerWidget(const ActivitiesPage(), overrides: overrides);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
when(() => activityMock.addComment(any())).thenAnswer((_) => Future.value());
|
||||
|
||||
final textField = find.byType(TextField);
|
||||
await tester.enterText(textField, 'Test comment');
|
||||
await tester.testTextInput.receiveAction(TextInputAction.done);
|
||||
|
||||
verify(() => activityMock.addComment('Test comment'));
|
||||
});
|
||||
|
||||
testWidgets("Owner can remove all activities", (tester) async {
|
||||
await tester.pumpConsumerWidget(const ActivitiesPage(), overrides: overrides);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final deletableActivityFinder = find.byWidgetPredicate(
|
||||
(widget) => widget is DismissibleActivity && widget.onDismiss != null,
|
||||
);
|
||||
expect(deletableActivityFinder, findsNWidgets(_activities.length));
|
||||
});
|
||||
|
||||
testWidgets("Non-Owner can remove only their activities", (tester) async {
|
||||
final mockCurrentUser = MockCurrentUserProvider();
|
||||
|
||||
await tester.pumpConsumerWidget(
|
||||
const ActivitiesPage(),
|
||||
overrides: [...overrides, currentUserProvider.overrideWith((ref) => mockCurrentUser)],
|
||||
);
|
||||
mockCurrentUser.state = UserStub.user1;
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final deletableActivityFinder = find.byWidgetPredicate(
|
||||
(widget) => widget is DismissibleActivity && widget.onDismiss != null,
|
||||
);
|
||||
expect(deletableActivityFinder, findsNWidgets(_activities.where((a) => a.user == UserStub.user1).length));
|
||||
});
|
||||
});
|
||||
}
|
||||
19
mobile/test/modules/activity/activity_mocks.dart
Normal file
19
mobile/test/modules/activity/activity_mocks.dart
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import 'package:immich_mobile/models/activities/activity.model.dart';
|
||||
import 'package:immich_mobile/providers/activity.provider.dart';
|
||||
import 'package:immich_mobile/providers/activity_statistics.provider.dart';
|
||||
import 'package:immich_mobile/services/activity.service.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
class ActivityServiceMock extends Mock implements ActivityService {}
|
||||
|
||||
class MockAlbumActivity extends AlbumActivityInternal with Mock implements AlbumActivity {
|
||||
List<Activity>? initActivities;
|
||||
MockAlbumActivity([this.initActivities]);
|
||||
|
||||
@override
|
||||
Future<List<Activity>> build(String albumId, [String? assetId]) async {
|
||||
return initActivities ?? [];
|
||||
}
|
||||
}
|
||||
|
||||
class ActivityStatisticsMock extends ActivityStatisticsInternal with Mock implements ActivityStatistics {}
|
||||
331
mobile/test/modules/activity/activity_provider_test.dart
Normal file
331
mobile/test/modules/activity/activity_provider_test.dart
Normal file
|
|
@ -0,0 +1,331 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/models/activities/activity.model.dart';
|
||||
import 'package:immich_mobile/providers/activity.provider.dart';
|
||||
import 'package:immich_mobile/providers/activity_service.provider.dart';
|
||||
import 'package:immich_mobile/providers/activity_statistics.provider.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
import '../../fixtures/user.stub.dart';
|
||||
import '../../test_utils.dart';
|
||||
import 'activity_mocks.dart';
|
||||
|
||||
final _activities = [
|
||||
Activity(
|
||||
id: '1',
|
||||
createdAt: DateTime(100),
|
||||
type: ActivityType.comment,
|
||||
comment: 'First Activity',
|
||||
assetId: 'asset-2',
|
||||
user: UserStub.admin,
|
||||
),
|
||||
Activity(
|
||||
id: '2',
|
||||
createdAt: DateTime(200),
|
||||
type: ActivityType.comment,
|
||||
comment: 'Second Activity',
|
||||
user: UserStub.user1,
|
||||
),
|
||||
Activity(id: '3', createdAt: DateTime(300), type: ActivityType.like, assetId: 'asset-1', user: UserStub.admin),
|
||||
Activity(id: '4', createdAt: DateTime(400), type: ActivityType.like, user: UserStub.user1),
|
||||
];
|
||||
|
||||
void main() {
|
||||
late ActivityServiceMock activityMock;
|
||||
late ActivityStatisticsMock activityStatisticsMock;
|
||||
late ActivityStatisticsMock albumActivityStatisticsMock;
|
||||
late ProviderContainer container;
|
||||
late AlbumActivityProvider provider;
|
||||
late ListenerMock<AsyncValue<List<Activity>>> listener;
|
||||
|
||||
setUpAll(() {
|
||||
registerFallbackValue(AsyncData<List<Activity>>([..._activities]));
|
||||
});
|
||||
|
||||
setUp(() async {
|
||||
activityMock = ActivityServiceMock();
|
||||
activityStatisticsMock = ActivityStatisticsMock();
|
||||
albumActivityStatisticsMock = ActivityStatisticsMock();
|
||||
|
||||
container = TestUtils.createContainer(
|
||||
overrides: [
|
||||
activityServiceProvider.overrideWith((ref) => activityMock),
|
||||
activityStatisticsProvider('test-album', 'test-asset').overrideWith(() => activityStatisticsMock),
|
||||
activityStatisticsProvider('test-album').overrideWith(() => albumActivityStatisticsMock),
|
||||
],
|
||||
);
|
||||
|
||||
// Mock values
|
||||
when(() => activityStatisticsMock.build(any(), any())).thenReturn(0);
|
||||
when(() => albumActivityStatisticsMock.build(any())).thenReturn(0);
|
||||
when(
|
||||
() => activityMock.getAllActivities('test-album', assetId: 'test-asset'),
|
||||
).thenAnswer((_) async => [..._activities]);
|
||||
when(() => activityMock.getAllActivities('test-album')).thenAnswer((_) async => [..._activities]);
|
||||
|
||||
// Init and wait for providers future to complete
|
||||
provider = albumActivityProvider('test-album', 'test-asset');
|
||||
listener = ListenerMock();
|
||||
container.listen(provider, listener.call, fireImmediately: true);
|
||||
|
||||
await container.read(provider.future);
|
||||
});
|
||||
|
||||
test('Returns a list of activity', () async {
|
||||
verifyInOrder([
|
||||
() => listener.call(null, const AsyncLoading()),
|
||||
() => listener.call(
|
||||
const AsyncLoading(),
|
||||
any(
|
||||
that: allOf([
|
||||
isA<AsyncData<List<Activity>>>(),
|
||||
predicate((AsyncData<List<Activity>> ad) => ad.requireValue.every((e) => _activities.contains(e))),
|
||||
]),
|
||||
),
|
||||
),
|
||||
]);
|
||||
|
||||
verifyNoMoreInteractions(listener);
|
||||
});
|
||||
|
||||
group('addLike()', () {
|
||||
test('Like successfully added', () async {
|
||||
final like = Activity(id: '5', createdAt: DateTime(2023), type: ActivityType.like, user: UserStub.admin);
|
||||
|
||||
when(
|
||||
() => activityMock.addActivity('test-album', ActivityType.like, assetId: 'test-asset'),
|
||||
).thenAnswer((_) async => AsyncData(like));
|
||||
|
||||
final albumProvider = albumActivityProvider('test-album');
|
||||
container.read(albumProvider.notifier);
|
||||
await container.read(albumProvider.future);
|
||||
|
||||
await container.read(provider.notifier).addLike();
|
||||
|
||||
verify(() => activityMock.addActivity('test-album', ActivityType.like, assetId: 'test-asset'));
|
||||
|
||||
final activities = await container.read(provider.future);
|
||||
expect(activities, hasLength(5));
|
||||
expect(activities, contains(like));
|
||||
|
||||
// Never bump activity count for new likes
|
||||
verifyNever(() => activityStatisticsMock.addActivity());
|
||||
verifyNever(() => albumActivityStatisticsMock.addActivity());
|
||||
|
||||
final albumActivities = container.read(albumProvider).requireValue;
|
||||
expect(albumActivities, hasLength(5));
|
||||
expect(albumActivities, contains(like));
|
||||
});
|
||||
|
||||
test('Like failed', () async {
|
||||
final like = Activity(id: '5', createdAt: DateTime(2023), type: ActivityType.like, user: UserStub.admin);
|
||||
when(
|
||||
() => activityMock.addActivity('test-album', ActivityType.like, assetId: 'test-asset'),
|
||||
).thenAnswer((_) async => AsyncError(Exception('Mock'), StackTrace.current));
|
||||
|
||||
final albumProvider = albumActivityProvider('test-album');
|
||||
container.read(albumProvider.notifier);
|
||||
await container.read(albumProvider.future);
|
||||
|
||||
await container.read(provider.notifier).addLike();
|
||||
|
||||
verify(() => activityMock.addActivity('test-album', ActivityType.like, assetId: 'test-asset'));
|
||||
|
||||
final activities = await container.read(provider.future);
|
||||
expect(activities, hasLength(4));
|
||||
expect(activities, isNot(contains(like)));
|
||||
|
||||
verifyNever(() => albumActivityStatisticsMock.addActivity());
|
||||
|
||||
final albumActivities = container.read(albumProvider).requireValue;
|
||||
expect(albumActivities, hasLength(4));
|
||||
expect(albumActivities, isNot(contains(like)));
|
||||
});
|
||||
});
|
||||
|
||||
group('removeActivity()', () {
|
||||
test('Like successfully removed', () async {
|
||||
when(() => activityMock.removeActivity('3')).thenAnswer((_) async => true);
|
||||
|
||||
await container.read(provider.notifier).removeActivity('3');
|
||||
|
||||
verify(() => activityMock.removeActivity('3'));
|
||||
|
||||
final activities = await container.read(provider.future);
|
||||
expect(activities, hasLength(3));
|
||||
expect(activities, isNot(anyElement(predicate((Activity a) => a.id == '3'))));
|
||||
|
||||
verifyNever(() => activityStatisticsMock.removeActivity());
|
||||
verifyNever(() => albumActivityStatisticsMock.removeActivity());
|
||||
});
|
||||
|
||||
test('Remove Like failed', () async {
|
||||
when(() => activityMock.removeActivity('3')).thenAnswer((_) async => false);
|
||||
|
||||
await container.read(provider.notifier).removeActivity('3');
|
||||
|
||||
final activities = await container.read(provider.future);
|
||||
expect(activities, hasLength(4));
|
||||
expect(activities, anyElement(predicate((Activity a) => a.id == '3')));
|
||||
|
||||
verifyNever(() => activityStatisticsMock.removeActivity());
|
||||
verifyNever(() => albumActivityStatisticsMock.removeActivity());
|
||||
});
|
||||
|
||||
test('Comment successfully removed', () async {
|
||||
when(() => activityMock.removeActivity('1')).thenAnswer((_) async => true);
|
||||
|
||||
await container.read(provider.notifier).removeActivity('1');
|
||||
|
||||
final activities = await container.read(provider.future);
|
||||
expect(activities, isNot(anyElement(predicate((Activity a) => a.id == '1'))));
|
||||
|
||||
verify(() => activityStatisticsMock.removeActivity());
|
||||
verify(() => albumActivityStatisticsMock.removeActivity());
|
||||
});
|
||||
|
||||
test('Removes activity from album state when asset scoped', () async {
|
||||
when(() => activityMock.removeActivity('3')).thenAnswer((_) async => true);
|
||||
when(() => activityMock.getAllActivities('test-album')).thenAnswer((_) async => [..._activities]);
|
||||
|
||||
final albumProvider = albumActivityProvider('test-album');
|
||||
container.read(albumProvider.notifier);
|
||||
await container.read(albumProvider.future);
|
||||
|
||||
await container.read(provider.notifier).removeActivity('3');
|
||||
|
||||
final assetActivities = container.read(provider).requireValue;
|
||||
final albumActivities = container.read(albumProvider).requireValue;
|
||||
|
||||
expect(assetActivities, hasLength(3));
|
||||
expect(assetActivities, isNot(anyElement(predicate((Activity a) => a.id == '3'))));
|
||||
|
||||
expect(albumActivities, hasLength(3));
|
||||
expect(albumActivities, isNot(anyElement(predicate((Activity a) => a.id == '3'))));
|
||||
|
||||
verify(() => activityMock.removeActivity('3'));
|
||||
verifyNever(() => activityStatisticsMock.removeActivity());
|
||||
verifyNever(() => albumActivityStatisticsMock.removeActivity());
|
||||
});
|
||||
});
|
||||
|
||||
group('addComment()', () {
|
||||
test('Comment successfully added', () async {
|
||||
final comment = Activity(
|
||||
id: '5',
|
||||
createdAt: DateTime(2023),
|
||||
type: ActivityType.comment,
|
||||
user: UserStub.admin,
|
||||
comment: 'Test-Comment',
|
||||
assetId: 'test-asset',
|
||||
);
|
||||
|
||||
final albumProvider = albumActivityProvider('test-album');
|
||||
container.read(albumProvider.notifier);
|
||||
await container.read(albumProvider.future);
|
||||
|
||||
when(
|
||||
() => activityMock.addActivity(
|
||||
'test-album',
|
||||
ActivityType.comment,
|
||||
assetId: 'test-asset',
|
||||
comment: 'Test-Comment',
|
||||
),
|
||||
).thenAnswer((_) async => AsyncData(comment));
|
||||
when(() => activityStatisticsMock.build('test-album', 'test-asset')).thenReturn(4);
|
||||
when(() => albumActivityStatisticsMock.build('test-album')).thenReturn(2);
|
||||
|
||||
await container.read(provider.notifier).addComment('Test-Comment');
|
||||
|
||||
verify(
|
||||
() => activityMock.addActivity(
|
||||
'test-album',
|
||||
ActivityType.comment,
|
||||
assetId: 'test-asset',
|
||||
comment: 'Test-Comment',
|
||||
),
|
||||
);
|
||||
|
||||
final activities = await container.read(provider.future);
|
||||
expect(activities, hasLength(5));
|
||||
expect(activities, contains(comment));
|
||||
|
||||
verify(() => activityStatisticsMock.addActivity());
|
||||
verify(() => albumActivityStatisticsMock.addActivity());
|
||||
|
||||
final albumActivities = container.read(albumProvider).requireValue;
|
||||
expect(albumActivities, hasLength(5));
|
||||
expect(albumActivities, contains(comment));
|
||||
});
|
||||
|
||||
test('Comment successfully added without assetId', () async {
|
||||
final comment = Activity(
|
||||
id: '5',
|
||||
createdAt: DateTime(2023),
|
||||
type: ActivityType.comment,
|
||||
user: UserStub.admin,
|
||||
assetId: 'test-asset',
|
||||
comment: 'Test-Comment',
|
||||
);
|
||||
|
||||
when(
|
||||
() => activityMock.addActivity('test-album', ActivityType.comment, comment: 'Test-Comment'),
|
||||
).thenAnswer((_) async => AsyncData(comment));
|
||||
when(() => albumActivityStatisticsMock.build('test-album')).thenReturn(2);
|
||||
when(() => activityMock.getAllActivities('test-album')).thenAnswer((_) async => [..._activities]);
|
||||
|
||||
final albumProvider = albumActivityProvider('test-album');
|
||||
container.read(albumProvider.notifier);
|
||||
await container.read(albumProvider.future);
|
||||
await container.read(albumProvider.notifier).addComment('Test-Comment');
|
||||
|
||||
verify(
|
||||
() => activityMock.addActivity('test-album', ActivityType.comment, assetId: null, comment: 'Test-Comment'),
|
||||
);
|
||||
|
||||
final activities = await container.read(albumProvider.future);
|
||||
expect(activities, hasLength(5));
|
||||
expect(activities, contains(comment));
|
||||
|
||||
verifyNever(() => activityStatisticsMock.addActivity());
|
||||
verify(() => albumActivityStatisticsMock.addActivity());
|
||||
});
|
||||
|
||||
test('Comment failed', () async {
|
||||
final comment = Activity(
|
||||
id: '5',
|
||||
createdAt: DateTime(2023),
|
||||
type: ActivityType.comment,
|
||||
user: UserStub.admin,
|
||||
comment: 'Test-Comment',
|
||||
assetId: 'test-asset',
|
||||
);
|
||||
|
||||
when(
|
||||
() => activityMock.addActivity(
|
||||
'test-album',
|
||||
ActivityType.comment,
|
||||
assetId: 'test-asset',
|
||||
comment: 'Test-Comment',
|
||||
),
|
||||
).thenAnswer((_) async => AsyncError(Exception('Error'), StackTrace.current));
|
||||
|
||||
final albumProvider = albumActivityProvider('test-album');
|
||||
container.read(albumProvider.notifier);
|
||||
await container.read(albumProvider.future);
|
||||
|
||||
await container.read(provider.notifier).addComment('Test-Comment');
|
||||
|
||||
final activities = await container.read(provider.future);
|
||||
expect(activities, hasLength(4));
|
||||
expect(activities, isNot(contains(comment)));
|
||||
|
||||
verifyNever(() => activityStatisticsMock.addActivity());
|
||||
verifyNever(() => albumActivityStatisticsMock.addActivity());
|
||||
|
||||
final albumActivities = container.read(albumProvider).requireValue;
|
||||
expect(albumActivities, hasLength(4));
|
||||
expect(albumActivities, isNot(contains(comment)));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/models/activities/activity.model.dart';
|
||||
import 'package:immich_mobile/providers/activity_service.provider.dart';
|
||||
import 'package:immich_mobile/providers/activity_statistics.provider.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
import '../../test_utils.dart';
|
||||
import 'activity_mocks.dart';
|
||||
|
||||
void main() {
|
||||
late ActivityServiceMock activityMock;
|
||||
late ProviderContainer container;
|
||||
late ListenerMock<int> listener;
|
||||
|
||||
setUp(() async {
|
||||
activityMock = ActivityServiceMock();
|
||||
container = TestUtils.createContainer(overrides: [activityServiceProvider.overrideWith((ref) => activityMock)]);
|
||||
listener = ListenerMock();
|
||||
});
|
||||
|
||||
test('Returns the proper count family', () async {
|
||||
when(
|
||||
() => activityMock.getStatistics('test-album', assetId: 'test-asset'),
|
||||
).thenAnswer((_) async => const ActivityStats(comments: 5));
|
||||
|
||||
// Read here to make the getStatistics call
|
||||
container.read(activityStatisticsProvider('test-album', 'test-asset'));
|
||||
|
||||
container.listen(activityStatisticsProvider('test-album', 'test-asset'), listener.call, fireImmediately: true);
|
||||
|
||||
// Sleep for the getStatistics future to resolve
|
||||
await Future.delayed(const Duration(milliseconds: 1));
|
||||
|
||||
verifyInOrder([() => listener.call(null, 0), () => listener.call(0, 5)]);
|
||||
|
||||
verifyNoMoreInteractions(listener);
|
||||
});
|
||||
|
||||
test('Adds activity', () async {
|
||||
when(() => activityMock.getStatistics('test-album')).thenAnswer((_) async => const ActivityStats(comments: 10));
|
||||
|
||||
final provider = activityStatisticsProvider('test-album');
|
||||
container.listen(provider, listener.call, fireImmediately: true);
|
||||
|
||||
// Sleep for the getStatistics future to resolve
|
||||
await Future.delayed(const Duration(milliseconds: 1));
|
||||
|
||||
container.read(provider.notifier).addActivity();
|
||||
container.read(provider.notifier).addActivity();
|
||||
|
||||
expect(container.read(provider), 12);
|
||||
});
|
||||
|
||||
test('Removes activity', () async {
|
||||
when(
|
||||
() => activityMock.getStatistics('new-album', assetId: 'test-asset'),
|
||||
).thenAnswer((_) async => const ActivityStats(comments: 10));
|
||||
|
||||
final provider = activityStatisticsProvider('new-album', 'test-asset');
|
||||
container.listen(provider, listener.call, fireImmediately: true);
|
||||
|
||||
// Sleep for the getStatistics future to resolve
|
||||
await Future.delayed(const Duration(milliseconds: 1));
|
||||
|
||||
container.read(provider.notifier).removeActivity();
|
||||
container.read(provider.notifier).removeActivity();
|
||||
|
||||
expect(container.read(provider), 8);
|
||||
});
|
||||
}
|
||||
149
mobile/test/modules/activity/activity_text_field_test.dart
Normal file
149
mobile/test/modules/activity/activity_text_field_test.dart
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
@Skip('currently failing due to mock HTTP client to download ISAR binaries')
|
||||
@Tags(['widget'])
|
||||
library;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/domain/services/store.service.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
|
||||
import 'package:immich_mobile/providers/activity.provider.dart';
|
||||
import 'package:immich_mobile/providers/album/current_album.provider.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
import 'package:immich_mobile/widgets/activities/activity_text_field.dart';
|
||||
import 'package:immich_mobile/widgets/common/user_circle_avatar.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
import '../../fixtures/album.stub.dart';
|
||||
import '../../fixtures/user.stub.dart';
|
||||
import '../../test_utils.dart';
|
||||
import '../../widget_tester_extensions.dart';
|
||||
import '../album/album_mocks.dart';
|
||||
import '../shared/shared_mocks.dart';
|
||||
import 'activity_mocks.dart';
|
||||
|
||||
void main() {
|
||||
late Isar db;
|
||||
late MockCurrentAlbumProvider mockCurrentAlbumProvider;
|
||||
late MockAlbumActivity activityMock;
|
||||
late List<Override> overrides;
|
||||
|
||||
setUpAll(() async {
|
||||
TestUtils.init();
|
||||
db = await TestUtils.initIsar();
|
||||
await StoreService.init(storeRepository: IsarStoreRepository(db));
|
||||
await Store.put(StoreKey.currentUser, UserStub.admin);
|
||||
await Store.put(StoreKey.serverEndpoint, '');
|
||||
});
|
||||
|
||||
setUp(() {
|
||||
mockCurrentAlbumProvider = MockCurrentAlbumProvider(AlbumStub.twoAsset);
|
||||
activityMock = MockAlbumActivity();
|
||||
overrides = [
|
||||
currentAlbumProvider.overrideWith(() => mockCurrentAlbumProvider),
|
||||
albumActivityProvider(AlbumStub.twoAsset.remoteId!).overrideWith(() => activityMock),
|
||||
];
|
||||
});
|
||||
|
||||
testWidgets('Returns an Input text field', (tester) async {
|
||||
await tester.pumpConsumerWidget(ActivityTextField(onSubmit: (_) {}), overrides: overrides);
|
||||
|
||||
expect(find.byType(TextField), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('No UserCircleAvatar when user == null', (tester) async {
|
||||
final userProvider = MockCurrentUserProvider();
|
||||
|
||||
await tester.pumpConsumerWidget(
|
||||
ActivityTextField(onSubmit: (_) {}),
|
||||
overrides: [currentUserProvider.overrideWith((ref) => userProvider), ...overrides],
|
||||
);
|
||||
|
||||
expect(find.byType(UserCircleAvatar), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('UserCircleAvatar displayed when user != null', (tester) async {
|
||||
await tester.pumpConsumerWidget(ActivityTextField(onSubmit: (_) {}), overrides: overrides);
|
||||
|
||||
expect(find.byType(UserCircleAvatar), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('Filled icon if likedId != null', (tester) async {
|
||||
await tester.pumpConsumerWidget(
|
||||
ActivityTextField(onSubmit: (_) {}, likeId: '1'),
|
||||
overrides: overrides,
|
||||
);
|
||||
|
||||
expect(find.widgetWithIcon(IconButton, Icons.thumb_up), findsOneWidget);
|
||||
expect(find.widgetWithIcon(IconButton, Icons.thumb_up_off_alt), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('Bordered icon if likedId == null', (tester) async {
|
||||
await tester.pumpConsumerWidget(ActivityTextField(onSubmit: (_) {}), overrides: overrides);
|
||||
|
||||
expect(find.widgetWithIcon(IconButton, Icons.thumb_up_off_alt), findsOneWidget);
|
||||
expect(find.widgetWithIcon(IconButton, Icons.thumb_up), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('Adds new like', (tester) async {
|
||||
await tester.pumpConsumerWidget(ActivityTextField(onSubmit: (_) {}), overrides: overrides);
|
||||
|
||||
when(() => activityMock.addLike()).thenAnswer((_) => Future.value());
|
||||
|
||||
final suffixIcon = find.byType(IconButton);
|
||||
await tester.tap(suffixIcon);
|
||||
|
||||
verify(() => activityMock.addLike());
|
||||
});
|
||||
|
||||
testWidgets('Removes like if already liked', (tester) async {
|
||||
await tester.pumpConsumerWidget(
|
||||
ActivityTextField(onSubmit: (_) {}, likeId: 'test-suffix'),
|
||||
overrides: overrides,
|
||||
);
|
||||
|
||||
when(() => activityMock.removeActivity(any())).thenAnswer((_) => Future.value());
|
||||
|
||||
final suffixIcon = find.byType(IconButton);
|
||||
await tester.tap(suffixIcon);
|
||||
|
||||
verify(() => activityMock.removeActivity('test-suffix'));
|
||||
});
|
||||
|
||||
testWidgets('Passes text entered to onSubmit on submit', (tester) async {
|
||||
String? receivedText;
|
||||
|
||||
await tester.pumpConsumerWidget(
|
||||
ActivityTextField(onSubmit: (text) => receivedText = text, likeId: 'test-suffix'),
|
||||
overrides: overrides,
|
||||
);
|
||||
|
||||
final textField = find.byType(TextField);
|
||||
await tester.enterText(textField, 'This is a test comment');
|
||||
await tester.testTextInput.receiveAction(TextInputAction.done);
|
||||
expect(receivedText, 'This is a test comment');
|
||||
});
|
||||
|
||||
testWidgets('Input disabled when isEnabled false', (tester) async {
|
||||
String? receviedText;
|
||||
|
||||
await tester.pumpConsumerWidget(
|
||||
ActivityTextField(onSubmit: (text) => receviedText = text, isEnabled: false, likeId: 'test-suffix'),
|
||||
overrides: overrides,
|
||||
);
|
||||
|
||||
final suffixIcon = find.byType(IconButton);
|
||||
await tester.tap(suffixIcon, warnIfMissed: false);
|
||||
|
||||
final textField = find.byType(TextField);
|
||||
await tester.enterText(textField, 'This is a test comment');
|
||||
await tester.testTextInput.receiveAction(TextInputAction.done);
|
||||
|
||||
expect(receviedText, isNull);
|
||||
verifyNever(() => activityMock.addLike());
|
||||
verifyNever(() => activityMock.removeActivity(any()));
|
||||
});
|
||||
}
|
||||
165
mobile/test/modules/activity/activity_tile_test.dart
Normal file
165
mobile/test/modules/activity/activity_tile_test.dart
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
@Skip('currently failing due to mock HTTP client to download ISAR binaries')
|
||||
@Tags(['widget'])
|
||||
library;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/domain/services/store.service.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
|
||||
import 'package:immich_mobile/models/activities/activity.model.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart';
|
||||
import 'package:immich_mobile/widgets/activities/activity_tile.dart';
|
||||
import 'package:immich_mobile/widgets/common/user_circle_avatar.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
import '../../fixtures/asset.stub.dart';
|
||||
import '../../fixtures/user.stub.dart';
|
||||
import '../../test_utils.dart';
|
||||
import '../../widget_tester_extensions.dart';
|
||||
import '../asset_viewer/asset_viewer_mocks.dart';
|
||||
|
||||
void main() {
|
||||
late MockCurrentAssetProvider assetProvider;
|
||||
late List<Override> overrides;
|
||||
late Isar db;
|
||||
|
||||
setUpAll(() async {
|
||||
TestUtils.init();
|
||||
db = await TestUtils.initIsar();
|
||||
// For UserCircleAvatar
|
||||
await StoreService.init(storeRepository: IsarStoreRepository(db));
|
||||
await Store.put(StoreKey.currentUser, UserStub.admin);
|
||||
await Store.put(StoreKey.serverEndpoint, '');
|
||||
await Store.put(StoreKey.accessToken, '');
|
||||
});
|
||||
|
||||
setUp(() {
|
||||
assetProvider = MockCurrentAssetProvider();
|
||||
overrides = [currentAssetProvider.overrideWith(() => assetProvider)];
|
||||
});
|
||||
|
||||
testWidgets('Returns a ListTile', (tester) async {
|
||||
await tester.pumpConsumerWidget(
|
||||
ActivityTile(Activity(id: '1', createdAt: DateTime(100), type: ActivityType.like, user: UserStub.admin)),
|
||||
overrides: overrides,
|
||||
);
|
||||
|
||||
expect(find.byType(ListTile), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('No trailing widget when activity assetId == null', (tester) async {
|
||||
await tester.pumpConsumerWidget(
|
||||
ActivityTile(Activity(id: '1', createdAt: DateTime(100), type: ActivityType.like, user: UserStub.admin)),
|
||||
overrides: overrides,
|
||||
);
|
||||
|
||||
final listTile = tester.widget<ListTile>(find.byType(ListTile));
|
||||
expect(listTile.trailing, isNull);
|
||||
});
|
||||
|
||||
testWidgets('Asset Thumbanil as trailing widget when activity assetId != null', (tester) async {
|
||||
await tester.pumpConsumerWidget(
|
||||
ActivityTile(
|
||||
Activity(id: '1', createdAt: DateTime(100), type: ActivityType.like, user: UserStub.admin, assetId: '1'),
|
||||
),
|
||||
overrides: overrides,
|
||||
);
|
||||
|
||||
final listTile = tester.widget<ListTile>(find.byType(ListTile));
|
||||
expect(listTile.trailing, isNotNull);
|
||||
// TODO: Validate this to be the common class after migrating ActivityTile#_ActivityAssetThumbnail to a common class
|
||||
});
|
||||
|
||||
testWidgets('No trailing widget when current asset != null', (tester) async {
|
||||
await tester.pumpConsumerWidget(
|
||||
ActivityTile(
|
||||
Activity(id: '1', createdAt: DateTime(100), type: ActivityType.like, user: UserStub.admin, assetId: '1'),
|
||||
),
|
||||
overrides: overrides,
|
||||
);
|
||||
|
||||
assetProvider.state = AssetStub.image1;
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final listTile = tester.widget<ListTile>(find.byType(ListTile));
|
||||
expect(listTile.trailing, isNull);
|
||||
});
|
||||
|
||||
group('Like Activity', () {
|
||||
final activity = Activity(id: '1', createdAt: DateTime(100), type: ActivityType.like, user: UserStub.admin);
|
||||
|
||||
testWidgets('Like contains filled thumbs-up as leading', (tester) async {
|
||||
await tester.pumpConsumerWidget(ActivityTile(activity), overrides: overrides);
|
||||
|
||||
// Leading widget should not be null
|
||||
final listTile = tester.widget<ListTile>(find.byType(ListTile));
|
||||
expect(listTile.leading, isNotNull);
|
||||
|
||||
// And should have a thumb_up icon
|
||||
final thumbUpIconFinder = find.widgetWithIcon(listTile.leading!.runtimeType, Icons.thumb_up);
|
||||
|
||||
expect(thumbUpIconFinder, findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('Like title is center aligned', (tester) async {
|
||||
await tester.pumpConsumerWidget(ActivityTile(activity), overrides: overrides);
|
||||
|
||||
final listTile = tester.widget<ListTile>(find.byType(ListTile));
|
||||
|
||||
expect(listTile.titleAlignment, ListTileTitleAlignment.center);
|
||||
});
|
||||
|
||||
testWidgets('No subtitle for likes', (tester) async {
|
||||
await tester.pumpConsumerWidget(ActivityTile(activity), overrides: overrides);
|
||||
|
||||
final listTile = tester.widget<ListTile>(find.byType(ListTile));
|
||||
|
||||
expect(listTile.subtitle, isNull);
|
||||
});
|
||||
});
|
||||
|
||||
group('Comment Activity', () {
|
||||
final activity = Activity(
|
||||
id: '1',
|
||||
createdAt: DateTime(100),
|
||||
type: ActivityType.comment,
|
||||
comment: 'This is a test comment',
|
||||
user: UserStub.admin,
|
||||
);
|
||||
|
||||
testWidgets('Comment contains User Circle Avatar as leading', (tester) async {
|
||||
await tester.pumpConsumerWidget(ActivityTile(activity), overrides: overrides);
|
||||
|
||||
final userAvatarFinder = find.byType(UserCircleAvatar);
|
||||
expect(userAvatarFinder, findsOneWidget);
|
||||
|
||||
// Leading widget should not be null
|
||||
final listTile = tester.widget<ListTile>(find.byType(ListTile));
|
||||
expect(listTile.leading, isNotNull);
|
||||
|
||||
// Make sure that the leading widget is the UserCircleAvatar
|
||||
final userAvatar = tester.widget<UserCircleAvatar>(userAvatarFinder);
|
||||
expect(listTile.leading, userAvatar);
|
||||
});
|
||||
|
||||
testWidgets('Comment title is top aligned', (tester) async {
|
||||
await tester.pumpConsumerWidget(ActivityTile(activity), overrides: overrides);
|
||||
|
||||
final listTile = tester.widget<ListTile>(find.byType(ListTile));
|
||||
|
||||
expect(listTile.titleAlignment, ListTileTitleAlignment.top);
|
||||
});
|
||||
|
||||
testWidgets('Contains comment text as subtitle', (tester) async {
|
||||
await tester.pumpConsumerWidget(ActivityTile(activity), overrides: overrides);
|
||||
|
||||
final listTile = tester.widget<ListTile>(find.byType(ListTile));
|
||||
|
||||
expect(listTile.subtitle, isNotNull);
|
||||
expect(find.descendant(of: find.byType(ListTile), matching: find.text(activity.comment!)), findsOneWidget);
|
||||
});
|
||||
});
|
||||
}
|
||||
99
mobile/test/modules/activity/dismissible_activity_test.dart
Normal file
99
mobile/test/modules/activity/dismissible_activity_test.dart
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
@Tags(['widget'])
|
||||
library;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/models/activities/activity.model.dart';
|
||||
import 'package:immich_mobile/widgets/activities/activity_tile.dart';
|
||||
import 'package:immich_mobile/widgets/activities/dismissible_activity.dart';
|
||||
import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart';
|
||||
import 'package:immich_mobile/widgets/common/confirm_dialog.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
import '../../fixtures/user.stub.dart';
|
||||
import '../../test_utils.dart';
|
||||
import '../../widget_tester_extensions.dart';
|
||||
import '../asset_viewer/asset_viewer_mocks.dart';
|
||||
|
||||
final activity = Activity(id: '1', createdAt: DateTime(100), type: ActivityType.like, user: UserStub.admin);
|
||||
|
||||
void main() {
|
||||
late MockCurrentAssetProvider assetProvider;
|
||||
late List<Override> overrides;
|
||||
|
||||
setUpAll(() => TestUtils.init());
|
||||
|
||||
setUp(() {
|
||||
assetProvider = MockCurrentAssetProvider();
|
||||
overrides = [currentAssetProvider.overrideWith(() => assetProvider)];
|
||||
});
|
||||
|
||||
testWidgets('Returns a Dismissible', (tester) async {
|
||||
await tester.pumpConsumerWidget(
|
||||
DismissibleActivity('1', ActivityTile(activity), onDismiss: (_) {}),
|
||||
overrides: overrides,
|
||||
);
|
||||
|
||||
expect(find.byType(Dismissible), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('Dialog displayed when onDismiss is set', (tester) async {
|
||||
await tester.pumpConsumerWidget(
|
||||
DismissibleActivity('1', ActivityTile(activity), onDismiss: (_) {}),
|
||||
overrides: overrides,
|
||||
);
|
||||
|
||||
final dismissible = find.byType(Dismissible);
|
||||
await tester.drag(dismissible, const Offset(500, 0));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(ConfirmDialog), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('Ok action in ConfirmDialog should call onDismiss with activityId', (tester) async {
|
||||
String? receivedActivityId;
|
||||
await tester.pumpConsumerWidget(
|
||||
DismissibleActivity('1', ActivityTile(activity), onDismiss: (id) => receivedActivityId = id),
|
||||
overrides: overrides,
|
||||
);
|
||||
|
||||
final dismissible = find.byType(Dismissible);
|
||||
await tester.drag(dismissible, const Offset(-500, 0));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final okButton = find.text('delete');
|
||||
await tester.tap(okButton);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(receivedActivityId, '1');
|
||||
});
|
||||
|
||||
testWidgets('Delete icon for background if onDismiss is set', (tester) async {
|
||||
await tester.pumpConsumerWidget(
|
||||
DismissibleActivity('1', ActivityTile(activity), onDismiss: (_) {}),
|
||||
overrides: overrides,
|
||||
);
|
||||
|
||||
final dismissible = find.byType(Dismissible);
|
||||
await tester.drag(dismissible, const Offset(500, 0));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byIcon(Icons.delete_sweep_rounded), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('No delete dialog if onDismiss is not set', (tester) async {
|
||||
await tester.pumpConsumerWidget(DismissibleActivity('1', ActivityTile(activity)), overrides: overrides);
|
||||
|
||||
// When onDismiss is not set, the widget should not be wrapped by a Dismissible
|
||||
expect(find.byType(Dismissible), findsNothing);
|
||||
expect(find.byType(ConfirmDialog), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('No icon for background if onDismiss is not set', (tester) async {
|
||||
await tester.pumpConsumerWidget(DismissibleActivity('1', ActivityTile(activity)), overrides: overrides);
|
||||
|
||||
// No Dismissible should exist when onDismiss is not provided, so no delete icon either
|
||||
expect(find.byType(Dismissible), findsNothing);
|
||||
expect(find.byIcon(Icons.delete_sweep_rounded), findsNothing);
|
||||
});
|
||||
}
|
||||
13
mobile/test/modules/album/album_mocks.dart
Normal file
13
mobile/test/modules/album/album_mocks.dart
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import 'package:immich_mobile/providers/album/current_album.provider.dart';
|
||||
import 'package:immich_mobile/entities/album.entity.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
class MockCurrentAlbumProvider extends CurrentAlbum with Mock implements CurrentAlbumInternal {
|
||||
Album? initAlbum;
|
||||
MockCurrentAlbumProvider([this.initAlbum]);
|
||||
|
||||
@override
|
||||
Album? build() {
|
||||
return initAlbum;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,270 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/entities/album.entity.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart';
|
||||
import 'package:immich_mobile/providers/app_settings.provider.dart';
|
||||
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
import '../../fixtures/album.stub.dart';
|
||||
import '../../fixtures/asset.stub.dart';
|
||||
import '../../test_utils.dart';
|
||||
import '../settings/settings_mocks.dart';
|
||||
|
||||
void main() {
|
||||
/// Verify the sort modes
|
||||
group("AlbumSortMode", () {
|
||||
late final Isar db;
|
||||
|
||||
setUpAll(() async {
|
||||
db = await TestUtils.initIsar();
|
||||
});
|
||||
|
||||
final albums = [AlbumStub.emptyAlbum, AlbumStub.sharedWithUser, AlbumStub.oneAsset, AlbumStub.twoAsset];
|
||||
|
||||
setUp(() {
|
||||
db.writeTxnSync(() {
|
||||
db.clearSync();
|
||||
// Save all assets
|
||||
db.assets.putAllSync([AssetStub.image1, AssetStub.image2]);
|
||||
db.albums.putAllSync(albums);
|
||||
for (final album in albums) {
|
||||
album.sharedUsers.saveSync();
|
||||
album.assets.saveSync();
|
||||
}
|
||||
});
|
||||
expect(db.albums.countSync(), 4);
|
||||
expect(db.assets.countSync(), 2);
|
||||
});
|
||||
|
||||
group("Album sort - Created Time", () {
|
||||
const created = AlbumSortMode.created;
|
||||
test("Created time - ASC", () {
|
||||
final sorted = created.sortFn(albums, false);
|
||||
final sortedList = [AlbumStub.emptyAlbum, AlbumStub.twoAsset, AlbumStub.oneAsset, AlbumStub.sharedWithUser];
|
||||
expect(sorted, orderedEquals(sortedList));
|
||||
});
|
||||
|
||||
test("Created time - DESC", () {
|
||||
final sorted = created.sortFn(albums, true);
|
||||
final sortedList = [AlbumStub.sharedWithUser, AlbumStub.oneAsset, AlbumStub.twoAsset, AlbumStub.emptyAlbum];
|
||||
expect(sorted, orderedEquals(sortedList));
|
||||
});
|
||||
});
|
||||
|
||||
group("Album sort - Asset count", () {
|
||||
const assetCount = AlbumSortMode.assetCount;
|
||||
test("Asset Count - ASC", () {
|
||||
final sorted = assetCount.sortFn(albums, false);
|
||||
final sortedList = [AlbumStub.emptyAlbum, AlbumStub.sharedWithUser, AlbumStub.oneAsset, AlbumStub.twoAsset];
|
||||
expect(sorted, orderedEquals(sortedList));
|
||||
});
|
||||
|
||||
test("Asset Count - DESC", () {
|
||||
final sorted = assetCount.sortFn(albums, true);
|
||||
final sortedList = [AlbumStub.twoAsset, AlbumStub.oneAsset, AlbumStub.sharedWithUser, AlbumStub.emptyAlbum];
|
||||
expect(sorted, orderedEquals(sortedList));
|
||||
});
|
||||
});
|
||||
|
||||
group("Album sort - Last modified", () {
|
||||
const lastModified = AlbumSortMode.lastModified;
|
||||
test("Last modified - ASC", () {
|
||||
final sorted = lastModified.sortFn(albums, false);
|
||||
final sortedList = [AlbumStub.twoAsset, AlbumStub.emptyAlbum, AlbumStub.sharedWithUser, AlbumStub.oneAsset];
|
||||
expect(sorted, orderedEquals(sortedList));
|
||||
});
|
||||
|
||||
test("Last modified - DESC", () {
|
||||
final sorted = lastModified.sortFn(albums, true);
|
||||
final sortedList = [AlbumStub.oneAsset, AlbumStub.sharedWithUser, AlbumStub.emptyAlbum, AlbumStub.twoAsset];
|
||||
expect(sorted, orderedEquals(sortedList));
|
||||
});
|
||||
});
|
||||
|
||||
group("Album sort - Created", () {
|
||||
const created = AlbumSortMode.created;
|
||||
test("Created - ASC", () {
|
||||
final sorted = created.sortFn(albums, false);
|
||||
final sortedList = [AlbumStub.emptyAlbum, AlbumStub.twoAsset, AlbumStub.oneAsset, AlbumStub.sharedWithUser];
|
||||
expect(sorted, orderedEquals(sortedList));
|
||||
});
|
||||
|
||||
test("Created - DESC", () {
|
||||
final sorted = created.sortFn(albums, true);
|
||||
final sortedList = [AlbumStub.sharedWithUser, AlbumStub.oneAsset, AlbumStub.twoAsset, AlbumStub.emptyAlbum];
|
||||
expect(sorted, orderedEquals(sortedList));
|
||||
});
|
||||
});
|
||||
|
||||
group("Album sort - Most Recent", () {
|
||||
const mostRecent = AlbumSortMode.mostRecent;
|
||||
|
||||
test("Most Recent - DESC", () {
|
||||
final sorted = mostRecent.sortFn([
|
||||
AlbumStub.create2020end2020Album,
|
||||
AlbumStub.create2020end2022Album,
|
||||
AlbumStub.create2020end2024Album,
|
||||
AlbumStub.create2020end2026Album,
|
||||
], false);
|
||||
final sortedList = [
|
||||
AlbumStub.create2020end2026Album,
|
||||
AlbumStub.create2020end2024Album,
|
||||
AlbumStub.create2020end2022Album,
|
||||
AlbumStub.create2020end2020Album,
|
||||
];
|
||||
expect(sorted, orderedEquals(sortedList));
|
||||
});
|
||||
|
||||
test("Most Recent - ASC", () {
|
||||
final sorted = mostRecent.sortFn([
|
||||
AlbumStub.create2020end2020Album,
|
||||
AlbumStub.create2020end2022Album,
|
||||
AlbumStub.create2020end2024Album,
|
||||
AlbumStub.create2020end2026Album,
|
||||
], true);
|
||||
final sortedList = [
|
||||
AlbumStub.create2020end2020Album,
|
||||
AlbumStub.create2020end2022Album,
|
||||
AlbumStub.create2020end2024Album,
|
||||
AlbumStub.create2020end2026Album,
|
||||
];
|
||||
expect(sorted, orderedEquals(sortedList));
|
||||
});
|
||||
});
|
||||
|
||||
group("Album sort - Most Oldest", () {
|
||||
const mostOldest = AlbumSortMode.mostOldest;
|
||||
|
||||
test("Most Oldest - ASC", () {
|
||||
final sorted = mostOldest.sortFn(albums, false);
|
||||
final sortedList = [AlbumStub.twoAsset, AlbumStub.emptyAlbum, AlbumStub.oneAsset, AlbumStub.sharedWithUser];
|
||||
expect(sorted, orderedEquals(sortedList));
|
||||
});
|
||||
|
||||
test("Most Oldest - DESC", () {
|
||||
final sorted = mostOldest.sortFn(albums, true);
|
||||
final sortedList = [AlbumStub.sharedWithUser, AlbumStub.oneAsset, AlbumStub.emptyAlbum, AlbumStub.twoAsset];
|
||||
expect(sorted, orderedEquals(sortedList));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/// Verify the sort mode provider
|
||||
group('AlbumSortByOptions', () {
|
||||
late AppSettingsService settingsMock;
|
||||
late ProviderContainer container;
|
||||
|
||||
setUp(() async {
|
||||
settingsMock = MockAppSettingsService();
|
||||
container = TestUtils.createContainer(
|
||||
overrides: [appSettingsServiceProvider.overrideWith((ref) => settingsMock)],
|
||||
);
|
||||
when(
|
||||
() => settingsMock.setSetting<bool>(AppSettingsEnum.selectedAlbumSortReverse, any()),
|
||||
).thenAnswer((_) async => {});
|
||||
when(
|
||||
() => settingsMock.setSetting<int>(AppSettingsEnum.selectedAlbumSortOrder, any()),
|
||||
).thenAnswer((_) async => {});
|
||||
});
|
||||
|
||||
test('Returns the default sort mode when none set', () {
|
||||
// Returns the default value when nothing is set
|
||||
when(() => settingsMock.getSetting(AppSettingsEnum.selectedAlbumSortOrder)).thenReturn(0);
|
||||
|
||||
expect(container.read(albumSortByOptionsProvider), AlbumSortMode.created);
|
||||
});
|
||||
|
||||
test('Returns the correct sort mode with index from Store', () {
|
||||
// Returns the default value when nothing is set
|
||||
when(() => settingsMock.getSetting(AppSettingsEnum.selectedAlbumSortOrder)).thenReturn(3);
|
||||
|
||||
expect(container.read(albumSortByOptionsProvider), AlbumSortMode.lastModified);
|
||||
});
|
||||
|
||||
test('Properly saves the correct store index of sort mode', () {
|
||||
container.read(albumSortByOptionsProvider.notifier).changeSortMode(AlbumSortMode.mostOldest);
|
||||
|
||||
verify(
|
||||
() => settingsMock.setSetting(AppSettingsEnum.selectedAlbumSortOrder, AlbumSortMode.mostOldest.storeIndex),
|
||||
);
|
||||
});
|
||||
|
||||
test('Notifies listeners on state change', () {
|
||||
when(() => settingsMock.getSetting(AppSettingsEnum.selectedAlbumSortOrder)).thenReturn(0);
|
||||
|
||||
final listener = ListenerMock<AlbumSortMode>();
|
||||
container.listen(albumSortByOptionsProvider, listener.call, fireImmediately: true);
|
||||
|
||||
// Created -> Most Oldest
|
||||
container.read(albumSortByOptionsProvider.notifier).changeSortMode(AlbumSortMode.mostOldest);
|
||||
|
||||
// Most Oldest -> Title
|
||||
container.read(albumSortByOptionsProvider.notifier).changeSortMode(AlbumSortMode.title);
|
||||
|
||||
verifyInOrder([
|
||||
() => listener.call(null, AlbumSortMode.created),
|
||||
() => listener.call(AlbumSortMode.created, AlbumSortMode.mostOldest),
|
||||
() => listener.call(AlbumSortMode.mostOldest, AlbumSortMode.title),
|
||||
]);
|
||||
|
||||
verifyNoMoreInteractions(listener);
|
||||
});
|
||||
});
|
||||
|
||||
/// Verify the sort order provider
|
||||
group('AlbumSortOrder', () {
|
||||
late AppSettingsService settingsMock;
|
||||
late ProviderContainer container;
|
||||
|
||||
registerFallbackValue(AppSettingsEnum.selectedAlbumSortReverse);
|
||||
|
||||
setUp(() async {
|
||||
settingsMock = MockAppSettingsService();
|
||||
container = TestUtils.createContainer(
|
||||
overrides: [appSettingsServiceProvider.overrideWith((ref) => settingsMock)],
|
||||
);
|
||||
when(
|
||||
() => settingsMock.setSetting<bool>(AppSettingsEnum.selectedAlbumSortReverse, any()),
|
||||
).thenAnswer((_) async => {});
|
||||
when(
|
||||
() => settingsMock.setSetting<int>(AppSettingsEnum.selectedAlbumSortOrder, any()),
|
||||
).thenAnswer((_) async => {});
|
||||
});
|
||||
|
||||
test('Returns the default sort order when none set - false', () {
|
||||
when(() => settingsMock.getSetting(AppSettingsEnum.selectedAlbumSortReverse)).thenReturn(false);
|
||||
|
||||
expect(container.read(albumSortOrderProvider), isFalse);
|
||||
});
|
||||
|
||||
test('Properly saves the correct order', () {
|
||||
container.read(albumSortOrderProvider.notifier).changeSortDirection(true);
|
||||
|
||||
verify(() => settingsMock.setSetting(AppSettingsEnum.selectedAlbumSortReverse, true));
|
||||
});
|
||||
|
||||
test('Notifies listeners on state change', () {
|
||||
when(() => settingsMock.getSetting(AppSettingsEnum.selectedAlbumSortReverse)).thenReturn(false);
|
||||
|
||||
final listener = ListenerMock<bool>();
|
||||
container.listen(albumSortOrderProvider, listener.call, fireImmediately: true);
|
||||
|
||||
// false -> true
|
||||
container.read(albumSortOrderProvider.notifier).changeSortDirection(true);
|
||||
|
||||
// true -> false
|
||||
container.read(albumSortOrderProvider.notifier).changeSortDirection(false);
|
||||
|
||||
verifyInOrder([
|
||||
() => listener.call(null, false),
|
||||
() => listener.call(false, true),
|
||||
() => listener.call(true, false),
|
||||
]);
|
||||
|
||||
verifyNoMoreInteractions(listener);
|
||||
});
|
||||
});
|
||||
}
|
||||
13
mobile/test/modules/asset_viewer/asset_viewer_mocks.dart
Normal file
13
mobile/test/modules/asset_viewer/asset_viewer_mocks.dart
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
class MockCurrentAssetProvider extends CurrentAssetInternal with Mock implements CurrentAsset {
|
||||
Asset? initAsset;
|
||||
MockCurrentAssetProvider([this.initAsset]);
|
||||
|
||||
@override
|
||||
Asset? build() {
|
||||
return initAsset;
|
||||
}
|
||||
}
|
||||
113
mobile/test/modules/extensions/asset_extensions_test.dart
Normal file
113
mobile/test/modules/extensions/asset_extensions_test.dart
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/domain/models/exif.model.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/extensions/asset_extensions.dart';
|
||||
import 'package:timezone/data/latest.dart';
|
||||
import 'package:timezone/timezone.dart';
|
||||
|
||||
ExifInfo makeExif({DateTime? dateTimeOriginal, String? timeZone}) {
|
||||
return ExifInfo(dateTimeOriginal: dateTimeOriginal, timeZone: timeZone);
|
||||
}
|
||||
|
||||
Asset makeAsset({required String id, required DateTime createdAt, ExifInfo? exifInfo}) {
|
||||
return Asset(
|
||||
checksum: '',
|
||||
localId: id,
|
||||
remoteId: id,
|
||||
ownerId: 1,
|
||||
fileCreatedAt: createdAt,
|
||||
fileModifiedAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
durationInSeconds: 0,
|
||||
type: AssetType.image,
|
||||
fileName: id,
|
||||
isFavorite: false,
|
||||
isArchived: false,
|
||||
isTrashed: false,
|
||||
exifInfo: exifInfo,
|
||||
);
|
||||
}
|
||||
|
||||
void main() {
|
||||
// Init Timezone DB
|
||||
initializeTimeZones();
|
||||
|
||||
group("Returns local time and offset if no exifInfo", () {
|
||||
test('returns createdAt directly if in local', () {
|
||||
final createdAt = DateTime(2023, 12, 12, 12, 12, 12);
|
||||
final a = makeAsset(id: '1', createdAt: createdAt);
|
||||
final (dt, tz) = a.getTZAdjustedTimeAndOffset();
|
||||
|
||||
expect(createdAt, dt);
|
||||
expect(createdAt.timeZoneOffset, tz);
|
||||
});
|
||||
|
||||
test('returns createdAt in local if in utc', () {
|
||||
final createdAt = DateTime.utc(2023, 12, 12, 12, 12, 12);
|
||||
final a = makeAsset(id: '1', createdAt: createdAt);
|
||||
final (dt, tz) = a.getTZAdjustedTimeAndOffset();
|
||||
|
||||
final localCreatedAt = createdAt.toLocal();
|
||||
expect(localCreatedAt, dt);
|
||||
expect(localCreatedAt.timeZoneOffset, tz);
|
||||
});
|
||||
});
|
||||
|
||||
group("Returns dateTimeOriginal", () {
|
||||
test('Returns dateTimeOriginal in UTC from exifInfo without timezone', () {
|
||||
final createdAt = DateTime.parse("2023-01-27T14:00:00-0500");
|
||||
final dateTimeOriginal = DateTime.parse("2022-01-27T14:00:00+0530");
|
||||
final e = makeExif(dateTimeOriginal: dateTimeOriginal);
|
||||
final a = makeAsset(id: '1', createdAt: createdAt, exifInfo: e);
|
||||
final (dt, tz) = a.getTZAdjustedTimeAndOffset();
|
||||
|
||||
final dateTimeInUTC = dateTimeOriginal.toUtc();
|
||||
expect(dateTimeInUTC, dt);
|
||||
expect(dateTimeInUTC.timeZoneOffset, tz);
|
||||
});
|
||||
|
||||
test('Returns dateTimeOriginal in UTC from exifInfo with invalid timezone', () {
|
||||
final createdAt = DateTime.parse("2023-01-27T14:00:00-0500");
|
||||
final dateTimeOriginal = DateTime.parse("2022-01-27T14:00:00+0530");
|
||||
final e = makeExif(dateTimeOriginal: dateTimeOriginal, timeZone: "#_#"); // Invalid timezone
|
||||
final a = makeAsset(id: '1', createdAt: createdAt, exifInfo: e);
|
||||
final (dt, tz) = a.getTZAdjustedTimeAndOffset();
|
||||
|
||||
final dateTimeInUTC = dateTimeOriginal.toUtc();
|
||||
expect(dateTimeInUTC, dt);
|
||||
expect(dateTimeInUTC.timeZoneOffset, tz);
|
||||
});
|
||||
});
|
||||
|
||||
group("Returns adjusted time if timezone available", () {
|
||||
test('With timezone as location', () {
|
||||
final createdAt = DateTime.parse("2023-01-27T14:00:00-0500");
|
||||
final dateTimeOriginal = DateTime.parse("2022-01-27T14:00:00+0530");
|
||||
const location = "Asia/Hong_Kong";
|
||||
final e = makeExif(dateTimeOriginal: dateTimeOriginal, timeZone: location);
|
||||
final a = makeAsset(id: '1', createdAt: createdAt, exifInfo: e);
|
||||
final (dt, tz) = a.getTZAdjustedTimeAndOffset();
|
||||
|
||||
final adjustedTime = TZDateTime.from(dateTimeOriginal.toUtc(), getLocation(location));
|
||||
expect(adjustedTime, dt);
|
||||
expect(adjustedTime.timeZoneOffset, tz);
|
||||
});
|
||||
|
||||
test('With timezone as offset', () {
|
||||
final createdAt = DateTime.parse("2023-01-27T14:00:00-0500");
|
||||
final dateTimeOriginal = DateTime.parse("2022-01-27T14:00:00+0530");
|
||||
const offset = "utc+08:00";
|
||||
final e = makeExif(dateTimeOriginal: dateTimeOriginal, timeZone: offset);
|
||||
final a = makeAsset(id: '1', createdAt: createdAt, exifInfo: e);
|
||||
final (dt, tz) = a.getTZAdjustedTimeAndOffset();
|
||||
|
||||
final location = getLocation("Asia/Hong_Kong");
|
||||
final offsetFromLocation = Duration(milliseconds: location.currentTimeZone.offset);
|
||||
final adjustedTime = dateTimeOriginal.toUtc().add(offsetFromLocation);
|
||||
|
||||
// Adds the offset to the actual time and returns the offset separately
|
||||
expect(adjustedTime, dt);
|
||||
expect(offsetFromLocation, tz);
|
||||
});
|
||||
});
|
||||
}
|
||||
50
mobile/test/modules/extensions/builtin_extensions_test.dart
Normal file
50
mobile/test/modules/extensions/builtin_extensions_test.dart
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/extensions/collection_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/string_extensions.dart';
|
||||
|
||||
void main() {
|
||||
group('Test toDuration', () {
|
||||
test('ok', () {
|
||||
expect("1:02:33".toDuration(), const Duration(hours: 1, minutes: 2, seconds: 33));
|
||||
});
|
||||
test('malformed', () {
|
||||
expect("".toDuration(), isNull);
|
||||
expect("1:2".toDuration(), isNull);
|
||||
expect("a:b:c".toDuration(), isNull);
|
||||
});
|
||||
});
|
||||
group('Test uniqueConsecutive', () {
|
||||
test('empty', () {
|
||||
final a = [];
|
||||
expect(a.uniqueConsecutive(), []);
|
||||
});
|
||||
|
||||
test('singleElement', () {
|
||||
final a = [5];
|
||||
expect(a.uniqueConsecutive(), [5]);
|
||||
});
|
||||
|
||||
test('noDuplicates', () {
|
||||
final a = [1, 2, 3];
|
||||
expect(a.uniqueConsecutive(), orderedEquals([1, 2, 3]));
|
||||
});
|
||||
|
||||
test('unsortedDuplicates', () {
|
||||
final a = [1, 2, 1, 3];
|
||||
expect(a.uniqueConsecutive(), orderedEquals([1, 2, 1, 3]));
|
||||
});
|
||||
|
||||
test('sortedDuplicates', () {
|
||||
final a = [6, 6, 2, 3, 3, 3, 4, 5, 1, 1];
|
||||
expect(a.uniqueConsecutive(), orderedEquals([6, 2, 3, 4, 5, 1]));
|
||||
});
|
||||
|
||||
test('withKey', () {
|
||||
final a = ["a", "bb", "cc", "ddd"];
|
||||
expect(
|
||||
a.uniqueConsecutive(compare: (s1, s2) => s1.length.compareTo(s2.length)),
|
||||
orderedEquals(["a", "bb", "ddd"]),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
46
mobile/test/modules/extensions/datetime_extensions_test.dart
Normal file
46
mobile/test/modules/extensions/datetime_extensions_test.dart
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/extensions/datetime_extensions.dart';
|
||||
import 'package:intl/date_symbol_data_local.dart';
|
||||
|
||||
void main() {
|
||||
setUpAll(() async {
|
||||
await initializeDateFormatting();
|
||||
});
|
||||
|
||||
group('DateRangeFormatting.formatDateRange', () {
|
||||
final currentYear = DateTime.now().year;
|
||||
|
||||
test('returns single date format for this year', () {
|
||||
final date = DateTime(currentYear, 8, 28); // Aug 28 this year
|
||||
final result = DateRangeFormatting.formatDateRange(date, date, null);
|
||||
expect(result, 'Aug 28');
|
||||
});
|
||||
|
||||
test('returns single date format for other year', () {
|
||||
final date = DateTime(2023, 8, 28); // Aug 28, 2023
|
||||
final result = DateRangeFormatting.formatDateRange(date, date, null);
|
||||
expect(result, 'Aug 28, 2023');
|
||||
});
|
||||
|
||||
test('returns date range format for this year', () {
|
||||
final startDate = DateTime(currentYear, 3, 23); // Mar 23
|
||||
final endDate = DateTime(currentYear, 5, 31); // May 31
|
||||
final result = DateRangeFormatting.formatDateRange(startDate, endDate, null);
|
||||
expect(result, 'Mar 23 - May 31');
|
||||
});
|
||||
|
||||
test('returns date range format for other year (same year)', () {
|
||||
final startDate = DateTime(2023, 8, 28); // Aug 28
|
||||
final endDate = DateTime(2023, 9, 30); // Sep 30
|
||||
final result = DateRangeFormatting.formatDateRange(startDate, endDate, null);
|
||||
expect(result, 'Aug 28 - Sep 30, 2023');
|
||||
});
|
||||
|
||||
test('returns date range format over multiple years', () {
|
||||
final startDate = DateTime(2021, 4, 17); // Apr 17, 2021
|
||||
final endDate = DateTime(2022, 4, 9); // Apr 9, 2022
|
||||
final result = DateRangeFormatting.formatDateRange(startDate, endDate, null);
|
||||
expect(result, 'Apr 17, 2021 - Apr 9, 2022');
|
||||
});
|
||||
});
|
||||
}
|
||||
113
mobile/test/modules/home/asset_grid_data_structure_test.dart
Normal file
113
mobile/test/modules/home/asset_grid_data_structure_test.dart
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
|
||||
void main() {
|
||||
final List<Asset> testAssets = [];
|
||||
|
||||
for (int i = 0; i < 150; i++) {
|
||||
int month = i ~/ 31;
|
||||
int day = (i % 31).toInt();
|
||||
|
||||
DateTime date = DateTime(2022, month, day);
|
||||
|
||||
testAssets.add(
|
||||
Asset(
|
||||
checksum: "",
|
||||
localId: '$i',
|
||||
ownerId: 1,
|
||||
fileCreatedAt: date,
|
||||
fileModifiedAt: date,
|
||||
updatedAt: date,
|
||||
durationInSeconds: 0,
|
||||
type: AssetType.image,
|
||||
fileName: '',
|
||||
isFavorite: false,
|
||||
isArchived: false,
|
||||
isTrashed: false,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final List<Asset> assets = [];
|
||||
|
||||
assets.addAll(
|
||||
testAssets.sublist(0, 5).map((e) {
|
||||
e.fileCreatedAt = DateTime(2022, 1, 5);
|
||||
return e;
|
||||
}).toList(),
|
||||
);
|
||||
assets.addAll(
|
||||
testAssets.sublist(5, 10).map((e) {
|
||||
e.fileCreatedAt = DateTime(2022, 1, 10);
|
||||
return e;
|
||||
}).toList(),
|
||||
);
|
||||
assets.addAll(
|
||||
testAssets.sublist(10, 15).map((e) {
|
||||
e.fileCreatedAt = DateTime(2022, 2, 17);
|
||||
return e;
|
||||
}).toList(),
|
||||
);
|
||||
assets.addAll(
|
||||
testAssets.sublist(15, 30).map((e) {
|
||||
e.fileCreatedAt = DateTime(2022, 10, 15);
|
||||
return e;
|
||||
}).toList(),
|
||||
);
|
||||
|
||||
group('Test grouped', () {
|
||||
test('test grouped check months', () async {
|
||||
final renderList = await RenderList.fromAssets(assets, GroupAssetsBy.day);
|
||||
|
||||
// Oct
|
||||
// Day 1
|
||||
// 15 Assets => 5 Rows
|
||||
// Feb
|
||||
// Day 1
|
||||
// 5 Assets => 2 Rows
|
||||
// Jan
|
||||
// Day 2
|
||||
// 5 Assets => 2 Rows
|
||||
// Day 1
|
||||
// 5 Assets => 2 Rows
|
||||
expect(renderList.elements, hasLength(4));
|
||||
expect(renderList.elements[0].type, RenderAssetGridElementType.monthTitle);
|
||||
expect(renderList.elements[0].date.month, 1);
|
||||
expect(renderList.elements[1].type, RenderAssetGridElementType.groupDividerTitle);
|
||||
expect(renderList.elements[1].date.month, 1);
|
||||
expect(renderList.elements[2].type, RenderAssetGridElementType.monthTitle);
|
||||
expect(renderList.elements[2].date.month, 2);
|
||||
expect(renderList.elements[3].type, RenderAssetGridElementType.monthTitle);
|
||||
expect(renderList.elements[3].date.month, 10);
|
||||
});
|
||||
|
||||
test('test grouped check types', () async {
|
||||
final renderList = await RenderList.fromAssets(assets, GroupAssetsBy.day);
|
||||
|
||||
// Oct
|
||||
// Day 1
|
||||
// 15 Assets => 3 Rows
|
||||
// Feb
|
||||
// Day 1
|
||||
// 5 Assets => 1 Row
|
||||
// Jan
|
||||
// Day 2
|
||||
// 5 Assets => 1 Row
|
||||
// Day 1
|
||||
// 5 Assets => 1 Row
|
||||
final types = [
|
||||
RenderAssetGridElementType.monthTitle,
|
||||
RenderAssetGridElementType.groupDividerTitle,
|
||||
RenderAssetGridElementType.monthTitle,
|
||||
RenderAssetGridElementType.monthTitle,
|
||||
];
|
||||
|
||||
expect(renderList.elements, hasLength(types.length));
|
||||
|
||||
for (int i = 0; i < renderList.elements.length; i++) {
|
||||
expect(renderList.elements[i].type, types[i]);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
16
mobile/test/modules/map/map_mocks.dart
Normal file
16
mobile/test/modules/map/map_mocks.dart
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/models/map/map_state.model.dart';
|
||||
import 'package:immich_mobile/providers/map/map_state.provider.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
class MockMapStateNotifier extends Notifier<MapState> with Mock implements MapStateNotifier {
|
||||
final MapState initState;
|
||||
|
||||
MockMapStateNotifier(this.initState);
|
||||
|
||||
@override
|
||||
MapState build() => initState;
|
||||
|
||||
@override
|
||||
set state(MapState mapState) => super.state = mapState;
|
||||
}
|
||||
163
mobile/test/modules/map/map_theme_override_test.dart
Normal file
163
mobile/test/modules/map/map_theme_override_test.dart
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
@Skip('Flaky test, needs investigation')
|
||||
@Tags(['widget'])
|
||||
library;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/services/store.service.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
|
||||
import 'package:immich_mobile/models/map/map_state.model.dart';
|
||||
import 'package:immich_mobile/providers/locale_provider.dart';
|
||||
import 'package:immich_mobile/providers/map/map_state.provider.dart';
|
||||
import 'package:immich_mobile/widgets/map/map_theme_override.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
import '../../test_utils.dart';
|
||||
import '../../widget_tester_extensions.dart';
|
||||
import 'map_mocks.dart';
|
||||
|
||||
void main() {
|
||||
late MockMapStateNotifier mapStateNotifier;
|
||||
late List<Override> overrides;
|
||||
late MapState mapState;
|
||||
late Isar db;
|
||||
|
||||
setUpAll(() async {
|
||||
db = await TestUtils.initIsar();
|
||||
TestUtils.init();
|
||||
});
|
||||
|
||||
setUp(() async {
|
||||
mapState = const MapState(themeMode: ThemeMode.dark);
|
||||
mapStateNotifier = MockMapStateNotifier(mapState);
|
||||
await StoreService.init(storeRepository: IsarStoreRepository(db));
|
||||
overrides = [
|
||||
mapStateNotifierProvider.overrideWith(() => mapStateNotifier),
|
||||
localeProvider.overrideWithValue(const Locale("en")),
|
||||
];
|
||||
});
|
||||
|
||||
testWidgets("Return dark theme style when theme mode is dark", (tester) async {
|
||||
AsyncValue<String>? mapStyle;
|
||||
await tester.pumpConsumerWidget(
|
||||
MapThemeOverride(
|
||||
mapBuilder: (AsyncValue<String> style) {
|
||||
mapStyle = style;
|
||||
return const Text("Mock");
|
||||
},
|
||||
),
|
||||
overrides: overrides,
|
||||
);
|
||||
|
||||
mapStateNotifier.state = mapState.copyWith(darkStyleFetched: const AsyncData("dark"));
|
||||
await tester.pumpAndSettle();
|
||||
expect(mapStyle?.valueOrNull, "dark");
|
||||
});
|
||||
|
||||
testWidgets("Return error when style is not fetched", (tester) async {
|
||||
AsyncValue<String>? mapStyle;
|
||||
await tester.pumpConsumerWidget(
|
||||
MapThemeOverride(
|
||||
mapBuilder: (AsyncValue<String> style) {
|
||||
mapStyle = style;
|
||||
return const Text("Mock");
|
||||
},
|
||||
),
|
||||
overrides: overrides,
|
||||
);
|
||||
|
||||
mapStateNotifier.state = mapState.copyWith(darkStyleFetched: const AsyncError("Error", StackTrace.empty));
|
||||
await tester.pumpAndSettle();
|
||||
expect(mapStyle?.hasError, isTrue);
|
||||
});
|
||||
|
||||
testWidgets("Return light theme style when theme mode is light", (tester) async {
|
||||
AsyncValue<String>? mapStyle;
|
||||
await tester.pumpConsumerWidget(
|
||||
MapThemeOverride(
|
||||
mapBuilder: (AsyncValue<String> style) {
|
||||
mapStyle = style;
|
||||
return const Text("Mock");
|
||||
},
|
||||
),
|
||||
overrides: overrides,
|
||||
);
|
||||
|
||||
mapStateNotifier.state = mapState.copyWith(themeMode: ThemeMode.light, lightStyleFetched: const AsyncData("light"));
|
||||
await tester.pumpAndSettle();
|
||||
expect(mapStyle?.valueOrNull, "light");
|
||||
});
|
||||
|
||||
group("System mode", () {
|
||||
testWidgets("Return dark theme style when system is dark", (tester) async {
|
||||
AsyncValue<String>? mapStyle;
|
||||
await tester.pumpConsumerWidget(
|
||||
MapThemeOverride(
|
||||
mapBuilder: (AsyncValue<String> style) {
|
||||
mapStyle = style;
|
||||
return const Text("Mock");
|
||||
},
|
||||
),
|
||||
overrides: overrides,
|
||||
);
|
||||
|
||||
tester.binding.platformDispatcher.platformBrightnessTestValue = Brightness.dark;
|
||||
mapStateNotifier.state = mapState.copyWith(
|
||||
themeMode: ThemeMode.system,
|
||||
darkStyleFetched: const AsyncData("dark"),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(mapStyle?.valueOrNull, "dark");
|
||||
});
|
||||
|
||||
testWidgets("Return light theme style when system is light", (tester) async {
|
||||
AsyncValue<String>? mapStyle;
|
||||
await tester.pumpConsumerWidget(
|
||||
MapThemeOverride(
|
||||
mapBuilder: (AsyncValue<String> style) {
|
||||
mapStyle = style;
|
||||
return const Text("Mock");
|
||||
},
|
||||
),
|
||||
overrides: overrides,
|
||||
);
|
||||
|
||||
tester.binding.platformDispatcher.platformBrightnessTestValue = Brightness.light;
|
||||
mapStateNotifier.state = mapState.copyWith(
|
||||
themeMode: ThemeMode.system,
|
||||
lightStyleFetched: const AsyncData("light"),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(mapStyle?.valueOrNull, "light");
|
||||
});
|
||||
|
||||
testWidgets("Switches style when system brightness changes", (tester) async {
|
||||
AsyncValue<String>? mapStyle;
|
||||
await tester.pumpConsumerWidget(
|
||||
MapThemeOverride(
|
||||
mapBuilder: (AsyncValue<String> style) {
|
||||
mapStyle = style;
|
||||
return const Text("Mock");
|
||||
},
|
||||
),
|
||||
overrides: overrides,
|
||||
);
|
||||
|
||||
tester.binding.platformDispatcher.platformBrightnessTestValue = Brightness.light;
|
||||
mapStateNotifier.state = mapState.copyWith(
|
||||
themeMode: ThemeMode.system,
|
||||
lightStyleFetched: const AsyncData("light"),
|
||||
darkStyleFetched: const AsyncData("dark"),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(mapStyle?.valueOrNull, "light");
|
||||
|
||||
tester.binding.platformDispatcher.platformBrightnessTestValue = Brightness.dark;
|
||||
await tester.pumpAndSettle();
|
||||
expect(mapStyle?.valueOrNull, "dark");
|
||||
});
|
||||
});
|
||||
}
|
||||
4
mobile/test/modules/settings/settings_mocks.dart
Normal file
4
mobile/test/modules/settings/settings_mocks.dart
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
import 'package:immich_mobile/services/app_settings.service.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
class MockAppSettingsService extends Mock implements AppSettingsService {}
|
||||
11
mobile/test/modules/shared/shared_mocks.dart
Normal file
11
mobile/test/modules/shared/shared_mocks.dart
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
import 'package:immich_mobile/providers/user.provider.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
class MockCurrentUserProvider extends StateNotifier<UserDto?> with Mock implements CurrentUserProvider {
|
||||
MockCurrentUserProvider() : super(null);
|
||||
|
||||
@override
|
||||
set state(UserDto? user) => super.state = user;
|
||||
}
|
||||
285
mobile/test/modules/shared/sync_service_test.dart
Normal file
285
mobile/test/modules/shared/sync_service_test.dart
Normal file
|
|
@ -0,0 +1,285 @@
|
|||
import 'package:drift/drift.dart';
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/domain/models/user.model.dart';
|
||||
import 'package:immich_mobile/domain/services/log.service.dart';
|
||||
import 'package:immich_mobile/domain/services/store.service.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/entities/etag.entity.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/log.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/logger_db.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
|
||||
import 'package:immich_mobile/repositories/asset.repository.dart';
|
||||
import 'package:immich_mobile/repositories/partner_api.repository.dart';
|
||||
import 'package:immich_mobile/services/sync.service.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
import '../../domain/service.mock.dart';
|
||||
import '../../fixtures/asset.stub.dart';
|
||||
import '../../infrastructure/repository.mock.dart';
|
||||
import '../../repository.mocks.dart';
|
||||
import '../../service.mocks.dart';
|
||||
import '../../test_utils.dart';
|
||||
|
||||
void main() {
|
||||
int assetIdCounter = 0;
|
||||
Asset makeAsset({
|
||||
required String checksum,
|
||||
String? localId,
|
||||
String? remoteId,
|
||||
int ownerId = 590700560494856554, // hash of "1"
|
||||
}) {
|
||||
final DateTime date = DateTime(2000);
|
||||
return Asset(
|
||||
id: assetIdCounter++,
|
||||
checksum: checksum,
|
||||
localId: localId,
|
||||
remoteId: remoteId,
|
||||
ownerId: ownerId,
|
||||
fileCreatedAt: date,
|
||||
fileModifiedAt: date,
|
||||
updatedAt: date,
|
||||
durationInSeconds: 0,
|
||||
type: AssetType.image,
|
||||
fileName: localId ?? remoteId ?? "",
|
||||
isFavorite: false,
|
||||
isArchived: false,
|
||||
isTrashed: false,
|
||||
);
|
||||
}
|
||||
|
||||
final owner = UserDto(
|
||||
id: "1",
|
||||
updatedAt: DateTime.now(),
|
||||
email: "a@b.c",
|
||||
name: "first last",
|
||||
isAdmin: false,
|
||||
profileChangedAt: DateTime.now(),
|
||||
);
|
||||
|
||||
setUpAll(() async {
|
||||
final loggerDb = DriftLogger(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true));
|
||||
final LogRepository logRepository = LogRepository(loggerDb);
|
||||
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
final db = await TestUtils.initIsar();
|
||||
|
||||
db.writeTxnSync(() => db.clearSync());
|
||||
await StoreService.init(storeRepository: IsarStoreRepository(db));
|
||||
await Store.put(StoreKey.currentUser, owner);
|
||||
await LogService.init(logRepository: logRepository, storeRepository: IsarStoreRepository(db));
|
||||
});
|
||||
|
||||
group('Test SyncService grouped', () {
|
||||
final MockHashService hs = MockHashService();
|
||||
final MockEntityService entityService = MockEntityService();
|
||||
final MockAlbumRepository albumRepository = MockAlbumRepository();
|
||||
final MockAssetRepository assetRepository = MockAssetRepository();
|
||||
final MockExifInfoRepository exifInfoRepository = MockExifInfoRepository();
|
||||
final MockIsarUserRepository userRepository = MockIsarUserRepository();
|
||||
final MockETagRepository eTagRepository = MockETagRepository();
|
||||
final MockAlbumMediaRepository albumMediaRepository = MockAlbumMediaRepository();
|
||||
final MockAlbumApiRepository albumApiRepository = MockAlbumApiRepository();
|
||||
final MockAppSettingService appSettingService = MockAppSettingService();
|
||||
final MockLocalFilesManagerRepository localFilesManagerRepository = MockLocalFilesManagerRepository();
|
||||
final MockPartnerApiRepository partnerApiRepository = MockPartnerApiRepository();
|
||||
final MockUserApiRepository userApiRepository = MockUserApiRepository();
|
||||
final MockPartnerRepository partnerRepository = MockPartnerRepository();
|
||||
final MockUserService userService = MockUserService();
|
||||
|
||||
final owner = UserDto(
|
||||
id: "1",
|
||||
updatedAt: DateTime.now(),
|
||||
email: "a@b.c",
|
||||
name: "first last",
|
||||
isAdmin: false,
|
||||
profileChangedAt: DateTime(2021),
|
||||
);
|
||||
|
||||
late SyncService s;
|
||||
|
||||
final List<Asset> initialAssets = [
|
||||
makeAsset(checksum: "a", remoteId: "0-1"),
|
||||
makeAsset(checksum: "b", remoteId: "2-1"),
|
||||
makeAsset(checksum: "c", localId: "1", remoteId: "1-1"),
|
||||
makeAsset(checksum: "d", localId: "2"),
|
||||
makeAsset(checksum: "e", localId: "3"),
|
||||
];
|
||||
setUp(() {
|
||||
s = SyncService(
|
||||
hs,
|
||||
entityService,
|
||||
albumMediaRepository,
|
||||
albumApiRepository,
|
||||
albumRepository,
|
||||
assetRepository,
|
||||
exifInfoRepository,
|
||||
partnerRepository,
|
||||
userRepository,
|
||||
userService,
|
||||
eTagRepository,
|
||||
appSettingService,
|
||||
localFilesManagerRepository,
|
||||
partnerApiRepository,
|
||||
userApiRepository,
|
||||
);
|
||||
when(() => userService.getMyUser()).thenReturn(owner);
|
||||
when(() => eTagRepository.get(owner.id)).thenAnswer((_) async => ETag(id: owner.id, time: DateTime.now()));
|
||||
when(() => eTagRepository.deleteByIds(["1"])).thenAnswer((_) async {});
|
||||
when(() => eTagRepository.upsertAll(any())).thenAnswer((_) async {});
|
||||
when(() => partnerRepository.getSharedWith()).thenAnswer((_) async => []);
|
||||
when(() => userRepository.getAll(sortBy: SortUserBy.id)).thenAnswer((_) async => [owner]);
|
||||
when(() => userRepository.getAll()).thenAnswer((_) async => [owner]);
|
||||
when(
|
||||
() => assetRepository.getAll(ownerId: owner.id, sortBy: AssetSort.checksum),
|
||||
).thenAnswer((_) async => initialAssets);
|
||||
when(
|
||||
() => assetRepository.getAllByOwnerIdChecksum(any(), any()),
|
||||
).thenAnswer((_) async => [initialAssets[3], null, null]);
|
||||
when(() => assetRepository.updateAll(any())).thenAnswer((_) async => []);
|
||||
when(() => assetRepository.deleteByIds(any())).thenAnswer((_) async {});
|
||||
when(() => exifInfoRepository.updateAll(any())).thenAnswer((_) async => []);
|
||||
when(
|
||||
() => assetRepository.transaction<void>(any()),
|
||||
).thenAnswer((call) => (call.positionalArguments.first as Function).call());
|
||||
when(
|
||||
() => assetRepository.transaction<Null>(any()),
|
||||
).thenAnswer((call) => (call.positionalArguments.first as Function).call());
|
||||
when(() => userApiRepository.getAll()).thenAnswer((_) async => [owner]);
|
||||
registerFallbackValue(Direction.sharedByMe);
|
||||
when(() => partnerApiRepository.getAll(any())).thenAnswer((_) async => []);
|
||||
});
|
||||
test('test inserting existing assets', () async {
|
||||
final List<Asset> remoteAssets = [
|
||||
makeAsset(checksum: "a", remoteId: "0-1"),
|
||||
makeAsset(checksum: "b", remoteId: "2-1"),
|
||||
makeAsset(checksum: "c", remoteId: "1-1"),
|
||||
];
|
||||
final bool c1 = await s.syncRemoteAssetsToDb(
|
||||
users: [owner],
|
||||
getChangedAssets: _failDiff,
|
||||
loadAssets: (u, d) => remoteAssets,
|
||||
);
|
||||
expect(c1, isFalse);
|
||||
verifyNever(() => assetRepository.updateAll(any()));
|
||||
});
|
||||
|
||||
test('test inserting new assets', () async {
|
||||
final List<Asset> remoteAssets = [
|
||||
makeAsset(checksum: "a", remoteId: "0-1"),
|
||||
makeAsset(checksum: "b", remoteId: "2-1"),
|
||||
makeAsset(checksum: "c", remoteId: "1-1"),
|
||||
makeAsset(checksum: "d", remoteId: "1-2"),
|
||||
makeAsset(checksum: "f", remoteId: "1-4"),
|
||||
makeAsset(checksum: "g", remoteId: "3-1"),
|
||||
];
|
||||
final bool c1 = await s.syncRemoteAssetsToDb(
|
||||
users: [owner],
|
||||
getChangedAssets: _failDiff,
|
||||
loadAssets: (u, d) => remoteAssets,
|
||||
);
|
||||
expect(c1, isTrue);
|
||||
final updatedAsset = initialAssets[3].updatedCopy(remoteAssets[3]);
|
||||
verify(() => assetRepository.updateAll([remoteAssets[4], remoteAssets[5], updatedAsset]));
|
||||
});
|
||||
|
||||
test('test syncing duplicate assets', () async {
|
||||
final List<Asset> remoteAssets = [
|
||||
makeAsset(checksum: "a", remoteId: "0-1"),
|
||||
makeAsset(checksum: "b", remoteId: "1-1"),
|
||||
makeAsset(checksum: "c", remoteId: "2-1"),
|
||||
makeAsset(checksum: "h", remoteId: "2-1b"),
|
||||
makeAsset(checksum: "i", remoteId: "2-1c"),
|
||||
makeAsset(checksum: "j", remoteId: "2-1d"),
|
||||
];
|
||||
final bool c1 = await s.syncRemoteAssetsToDb(
|
||||
users: [owner],
|
||||
getChangedAssets: _failDiff,
|
||||
loadAssets: (u, d) => remoteAssets,
|
||||
);
|
||||
expect(c1, isTrue);
|
||||
when(
|
||||
() => assetRepository.getAll(ownerId: owner.id, sortBy: AssetSort.checksum),
|
||||
).thenAnswer((_) async => remoteAssets);
|
||||
final bool c2 = await s.syncRemoteAssetsToDb(
|
||||
users: [owner],
|
||||
getChangedAssets: _failDiff,
|
||||
loadAssets: (u, d) => remoteAssets,
|
||||
);
|
||||
expect(c2, isFalse);
|
||||
final currentState = [...remoteAssets];
|
||||
when(
|
||||
() => assetRepository.getAll(ownerId: owner.id, sortBy: AssetSort.checksum),
|
||||
).thenAnswer((_) async => currentState);
|
||||
remoteAssets.removeAt(4);
|
||||
final bool c3 = await s.syncRemoteAssetsToDb(
|
||||
users: [owner],
|
||||
getChangedAssets: _failDiff,
|
||||
loadAssets: (u, d) => remoteAssets,
|
||||
);
|
||||
expect(c3, isTrue);
|
||||
remoteAssets.add(makeAsset(checksum: "k", remoteId: "2-1e"));
|
||||
remoteAssets.add(makeAsset(checksum: "l", remoteId: "2-2"));
|
||||
final bool c4 = await s.syncRemoteAssetsToDb(
|
||||
users: [owner],
|
||||
getChangedAssets: _failDiff,
|
||||
loadAssets: (u, d) => remoteAssets,
|
||||
);
|
||||
expect(c4, isTrue);
|
||||
});
|
||||
|
||||
test('test efficient sync', () async {
|
||||
when(
|
||||
() => assetRepository.deleteAllByRemoteId([
|
||||
initialAssets[1].remoteId!,
|
||||
initialAssets[2].remoteId!,
|
||||
], state: AssetState.remote),
|
||||
).thenAnswer((_) async {
|
||||
return;
|
||||
});
|
||||
when(
|
||||
() => assetRepository.getAllByRemoteId(["2-1", "1-1"], state: AssetState.merged),
|
||||
).thenAnswer((_) async => [initialAssets[2]]);
|
||||
when(
|
||||
() => assetRepository.getAllByOwnerIdChecksum(any(), any()),
|
||||
).thenAnswer((_) async => [initialAssets[0], null, null]); //afg
|
||||
final List<Asset> toUpsert = [
|
||||
makeAsset(checksum: "a", remoteId: "0-1"), // changed
|
||||
makeAsset(checksum: "f", remoteId: "0-2"), // new
|
||||
makeAsset(checksum: "g", remoteId: "0-3"), // new
|
||||
];
|
||||
toUpsert[0].isFavorite = true;
|
||||
final List<String> toDelete = ["2-1", "1-1"];
|
||||
final expected = [...toUpsert];
|
||||
expected[0].id = initialAssets[0].id;
|
||||
final bool c = await s.syncRemoteAssetsToDb(
|
||||
users: [owner],
|
||||
getChangedAssets: (user, since) async => (toUpsert, toDelete),
|
||||
loadAssets: (user, date) => throw Exception(),
|
||||
);
|
||||
expect(c, isTrue);
|
||||
verify(() => assetRepository.updateAll(expected));
|
||||
});
|
||||
|
||||
group("upsertAssetsWithExif", () {
|
||||
test('test upsert with EXIF data', () async {
|
||||
final assets = [AssetStub.image1, AssetStub.image2];
|
||||
|
||||
expect(assets.map((a) => a.exifInfo?.assetId), List.filled(assets.length, null));
|
||||
await s.upsertAssetsWithExif(assets);
|
||||
verify(
|
||||
() => exifInfoRepository.updateAll(
|
||||
any(that: containsAll(assets.map((a) => a.exifInfo!.copyWith(assetId: a.id)))),
|
||||
),
|
||||
);
|
||||
expect(assets.map((a) => a.exifInfo?.assetId), assets.map((a) => a.id));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Future<(List<Asset>?, List<String>?)> _failDiff(List<UserDto> user, DateTime time) => Future.value((null, null));
|
||||
23
mobile/test/modules/utils/async_mutex_test.dart
Normal file
23
mobile/test/modules/utils/async_mutex_test.dart
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/utils/async_mutex.dart';
|
||||
|
||||
void main() {
|
||||
group('Test AsyncMutex grouped', () {
|
||||
test('test ordered execution', () async {
|
||||
AsyncMutex lock = AsyncMutex();
|
||||
List<int> events = [];
|
||||
expect(0, lock.enqueued);
|
||||
unawaited(lock.run(() => Future.delayed(const Duration(milliseconds: 10), () => events.add(1))));
|
||||
expect(1, lock.enqueued);
|
||||
unawaited(lock.run(() => Future.delayed(const Duration(milliseconds: 3), () => events.add(2))));
|
||||
expect(2, lock.enqueued);
|
||||
unawaited(lock.run(() => Future.delayed(const Duration(milliseconds: 1), () => events.add(3))));
|
||||
expect(3, lock.enqueued);
|
||||
await lock.run(() => Future.delayed(const Duration(milliseconds: 10), () => events.add(4)));
|
||||
expect(0, lock.enqueued);
|
||||
expect(events, [1, 2, 3, 4]);
|
||||
});
|
||||
});
|
||||
}
|
||||
58
mobile/test/modules/utils/datetime_helpers_test.dart
Normal file
58
mobile/test/modules/utils/datetime_helpers_test.dart
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/utils/datetime_helpers.dart';
|
||||
|
||||
void main() {
|
||||
group('tryFromSecondsSinceEpoch', () {
|
||||
test('returns null for null input', () {
|
||||
final result = tryFromSecondsSinceEpoch(null);
|
||||
expect(result, isNull);
|
||||
});
|
||||
|
||||
test('returns null for value below minimum allowed range', () {
|
||||
// _minMillisecondsSinceEpoch = -62135596800000
|
||||
final seconds = -62135596800000 ~/ 1000 - 1; // One second before min allowed
|
||||
final result = tryFromSecondsSinceEpoch(seconds);
|
||||
expect(result, isNull);
|
||||
});
|
||||
|
||||
test('returns null for value above maximum allowed range', () {
|
||||
// _maxMillisecondsSinceEpoch = 8640000000000000
|
||||
final seconds = 8640000000000000 ~/ 1000 + 1; // One second after max allowed
|
||||
final result = tryFromSecondsSinceEpoch(seconds);
|
||||
expect(result, isNull);
|
||||
});
|
||||
|
||||
test('returns correct DateTime for minimum allowed value', () {
|
||||
final seconds = -62135596800000 ~/ 1000; // Minimum allowed timestamp
|
||||
final result = tryFromSecondsSinceEpoch(seconds);
|
||||
expect(result, DateTime.fromMillisecondsSinceEpoch(-62135596800000));
|
||||
});
|
||||
|
||||
test('returns correct DateTime for maximum allowed value', () {
|
||||
final seconds = 8640000000000000 ~/ 1000; // Maximum allowed timestamp
|
||||
final result = tryFromSecondsSinceEpoch(seconds);
|
||||
expect(result, DateTime.fromMillisecondsSinceEpoch(8640000000000000));
|
||||
});
|
||||
|
||||
test('returns correct DateTime for negative timestamp', () {
|
||||
final seconds = -1577836800; // Dec 31, 1919 (pre-epoch)
|
||||
final result = tryFromSecondsSinceEpoch(seconds);
|
||||
expect(result, DateTime.fromMillisecondsSinceEpoch(-1577836800 * 1000));
|
||||
});
|
||||
|
||||
test('returns correct DateTime for zero timestamp', () {
|
||||
final seconds = 0; // Jan 1, 1970 (epoch)
|
||||
final result = tryFromSecondsSinceEpoch(seconds);
|
||||
expect(result, DateTime.fromMillisecondsSinceEpoch(0));
|
||||
});
|
||||
|
||||
test('returns correct DateTime for recent timestamp', () {
|
||||
final now = DateTime.now();
|
||||
final seconds = now.millisecondsSinceEpoch ~/ 1000;
|
||||
final result = tryFromSecondsSinceEpoch(seconds);
|
||||
expect(result?.year, now.year);
|
||||
expect(result?.month, now.month);
|
||||
expect(result?.day, now.day);
|
||||
});
|
||||
});
|
||||
}
|
||||
41
mobile/test/modules/utils/debouncer_test.dart
Normal file
41
mobile/test/modules/utils/debouncer_test.dart
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/utils/debounce.dart';
|
||||
|
||||
class _Counter {
|
||||
int _count = 0;
|
||||
_Counter();
|
||||
|
||||
int get count => _count;
|
||||
void increment() => _count = _count + 1;
|
||||
}
|
||||
|
||||
void main() {
|
||||
test('Executes the method after the interval', () async {
|
||||
var counter = _Counter();
|
||||
final debouncer = Debouncer(interval: const Duration(milliseconds: 300));
|
||||
debouncer.run(() => counter.increment());
|
||||
expect(counter.count, 0);
|
||||
await Future.delayed(const Duration(milliseconds: 300));
|
||||
expect(counter.count, 1);
|
||||
});
|
||||
|
||||
test('Executes the method immediately if zero interval', () async {
|
||||
var counter = _Counter();
|
||||
final debouncer = Debouncer(interval: const Duration(milliseconds: 0));
|
||||
debouncer.run(() => counter.increment());
|
||||
// Even though it is supposed to be executed immediately, it is added to the async queue and so
|
||||
// we need this delay to make sure the actual debounced method is called
|
||||
await Future.delayed(const Duration(milliseconds: 0));
|
||||
expect(counter.count, 1);
|
||||
});
|
||||
|
||||
test('Delayes method execution after all the calls are completed', () async {
|
||||
var counter = _Counter();
|
||||
final debouncer = Debouncer(interval: const Duration(milliseconds: 100));
|
||||
debouncer.run(() => counter.increment());
|
||||
debouncer.run(() => counter.increment());
|
||||
debouncer.run(() => counter.increment());
|
||||
await Future.delayed(const Duration(milliseconds: 300));
|
||||
expect(counter.count, 1);
|
||||
});
|
||||
}
|
||||
50
mobile/test/modules/utils/diff_test.dart
Normal file
50
mobile/test/modules/utils/diff_test.dart
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/utils/diff.dart';
|
||||
|
||||
void main() {
|
||||
final List<int> listA = [1, 2, 3, 4, 6];
|
||||
final List<int> listB = [1, 3, 5, 7];
|
||||
|
||||
group('Test grouped', () {
|
||||
test('test partial overlap', () async {
|
||||
final List<int> onlyInA = [];
|
||||
final List<int> onlyInB = [];
|
||||
final List<int> inBoth = [];
|
||||
final changes = await diffSortedLists(
|
||||
listA,
|
||||
listB,
|
||||
compare: (int a, int b) => a.compareTo(b),
|
||||
both: (int a, int b) {
|
||||
inBoth.add(b);
|
||||
return false;
|
||||
},
|
||||
onlyFirst: (int a) => onlyInA.add(a),
|
||||
onlySecond: (int b) => onlyInB.add(b),
|
||||
);
|
||||
expect(changes, true);
|
||||
expect(onlyInA, [2, 4, 6]);
|
||||
expect(onlyInB, [5, 7]);
|
||||
expect(inBoth, [1, 3]);
|
||||
});
|
||||
test('test partial overlap sync', () {
|
||||
final List<int> onlyInA = [];
|
||||
final List<int> onlyInB = [];
|
||||
final List<int> inBoth = [];
|
||||
final changes = diffSortedListsSync(
|
||||
listA,
|
||||
listB,
|
||||
compare: (int a, int b) => a.compareTo(b),
|
||||
both: (int a, int b) {
|
||||
inBoth.add(b);
|
||||
return false;
|
||||
},
|
||||
onlyFirst: (int a) => onlyInA.add(a),
|
||||
onlySecond: (int b) => onlyInB.add(b),
|
||||
);
|
||||
expect(changes, true);
|
||||
expect(onlyInA, [2, 4, 6]);
|
||||
expect(onlyInB, [5, 7]);
|
||||
expect(inBoth, [1, 3]);
|
||||
});
|
||||
});
|
||||
}
|
||||
131
mobile/test/modules/utils/migration_test.dart
Normal file
131
mobile/test/modules/utils/migration_test.dart
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
import 'package:drift/drift.dart' hide isNull;
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/domain/models/store.model.dart';
|
||||
import 'package:immich_mobile/domain/services/store.service.dart';
|
||||
import 'package:immich_mobile/entities/store.entity.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/sync_stream.repository.dart';
|
||||
import 'package:immich_mobile/utils/migration.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
import '../../infrastructure/repository.mock.dart';
|
||||
|
||||
void main() {
|
||||
late Drift db;
|
||||
late SyncStreamRepository mockSyncStreamRepository;
|
||||
|
||||
setUpAll(() async {
|
||||
db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true));
|
||||
await StoreService.init(storeRepository: DriftStoreRepository(db));
|
||||
mockSyncStreamRepository = MockSyncStreamRepository();
|
||||
when(() => mockSyncStreamRepository.reset()).thenAnswer((_) async => {});
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await Store.clear();
|
||||
});
|
||||
|
||||
group('handleBetaMigration Tests', () {
|
||||
group("version < 15", () {
|
||||
test('already on new timeline', () async {
|
||||
await Store.put(StoreKey.betaTimeline, true);
|
||||
|
||||
await handleBetaMigration(14, false, mockSyncStreamRepository);
|
||||
|
||||
expect(Store.tryGet(StoreKey.betaTimeline), true);
|
||||
expect(Store.tryGet(StoreKey.needBetaMigration), false);
|
||||
});
|
||||
|
||||
test('already on old timeline', () async {
|
||||
await Store.put(StoreKey.betaTimeline, false);
|
||||
|
||||
await handleBetaMigration(14, false, mockSyncStreamRepository);
|
||||
|
||||
expect(Store.tryGet(StoreKey.needBetaMigration), true);
|
||||
});
|
||||
|
||||
test('fresh install', () async {
|
||||
await Store.delete(StoreKey.betaTimeline);
|
||||
await handleBetaMigration(14, true, mockSyncStreamRepository);
|
||||
|
||||
expect(Store.tryGet(StoreKey.betaTimeline), true);
|
||||
expect(Store.tryGet(StoreKey.needBetaMigration), false);
|
||||
});
|
||||
});
|
||||
|
||||
group("version == 15", () {
|
||||
test('already on new timeline', () async {
|
||||
await Store.put(StoreKey.betaTimeline, true);
|
||||
|
||||
await handleBetaMigration(15, false, mockSyncStreamRepository);
|
||||
|
||||
expect(Store.tryGet(StoreKey.betaTimeline), true);
|
||||
expect(Store.tryGet(StoreKey.needBetaMigration), false);
|
||||
});
|
||||
|
||||
test('already on old timeline', () async {
|
||||
await Store.put(StoreKey.betaTimeline, false);
|
||||
|
||||
await handleBetaMigration(15, false, mockSyncStreamRepository);
|
||||
|
||||
expect(Store.tryGet(StoreKey.needBetaMigration), true);
|
||||
});
|
||||
|
||||
test('fresh install', () async {
|
||||
await Store.delete(StoreKey.betaTimeline);
|
||||
await handleBetaMigration(15, true, mockSyncStreamRepository);
|
||||
|
||||
expect(Store.tryGet(StoreKey.betaTimeline), true);
|
||||
expect(Store.tryGet(StoreKey.needBetaMigration), false);
|
||||
});
|
||||
});
|
||||
|
||||
group("version > 15", () {
|
||||
test('already on new timeline', () async {
|
||||
await Store.put(StoreKey.betaTimeline, true);
|
||||
|
||||
await handleBetaMigration(16, false, mockSyncStreamRepository);
|
||||
|
||||
expect(Store.tryGet(StoreKey.betaTimeline), true);
|
||||
expect(Store.tryGet(StoreKey.needBetaMigration), false);
|
||||
});
|
||||
|
||||
test('already on old timeline', () async {
|
||||
await Store.put(StoreKey.betaTimeline, false);
|
||||
|
||||
await handleBetaMigration(16, false, mockSyncStreamRepository);
|
||||
|
||||
expect(Store.tryGet(StoreKey.betaTimeline), false);
|
||||
expect(Store.tryGet(StoreKey.needBetaMigration), false);
|
||||
});
|
||||
|
||||
test('fresh install', () async {
|
||||
await Store.delete(StoreKey.betaTimeline);
|
||||
await handleBetaMigration(16, true, mockSyncStreamRepository);
|
||||
|
||||
expect(Store.tryGet(StoreKey.betaTimeline), true);
|
||||
expect(Store.tryGet(StoreKey.needBetaMigration), false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
group('sync reset tests', () {
|
||||
test('version < 16', () async {
|
||||
await Store.put(StoreKey.shouldResetSync, false);
|
||||
|
||||
await handleBetaMigration(15, false, mockSyncStreamRepository);
|
||||
|
||||
expect(Store.tryGet(StoreKey.shouldResetSync), true);
|
||||
});
|
||||
|
||||
test('version >= 16', () async {
|
||||
await Store.put(StoreKey.shouldResetSync, false);
|
||||
|
||||
await handleBetaMigration(16, false, mockSyncStreamRepository);
|
||||
|
||||
expect(Store.tryGet(StoreKey.shouldResetSync), false);
|
||||
});
|
||||
});
|
||||
}
|
||||
61
mobile/test/modules/utils/openapi_patching_test.dart
Normal file
61
mobile/test/modules/utils/openapi_patching_test.dart
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:openapi/api.dart';
|
||||
import 'package:immich_mobile/utils/openapi_patching.dart';
|
||||
|
||||
void main() {
|
||||
group('Test OpenApi Patching', () {
|
||||
test('upgradeDto', () {
|
||||
dynamic value;
|
||||
String targetType;
|
||||
|
||||
targetType = 'UserPreferencesResponseDto';
|
||||
value = jsonDecode("""
|
||||
{
|
||||
"download": {
|
||||
"archiveSize": 4294967296,
|
||||
"includeEmbeddedVideos": false
|
||||
}
|
||||
}
|
||||
""");
|
||||
|
||||
upgradeDto(value, targetType);
|
||||
expect(value['tags'], TagsResponse().toJson());
|
||||
expect(value['download']['includeEmbeddedVideos'], false);
|
||||
});
|
||||
|
||||
test('addDefault', () {
|
||||
dynamic value = jsonDecode("""
|
||||
{
|
||||
"download": {
|
||||
"archiveSize": 4294967296,
|
||||
"includeEmbeddedVideos": false
|
||||
}
|
||||
}
|
||||
""");
|
||||
String keys = 'download.unknownKey';
|
||||
dynamic defaultValue = 69420;
|
||||
|
||||
addDefault(value, keys, defaultValue);
|
||||
expect(value['download']['unknownKey'], 69420);
|
||||
|
||||
keys = 'alpha.beta';
|
||||
defaultValue = 'gamma';
|
||||
addDefault(value, keys, defaultValue);
|
||||
expect(value['alpha']['beta'], 'gamma');
|
||||
});
|
||||
|
||||
test('addDefault with null', () {
|
||||
dynamic value = jsonDecode("""
|
||||
{
|
||||
"download": {
|
||||
"archiveSize": 4294967296,
|
||||
"includeEmbeddedVideos": false
|
||||
}
|
||||
}
|
||||
""");
|
||||
expect(value['download']['unknownKey'], isNull);
|
||||
});
|
||||
});
|
||||
}
|
||||
46
mobile/test/modules/utils/throttler_test.dart
Normal file
46
mobile/test/modules/utils/throttler_test.dart
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/utils/throttle.dart';
|
||||
import 'package:immich_mobile/utils/debug_print.dart';
|
||||
|
||||
class _Counter {
|
||||
int _count = 0;
|
||||
_Counter();
|
||||
|
||||
int get count => _count;
|
||||
void increment() {
|
||||
dPrint(() => "Counter inside increment: $count");
|
||||
_count = _count + 1;
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
test('Executes the method immediately if no calls received previously', () async {
|
||||
var counter = _Counter();
|
||||
final throttler = Throttler(interval: const Duration(milliseconds: 300));
|
||||
throttler.run(() => counter.increment());
|
||||
expect(counter.count, 1);
|
||||
});
|
||||
|
||||
test('Does not execute calls before throttle interval', () async {
|
||||
var counter = _Counter();
|
||||
final throttler = Throttler(interval: const Duration(milliseconds: 100));
|
||||
throttler.run(() => counter.increment());
|
||||
throttler.run(() => counter.increment());
|
||||
throttler.run(() => counter.increment());
|
||||
throttler.run(() => counter.increment());
|
||||
throttler.run(() => counter.increment());
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
expect(counter.count, 1);
|
||||
});
|
||||
|
||||
test('Executes the method if received in intervals', () async {
|
||||
var counter = _Counter();
|
||||
final throttler = Throttler(interval: const Duration(milliseconds: 100));
|
||||
for (final _ in Iterable<int>.generate(10)) {
|
||||
throttler.run(() => counter.increment());
|
||||
await Future.delayed(const Duration(milliseconds: 50));
|
||||
}
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
expect(counter.count, 5);
|
||||
});
|
||||
}
|
||||
63
mobile/test/modules/utils/thumbnail_utils_test.dart
Normal file
63
mobile/test/modules/utils/thumbnail_utils_test.dart
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/domain/models/exif.model.dart';
|
||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||
import 'package:immich_mobile/utils/thumbnail_utils.dart';
|
||||
|
||||
void main() {
|
||||
final dateTime = DateTime(2025, 04, 25, 12, 13, 14);
|
||||
final dateTimeString = DateFormat.yMMMMd().format(dateTime);
|
||||
|
||||
test('returns description if it has one', () {
|
||||
final result = getAltText(const ExifInfo(description: 'description'), dateTime, AssetType.image, []);
|
||||
expect(result, 'description');
|
||||
});
|
||||
|
||||
test('returns image alt text with date if no location', () {
|
||||
final (template, args) = getAltTextTemplate(const ExifInfo(), dateTime, AssetType.image, []);
|
||||
expect(template, "image_alt_text_date");
|
||||
expect(args["isVideo"], "false");
|
||||
expect(args["date"], dateTimeString);
|
||||
});
|
||||
|
||||
test('returns image alt text with date and place', () {
|
||||
final (template, args) = getAltTextTemplate(
|
||||
const ExifInfo(city: 'city', country: 'country'),
|
||||
dateTime,
|
||||
AssetType.video,
|
||||
[],
|
||||
);
|
||||
expect(template, "image_alt_text_date_place");
|
||||
expect(args["isVideo"], "true");
|
||||
expect(args["date"], dateTimeString);
|
||||
expect(args["city"], "city");
|
||||
expect(args["country"], "country");
|
||||
});
|
||||
|
||||
test('returns image alt text with date and some people', () {
|
||||
final (template, args) = getAltTextTemplate(const ExifInfo(), dateTime, AssetType.image, ["Alice", "Bob"]);
|
||||
expect(template, "image_alt_text_date_2_people");
|
||||
expect(args["isVideo"], "false");
|
||||
expect(args["date"], dateTimeString);
|
||||
expect(args["person1"], "Alice");
|
||||
expect(args["person2"], "Bob");
|
||||
});
|
||||
|
||||
test('returns image alt text with date and location and many people', () {
|
||||
final (template, args) = getAltTextTemplate(
|
||||
const ExifInfo(city: "city", country: 'country'),
|
||||
dateTime,
|
||||
AssetType.video,
|
||||
["Alice", "Bob", "Carol", "David", "Eve"],
|
||||
);
|
||||
expect(template, "image_alt_text_date_place_4_or_more_people");
|
||||
expect(args["isVideo"], "true");
|
||||
expect(args["date"], dateTimeString);
|
||||
expect(args["city"], "city");
|
||||
expect(args["country"], "country");
|
||||
expect(args["person1"], "Alice");
|
||||
expect(args["person2"], "Bob");
|
||||
expect(args["person3"], "Carol");
|
||||
expect(args["additionalCount"], "2");
|
||||
});
|
||||
}
|
||||
136
mobile/test/modules/utils/url_helper_test.dart
Normal file
136
mobile/test/modules/utils/url_helper_test.dart
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/utils/url_helper.dart';
|
||||
|
||||
void main() {
|
||||
group('punycodeEncodeUrl', () {
|
||||
test('should return empty string for invalid URL', () {
|
||||
expect(punycodeEncodeUrl('not a url'), equals(''));
|
||||
});
|
||||
|
||||
test('should handle empty input', () {
|
||||
expect(punycodeEncodeUrl(''), equals(''));
|
||||
});
|
||||
|
||||
test('should return ASCII-only URL unchanged', () {
|
||||
const url = 'https://example.com';
|
||||
expect(punycodeEncodeUrl(url), equals(url));
|
||||
});
|
||||
|
||||
test('should encode single-segment Unicode host', () {
|
||||
const url = 'https://bücher';
|
||||
const expected = 'https://xn--bcher-kva';
|
||||
expect(punycodeEncodeUrl(url), equals(expected));
|
||||
});
|
||||
|
||||
test('should encode multi-segment Unicode host', () {
|
||||
const url = 'https://bücher.de';
|
||||
const expected = 'https://xn--bcher-kva.de';
|
||||
expect(punycodeEncodeUrl(url), equals(expected));
|
||||
});
|
||||
|
||||
test('should encode multi-segment Unicode host with multiple non-ASCII segments', () {
|
||||
const url = 'https://bücher.münchen';
|
||||
const expected = 'https://xn--bcher-kva.xn--mnchen-3ya';
|
||||
expect(punycodeEncodeUrl(url), equals(expected));
|
||||
});
|
||||
|
||||
test('should handle URL with port', () {
|
||||
const url = 'https://bücher.de:8080';
|
||||
const expected = 'https://xn--bcher-kva.de:8080';
|
||||
expect(punycodeEncodeUrl(url), equals(expected));
|
||||
});
|
||||
|
||||
test('should handle URL with path', () {
|
||||
const url = 'https://bücher.de/path/to/resource';
|
||||
const expected = 'https://xn--bcher-kva.de/path/to/resource';
|
||||
expect(punycodeEncodeUrl(url), equals(expected));
|
||||
});
|
||||
|
||||
test('should handle URL with port and path', () {
|
||||
const url = 'https://bücher.de:3000/path';
|
||||
const expected = 'https://xn--bcher-kva.de:3000/path';
|
||||
expect(punycodeEncodeUrl(url), equals(expected));
|
||||
});
|
||||
|
||||
test('should not encode ASCII segment in multi-segment host', () {
|
||||
const url = 'https://shop.bücher.de';
|
||||
const expected = 'https://shop.xn--bcher-kva.de';
|
||||
expect(punycodeEncodeUrl(url), equals(expected));
|
||||
});
|
||||
|
||||
test('should handle host with hyphen in Unicode segment', () {
|
||||
const url = 'https://bü-cher.de';
|
||||
const expected = 'https://xn--b-cher-3ya.de';
|
||||
expect(punycodeEncodeUrl(url), equals(expected));
|
||||
});
|
||||
|
||||
test('should handle host with numbers in Unicode segment', () {
|
||||
const url = 'https://bücher123.de';
|
||||
const expected = 'https://xn--bcher123-65a.de';
|
||||
expect(punycodeEncodeUrl(url), equals(expected));
|
||||
});
|
||||
|
||||
test('should encode the domain of the original issue poster :)', () {
|
||||
const url = 'https://фото.большойчлен.рф/';
|
||||
const expected = 'https://xn--n1aalg.xn--90ailhbncb6fh7b.xn--p1ai/';
|
||||
expect(punycodeEncodeUrl(url), expected);
|
||||
});
|
||||
});
|
||||
|
||||
group('punycodeDecodeUrl', () {
|
||||
test('should return null for null input', () {
|
||||
expect(punycodeDecodeUrl(null), isNull);
|
||||
});
|
||||
|
||||
test('should return null for an invalid URL', () {
|
||||
// "not a url" should fail to parse.
|
||||
expect(punycodeDecodeUrl('not a url'), isNull);
|
||||
});
|
||||
|
||||
test('should return null for a URL with empty host', () {
|
||||
// "https://" is a valid scheme but with no host.
|
||||
expect(punycodeDecodeUrl('https://'), isNull);
|
||||
});
|
||||
|
||||
test('should return ASCII-only URL unchanged', () {
|
||||
const url = 'https://example.com';
|
||||
expect(punycodeDecodeUrl(url), equals(url));
|
||||
});
|
||||
|
||||
test('should decode a single-segment Punycode domain', () {
|
||||
const input = 'https://xn--bcher-kva.de';
|
||||
const expected = 'https://bücher.de';
|
||||
expect(punycodeDecodeUrl(input), equals(expected));
|
||||
});
|
||||
|
||||
test('should decode a multi-segment Punycode domain', () {
|
||||
const input = 'https://shop.xn--bcher-kva.de';
|
||||
const expected = 'https://shop.bücher.de';
|
||||
expect(punycodeDecodeUrl(input), equals(expected));
|
||||
});
|
||||
|
||||
test('should decode URL with port', () {
|
||||
const input = 'https://xn--bcher-kva.de:8080';
|
||||
const expected = 'https://bücher.de:8080';
|
||||
expect(punycodeDecodeUrl(input), equals(expected));
|
||||
});
|
||||
|
||||
test('should decode domains with uppercase punycode prefix correctly', () {
|
||||
const input = 'https://XN--BCHER-KVA.de';
|
||||
const expected = 'https://bücher.de';
|
||||
expect(punycodeDecodeUrl(input), equals(expected));
|
||||
});
|
||||
|
||||
test('should handle mixed segments with no punycode in some parts', () {
|
||||
const input = 'https://news.xn--bcher-kva.de';
|
||||
const expected = 'https://news.bücher.de';
|
||||
expect(punycodeDecodeUrl(input), equals(expected));
|
||||
});
|
||||
|
||||
test('should decode the domain of the original issue poster :)', () {
|
||||
const url = 'https://xn--n1aalg.xn--90ailhbncb6fh7b.xn--p1ai/';
|
||||
const expected = 'https://фото.большойчлен.рф/';
|
||||
expect(punycodeDecodeUrl(url), expected);
|
||||
});
|
||||
});
|
||||
}
|
||||
32
mobile/test/modules/utils/version_compatibility_test.dart
Normal file
32
mobile/test/modules/utils/version_compatibility_test.dart
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/utils/version_compatibility.dart';
|
||||
|
||||
void main() {
|
||||
test('getVersionCompatibilityMessage', () {
|
||||
String? result;
|
||||
|
||||
result = getVersionCompatibilityMessage(1, 0, 2, 0);
|
||||
expect(result, 'Your app major version is not compatible with the server!');
|
||||
|
||||
result = getVersionCompatibilityMessage(1, 106, 1, 105);
|
||||
expect(
|
||||
result,
|
||||
'Your app minor version is not compatible with the server! Please update your server to version v1.106.0 or newer to login',
|
||||
);
|
||||
|
||||
result = getVersionCompatibilityMessage(1, 107, 1, 105);
|
||||
expect(
|
||||
result,
|
||||
'Your app minor version is not compatible with the server! Please update your server to version v1.106.0 or newer to login',
|
||||
);
|
||||
|
||||
result = getVersionCompatibilityMessage(1, 106, 1, 106);
|
||||
expect(result, null);
|
||||
|
||||
result = getVersionCompatibilityMessage(1, 107, 1, 106);
|
||||
expect(result, null);
|
||||
|
||||
result = getVersionCompatibilityMessage(1, 107, 1, 108);
|
||||
expect(result, null);
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue