Source Code added

This commit is contained in:
Fr4nz D13trich 2026-02-02 15:06:40 +01:00
parent 800376eafd
commit 9efa9bc6dd
3912 changed files with 754770 additions and 2 deletions

View 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));
});
});
}

View 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 {}

View 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)));
});
});
}

View file

@ -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);
});
}

View 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()));
});
}

View 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);
});
});
}

View 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);
});
}