1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2025-03-01 15:11:21 +01:00

fix(mobile): mobile album sort not persisting (#5584)

* chore(deps): use mocktail instead of mockito

* refactor: move stubs to fixtures/

* fix: fetch assetsortmode based on storeindex

* test: validate AlbumSortByOptions provider

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
This commit is contained in:
shenlong 2023-12-10 02:31:23 +00:00 committed by GitHub
parent 188cdf9367
commit 8847ebeef2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 372 additions and 152 deletions

1
.gitignore vendored
View file

@ -11,3 +11,4 @@ coverage
mobile/gradle.properties mobile/gradle.properties
mobile/openapi/pubspec.lock mobile/openapi/pubspec.lock
mobile/*.jks mobile/*.jks
mobile/libisar.dylib

View file

@ -99,7 +99,7 @@ class AlbumSortByOptions extends _$AlbumSortByOptions {
.watch(appSettingsServiceProvider) .watch(appSettingsServiceProvider)
.getSetting(AppSettingsEnum.selectedAlbumSortOrder); .getSetting(AppSettingsEnum.selectedAlbumSortOrder);
return AlbumSortMode.values.firstWhere( return AlbumSortMode.values.firstWhere(
(e) => e.index == sortOpt, (e) => e.storeIndex == sortOpt,
orElse: () => AlbumSortMode.title, orElse: () => AlbumSortMode.title,
); );
} }

View file

@ -964,14 +964,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.4" version: "1.0.4"
mockito: mocktail:
dependency: "direct dev" dependency: "direct main"
description: description:
name: mockito name: mocktail
sha256: "7d5b53bcd556c1bc7ffbe4e4d5a19c3e112b7e925e9e172dd7c6ad0630812616" sha256: bac151b31e4ed78bd59ab89aa4c0928f297b1180186d5daf03734519e5f596c1
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.4.2" version: "1.0.1"
nested: nested:
dependency: transitive dependency: transitive
description: description:

View file

@ -58,6 +58,7 @@ dependencies:
wakelock_plus: ^1.1.1 wakelock_plus: ^1.1.1
flutter_local_notifications: ^15.1.0+1 flutter_local_notifications: ^15.1.0+1
timezone: ^0.9.2 timezone: ^0.9.2
mocktail: ^1.0.1
openapi: openapi:
path: openapi path: openapi
@ -84,7 +85,6 @@ dev_dependencies:
flutter_launcher_icons: "^0.9.2" flutter_launcher_icons: "^0.9.2"
flutter_native_splash: ^2.2.16 flutter_native_splash: ^2.2.16
isar_generator: *isar_version isar_generator: *isar_version
mockito: ^5.3.2
integration_test: integration_test:
sdk: flutter sdk: flutter
custom_lint: ^0.5.6 custom_lint: ^0.5.6

View file

@ -1,182 +1,321 @@
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/album/providers/album_sort_by_options.provider.dart'; import 'package:immich_mobile/modules/album/providers/album_sort_by_options.provider.dart';
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
import 'package:immich_mobile/shared/models/album.dart'; import 'package:immich_mobile/shared/models/album.dart';
import 'package:immich_mobile/shared/models/asset.dart'; import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/models/user.dart';
import 'package:isar/isar.dart'; import 'package:isar/isar.dart';
import 'package:mocktail/mocktail.dart';
import 'album.stub.dart'; import 'fixtures/album.stub.dart';
import 'asset.stub.dart'; import 'fixtures/asset.stub.dart';
import 'mocks/app_settings_provider.mock.dart';
import 'test_utils.dart';
void main() { void main() {
late final Isar db; /// Verify the sort modes
group("AlbumSortMode", () {
late final Isar db;
setUpAll(() async { setUpAll(() async {
await Isar.initializeIsarCore(download: true); db = await TestUtils.initIsar();
db = await Isar.open(
[
AssetSchema,
AlbumSchema,
UserSchema,
],
maxSizeMiB: 256,
directory: ".",
);
});
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);
expect(sorted.isSortedBy((a) => a.createdAt), true);
}); });
test("Created time - DESC", () { final albums = [
final sorted = created.sortFn(albums, true); AlbumStub.emptyAlbum,
expect( AlbumStub.sharedWithUser,
sorted.isSorted((b, a) => a.createdAt.compareTo(b.createdAt)), AlbumStub.oneAsset,
true, 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);
expect(sorted.isSortedBy((a) => a.createdAt), true);
});
test("Created time - DESC", () {
final sorted = created.sortFn(albums, true);
expect(
sorted.isSorted((b, a) => a.createdAt.compareTo(b.createdAt)),
true,
);
});
});
group("Album sort - Asset count", () {
const assetCount = AlbumSortMode.assetCount;
test("Asset Count - ASC", () {
final sorted = assetCount.sortFn(albums, false);
expect(
sorted.isSorted((a, b) => a.assetCount.compareTo(b.assetCount)),
true,
);
});
test("Asset Count - DESC", () {
final sorted = assetCount.sortFn(albums, true);
expect(
sorted.isSorted((b, a) => a.assetCount.compareTo(b.assetCount)),
true,
);
});
});
group("Album sort - Last modified", () {
const lastModified = AlbumSortMode.lastModified;
test("Last modified - ASC", () {
final sorted = lastModified.sortFn(albums, false);
expect(
sorted.isSorted((a, b) => a.modifiedAt.compareTo(b.modifiedAt)),
true,
);
});
test("Last modified - DESC", () {
final sorted = lastModified.sortFn(albums, true);
expect(
sorted.isSorted((b, a) => a.modifiedAt.compareTo(b.modifiedAt)),
true,
);
});
});
group("Album sort - Created", () {
const created = AlbumSortMode.created;
test("Created - ASC", () {
final sorted = created.sortFn(albums, false);
expect(
sorted.isSorted((a, b) => a.createdAt.compareTo(b.createdAt)),
true,
);
});
test("Created - DESC", () {
final sorted = created.sortFn(albums, true);
expect(
sorted.isSorted((b, a) => a.createdAt.compareTo(b.createdAt)),
true,
);
});
});
group("Album sort - Most Recent", () {
const mostRecent = AlbumSortMode.mostRecent;
test("Most Recent - ASC", () {
final sorted = mostRecent.sortFn(albums, false);
expect(
sorted,
[
AlbumStub.sharedWithUser,
AlbumStub.twoAsset,
AlbumStub.oneAsset,
AlbumStub.emptyAlbum,
],
);
});
test("Most Recent - DESC", () {
final sorted = mostRecent.sortFn(albums, true);
expect(
sorted,
[
AlbumStub.emptyAlbum,
AlbumStub.oneAsset,
AlbumStub.twoAsset,
AlbumStub.sharedWithUser,
],
);
});
});
group("Album sort - Most Oldest", () {
const mostOldest = AlbumSortMode.mostOldest;
test("Most Oldest - ASC", () {
final sorted = mostOldest.sortFn(albums, false);
expect(
sorted,
[
AlbumStub.twoAsset,
AlbumStub.emptyAlbum,
AlbumStub.oneAsset,
AlbumStub.sharedWithUser,
],
);
});
test("Most Oldest - DESC", () {
final sorted = mostOldest.sortFn(albums, true);
expect(
sorted,
[
AlbumStub.sharedWithUser,
AlbumStub.oneAsset,
AlbumStub.emptyAlbum,
AlbumStub.twoAsset,
],
);
});
}); });
}); });
group("Album sort - Asset count", () { /// Verify the sort mode provider
const assetCount = AlbumSortMode.assetCount; group('AlbumSortByOptions', () {
test("Asset Count - ASC", () { late AppSettingsService settingsMock;
final sorted = assetCount.sortFn(albums, false); late ProviderContainer container;
expect(
sorted.isSorted((a, b) => a.assetCount.compareTo(b.assetCount)), setUp(() async {
true, settingsMock = AppSettingsServiceMock();
container = TestUtils.createContainer(
overrides: [getAppSettingsServiceMock(settingsMock)],
); );
}); });
test("Asset Count - DESC", () { test('Returns the default sort mode when none set', () {
final sorted = assetCount.sortFn(albums, true); // Returns the default value when nothing is set
when(
() => settingsMock.getSetting(AppSettingsEnum.selectedAlbumSortOrder),
).thenReturn(0);
expect(AlbumSortMode.created, container.read(albumSortByOptionsProvider));
});
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( expect(
sorted.isSorted((b, a) => a.assetCount.compareTo(b.assetCount)), AlbumSortMode.lastModified,
true, container.read(albumSortByOptionsProvider),
); );
}); });
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,
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);
});
}); });
group("Album sort - Last modified", () { /// Verify the sort order provider
const lastModified = AlbumSortMode.lastModified; group('AlbumSortOrder', () {
test("Last modified - ASC", () { late AppSettingsService settingsMock;
final sorted = lastModified.sortFn(albums, false); late ProviderContainer container;
expect(
sorted.isSorted((a, b) => a.modifiedAt.compareTo(b.modifiedAt)), setUp(() async {
true, settingsMock = AppSettingsServiceMock();
container = TestUtils.createContainer(
overrides: [getAppSettingsServiceMock(settingsMock)],
); );
}); });
test("Last modified - DESC", () { test('Returns the default sort order when none set - false', () {
final sorted = lastModified.sortFn(albums, true); when(
expect( () => settingsMock.getSetting(AppSettingsEnum.selectedAlbumSortReverse),
sorted.isSorted((b, a) => a.modifiedAt.compareTo(b.modifiedAt)), ).thenReturn(false);
true,
);
});
});
group("Album sort - Created", () { expect(false, container.read(albumSortOrderProvider));
const created = AlbumSortMode.created; });
test("Created - ASC", () {
final sorted = created.sortFn(albums, false); test('Properly saves the correct order', () {
expect( container.read(albumSortOrderProvider.notifier).changeSortDirection(true);
sorted.isSorted((a, b) => a.createdAt.compareTo(b.createdAt)),
true, verify(
() => settingsMock.setSetting(
AppSettingsEnum.selectedAlbumSortReverse,
true,
),
); );
}); });
test("Created - DESC", () { test('Notifies listeners on state change', () {
final sorted = created.sortFn(albums, true); when(
expect( () => settingsMock.getSetting(AppSettingsEnum.selectedAlbumSortReverse),
sorted.isSorted((b, a) => a.createdAt.compareTo(b.createdAt)), ).thenReturn(false);
true,
final listener = ListenerMock<bool>();
container.listen(
albumSortOrderProvider,
listener,
fireImmediately: true,
); );
});
});
group("Album sort - Most Recent", () { // false -> true
const mostRecent = AlbumSortMode.mostRecent; container.read(albumSortOrderProvider.notifier).changeSortDirection(true);
test("Most Recent - ASC", () { // true -> false
final sorted = mostRecent.sortFn(albums, false); container
expect( .read(albumSortOrderProvider.notifier)
sorted, .changeSortDirection(false);
[
AlbumStub.sharedWithUser,
AlbumStub.twoAsset,
AlbumStub.oneAsset,
AlbumStub.emptyAlbum,
],
);
});
test("Most Recent - DESC", () { verifyInOrder([
final sorted = mostRecent.sortFn(albums, true); () => listener.call(null, false),
expect( () => listener.call(false, true),
sorted, () => listener.call(true, false),
[ ]);
AlbumStub.emptyAlbum,
AlbumStub.oneAsset,
AlbumStub.twoAsset,
AlbumStub.sharedWithUser,
],
);
});
});
group("Album sort - Most Oldest", () { verifyNoMoreInteractions(listener);
const mostOldest = AlbumSortMode.mostOldest;
test("Most Oldest - ASC", () {
final sorted = mostOldest.sortFn(albums, false);
expect(
sorted,
[
AlbumStub.twoAsset,
AlbumStub.emptyAlbum,
AlbumStub.oneAsset,
AlbumStub.sharedWithUser,
],
);
});
test("Most Oldest - DESC", () {
final sorted = mostOldest.sortFn(albums, true);
expect(
sorted,
[
AlbumStub.sharedWithUser,
AlbumStub.oneAsset,
AlbumStub.emptyAlbum,
AlbumStub.twoAsset,
],
);
}); });
}); });
} }

View file

@ -0,0 +1,9 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
import 'package:mocktail/mocktail.dart';
class AppSettingsServiceMock with Mock implements AppSettingsService {}
Override getAppSettingsServiceMock(AppSettingsService service) =>
appSettingsServiceProvider.overrideWith((ref) => service);

View file

@ -11,7 +11,7 @@ import 'package:immich_mobile/shared/services/hash.service.dart';
import 'package:immich_mobile/shared/services/immich_logger.service.dart'; import 'package:immich_mobile/shared/services/immich_logger.service.dart';
import 'package:immich_mobile/shared/services/sync.service.dart'; import 'package:immich_mobile/shared/services/sync.service.dart';
import 'package:isar/isar.dart'; import 'package:isar/isar.dart';
import 'package:mockito/mockito.dart'; import 'package:mocktail/mocktail.dart';
void main() { void main() {
Asset makeAsset({ Asset makeAsset({

View file

@ -0,0 +1,71 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/backup/models/backup_album.model.dart';
import 'package:immich_mobile/modules/backup/models/duplicated_asset.model.dart';
import 'package:immich_mobile/shared/models/album.dart';
import 'package:immich_mobile/shared/models/android_device_asset.dart';
import 'package:immich_mobile/shared/models/asset.dart';
import 'package:immich_mobile/shared/models/etag.dart';
import 'package:immich_mobile/shared/models/exif_info.dart';
import 'package:immich_mobile/shared/models/ios_device_asset.dart';
import 'package:immich_mobile/shared/models/logger_message.model.dart';
import 'package:immich_mobile/shared/models/store.dart';
import 'package:immich_mobile/shared/models/user.dart';
import 'package:isar/isar.dart';
import 'package:mocktail/mocktail.dart';
// Listener Mock to test when a provider notifies its listeners
class ListenerMock<T> extends Mock {
// ignore: avoid-declaring-call-method
void call(T? previous, T next);
}
final class TestUtils {
const TestUtils._();
/// Downloads Isar binaries (if required) and initializes a new Isar db
static Future<Isar> initIsar() async {
await Isar.initializeIsarCore(download: true);
final db = await Isar.open(
[
StoreValueSchema,
ExifInfoSchema,
AssetSchema,
AlbumSchema,
UserSchema,
BackupAlbumSchema,
DuplicatedAssetSchema,
LoggerMessageSchema,
ETagSchema,
AndroidDeviceAssetSchema,
IOSDeviceAssetSchema,
],
maxSizeMiB: 256,
directory: ".",
);
// Clear and close db on test end
addTearDown(() async {
await db.writeTxn(() => db.clear());
await db.close();
});
return db;
}
/// Creates a new ProviderContainer to test Riverpod providers
static ProviderContainer createContainer({
ProviderContainer? parent,
List<Override> overrides = const [],
List<ProviderObserver>? observers,
}) {
final container = ProviderContainer(
parent: parent,
overrides: overrides,
observers: observers,
);
// Dispose on test end
addTearDown(container.dispose);
return container;
}
}