You've already forked immich
mirror of
https://github.com/immich-app/immich.git
synced 2025-08-08 23:07:06 +02:00
refactor(mobile): Activities (#5990)
* refactor: autoroutex pushroute * refactor: autoroutex popRoute * refactor: autoroutex navigate and replace * chore: add doc comments for extension methods * refactor: Add LoggerMixin and refactor Album activities to use mixin * refactor: Activity page * chore: activity user from user constructor * fix: update current asset after build method * refactor: tests with similar structure as lib * chore: remove avoid-declaring-call-method rule from dcm analysis * test: fix proper expect order * test: activity_statistics_provider_test * test: activity_provider_test * test: use proper matchers * test: activity_text_field_test & dismissible_activity_test added * test: add http mock to return transparent image * test: download isar core libs during test * test: add widget tags to widget test cases * test: activity_tile_test * build: currentAlbumProvider to generator * movie add / remove like to activity input tile * test: activities_page_test.dart * chore: better error logs * chore: dismissibleactivity as statelesswidget --------- Co-authored-by: shalong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
This commit is contained in:
5
mobile/test/fixtures/album.stub.dart
vendored
5
mobile/test/fixtures/album.stub.dart
vendored
@ -50,5 +50,8 @@ final class AlbumStub {
|
||||
activityEnabled: false,
|
||||
startDate: DateTime(2019),
|
||||
endDate: DateTime(2020),
|
||||
)..assets.addAll([AssetStub.image1, AssetStub.image2]);
|
||||
)
|
||||
..assets.addAll([AssetStub.image1, AssetStub.image2])
|
||||
..activityEnabled = true
|
||||
..owner.value = UserStub.admin;
|
||||
}
|
||||
|
2
mobile/test/fixtures/asset.stub.dart
vendored
2
mobile/test/fixtures/asset.stub.dart
vendored
@ -6,6 +6,7 @@ final class AssetStub {
|
||||
static final image1 = Asset(
|
||||
checksum: "image1-checksum",
|
||||
localId: "image1",
|
||||
remoteId: 'image1-remote',
|
||||
ownerId: 1,
|
||||
fileCreatedAt: DateTime.now(),
|
||||
fileModifiedAt: DateTime.now(),
|
||||
@ -22,6 +23,7 @@ final class AssetStub {
|
||||
static final image2 = Asset(
|
||||
checksum: "image2-checksum",
|
||||
localId: "image2",
|
||||
remoteId: 'image2-remote',
|
||||
ownerId: 1,
|
||||
fileCreatedAt: DateTime(2000),
|
||||
fileModifiedAt: DateTime(2010),
|
||||
|
14
mobile/test/fixtures/user.stub.dart
vendored
14
mobile/test/fixtures/user.stub.dart
vendored
@ -8,6 +8,8 @@ final class UserStub {
|
||||
updatedAt: DateTime(2021),
|
||||
email: "admin@test.com",
|
||||
name: "admin",
|
||||
avatarColor: AvatarColorEnum.green,
|
||||
profileImagePath: '',
|
||||
isAdmin: true,
|
||||
);
|
||||
|
||||
@ -16,6 +18,18 @@ final class UserStub {
|
||||
updatedAt: DateTime(2022),
|
||||
email: "user1@test.com",
|
||||
name: "user1",
|
||||
avatarColor: AvatarColorEnum.red,
|
||||
profileImagePath: '',
|
||||
isAdmin: false,
|
||||
);
|
||||
|
||||
static final user2 = User(
|
||||
id: "user2",
|
||||
updatedAt: DateTime(2023),
|
||||
email: "user2@test.com",
|
||||
name: "user2",
|
||||
avatarColor: AvatarColorEnum.primary,
|
||||
profileImagePath: '',
|
||||
isAdmin: false,
|
||||
);
|
||||
}
|
||||
|
67
mobile/test/mock_http_override.dart
Normal file
67
mobile/test/mock_http_override.dart
Normal file
@ -0,0 +1,67 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:immich_mobile/shared/ui/transparent_image.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
/// Mocks the http client to always return a transparent image for all the requests. Only useful in widget
|
||||
/// tests to return network images
|
||||
class MockHttpOverrides extends HttpOverrides {
|
||||
@override
|
||||
HttpClient createHttpClient(SecurityContext? context) {
|
||||
final client = _MockHttpClient();
|
||||
final request = _MockHttpClientRequest();
|
||||
final response = _MockHttpClientResponse();
|
||||
final headers = _MockHttpHeaders();
|
||||
|
||||
// Client mocks
|
||||
when(() => client.autoUncompress).thenReturn(true);
|
||||
|
||||
// Request mocks
|
||||
when(() => request.headers).thenAnswer((_) => headers);
|
||||
when(() => request.close())
|
||||
.thenAnswer((_) => Future<HttpClientResponse>.value(response));
|
||||
|
||||
// Response mocks
|
||||
when(() => response.statusCode).thenReturn(HttpStatus.ok);
|
||||
when(() => response.compressionState)
|
||||
.thenReturn(HttpClientResponseCompressionState.decompressed);
|
||||
when(() => response.contentLength)
|
||||
.thenAnswer((_) => kTransparentImage.length);
|
||||
when(
|
||||
() => response.listen(
|
||||
captureAny(),
|
||||
cancelOnError: captureAny(named: 'cancelOnError'),
|
||||
onDone: captureAny(named: 'onDone'),
|
||||
onError: captureAny(named: 'onError'),
|
||||
),
|
||||
).thenAnswer((invocation) {
|
||||
final onData =
|
||||
invocation.positionalArguments[0] as void Function(List<int>);
|
||||
|
||||
final onDone = invocation.namedArguments[#onDone] as void Function();
|
||||
|
||||
final onError = invocation.namedArguments[#onError] as void
|
||||
Function(Object, [StackTrace]);
|
||||
|
||||
final cancelOnError = invocation.namedArguments[#cancelOnError] as bool;
|
||||
|
||||
return Stream<List<int>>.fromIterable([kTransparentImage.toList()])
|
||||
.listen(
|
||||
onData,
|
||||
onDone: onDone,
|
||||
onError: onError,
|
||||
cancelOnError: cancelOnError,
|
||||
);
|
||||
});
|
||||
|
||||
return client;
|
||||
}
|
||||
}
|
||||
|
||||
class _MockHttpClient extends Mock implements HttpClient {}
|
||||
|
||||
class _MockHttpClientRequest extends Mock implements HttpClientRequest {}
|
||||
|
||||
class _MockHttpClientResponse extends Mock implements HttpClientResponse {}
|
||||
|
||||
class _MockHttpHeaders extends Mock implements HttpHeaders {}
|
@ -1,9 +0,0 @@
|
||||
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);
|
250
mobile/test/modules/activity/activities_page_test.dart
Normal file
250
mobile/test/modules/activity/activities_page_test.dart
Normal file
@ -0,0 +1,250 @@
|
||||
@Tags(['widget'])
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/modules/activities/models/activity.model.dart';
|
||||
import 'package:immich_mobile/modules/activities/providers/activity.provider.dart';
|
||||
import 'package:immich_mobile/modules/activities/views/activities_page.dart';
|
||||
import 'package:immich_mobile/modules/activities/widgets/activity_text_field.dart';
|
||||
import 'package:immich_mobile/modules/activities/widgets/dismissible_activity.dart';
|
||||
import 'package:immich_mobile/modules/album/providers/current_album.provider.dart';
|
||||
import 'package:immich_mobile/modules/asset_viewer/providers/current_asset.provider.dart';
|
||||
import 'package:immich_mobile/shared/models/album.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:immich_mobile/shared/models/store.dart';
|
||||
import 'package:immich_mobile/shared/models/user.dart';
|
||||
import 'package:immich_mobile/shared/providers/user.provider.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 '../asset_viewer/asset_viewer_mocks.dart';
|
||||
import '../album/album_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();
|
||||
Store.init(db);
|
||||
Store.put(StoreKey.currentUser, UserStub.admin);
|
||||
Store.put(StoreKey.serverEndpoint, '');
|
||||
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(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,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
23
mobile/test/modules/activity/activity_mocks.dart
Normal file
23
mobile/test/modules/activity/activity_mocks.dart
Normal file
@ -0,0 +1,23 @@
|
||||
import 'package:immich_mobile/modules/activities/models/activity.model.dart';
|
||||
import 'package:immich_mobile/modules/activities/providers/activity.provider.dart';
|
||||
import 'package:immich_mobile/modules/activities/providers/activity_statistics.provider.dart';
|
||||
import 'package:immich_mobile/modules/activities/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 {}
|
353
mobile/test/modules/activity/activity_provider_test.dart
Normal file
353
mobile/test/modules/activity/activity_provider_test.dart
Normal file
@ -0,0 +1,353 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/modules/activities/models/activity.model.dart';
|
||||
import 'package:immich_mobile/modules/activities/providers/activity.provider.dart';
|
||||
import 'package:immich_mobile/modules/activities/providers/activity_service.provider.dart';
|
||||
import 'package:immich_mobile/modules/activities/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 ProviderContainer container;
|
||||
late AlbumActivityProvider provider;
|
||||
late ListenerMock<AsyncValue<List<Activity>>> listener;
|
||||
|
||||
setUpAll(() {
|
||||
registerFallbackValue(AsyncData<List<Activity>>([..._activities]));
|
||||
});
|
||||
|
||||
setUp(() async {
|
||||
activityMock = ActivityServiceMock();
|
||||
activityStatisticsMock = ActivityStatisticsMock();
|
||||
container = TestUtils.createContainer(
|
||||
overrides: [
|
||||
activityServiceProvider.overrideWith((ref) => activityMock),
|
||||
activityStatisticsProvider('test-album', 'test-asset')
|
||||
.overrideWith(() => activityStatisticsMock),
|
||||
],
|
||||
);
|
||||
|
||||
// Mock values
|
||||
when(
|
||||
() => activityMock.getAllActivities('test-album', assetId: 'test-asset'),
|
||||
).thenAnswer((_) async => [..._activities]);
|
||||
|
||||
// Init and wait for providers future to complete
|
||||
provider = albumActivityProvider('test-album', 'test-asset');
|
||||
listener = ListenerMock();
|
||||
container.listen(
|
||||
provider,
|
||||
listener,
|
||||
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));
|
||||
|
||||
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());
|
||||
});
|
||||
|
||||
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),
|
||||
);
|
||||
|
||||
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)));
|
||||
});
|
||||
});
|
||||
|
||||
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());
|
||||
});
|
||||
|
||||
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')),
|
||||
);
|
||||
});
|
||||
|
||||
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());
|
||||
});
|
||||
});
|
||||
|
||||
group('addComment()', () {
|
||||
late ActivityStatisticsMock albumActivityStatisticsMock;
|
||||
|
||||
setUp(() {
|
||||
albumActivityStatisticsMock = ActivityStatisticsMock();
|
||||
container = TestUtils.createContainer(
|
||||
overrides: [
|
||||
activityServiceProvider.overrideWith((ref) => activityMock),
|
||||
activityStatisticsProvider('test-album', 'test-asset')
|
||||
.overrideWith(() => activityStatisticsMock),
|
||||
activityStatisticsProvider('test-album')
|
||||
.overrideWith(() => albumActivityStatisticsMock),
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
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',
|
||||
);
|
||||
|
||||
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());
|
||||
});
|
||||
|
||||
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');
|
||||
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),
|
||||
);
|
||||
|
||||
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());
|
||||
});
|
||||
});
|
||||
}
|
@ -0,0 +1,91 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/modules/activities/providers/activity_service.provider.dart';
|
||||
import 'package:immich_mobile/modules/activities/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 => 5);
|
||||
|
||||
// Read here to make the getStatistics call
|
||||
container.read(activityStatisticsProvider('test-album', 'test-asset'));
|
||||
|
||||
container.listen(
|
||||
activityStatisticsProvider('test-album', 'test-asset'),
|
||||
listener,
|
||||
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 => 10);
|
||||
|
||||
final provider = activityStatisticsProvider('test-album');
|
||||
container.listen(
|
||||
provider,
|
||||
listener,
|
||||
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 => 10);
|
||||
|
||||
final provider = activityStatisticsProvider('new-album', 'test-asset');
|
||||
container.listen(
|
||||
provider,
|
||||
listener,
|
||||
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);
|
||||
});
|
||||
}
|
199
mobile/test/modules/activity/activity_text_field_test.dart
Normal file
199
mobile/test/modules/activity/activity_text_field_test.dart
Normal file
@ -0,0 +1,199 @@
|
||||
@Tags(['widget'])
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/modules/activities/providers/activity.provider.dart';
|
||||
import 'package:immich_mobile/modules/activities/widgets/activity_text_field.dart';
|
||||
import 'package:immich_mobile/modules/album/providers/current_album.provider.dart';
|
||||
import 'package:immich_mobile/shared/models/store.dart';
|
||||
import 'package:immich_mobile/shared/providers/user.provider.dart';
|
||||
import 'package:immich_mobile/shared/ui/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();
|
||||
Store.init(db);
|
||||
Store.put(StoreKey.currentUser, UserStub.admin);
|
||||
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.favorite_rounded),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(
|
||||
find.widgetWithIcon(IconButton, Icons.favorite_border_rounded),
|
||||
findsNothing,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('Bordered icon if likedId == null', (tester) async {
|
||||
await tester.pumpConsumerWidget(
|
||||
ActivityTextField(
|
||||
onSubmit: (_) {},
|
||||
),
|
||||
overrides: overrides,
|
||||
);
|
||||
|
||||
expect(
|
||||
find.widgetWithIcon(IconButton, Icons.favorite_border_rounded),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(
|
||||
find.widgetWithIcon(IconButton, Icons.favorite_rounded),
|
||||
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()));
|
||||
});
|
||||
}
|
222
mobile/test/modules/activity/activity_tile_test.dart
Normal file
222
mobile/test/modules/activity/activity_tile_test.dart
Normal file
@ -0,0 +1,222 @@
|
||||
@Tags(['widget'])
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/modules/activities/models/activity.model.dart';
|
||||
import 'package:immich_mobile/modules/activities/widgets/activity_tile.dart';
|
||||
import 'package:immich_mobile/modules/asset_viewer/providers/current_asset.provider.dart';
|
||||
import 'package:immich_mobile/shared/models/store.dart';
|
||||
import 'package:immich_mobile/shared/ui/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
|
||||
Store.init(db);
|
||||
Store.put(StoreKey.currentUser, UserStub.admin);
|
||||
Store.put(StoreKey.serverEndpoint, '');
|
||||
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 heart 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 favorite icon
|
||||
final favoIconFinder = find.widgetWithIcon(
|
||||
listTile.leading!.runtimeType,
|
||||
Icons.favorite_rounded,
|
||||
);
|
||||
|
||||
expect(favoIconFinder, 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,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
119
mobile/test/modules/activity/dismissible_activity_test.dart
Normal file
119
mobile/test/modules/activity/dismissible_activity_test.dart
Normal file
@ -0,0 +1,119 @@
|
||||
@Tags(['widget'])
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/modules/activities/models/activity.model.dart';
|
||||
import 'package:immich_mobile/modules/activities/widgets/activity_tile.dart';
|
||||
import 'package:immich_mobile/modules/activities/widgets/dismissible_activity.dart';
|
||||
import 'package:immich_mobile/modules/asset_viewer/providers/current_asset.provider.dart';
|
||||
import 'package:immich_mobile/shared/ui/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)),
|
||||
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_dialog_ok');
|
||||
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,
|
||||
);
|
||||
|
||||
final dismissible = find.byType(Dismissible);
|
||||
await tester.drag(dismissible, const Offset(500, 0));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
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,
|
||||
);
|
||||
|
||||
final dismissible = find.byType(Dismissible);
|
||||
await tester.drag(dismissible, const Offset(-500, 0));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byIcon(Icons.delete_sweep_rounded), findsNothing);
|
||||
});
|
||||
}
|
15
mobile/test/modules/album/album_mocks.dart
Normal file
15
mobile/test/modules/album/album_mocks.dart
Normal file
@ -0,0 +1,15 @@
|
||||
import 'package:immich_mobile/modules/album/providers/current_album.provider.dart';
|
||||
import 'package:immich_mobile/shared/models/album.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
class MockCurrentAlbumProvider extends CurrentAlbum
|
||||
with Mock
|
||||
implements CurrentAlbumInternal {
|
||||
Album? initAlbum;
|
||||
MockCurrentAlbumProvider([this.initAlbum]);
|
||||
|
||||
@override
|
||||
Album? build() {
|
||||
return initAlbum;
|
||||
}
|
||||
}
|
@ -1,17 +1,17 @@
|
||||
import 'package:collection/collection.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/settings/providers/app_settings.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/asset.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
import 'fixtures/album.stub.dart';
|
||||
import 'fixtures/asset.stub.dart';
|
||||
import 'mocks/app_settings_provider.mock.dart';
|
||||
import 'test_utils.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
|
||||
@ -48,15 +48,24 @@ void main() {
|
||||
const created = AlbumSortMode.created;
|
||||
test("Created time - ASC", () {
|
||||
final sorted = created.sortFn(albums, false);
|
||||
expect(sorted.isSortedBy((a) => a.createdAt), true);
|
||||
final sortedList = [
|
||||
AlbumStub.emptyAlbum,
|
||||
AlbumStub.twoAsset,
|
||||
AlbumStub.oneAsset,
|
||||
AlbumStub.sharedWithUser,
|
||||
];
|
||||
expect(sorted, orderedEquals(sortedList));
|
||||
});
|
||||
|
||||
test("Created time - DESC", () {
|
||||
final sorted = created.sortFn(albums, true);
|
||||
expect(
|
||||
sorted.isSorted((b, a) => a.createdAt.compareTo(b.createdAt)),
|
||||
true,
|
||||
);
|
||||
final sortedList = [
|
||||
AlbumStub.sharedWithUser,
|
||||
AlbumStub.oneAsset,
|
||||
AlbumStub.twoAsset,
|
||||
AlbumStub.emptyAlbum,
|
||||
];
|
||||
expect(sorted, orderedEquals(sortedList));
|
||||
});
|
||||
});
|
||||
|
||||
@ -64,18 +73,24 @@ void main() {
|
||||
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,
|
||||
);
|
||||
final sortedList = [
|
||||
AlbumStub.emptyAlbum,
|
||||
AlbumStub.sharedWithUser,
|
||||
AlbumStub.oneAsset,
|
||||
AlbumStub.twoAsset,
|
||||
];
|
||||
expect(sorted, orderedEquals(sortedList));
|
||||
});
|
||||
|
||||
test("Asset Count - DESC", () {
|
||||
final sorted = assetCount.sortFn(albums, true);
|
||||
expect(
|
||||
sorted.isSorted((b, a) => a.assetCount.compareTo(b.assetCount)),
|
||||
true,
|
||||
);
|
||||
final sortedList = [
|
||||
AlbumStub.twoAsset,
|
||||
AlbumStub.oneAsset,
|
||||
AlbumStub.sharedWithUser,
|
||||
AlbumStub.emptyAlbum,
|
||||
];
|
||||
expect(sorted, orderedEquals(sortedList));
|
||||
});
|
||||
});
|
||||
|
||||
@ -83,18 +98,24 @@ void main() {
|
||||
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,
|
||||
);
|
||||
final sortedList = [
|
||||
AlbumStub.twoAsset,
|
||||
AlbumStub.emptyAlbum,
|
||||
AlbumStub.sharedWithUser,
|
||||
AlbumStub.oneAsset,
|
||||
];
|
||||
expect(sorted, orderedEquals(sortedList));
|
||||
});
|
||||
|
||||
test("Last modified - DESC", () {
|
||||
final sorted = lastModified.sortFn(albums, true);
|
||||
expect(
|
||||
sorted.isSorted((b, a) => a.modifiedAt.compareTo(b.modifiedAt)),
|
||||
true,
|
||||
);
|
||||
final sortedList = [
|
||||
AlbumStub.oneAsset,
|
||||
AlbumStub.sharedWithUser,
|
||||
AlbumStub.emptyAlbum,
|
||||
AlbumStub.twoAsset,
|
||||
];
|
||||
expect(sorted, orderedEquals(sortedList));
|
||||
});
|
||||
});
|
||||
|
||||
@ -102,18 +123,24 @@ void main() {
|
||||
const created = AlbumSortMode.created;
|
||||
test("Created - ASC", () {
|
||||
final sorted = created.sortFn(albums, false);
|
||||
expect(
|
||||
sorted.isSorted((a, b) => a.createdAt.compareTo(b.createdAt)),
|
||||
true,
|
||||
);
|
||||
final sortedList = [
|
||||
AlbumStub.emptyAlbum,
|
||||
AlbumStub.twoAsset,
|
||||
AlbumStub.oneAsset,
|
||||
AlbumStub.sharedWithUser,
|
||||
];
|
||||
expect(sorted, orderedEquals(sortedList));
|
||||
});
|
||||
|
||||
test("Created - DESC", () {
|
||||
final sorted = created.sortFn(albums, true);
|
||||
expect(
|
||||
sorted.isSorted((b, a) => a.createdAt.compareTo(b.createdAt)),
|
||||
true,
|
||||
);
|
||||
final sortedList = [
|
||||
AlbumStub.sharedWithUser,
|
||||
AlbumStub.oneAsset,
|
||||
AlbumStub.twoAsset,
|
||||
AlbumStub.emptyAlbum,
|
||||
];
|
||||
expect(sorted, orderedEquals(sortedList));
|
||||
});
|
||||
});
|
||||
|
||||
@ -122,28 +149,24 @@ void main() {
|
||||
|
||||
test("Most Recent - ASC", () {
|
||||
final sorted = mostRecent.sortFn(albums, false);
|
||||
expect(
|
||||
sorted,
|
||||
[
|
||||
AlbumStub.sharedWithUser,
|
||||
AlbumStub.twoAsset,
|
||||
AlbumStub.oneAsset,
|
||||
AlbumStub.emptyAlbum,
|
||||
],
|
||||
);
|
||||
final sortedList = [
|
||||
AlbumStub.sharedWithUser,
|
||||
AlbumStub.twoAsset,
|
||||
AlbumStub.oneAsset,
|
||||
AlbumStub.emptyAlbum,
|
||||
];
|
||||
expect(sorted, orderedEquals(sortedList));
|
||||
});
|
||||
|
||||
test("Most Recent - DESC", () {
|
||||
final sorted = mostRecent.sortFn(albums, true);
|
||||
expect(
|
||||
sorted,
|
||||
[
|
||||
AlbumStub.emptyAlbum,
|
||||
AlbumStub.oneAsset,
|
||||
AlbumStub.twoAsset,
|
||||
AlbumStub.sharedWithUser,
|
||||
],
|
||||
);
|
||||
final sortedList = [
|
||||
AlbumStub.emptyAlbum,
|
||||
AlbumStub.oneAsset,
|
||||
AlbumStub.twoAsset,
|
||||
AlbumStub.sharedWithUser,
|
||||
];
|
||||
expect(sorted, orderedEquals(sortedList));
|
||||
});
|
||||
});
|
||||
|
||||
@ -152,28 +175,24 @@ void main() {
|
||||
|
||||
test("Most Oldest - ASC", () {
|
||||
final sorted = mostOldest.sortFn(albums, false);
|
||||
expect(
|
||||
sorted,
|
||||
[
|
||||
AlbumStub.twoAsset,
|
||||
AlbumStub.emptyAlbum,
|
||||
AlbumStub.oneAsset,
|
||||
AlbumStub.sharedWithUser,
|
||||
],
|
||||
);
|
||||
final sortedList = [
|
||||
AlbumStub.twoAsset,
|
||||
AlbumStub.emptyAlbum,
|
||||
AlbumStub.oneAsset,
|
||||
AlbumStub.sharedWithUser,
|
||||
];
|
||||
expect(sorted, orderedEquals(sortedList));
|
||||
});
|
||||
|
||||
test("Most Oldest - DESC", () {
|
||||
final sorted = mostOldest.sortFn(albums, true);
|
||||
expect(
|
||||
sorted,
|
||||
[
|
||||
AlbumStub.sharedWithUser,
|
||||
AlbumStub.oneAsset,
|
||||
AlbumStub.emptyAlbum,
|
||||
AlbumStub.twoAsset,
|
||||
],
|
||||
);
|
||||
final sortedList = [
|
||||
AlbumStub.sharedWithUser,
|
||||
AlbumStub.oneAsset,
|
||||
AlbumStub.emptyAlbum,
|
||||
AlbumStub.twoAsset,
|
||||
];
|
||||
expect(sorted, orderedEquals(sortedList));
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -186,7 +205,9 @@ void main() {
|
||||
setUp(() async {
|
||||
settingsMock = AppSettingsServiceMock();
|
||||
container = TestUtils.createContainer(
|
||||
overrides: [getAppSettingsServiceMock(settingsMock)],
|
||||
overrides: [
|
||||
appSettingsServiceProvider.overrideWith((ref) => settingsMock),
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
@ -196,7 +217,7 @@ void main() {
|
||||
() => settingsMock.getSetting(AppSettingsEnum.selectedAlbumSortOrder),
|
||||
).thenReturn(0);
|
||||
|
||||
expect(AlbumSortMode.created, container.read(albumSortByOptionsProvider));
|
||||
expect(container.read(albumSortByOptionsProvider), AlbumSortMode.created);
|
||||
});
|
||||
|
||||
test('Returns the correct sort mode with index from Store', () {
|
||||
@ -206,8 +227,8 @@ void main() {
|
||||
).thenReturn(3);
|
||||
|
||||
expect(
|
||||
AlbumSortMode.lastModified,
|
||||
container.read(albumSortByOptionsProvider),
|
||||
AlbumSortMode.lastModified,
|
||||
);
|
||||
});
|
||||
|
||||
@ -230,7 +251,6 @@ void main() {
|
||||
).thenReturn(0);
|
||||
|
||||
final listener = ListenerMock<AlbumSortMode>();
|
||||
|
||||
container.listen(
|
||||
albumSortByOptionsProvider,
|
||||
listener,
|
||||
@ -265,7 +285,9 @@ void main() {
|
||||
setUp(() async {
|
||||
settingsMock = AppSettingsServiceMock();
|
||||
container = TestUtils.createContainer(
|
||||
overrides: [getAppSettingsServiceMock(settingsMock)],
|
||||
overrides: [
|
||||
appSettingsServiceProvider.overrideWith((ref) => settingsMock),
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
@ -274,7 +296,7 @@ void main() {
|
||||
() => settingsMock.getSetting(AppSettingsEnum.selectedAlbumSortReverse),
|
||||
).thenReturn(false);
|
||||
|
||||
expect(false, container.read(albumSortOrderProvider));
|
||||
expect(container.read(albumSortOrderProvider), isFalse);
|
||||
});
|
||||
|
||||
test('Properly saves the correct order', () {
|
||||
@ -294,7 +316,6 @@ void main() {
|
||||
).thenReturn(false);
|
||||
|
||||
final listener = ListenerMock<bool>();
|
||||
|
||||
container.listen(
|
||||
albumSortOrderProvider,
|
||||
listener,
|
15
mobile/test/modules/asset_viewer/asset_viewer_mocks.dart
Normal file
15
mobile/test/modules/asset_viewer/asset_viewer_mocks.dart
Normal file
@ -0,0 +1,15 @@
|
||||
import 'package:immich_mobile/modules/asset_viewer/providers/current_asset.provider.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
class MockCurrentAssetProvider extends CurrentAssetInternal
|
||||
with Mock
|
||||
implements CurrentAsset {
|
||||
Asset? initAsset;
|
||||
MockCurrentAssetProvider([this.initAsset]);
|
||||
|
||||
@override
|
||||
Asset? build() {
|
||||
return initAsset;
|
||||
}
|
||||
}
|
@ -49,8 +49,8 @@ void main() {
|
||||
final a = makeAsset(id: '1', createdAt: createdAt);
|
||||
final (dt, tz) = a.getTZAdjustedTimeAndOffset();
|
||||
|
||||
expect(dt, createdAt);
|
||||
expect(tz, createdAt.timeZoneOffset);
|
||||
expect(createdAt, dt);
|
||||
expect(createdAt.timeZoneOffset, tz);
|
||||
});
|
||||
|
||||
test('returns createdAt in local if in utc', () {
|
||||
@ -59,8 +59,8 @@ void main() {
|
||||
final (dt, tz) = a.getTZAdjustedTimeAndOffset();
|
||||
|
||||
final localCreatedAt = createdAt.toLocal();
|
||||
expect(dt, localCreatedAt);
|
||||
expect(tz, localCreatedAt.timeZoneOffset);
|
||||
expect(localCreatedAt, dt);
|
||||
expect(localCreatedAt.timeZoneOffset, tz);
|
||||
});
|
||||
});
|
||||
|
||||
@ -73,8 +73,8 @@ void main() {
|
||||
final (dt, tz) = a.getTZAdjustedTimeAndOffset();
|
||||
|
||||
final dateTimeInUTC = dateTimeOriginal.toUtc();
|
||||
expect(dt, dateTimeInUTC);
|
||||
expect(tz, dateTimeInUTC.timeZoneOffset);
|
||||
expect(dateTimeInUTC, dt);
|
||||
expect(dateTimeInUTC.timeZoneOffset, tz);
|
||||
});
|
||||
|
||||
test('Returns dateTimeOriginal in UTC from exifInfo with invalid timezone',
|
||||
@ -89,8 +89,8 @@ void main() {
|
||||
final (dt, tz) = a.getTZAdjustedTimeAndOffset();
|
||||
|
||||
final dateTimeInUTC = dateTimeOriginal.toUtc();
|
||||
expect(dt, dateTimeInUTC);
|
||||
expect(tz, dateTimeInUTC.timeZoneOffset);
|
||||
expect(dateTimeInUTC, dt);
|
||||
expect(dateTimeInUTC.timeZoneOffset, tz);
|
||||
});
|
||||
});
|
||||
|
||||
@ -106,8 +106,8 @@ void main() {
|
||||
|
||||
final adjustedTime =
|
||||
TZDateTime.from(dateTimeOriginal.toUtc(), getLocation(location));
|
||||
expect(dt, adjustedTime);
|
||||
expect(tz, adjustedTime.timeZoneOffset);
|
||||
expect(adjustedTime, dt);
|
||||
expect(adjustedTime.timeZoneOffset, tz);
|
||||
});
|
||||
|
||||
test('With timezone as offset', () {
|
||||
@ -124,8 +124,8 @@ void main() {
|
||||
final adjustedTime = dateTimeOriginal.toUtc().add(offsetFromLocation);
|
||||
|
||||
// Adds the offset to the actual time and returns the offset separately
|
||||
expect(dt, adjustedTime);
|
||||
expect(tz, offsetFromLocation);
|
||||
expect(adjustedTime, dt);
|
||||
expect(offsetFromLocation, tz);
|
||||
});
|
||||
});
|
||||
}
|
@ -11,9 +11,9 @@ void main() {
|
||||
);
|
||||
});
|
||||
test('malformed', () {
|
||||
expect("".toDuration(), null);
|
||||
expect("1:2".toDuration(), null);
|
||||
expect("a:b:c".toDuration(), null);
|
||||
expect("".toDuration(), isNull);
|
||||
expect("1:2".toDuration(), isNull);
|
||||
expect("a:b:c".toDuration(), isNull);
|
||||
});
|
||||
});
|
||||
group('Test uniqueConsecutive', () {
|
||||
@ -29,17 +29,17 @@ void main() {
|
||||
|
||||
test('noDuplicates', () {
|
||||
final a = [1, 2, 3];
|
||||
expect(a.uniqueConsecutive(), [1, 2, 3]);
|
||||
expect(a.uniqueConsecutive(), orderedEquals([1, 2, 3]));
|
||||
});
|
||||
|
||||
test('unsortedDuplicates', () {
|
||||
final a = [1, 2, 1, 3];
|
||||
expect(a.uniqueConsecutive(), [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(), [6, 2, 3, 4, 5, 1]);
|
||||
expect(a.uniqueConsecutive(), orderedEquals([6, 2, 3, 4, 5, 1]));
|
||||
});
|
||||
|
||||
test('withKey', () {
|
||||
@ -48,7 +48,7 @@ void main() {
|
||||
a.uniqueConsecutive(
|
||||
compare: (s1, s2) => s1.length.compareTo(s2.length),
|
||||
),
|
||||
["a", "bb", "ddd"],
|
||||
orderedEquals(["a", "bb", "ddd"]),
|
||||
);
|
||||
});
|
||||
});
|
@ -75,7 +75,7 @@ void main() {
|
||||
// 5 Assets => 2 Rows
|
||||
// Day 1
|
||||
// 5 Assets => 2 Rows
|
||||
expect(renderList.elements.length, 4);
|
||||
expect(renderList.elements, hasLength(4));
|
||||
expect(
|
||||
renderList.elements[0].type,
|
||||
RenderAssetGridElementType.monthTitle,
|
||||
@ -122,7 +122,7 @@ void main() {
|
||||
RenderAssetGridElementType.monthTitle,
|
||||
];
|
||||
|
||||
expect(renderList.elements.length, types.length);
|
||||
expect(renderList.elements, hasLength(types.length));
|
||||
|
||||
for (int i = 0; i < renderList.elements.length; i++) {
|
||||
expect(renderList.elements[i].type, types[i]);
|
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/modules/settings/services/app_settings.service.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
class AppSettingsServiceMock extends Mock implements AppSettingsService {}
|
16
mobile/test/modules/shared/shared_mocks.dart
Normal file
16
mobile/test/modules/shared/shared_mocks.dart
Normal file
@ -0,0 +1,16 @@
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/shared/models/user.dart';
|
||||
import 'package:immich_mobile/shared/providers/user.provider.dart';
|
||||
import 'package:immich_mobile/shared/services/hash.service.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
class MockHashService extends Mock implements HashService {}
|
||||
|
||||
class MockCurrentUserProvider extends StateNotifier<User?>
|
||||
with Mock
|
||||
implements CurrentUserProvider {
|
||||
MockCurrentUserProvider() : super(null);
|
||||
|
||||
@override
|
||||
set state(User? user) => super.state = user;
|
||||
}
|
@ -1,17 +1,14 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/shared/models/album.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/logger_message.model.dart';
|
||||
import 'package:immich_mobile/shared/models/store.dart';
|
||||
import 'package:immich_mobile/shared/models/user.dart';
|
||||
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/sync.service.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
import '../../test_utils.dart';
|
||||
import 'shared_mocks.dart';
|
||||
|
||||
void main() {
|
||||
Asset makeAsset({
|
||||
@ -39,22 +36,6 @@ void main() {
|
||||
);
|
||||
}
|
||||
|
||||
Isar loadDb() {
|
||||
return Isar.openSync(
|
||||
[
|
||||
ExifInfoSchema,
|
||||
AssetSchema,
|
||||
AlbumSchema,
|
||||
UserSchema,
|
||||
StoreValueSchema,
|
||||
LoggerMessageSchema,
|
||||
ETagSchema,
|
||||
],
|
||||
maxSizeMiB: 256,
|
||||
directory: ".",
|
||||
);
|
||||
}
|
||||
|
||||
group('Test SyncService grouped', () {
|
||||
late final Isar db;
|
||||
final MockHashService hs = MockHashService();
|
||||
@ -67,8 +48,7 @@ void main() {
|
||||
);
|
||||
setUpAll(() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await Isar.initializeIsarCore(download: true);
|
||||
db = loadDb();
|
||||
db = await TestUtils.initIsar();
|
||||
ImmichLogger();
|
||||
db.writeTxnSync(() => db.clearSync());
|
||||
Store.init(db);
|
||||
@ -97,7 +77,7 @@ void main() {
|
||||
expect(db.assets.countSync(), 5);
|
||||
final bool c1 =
|
||||
await s.syncRemoteAssetsToDb(owner, _failDiff, (u) => remoteAssets);
|
||||
expect(c1, false);
|
||||
expect(c1, isFalse);
|
||||
expect(db.assets.countSync(), 5);
|
||||
});
|
||||
|
||||
@ -114,7 +94,7 @@ void main() {
|
||||
expect(db.assets.countSync(), 5);
|
||||
final bool c1 =
|
||||
await s.syncRemoteAssetsToDb(owner, _failDiff, (u) => remoteAssets);
|
||||
expect(c1, true);
|
||||
expect(c1, isTrue);
|
||||
expect(db.assets.countSync(), 7);
|
||||
});
|
||||
|
||||
@ -131,22 +111,22 @@ void main() {
|
||||
expect(db.assets.countSync(), 5);
|
||||
final bool c1 =
|
||||
await s.syncRemoteAssetsToDb(owner, _failDiff, (u) => remoteAssets);
|
||||
expect(c1, true);
|
||||
expect(c1, isTrue);
|
||||
expect(db.assets.countSync(), 8);
|
||||
final bool c2 =
|
||||
await s.syncRemoteAssetsToDb(owner, _failDiff, (u) => remoteAssets);
|
||||
expect(c2, false);
|
||||
expect(c2, isFalse);
|
||||
expect(db.assets.countSync(), 8);
|
||||
remoteAssets.removeAt(4);
|
||||
final bool c3 =
|
||||
await s.syncRemoteAssetsToDb(owner, _failDiff, (u) => remoteAssets);
|
||||
expect(c3, true);
|
||||
expect(c3, isTrue);
|
||||
expect(db.assets.countSync(), 7);
|
||||
remoteAssets.add(makeAsset(checksum: "k", remoteId: "2-1e"));
|
||||
remoteAssets.add(makeAsset(checksum: "l", remoteId: "2-2"));
|
||||
final bool c4 =
|
||||
await s.syncRemoteAssetsToDb(owner, _failDiff, (u) => remoteAssets);
|
||||
expect(c4, true);
|
||||
expect(c4, isTrue);
|
||||
expect(db.assets.countSync(), 9);
|
||||
});
|
||||
|
||||
@ -164,7 +144,7 @@ void main() {
|
||||
(user, since) async => (toUpsert, toDelete),
|
||||
(user) => throw Exception(),
|
||||
);
|
||||
expect(c, true);
|
||||
expect(c, isTrue);
|
||||
expect(db.assets.countSync(), 6);
|
||||
});
|
||||
});
|
||||
@ -172,5 +152,3 @@ void main() {
|
||||
|
||||
Future<(List<Asset>?, List<String>?)> _failDiff(User user, DateTime time) =>
|
||||
Future.value((null, null));
|
||||
|
||||
class MockHashService extends Mock implements HashService {}
|
@ -1,3 +1,6 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
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';
|
||||
@ -14,9 +17,10 @@ import 'package:immich_mobile/shared/models/user.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
import 'mock_http_override.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);
|
||||
}
|
||||
|
||||
@ -26,6 +30,12 @@ final class TestUtils {
|
||||
/// Downloads Isar binaries (if required) and initializes a new Isar db
|
||||
static Future<Isar> initIsar() async {
|
||||
await Isar.initializeIsarCore(download: true);
|
||||
|
||||
final instance = Isar.getInstance();
|
||||
if (instance != null) {
|
||||
return instance;
|
||||
}
|
||||
|
||||
final db = await Isar.open(
|
||||
[
|
||||
StoreValueSchema,
|
||||
@ -41,8 +51,9 @@ final class TestUtils {
|
||||
IOSDeviceAssetSchema,
|
||||
],
|
||||
maxSizeMiB: 256,
|
||||
directory: ".",
|
||||
directory: "test/",
|
||||
);
|
||||
|
||||
// Clear and close db on test end
|
||||
addTearDown(() async {
|
||||
await db.writeTxn(() => db.clear());
|
||||
@ -68,4 +79,11 @@ final class TestUtils {
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
static void init() {
|
||||
// Turn off easy localization logging
|
||||
EasyLocalization.logger.enableBuildModes = [];
|
||||
WidgetController.hitTestWarningShouldBeFatal = true;
|
||||
HttpOverrides.global = MockHttpOverrides();
|
||||
}
|
||||
}
|
||||
|
30
mobile/test/widget_tester_extensions.dart
Normal file
30
mobile/test/widget_tester_extensions.dart
Normal file
@ -0,0 +1,30 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
extension PumpConsumerWidget on WidgetTester {
|
||||
/// Wraps the provided [widget] with Material app such that it becomes:
|
||||
///
|
||||
/// ProviderScope
|
||||
/// |-MaterialApp
|
||||
/// |-Material
|
||||
/// |-[widget]
|
||||
Future<void> pumpConsumerWidget(
|
||||
Widget widget, {
|
||||
Duration? duration,
|
||||
EnginePhase phase = EnginePhase.sendSemanticsUpdate,
|
||||
List<Override> overrides = const [],
|
||||
}) async {
|
||||
return pumpWidget(
|
||||
ProviderScope(
|
||||
overrides: overrides,
|
||||
child: MaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
home: Material(child: widget),
|
||||
),
|
||||
),
|
||||
duration,
|
||||
phase,
|
||||
);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user