mirror of
https://github.com/immich-app/immich.git
synced 2026-05-18 03:10:24 +03:00
Merge branch 'main' into feature/bottom-buttons-order
This commit is contained in:
@@ -87,6 +87,25 @@ void main() {
|
||||
verify(() => mockLocalAssetRepository.get('local-1')).called(1);
|
||||
});
|
||||
|
||||
test('uses fetched asset orientation when dimensions are missing on Android', () async {
|
||||
debugDefaultTargetPlatformOverride = TargetPlatform.android;
|
||||
addTearDown(() => debugDefaultTargetPlatformOverride = null);
|
||||
|
||||
// Original asset has default orientation 0, but dimensions are missing
|
||||
final localAsset = TestUtils.createLocalAsset(id: 'local-1', width: null, height: null, orientation: 0);
|
||||
|
||||
// Fetched asset has 90° orientation and proper dimensions
|
||||
final fetchedAsset = TestUtils.createLocalAsset(id: 'local-1', width: 1920, height: 1080, orientation: 90);
|
||||
|
||||
when(() => mockLocalAssetRepository.get('local-1')).thenAnswer((_) async => fetchedAsset);
|
||||
|
||||
final result = await sut.getAspectRatio(localAsset);
|
||||
|
||||
// Should flip dimensions since fetched asset has 90° orientation
|
||||
expect(result, 1080 / 1920);
|
||||
verify(() => mockLocalAssetRepository.get('local-1')).called(1);
|
||||
});
|
||||
|
||||
test('returns 1.0 when dimensions are still unavailable after fetching', () async {
|
||||
final remoteAsset = TestUtils.createRemoteAsset(id: 'remote-1', width: null, height: null);
|
||||
|
||||
@@ -112,7 +131,9 @@ void main() {
|
||||
expect(result, 1.0);
|
||||
});
|
||||
|
||||
test('handles local asset with remoteId and uses exif from remote', () async {
|
||||
test('handles local asset with remoteId using local orientation not remote exif', () async {
|
||||
// When a LocalAsset has a remoteId (merged), we should use local orientation
|
||||
// because the width/height come from the local asset (pre-corrected on iOS)
|
||||
final localAsset = TestUtils.createLocalAsset(
|
||||
id: 'local-1',
|
||||
remoteId: 'remote-1',
|
||||
@@ -121,9 +142,24 @@ void main() {
|
||||
orientation: 0,
|
||||
);
|
||||
|
||||
final exif = const ExifInfo(orientation: '6');
|
||||
final result = await sut.getAspectRatio(localAsset);
|
||||
|
||||
when(() => mockRemoteAssetRepository.getExif('remote-1')).thenAnswer((_) async => exif);
|
||||
expect(result, 1920 / 1080);
|
||||
// Should not call remote exif for LocalAsset
|
||||
verifyNever(() => mockRemoteAssetRepository.getExif(any()));
|
||||
});
|
||||
|
||||
test('handles local asset with remoteId and 90 degree rotation on Android', () async {
|
||||
debugDefaultTargetPlatformOverride = TargetPlatform.android;
|
||||
addTearDown(() => debugDefaultTargetPlatformOverride = null);
|
||||
|
||||
final localAsset = TestUtils.createLocalAsset(
|
||||
id: 'local-1',
|
||||
remoteId: 'remote-1',
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
orientation: 90,
|
||||
);
|
||||
|
||||
final result = await sut.getAspectRatio(localAsset);
|
||||
|
||||
|
||||
@@ -0,0 +1,438 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/constants/enums.dart';
|
||||
import 'package:immich_mobile/domain/models/album/local_album.model.dart';
|
||||
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/local_album.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/local_album_asset.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/remote_asset.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/entities/user.entity.drift.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/db.repository.dart';
|
||||
import 'package:immich_mobile/infrastructure/repositories/local_asset.repository.dart';
|
||||
|
||||
void main() {
|
||||
late Drift db;
|
||||
late DriftLocalAssetRepository repository;
|
||||
|
||||
setUp(() {
|
||||
db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true));
|
||||
repository = DriftLocalAssetRepository(db);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await db.close();
|
||||
});
|
||||
|
||||
group('getRemovalCandidates', () {
|
||||
final userId = 'user-123';
|
||||
final otherUserId = 'user-456';
|
||||
final now = DateTime(2024, 1, 15);
|
||||
final cutoffDate = DateTime(2024, 1, 10);
|
||||
final beforeCutoff = DateTime(2024, 1, 5);
|
||||
final afterCutoff = DateTime(2024, 1, 12);
|
||||
|
||||
Future<void> insertUser(String id, String email) async {
|
||||
await db.into(db.userEntity).insert(UserEntityCompanion.insert(id: id, email: email, name: email));
|
||||
}
|
||||
|
||||
setUp(() async {
|
||||
await insertUser(userId, 'user@test.com');
|
||||
await insertUser(otherUserId, 'other@test.com');
|
||||
});
|
||||
|
||||
Future<void> insertLocalAsset({
|
||||
required String id,
|
||||
required String checksum,
|
||||
required DateTime createdAt,
|
||||
required AssetType type,
|
||||
required bool isFavorite,
|
||||
}) async {
|
||||
await db
|
||||
.into(db.localAssetEntity)
|
||||
.insert(
|
||||
LocalAssetEntityCompanion.insert(
|
||||
id: id,
|
||||
name: 'asset_$id.jpg',
|
||||
checksum: Value(checksum),
|
||||
type: type,
|
||||
createdAt: Value(createdAt),
|
||||
updatedAt: Value(createdAt),
|
||||
isFavorite: Value(isFavorite),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> insertRemoteAsset({
|
||||
required String id,
|
||||
required String checksum,
|
||||
required String ownerId,
|
||||
DateTime? deletedAt,
|
||||
}) async {
|
||||
await db
|
||||
.into(db.remoteAssetEntity)
|
||||
.insert(
|
||||
RemoteAssetEntityCompanion.insert(
|
||||
id: id,
|
||||
name: 'remote_$id.jpg',
|
||||
checksum: checksum,
|
||||
type: AssetType.image,
|
||||
createdAt: Value(now),
|
||||
updatedAt: Value(now),
|
||||
ownerId: ownerId,
|
||||
visibility: AssetVisibility.timeline,
|
||||
deletedAt: Value(deletedAt),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> insertLocalAlbum({required String id, required String name, required bool isIosSharedAlbum}) async {
|
||||
await db
|
||||
.into(db.localAlbumEntity)
|
||||
.insert(
|
||||
LocalAlbumEntityCompanion.insert(
|
||||
id: id,
|
||||
name: name,
|
||||
updatedAt: Value(now),
|
||||
backupSelection: BackupSelection.none,
|
||||
isIosSharedAlbum: Value(isIosSharedAlbum),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> insertLocalAlbumAsset({required String albumId, required String assetId}) async {
|
||||
await db
|
||||
.into(db.localAlbumAssetEntity)
|
||||
.insert(LocalAlbumAssetEntityCompanion.insert(albumId: albumId, assetId: assetId));
|
||||
}
|
||||
|
||||
test('returns only assets that match all criteria', () async {
|
||||
// Asset 1: Should be included - backed up, before cutoff, correct owner, not deleted, not favorite
|
||||
await insertLocalAsset(
|
||||
id: 'local-1',
|
||||
checksum: 'checksum-1',
|
||||
createdAt: beforeCutoff,
|
||||
type: AssetType.image,
|
||||
isFavorite: false,
|
||||
);
|
||||
await insertRemoteAsset(id: 'remote-1', checksum: 'checksum-1', ownerId: userId);
|
||||
|
||||
// Asset 2: Should NOT be included - not backed up (no remote asset)
|
||||
await insertLocalAsset(
|
||||
id: 'local-2',
|
||||
checksum: 'checksum-2',
|
||||
createdAt: beforeCutoff,
|
||||
type: AssetType.image,
|
||||
isFavorite: false,
|
||||
);
|
||||
|
||||
// Asset 3: Should NOT be included - after cutoff date
|
||||
await insertLocalAsset(
|
||||
id: 'local-3',
|
||||
checksum: 'checksum-3',
|
||||
createdAt: afterCutoff,
|
||||
type: AssetType.image,
|
||||
isFavorite: false,
|
||||
);
|
||||
await insertRemoteAsset(id: 'remote-3', checksum: 'checksum-3', ownerId: userId);
|
||||
|
||||
// Asset 4: Should NOT be included - different owner
|
||||
await insertLocalAsset(
|
||||
id: 'local-4',
|
||||
checksum: 'checksum-4',
|
||||
createdAt: beforeCutoff,
|
||||
type: AssetType.image,
|
||||
isFavorite: false,
|
||||
);
|
||||
await insertRemoteAsset(id: 'remote-4', checksum: 'checksum-4', ownerId: otherUserId);
|
||||
|
||||
// Asset 5: Should NOT be included - remote asset is deleted
|
||||
await insertLocalAsset(
|
||||
id: 'local-5',
|
||||
checksum: 'checksum-5',
|
||||
createdAt: beforeCutoff,
|
||||
type: AssetType.image,
|
||||
isFavorite: false,
|
||||
);
|
||||
await insertRemoteAsset(id: 'remote-5', checksum: 'checksum-5', ownerId: userId, deletedAt: now);
|
||||
|
||||
// Asset 6: Should NOT be included - is favorite (when keepFavorites=true)
|
||||
await insertLocalAsset(
|
||||
id: 'local-6',
|
||||
checksum: 'checksum-6',
|
||||
createdAt: beforeCutoff,
|
||||
type: AssetType.image,
|
||||
isFavorite: true,
|
||||
);
|
||||
await insertRemoteAsset(id: 'remote-6', checksum: 'checksum-6', ownerId: userId);
|
||||
|
||||
final candidates = await repository.getRemovalCandidates(userId, cutoffDate, keepFavorites: true);
|
||||
|
||||
expect(candidates.length, 1);
|
||||
expect(candidates[0].id, 'local-1');
|
||||
});
|
||||
|
||||
test('includes favorites when keepFavorites is false', () async {
|
||||
await insertLocalAsset(
|
||||
id: 'local-favorite',
|
||||
checksum: 'checksum-fav',
|
||||
createdAt: beforeCutoff,
|
||||
type: AssetType.image,
|
||||
isFavorite: true,
|
||||
);
|
||||
await insertRemoteAsset(id: 'remote-favorite', checksum: 'checksum-fav', ownerId: userId);
|
||||
|
||||
final candidates = await repository.getRemovalCandidates(userId, cutoffDate, keepFavorites: false);
|
||||
|
||||
expect(candidates.length, 1);
|
||||
expect(candidates[0].id, 'local-favorite');
|
||||
expect(candidates[0].isFavorite, true);
|
||||
});
|
||||
|
||||
test('filters by photos only', () async {
|
||||
// Photo
|
||||
await insertLocalAsset(
|
||||
id: 'local-photo',
|
||||
checksum: 'checksum-photo',
|
||||
createdAt: beforeCutoff,
|
||||
type: AssetType.image,
|
||||
isFavorite: false,
|
||||
);
|
||||
await insertRemoteAsset(id: 'remote-photo', checksum: 'checksum-photo', ownerId: userId);
|
||||
|
||||
// Video
|
||||
await insertLocalAsset(
|
||||
id: 'local-video',
|
||||
checksum: 'checksum-video',
|
||||
createdAt: beforeCutoff,
|
||||
type: AssetType.video,
|
||||
isFavorite: false,
|
||||
);
|
||||
await insertRemoteAsset(id: 'remote-video', checksum: 'checksum-video', ownerId: userId);
|
||||
|
||||
final candidates = await repository.getRemovalCandidates(
|
||||
userId,
|
||||
cutoffDate,
|
||||
filterType: AssetFilterType.photosOnly,
|
||||
);
|
||||
|
||||
expect(candidates.length, 1);
|
||||
expect(candidates[0].id, 'local-photo');
|
||||
expect(candidates[0].type, AssetType.image);
|
||||
});
|
||||
|
||||
test('filters by videos only', () async {
|
||||
// Photo
|
||||
await insertLocalAsset(
|
||||
id: 'local-photo',
|
||||
checksum: 'checksum-photo',
|
||||
createdAt: beforeCutoff,
|
||||
type: AssetType.image,
|
||||
isFavorite: false,
|
||||
);
|
||||
await insertRemoteAsset(id: 'remote-photo', checksum: 'checksum-photo', ownerId: userId);
|
||||
|
||||
// Video
|
||||
await insertLocalAsset(
|
||||
id: 'local-video',
|
||||
checksum: 'checksum-video',
|
||||
createdAt: beforeCutoff,
|
||||
type: AssetType.video,
|
||||
isFavorite: false,
|
||||
);
|
||||
await insertRemoteAsset(id: 'remote-video', checksum: 'checksum-video', ownerId: userId);
|
||||
|
||||
final candidates = await repository.getRemovalCandidates(
|
||||
userId,
|
||||
cutoffDate,
|
||||
filterType: AssetFilterType.videosOnly,
|
||||
);
|
||||
|
||||
expect(candidates.length, 1);
|
||||
expect(candidates[0].id, 'local-video');
|
||||
expect(candidates[0].type, AssetType.video);
|
||||
});
|
||||
|
||||
test('returns both photos and videos with filterType.all', () async {
|
||||
// Photo
|
||||
await insertLocalAsset(
|
||||
id: 'local-photo',
|
||||
checksum: 'checksum-photo',
|
||||
createdAt: beforeCutoff,
|
||||
type: AssetType.image,
|
||||
isFavorite: false,
|
||||
);
|
||||
await insertRemoteAsset(id: 'remote-photo', checksum: 'checksum-photo', ownerId: userId);
|
||||
|
||||
// Video
|
||||
await insertLocalAsset(
|
||||
id: 'local-video',
|
||||
checksum: 'checksum-video',
|
||||
createdAt: beforeCutoff,
|
||||
type: AssetType.video,
|
||||
isFavorite: false,
|
||||
);
|
||||
await insertRemoteAsset(id: 'remote-video', checksum: 'checksum-video', ownerId: userId);
|
||||
|
||||
final candidates = await repository.getRemovalCandidates(userId, cutoffDate, filterType: AssetFilterType.all);
|
||||
|
||||
expect(candidates.length, 2);
|
||||
final ids = candidates.map((a) => a.id).toSet();
|
||||
expect(ids, containsAll(['local-photo', 'local-video']));
|
||||
});
|
||||
|
||||
test('excludes assets in iOS shared albums', () async {
|
||||
// Regular album
|
||||
await insertLocalAlbum(id: 'album-regular', name: 'Regular Album', isIosSharedAlbum: false);
|
||||
|
||||
// iOS shared album
|
||||
await insertLocalAlbum(id: 'album-shared', name: 'Shared Album', isIosSharedAlbum: true);
|
||||
|
||||
// Asset in regular album (should be included)
|
||||
await insertLocalAsset(
|
||||
id: 'local-regular',
|
||||
checksum: 'checksum-regular',
|
||||
createdAt: beforeCutoff,
|
||||
type: AssetType.image,
|
||||
isFavorite: false,
|
||||
);
|
||||
await insertRemoteAsset(id: 'remote-regular', checksum: 'checksum-regular', ownerId: userId);
|
||||
await insertLocalAlbumAsset(albumId: 'album-regular', assetId: 'local-regular');
|
||||
|
||||
// Asset in iOS shared album (should be excluded)
|
||||
await insertLocalAsset(
|
||||
id: 'local-shared',
|
||||
checksum: 'checksum-shared',
|
||||
createdAt: beforeCutoff,
|
||||
type: AssetType.image,
|
||||
isFavorite: false,
|
||||
);
|
||||
await insertRemoteAsset(id: 'remote-shared', checksum: 'checksum-shared', ownerId: userId);
|
||||
await insertLocalAlbumAsset(albumId: 'album-shared', assetId: 'local-shared');
|
||||
|
||||
final candidates = await repository.getRemovalCandidates(userId, cutoffDate);
|
||||
|
||||
expect(candidates.length, 1);
|
||||
expect(candidates[0].id, 'local-regular');
|
||||
});
|
||||
|
||||
test('includes assets at exact cutoff date', () async {
|
||||
await insertLocalAsset(
|
||||
id: 'local-exact',
|
||||
checksum: 'checksum-exact',
|
||||
createdAt: cutoffDate,
|
||||
type: AssetType.image,
|
||||
isFavorite: false,
|
||||
);
|
||||
await insertRemoteAsset(id: 'remote-exact', checksum: 'checksum-exact', ownerId: userId);
|
||||
|
||||
final candidates = await repository.getRemovalCandidates(userId, cutoffDate);
|
||||
|
||||
expect(candidates.length, 1);
|
||||
expect(candidates[0].id, 'local-exact');
|
||||
});
|
||||
|
||||
test('returns empty list when no assets match criteria', () async {
|
||||
// Only assets after cutoff
|
||||
await insertLocalAsset(
|
||||
id: 'local-after',
|
||||
checksum: 'checksum-after',
|
||||
createdAt: afterCutoff,
|
||||
type: AssetType.image,
|
||||
isFavorite: false,
|
||||
);
|
||||
await insertRemoteAsset(id: 'remote-after', checksum: 'checksum-after', ownerId: userId);
|
||||
|
||||
final candidates = await repository.getRemovalCandidates(userId, cutoffDate);
|
||||
|
||||
expect(candidates, isEmpty);
|
||||
});
|
||||
|
||||
test('handles multiple assets with same checksum', () async {
|
||||
// Two local assets with same checksum (edge case, but should handle it)
|
||||
await insertLocalAsset(
|
||||
id: 'local-dup1',
|
||||
checksum: 'checksum-dup',
|
||||
createdAt: beforeCutoff,
|
||||
type: AssetType.image,
|
||||
isFavorite: false,
|
||||
);
|
||||
await insertLocalAsset(
|
||||
id: 'local-dup2',
|
||||
checksum: 'checksum-dup',
|
||||
createdAt: beforeCutoff,
|
||||
type: AssetType.image,
|
||||
isFavorite: false,
|
||||
);
|
||||
await insertRemoteAsset(id: 'remote-dup', checksum: 'checksum-dup', ownerId: userId);
|
||||
|
||||
final candidates = await repository.getRemovalCandidates(userId, cutoffDate);
|
||||
|
||||
expect(candidates.length, 2);
|
||||
expect(candidates.map((a) => a.checksum).toSet(), equals({'checksum-dup'}));
|
||||
});
|
||||
|
||||
test('includes assets not in any album', () async {
|
||||
// Asset not in any album should be included
|
||||
await insertLocalAsset(
|
||||
id: 'local-no-album',
|
||||
checksum: 'checksum-no-album',
|
||||
createdAt: beforeCutoff,
|
||||
type: AssetType.image,
|
||||
isFavorite: false,
|
||||
);
|
||||
await insertRemoteAsset(id: 'remote-no-album', checksum: 'checksum-no-album', ownerId: userId);
|
||||
|
||||
final candidates = await repository.getRemovalCandidates(userId, cutoffDate);
|
||||
|
||||
expect(candidates.length, 1);
|
||||
expect(candidates[0].id, 'local-no-album');
|
||||
});
|
||||
|
||||
test('excludes asset that is in both regular and iOS shared album', () async {
|
||||
// Regular album
|
||||
await insertLocalAlbum(id: 'album-regular', name: 'Regular Album', isIosSharedAlbum: false);
|
||||
|
||||
// iOS shared album
|
||||
await insertLocalAlbum(id: 'album-shared', name: 'Shared Album', isIosSharedAlbum: true);
|
||||
|
||||
// Asset in BOTH albums - should be excluded because it's in an iOS shared album
|
||||
await insertLocalAsset(
|
||||
id: 'local-both',
|
||||
checksum: 'checksum-both',
|
||||
createdAt: beforeCutoff,
|
||||
type: AssetType.image,
|
||||
isFavorite: false,
|
||||
);
|
||||
await insertRemoteAsset(id: 'remote-both', checksum: 'checksum-both', ownerId: userId);
|
||||
await insertLocalAlbumAsset(albumId: 'album-regular', assetId: 'local-both');
|
||||
await insertLocalAlbumAsset(albumId: 'album-shared', assetId: 'local-both');
|
||||
|
||||
final candidates = await repository.getRemovalCandidates(userId, cutoffDate);
|
||||
|
||||
expect(candidates, isEmpty);
|
||||
});
|
||||
|
||||
test('excludes assets with null checksum (not backed up)', () async {
|
||||
// Asset with null checksum cannot be matched to remote asset
|
||||
await db
|
||||
.into(db.localAssetEntity)
|
||||
.insert(
|
||||
LocalAssetEntityCompanion.insert(
|
||||
id: 'local-null-checksum',
|
||||
name: 'asset_null.jpg',
|
||||
checksum: const Value.absent(), // null checksum
|
||||
type: AssetType.image,
|
||||
createdAt: Value(beforeCutoff),
|
||||
updatedAt: Value(beforeCutoff),
|
||||
isFavorite: const Value(false),
|
||||
),
|
||||
);
|
||||
|
||||
final candidates = await repository.getRemovalCandidates(userId, cutoffDate);
|
||||
|
||||
expect(candidates, isEmpty);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,500 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:immich_mobile/presentation/widgets/remote_album/drift_album_option.widget.dart';
|
||||
|
||||
import '../../../widget_tester_extensions.dart';
|
||||
|
||||
void main() {
|
||||
group('DriftRemoteAlbumOption', () {
|
||||
testWidgets('shows kebab menu icon button', (tester) async {
|
||||
await tester.pumpConsumerWidget(
|
||||
const DriftRemoteAlbumOption(),
|
||||
);
|
||||
|
||||
expect(find.byIcon(Icons.more_vert_rounded), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('opens menu when icon button is tapped', (tester) async {
|
||||
await tester.pumpConsumerWidget(
|
||||
DriftRemoteAlbumOption(
|
||||
onEditAlbum: () {},
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.byIcon(Icons.more_vert_rounded));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byIcon(Icons.edit), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('shows edit album option when onEditAlbum is provided',
|
||||
(tester) async {
|
||||
bool editCalled = false;
|
||||
|
||||
await tester.pumpConsumerWidget(
|
||||
DriftRemoteAlbumOption(
|
||||
onEditAlbum: () => editCalled = true,
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.byIcon(Icons.more_vert_rounded));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byIcon(Icons.edit), findsOneWidget);
|
||||
|
||||
await tester.tap(find.byIcon(Icons.edit));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(editCalled, isTrue);
|
||||
});
|
||||
|
||||
testWidgets('hides edit album option when onEditAlbum is null',
|
||||
(tester) async {
|
||||
await tester.pumpConsumerWidget(
|
||||
DriftRemoteAlbumOption(
|
||||
onAddPhotos: () {},
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.byIcon(Icons.more_vert_rounded));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byIcon(Icons.edit), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('shows add photos option when onAddPhotos is provided',
|
||||
(tester) async {
|
||||
bool addPhotosCalled = false;
|
||||
|
||||
await tester.pumpConsumerWidget(
|
||||
DriftRemoteAlbumOption(
|
||||
onAddPhotos: () => addPhotosCalled = true,
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.byIcon(Icons.more_vert_rounded));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byIcon(Icons.add_a_photo), findsOneWidget);
|
||||
|
||||
await tester.tap(find.byIcon(Icons.add_a_photo));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(addPhotosCalled, isTrue);
|
||||
});
|
||||
|
||||
testWidgets('hides add photos option when onAddPhotos is null',
|
||||
(tester) async {
|
||||
await tester.pumpConsumerWidget(
|
||||
DriftRemoteAlbumOption(
|
||||
onEditAlbum: () {},
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.byIcon(Icons.more_vert_rounded));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byIcon(Icons.add_a_photo), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('shows add users option when onAddUsers is provided',
|
||||
(tester) async {
|
||||
bool addUsersCalled = false;
|
||||
|
||||
await tester.pumpConsumerWidget(
|
||||
DriftRemoteAlbumOption(
|
||||
onAddUsers: () => addUsersCalled = true,
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.byIcon(Icons.more_vert_rounded));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byIcon(Icons.group_add), findsOneWidget);
|
||||
|
||||
await tester.tap(find.byIcon(Icons.group_add));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(addUsersCalled, isTrue);
|
||||
});
|
||||
|
||||
testWidgets('hides add users option when onAddUsers is null',
|
||||
(tester) async {
|
||||
await tester.pumpConsumerWidget(
|
||||
DriftRemoteAlbumOption(
|
||||
onEditAlbum: () {},
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.byIcon(Icons.more_vert_rounded));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byIcon(Icons.group_add), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('shows leave album option when onLeaveAlbum is provided',
|
||||
(tester) async {
|
||||
bool leaveAlbumCalled = false;
|
||||
|
||||
await tester.pumpConsumerWidget(
|
||||
DriftRemoteAlbumOption(
|
||||
onLeaveAlbum: () => leaveAlbumCalled = true,
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.byIcon(Icons.more_vert_rounded));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byIcon(Icons.person_remove_rounded), findsOneWidget);
|
||||
|
||||
await tester.tap(find.byIcon(Icons.person_remove_rounded));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(leaveAlbumCalled, isTrue);
|
||||
});
|
||||
|
||||
testWidgets('hides leave album option when onLeaveAlbum is null',
|
||||
(tester) async {
|
||||
await tester.pumpConsumerWidget(
|
||||
DriftRemoteAlbumOption(
|
||||
onEditAlbum: () {},
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.byIcon(Icons.more_vert_rounded));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byIcon(Icons.person_remove_rounded), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'shows toggle album order option when onToggleAlbumOrder is provided',
|
||||
(tester) async {
|
||||
bool toggleOrderCalled = false;
|
||||
|
||||
await tester.pumpConsumerWidget(
|
||||
DriftRemoteAlbumOption(
|
||||
onToggleAlbumOrder: () => toggleOrderCalled = true,
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.byIcon(Icons.more_vert_rounded));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byIcon(Icons.swap_vert_rounded), findsOneWidget);
|
||||
|
||||
await tester.tap(find.byIcon(Icons.swap_vert_rounded));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(toggleOrderCalled, isTrue);
|
||||
});
|
||||
|
||||
testWidgets('hides toggle album order option when onToggleAlbumOrder is null',
|
||||
(tester) async {
|
||||
await tester.pumpConsumerWidget(
|
||||
DriftRemoteAlbumOption(
|
||||
onEditAlbum: () {},
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.byIcon(Icons.more_vert_rounded));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byIcon(Icons.swap_vert_rounded), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'shows create shared link option when onCreateSharedLink is provided',
|
||||
(tester) async {
|
||||
bool createSharedLinkCalled = false;
|
||||
|
||||
await tester.pumpConsumerWidget(
|
||||
DriftRemoteAlbumOption(
|
||||
onCreateSharedLink: () => createSharedLinkCalled = true,
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.byIcon(Icons.more_vert_rounded));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byIcon(Icons.link), findsOneWidget);
|
||||
|
||||
await tester.tap(find.byIcon(Icons.link));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(createSharedLinkCalled, isTrue);
|
||||
});
|
||||
|
||||
testWidgets('hides create shared link option when onCreateSharedLink is null',
|
||||
(tester) async {
|
||||
await tester.pumpConsumerWidget(
|
||||
DriftRemoteAlbumOption(
|
||||
onEditAlbum: () {},
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.byIcon(Icons.more_vert_rounded));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byIcon(Icons.link), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('shows options option when onShowOptions is provided',
|
||||
(tester) async {
|
||||
bool showOptionsCalled = false;
|
||||
|
||||
await tester.pumpConsumerWidget(
|
||||
DriftRemoteAlbumOption(
|
||||
onShowOptions: () => showOptionsCalled = true,
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.byIcon(Icons.more_vert_rounded));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byIcon(Icons.settings), findsOneWidget);
|
||||
|
||||
await tester.tap(find.byIcon(Icons.settings));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(showOptionsCalled, isTrue);
|
||||
});
|
||||
|
||||
testWidgets('hides options option when onShowOptions is null',
|
||||
(tester) async {
|
||||
await tester.pumpConsumerWidget(
|
||||
DriftRemoteAlbumOption(
|
||||
onEditAlbum: () {},
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.byIcon(Icons.more_vert_rounded));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byIcon(Icons.settings), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('shows delete album option when onDeleteAlbum is provided',
|
||||
(tester) async {
|
||||
bool deleteAlbumCalled = false;
|
||||
|
||||
await tester.pumpConsumerWidget(
|
||||
DriftRemoteAlbumOption(
|
||||
onDeleteAlbum: () => deleteAlbumCalled = true,
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.byIcon(Icons.more_vert_rounded));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byIcon(Icons.delete), findsOneWidget);
|
||||
|
||||
await tester.tap(find.byIcon(Icons.delete));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(deleteAlbumCalled, isTrue);
|
||||
});
|
||||
|
||||
testWidgets('hides delete album option when onDeleteAlbum is null',
|
||||
(tester) async {
|
||||
await tester.pumpConsumerWidget(
|
||||
DriftRemoteAlbumOption(
|
||||
onEditAlbum: () {},
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.byIcon(Icons.more_vert_rounded));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byIcon(Icons.delete), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('shows divider before delete album option', (tester) async {
|
||||
await tester.pumpConsumerWidget(
|
||||
DriftRemoteAlbumOption(
|
||||
onEditAlbum: () {},
|
||||
onDeleteAlbum: () {},
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.byIcon(Icons.more_vert_rounded));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(Divider), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('shows all options when all callbacks are provided',
|
||||
(tester) async {
|
||||
await tester.pumpConsumerWidget(
|
||||
DriftRemoteAlbumOption(
|
||||
onEditAlbum: () {},
|
||||
onAddPhotos: () {},
|
||||
onAddUsers: () {},
|
||||
onLeaveAlbum: () {},
|
||||
onToggleAlbumOrder: () {},
|
||||
onCreateSharedLink: () {},
|
||||
onShowOptions: () {},
|
||||
onDeleteAlbum: () {},
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.byIcon(Icons.more_vert_rounded));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byIcon(Icons.edit), findsOneWidget);
|
||||
expect(find.byIcon(Icons.add_a_photo), findsOneWidget);
|
||||
expect(find.byIcon(Icons.group_add), findsOneWidget);
|
||||
expect(find.byIcon(Icons.person_remove_rounded), findsOneWidget);
|
||||
expect(find.byIcon(Icons.swap_vert_rounded), findsOneWidget);
|
||||
expect(find.byIcon(Icons.link), findsOneWidget);
|
||||
expect(find.byIcon(Icons.settings), findsOneWidget);
|
||||
expect(find.byIcon(Icons.delete), findsOneWidget);
|
||||
expect(find.byType(Divider), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('shows no options when all callbacks are null', (tester) async {
|
||||
await tester.pumpConsumerWidget(
|
||||
const DriftRemoteAlbumOption(),
|
||||
);
|
||||
|
||||
await tester.tap(find.byIcon(Icons.more_vert_rounded));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byIcon(Icons.edit), findsNothing);
|
||||
expect(find.byIcon(Icons.add_a_photo), findsNothing);
|
||||
expect(find.byIcon(Icons.group_add), findsNothing);
|
||||
expect(find.byIcon(Icons.person_remove_rounded), findsNothing);
|
||||
expect(find.byIcon(Icons.swap_vert_rounded), findsNothing);
|
||||
expect(find.byIcon(Icons.link), findsNothing);
|
||||
expect(find.byIcon(Icons.settings), findsNothing);
|
||||
expect(find.byIcon(Icons.delete), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('uses custom icon color when provided', (tester) async {
|
||||
const customColor = Colors.red;
|
||||
|
||||
await tester.pumpConsumerWidget(
|
||||
const DriftRemoteAlbumOption(
|
||||
iconColor: customColor,
|
||||
),
|
||||
);
|
||||
|
||||
final iconButton = tester.widget<IconButton>(find.byType(IconButton));
|
||||
final icon = iconButton.icon as Icon;
|
||||
|
||||
expect(icon.color, equals(customColor));
|
||||
});
|
||||
|
||||
testWidgets('uses default white color when iconColor is null',
|
||||
(tester) async {
|
||||
await tester.pumpConsumerWidget(
|
||||
const DriftRemoteAlbumOption(),
|
||||
);
|
||||
|
||||
final iconButton = tester.widget<IconButton>(find.byType(IconButton));
|
||||
final icon = iconButton.icon as Icon;
|
||||
|
||||
expect(icon.color, equals(Colors.white));
|
||||
});
|
||||
|
||||
testWidgets('applies icon shadows when provided', (tester) async {
|
||||
final shadows = [
|
||||
const Shadow(offset: Offset(0, 2), blurRadius: 5, color: Colors.black),
|
||||
];
|
||||
|
||||
await tester.pumpConsumerWidget(
|
||||
DriftRemoteAlbumOption(
|
||||
iconShadows: shadows,
|
||||
),
|
||||
);
|
||||
|
||||
final iconButton = tester.widget<IconButton>(find.byType(IconButton));
|
||||
final icon = iconButton.icon as Icon;
|
||||
|
||||
expect(icon.shadows, equals(shadows));
|
||||
});
|
||||
|
||||
group('owner vs non-owner scenarios', () {
|
||||
testWidgets('owner sees all management options', (tester) async {
|
||||
// Simulating owner scenario - all callbacks provided
|
||||
await tester.pumpConsumerWidget(
|
||||
DriftRemoteAlbumOption(
|
||||
onEditAlbum: () {},
|
||||
onAddPhotos: () {},
|
||||
onAddUsers: () {},
|
||||
onToggleAlbumOrder: () {},
|
||||
onCreateSharedLink: () {},
|
||||
onShowOptions: () {},
|
||||
onDeleteAlbum: () {},
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.byIcon(Icons.more_vert_rounded));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Owner should see all management options
|
||||
expect(find.byIcon(Icons.edit), findsOneWidget);
|
||||
expect(find.byIcon(Icons.add_a_photo), findsOneWidget);
|
||||
expect(find.byIcon(Icons.group_add), findsOneWidget);
|
||||
expect(find.byIcon(Icons.swap_vert_rounded), findsOneWidget);
|
||||
expect(find.byIcon(Icons.link), findsOneWidget);
|
||||
expect(find.byIcon(Icons.delete), findsOneWidget);
|
||||
// Owner should NOT see leave album
|
||||
expect(find.byIcon(Icons.person_remove_rounded), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('non-owner with editor role sees limited options',
|
||||
(tester) async {
|
||||
// Simulating non-owner with editor role - can add photos, show options, leave
|
||||
await tester.pumpConsumerWidget(
|
||||
DriftRemoteAlbumOption(
|
||||
onAddPhotos: () {},
|
||||
onShowOptions: () {},
|
||||
onLeaveAlbum: () {},
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.byIcon(Icons.more_vert_rounded));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Editor can add photos
|
||||
expect(find.byIcon(Icons.add_a_photo), findsOneWidget);
|
||||
// Can see options
|
||||
expect(find.byIcon(Icons.settings), findsOneWidget);
|
||||
// Can leave album
|
||||
expect(find.byIcon(Icons.person_remove_rounded), findsOneWidget);
|
||||
// Cannot see owner-only options
|
||||
expect(find.byIcon(Icons.edit), findsNothing);
|
||||
expect(find.byIcon(Icons.group_add), findsNothing);
|
||||
expect(find.byIcon(Icons.swap_vert_rounded), findsNothing);
|
||||
expect(find.byIcon(Icons.link), findsNothing);
|
||||
expect(find.byIcon(Icons.delete), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('non-owner viewer sees minimal options', (tester) async {
|
||||
// Simulating viewer - can only show options and leave
|
||||
await tester.pumpConsumerWidget(
|
||||
DriftRemoteAlbumOption(
|
||||
onShowOptions: () {},
|
||||
onLeaveAlbum: () {},
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.byIcon(Icons.more_vert_rounded));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Can see options
|
||||
expect(find.byIcon(Icons.settings), findsOneWidget);
|
||||
// Can leave album
|
||||
expect(find.byIcon(Icons.person_remove_rounded), findsOneWidget);
|
||||
// Cannot see any other options
|
||||
expect(find.byIcon(Icons.edit), findsNothing);
|
||||
expect(find.byIcon(Icons.add_a_photo), findsNothing);
|
||||
expect(find.byIcon(Icons.group_add), findsNothing);
|
||||
expect(find.byIcon(Icons.swap_vert_rounded), findsNothing);
|
||||
expect(find.byIcon(Icons.link), findsNothing);
|
||||
expect(find.byIcon(Icons.delete), findsNothing);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user