chore: bump dart sdk to 3.8 (#20355)

* chore: bump dart sdk to 3.8

* chore: make build

* make pigeon

* chore: format files

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
This commit is contained in:
shenlong
2025-07-29 00:34:03 +05:30
committed by GitHub
parent 9b3718120b
commit e52b9d15b5
643 changed files with 32561 additions and 35292 deletions
@@ -21,10 +21,7 @@ void main() {
late MockLocalAssetRepository mockAssetRepo;
late MockStorageRepository mockStorageRepo;
late MockNativeSyncApi mockNativeApi;
final sortBy = {
SortLocalAlbumsBy.backupSelection,
SortLocalAlbumsBy.isIosSharedAlbum,
};
final sortBy = {SortLocalAlbumsBy.backupSelection, SortLocalAlbumsBy.isIosSharedAlbum};
setUp(() {
mockAlbumRepo = MockLocalAlbumRepository();
@@ -47,9 +44,9 @@ void main() {
group('HashService hashAssets', () {
test('skips albums with no assets to hash', () async {
when(() => mockAlbumRepo.getAll(sortBy: sortBy)).thenAnswer(
(_) async => [LocalAlbumStub.recent.copyWith(assetCount: 0)],
);
when(
() => mockAlbumRepo.getAll(sortBy: sortBy),
).thenAnswer((_) async => [LocalAlbumStub.recent.copyWith(assetCount: 0)]);
when(() => mockAlbumRepo.getAssetsToHash(LocalAlbumStub.recent.id)).thenAnswer((_) async => []);
await sut.hashAssets();
@@ -84,9 +81,7 @@ void main() {
when(() => mockAlbumRepo.getAll(sortBy: sortBy)).thenAnswer((_) async => [album]);
when(() => mockAlbumRepo.getAssetsToHash(album.id)).thenAnswer((_) async => [asset]);
when(() => mockStorageRepo.getFileForAsset(asset.id)).thenAnswer((_) async => mockFile);
when(() => mockNativeApi.hashPaths(['image-path'])).thenAnswer(
(_) async => [hash],
);
when(() => mockNativeApi.hashPaths(['image-path'])).thenAnswer((_) async => [hash]);
await sut.hashAssets();
@@ -43,10 +43,7 @@ void main() {
when(() => mockLogRepo.insert(any())).thenAnswer((_) async => true);
when(() => mockLogRepo.insertAll(any())).thenAnswer((_) async => true);
sut = await LogService.create(
logRepository: mockLogRepo,
storeRepository: mockStoreRepo,
);
sut = await LogService.create(logRepository: mockLogRepo, storeRepository: mockStoreRepo);
});
tearDown(() async {
@@ -72,9 +69,7 @@ void main() {
});
test('Updates the log level in store', () {
final index = verify(
() => mockStoreRepo.insert<int>(StoreKey.logLevel, captureAny()),
).captured.firstOrNull;
final index = verify(() => mockStoreRepo.insert<int>(StoreKey.logLevel, captureAny())).captured.firstOrNull;
expect(index, LogLevel.shout.index);
});
@@ -86,11 +81,7 @@ void main() {
group("Log Service Buffer:", () {
test('Buffers logs until timer elapses', () {
TestUtils.fakeAsync((time) async {
sut = await LogService.create(
logRepository: mockLogRepo,
storeRepository: mockStoreRepo,
shouldBuffer: true,
);
sut = await LogService.create(logRepository: mockLogRepo, storeRepository: mockStoreRepo, shouldBuffer: true);
final logger = Logger(_kInfoLog.logger!);
logger.info(_kInfoLog.message);
@@ -104,11 +95,7 @@ void main() {
test('Batch inserts all logs on timer', () {
TestUtils.fakeAsync((time) async {
sut = await LogService.create(
logRepository: mockLogRepo,
storeRepository: mockStoreRepo,
shouldBuffer: true,
);
sut = await LogService.create(logRepository: mockLogRepo, storeRepository: mockStoreRepo, shouldBuffer: true);
final logger = Logger(_kInfoLog.logger!);
logger.info(_kInfoLog.message);
@@ -125,11 +112,7 @@ void main() {
test('Does not buffer when off', () {
TestUtils.fakeAsync((time) async {
sut = await LogService.create(
logRepository: mockLogRepo,
storeRepository: mockStoreRepo,
shouldBuffer: false,
);
sut = await LogService.create(logRepository: mockLogRepo, storeRepository: mockStoreRepo, shouldBuffer: false);
final logger = Logger(_kInfoLog.logger!);
logger.info(_kInfoLog.message);
@@ -159,11 +142,7 @@ void main() {
test('Combines result from both DB + Buffer', () {
TestUtils.fakeAsync((time) async {
sut = await LogService.create(
logRepository: mockLogRepo,
storeRepository: mockStoreRepo,
shouldBuffer: true,
);
sut = await LogService.create(logRepository: mockLogRepo, storeRepository: mockStoreRepo, shouldBuffer: true);
final logger = Logger(_kWarnLog.logger!);
logger.warning(_kWarnLog.message);
@@ -73,10 +73,7 @@ void main() {
});
test('Throws StoreKeyNotFoundException for nonexistent keys', () {
expect(
() => sut.get(StoreKey.currentUser),
throwsA(isA<StoreKeyNotFoundException>()),
);
expect(() => sut.get(StoreKey.currentUser), throwsA(isA<StoreKeyNotFoundException>()));
});
test('Returns the stored value for the given key or the defaultValue', () {
@@ -91,17 +88,13 @@ void main() {
test('Skip insert when value is not modified', () async {
await sut.put(StoreKey.accessToken, _kAccessToken);
verifyNever(
() => mockStoreRepo.insert<String>(StoreKey.accessToken, any()),
);
verifyNever(() => mockStoreRepo.insert<String>(StoreKey.accessToken, any()));
});
test('Insert value when modified', () async {
final newAccessToken = _kAccessToken.toUpperCase();
await sut.put(StoreKey.accessToken, newAccessToken);
verify(
() => mockStoreRepo.insert<String>(StoreKey.accessToken, newAccessToken),
).called(1);
verify(() => mockStoreRepo.insert<String>(StoreKey.accessToken, newAccessToken)).called(1);
expect(sut.tryGet(StoreKey.accessToken), newAccessToken);
});
});
@@ -120,12 +113,7 @@ void main() {
test('Watches a specific key for changes', () async {
final stream = sut.watch(StoreKey.accessToken);
final events = <String?>[
_kAccessToken,
_kAccessToken.toUpperCase(),
null,
_kAccessToken.toLowerCase(),
];
final events = <String?>[_kAccessToken, _kAccessToken.toUpperCase(), null, _kAccessToken.toLowerCase()];
expectLater(stream, emitsInOrder(events));
@@ -54,40 +54,25 @@ void main() {
when(() => mockSyncStreamRepo.deletePartnerV1(any())).thenAnswer(successHandler);
when(() => mockSyncStreamRepo.updateAssetsV1(any())).thenAnswer(successHandler);
when(
() => mockSyncStreamRepo.updateAssetsV1(
any(),
debugLabel: any(named: 'debugLabel'),
),
() => mockSyncStreamRepo.updateAssetsV1(any(), debugLabel: any(named: 'debugLabel')),
).thenAnswer(successHandler);
when(() => mockSyncStreamRepo.deleteAssetsV1(any())).thenAnswer(successHandler);
when(
() => mockSyncStreamRepo.deleteAssetsV1(
any(),
debugLabel: any(named: 'debugLabel'),
),
() => mockSyncStreamRepo.deleteAssetsV1(any(), debugLabel: any(named: 'debugLabel')),
).thenAnswer(successHandler);
when(() => mockSyncStreamRepo.updateAssetsExifV1(any())).thenAnswer(successHandler);
when(
() => mockSyncStreamRepo.updateAssetsExifV1(
any(),
debugLabel: any(named: 'debugLabel'),
),
() => mockSyncStreamRepo.updateAssetsExifV1(any(), debugLabel: any(named: 'debugLabel')),
).thenAnswer(successHandler);
when(() => mockSyncStreamRepo.updateMemoriesV1(any())).thenAnswer(successHandler);
when(() => mockSyncStreamRepo.deleteMemoriesV1(any())).thenAnswer(successHandler);
when(() => mockSyncStreamRepo.updateMemoryAssetsV1(any())).thenAnswer(successHandler);
when(() => mockSyncStreamRepo.deleteMemoryAssetsV1(any())).thenAnswer(successHandler);
when(
() => mockSyncStreamRepo.updateStacksV1(
any(),
debugLabel: any(named: 'debugLabel'),
),
() => mockSyncStreamRepo.updateStacksV1(any(), debugLabel: any(named: 'debugLabel')),
).thenAnswer(successHandler);
when(
() => mockSyncStreamRepo.deleteStacksV1(
any(),
debugLabel: any(named: 'debugLabel'),
),
() => mockSyncStreamRepo.deleteStacksV1(any(), debugLabel: any(named: 'debugLabel')),
).thenAnswer(successHandler);
when(() => mockSyncStreamRepo.updateUserMetadatasV1(any())).thenAnswer(successHandler);
when(() => mockSyncStreamRepo.deleteUserMetadatasV1(any())).thenAnswer(successHandler);
@@ -96,10 +81,7 @@ void main() {
when(() => mockSyncStreamRepo.updateAssetFacesV1(any())).thenAnswer(successHandler);
when(() => mockSyncStreamRepo.deleteAssetFacesV1(any())).thenAnswer(successHandler);
sut = SyncStreamService(
syncApiRepository: mockSyncApiRepo,
syncStreamRepository: mockSyncStreamRepo,
);
sut = SyncStreamService(syncApiRepository: mockSyncApiRepo, syncStreamRepository: mockSyncStreamRepo);
});
Future<void> simulateEvents(List<SyncEvent> events) async {
@@ -108,41 +90,35 @@ void main() {
}
group("SyncStreamService - _handleEvents", () {
test(
"processes events and acks successfully when handlers succeed",
() async {
final events = [
SyncStreamStub.userDeleteV1,
SyncStreamStub.userV1Admin,
SyncStreamStub.userV1User,
SyncStreamStub.partnerDeleteV1,
SyncStreamStub.partnerV1,
];
await simulateEvents(events);
verifyInOrder([
() => mockSyncStreamRepo.deleteUsersV1(any()),
() => mockSyncApiRepo.ack(["2"]),
() => mockSyncStreamRepo.updateUsersV1(any()),
() => mockSyncApiRepo.ack(["5"]),
() => mockSyncStreamRepo.deletePartnerV1(any()),
() => mockSyncApiRepo.ack(["4"]),
() => mockSyncStreamRepo.updatePartnerV1(any()),
() => mockSyncApiRepo.ack(["3"]),
]);
verifyNever(() => mockAbortCallbackWrapper());
},
);
test("processes final batch correctly", () async {
test("processes events and acks successfully when handlers succeed", () async {
final events = [
SyncStreamStub.userDeleteV1,
SyncStreamStub.userV1Admin,
SyncStreamStub.userV1User,
SyncStreamStub.partnerDeleteV1,
SyncStreamStub.partnerV1,
];
await simulateEvents(events);
verifyInOrder([
() => mockSyncStreamRepo.deleteUsersV1(any()),
() => mockSyncApiRepo.ack(["2"]),
() => mockSyncStreamRepo.updateUsersV1(any()),
() => mockSyncApiRepo.ack(["5"]),
() => mockSyncStreamRepo.deletePartnerV1(any()),
() => mockSyncApiRepo.ack(["4"]),
() => mockSyncStreamRepo.updatePartnerV1(any()),
() => mockSyncApiRepo.ack(["3"]),
]);
verifyNever(() => mockAbortCallbackWrapper());
});
test("processes final batch correctly", () async {
final events = [SyncStreamStub.userDeleteV1, SyncStreamStub.userV1Admin];
await simulateEvents(events);
verifyInOrder([
() => mockSyncStreamRepo.deleteUsersV1(any()),
() => mockSyncApiRepo.ack(["2"]),
@@ -174,11 +150,7 @@ void main() {
);
await sut.sync();
final events = [
SyncStreamStub.userDeleteV1,
SyncStreamStub.userV1Admin,
SyncStreamStub.partnerDeleteV1,
];
final events = [SyncStreamStub.userDeleteV1, SyncStreamStub.userV1Admin, SyncStreamStub.partnerDeleteV1];
when(() => mockSyncStreamRepo.deleteUsersV1(any())).thenAnswer((_) async {
when(() => cancellationChecker()).thenReturn(true);
@@ -195,50 +167,43 @@ void main() {
verify(() => mockSyncApiRepo.ack(["2"])).called(1);
});
test(
"aborts and stops processing if cancelled before processing batch",
() async {
final cancellationChecker = _MockCancellationWrapper();
when(() => cancellationChecker()).thenReturn(false);
test("aborts and stops processing if cancelled before processing batch", () async {
final cancellationChecker = _MockCancellationWrapper();
when(() => cancellationChecker()).thenReturn(false);
final processingCompleter = Completer<void>();
bool handler1Started = false;
when(() => mockSyncStreamRepo.deleteUsersV1(any())).thenAnswer((_) async {
handler1Started = true;
return processingCompleter.future;
});
final processingCompleter = Completer<void>();
bool handler1Started = false;
when(() => mockSyncStreamRepo.deleteUsersV1(any())).thenAnswer((_) async {
handler1Started = true;
return processingCompleter.future;
});
sut = SyncStreamService(
syncApiRepository: mockSyncApiRepo,
syncStreamRepository: mockSyncStreamRepo,
cancelChecker: cancellationChecker.call,
);
sut = SyncStreamService(
syncApiRepository: mockSyncApiRepo,
syncStreamRepository: mockSyncStreamRepo,
cancelChecker: cancellationChecker.call,
);
await sut.sync();
await sut.sync();
final events = [
SyncStreamStub.userDeleteV1,
SyncStreamStub.userV1Admin,
SyncStreamStub.partnerDeleteV1,
];
final events = [SyncStreamStub.userDeleteV1, SyncStreamStub.userV1Admin, SyncStreamStub.partnerDeleteV1];
final processingFuture = handleEventsCallback(events, mockAbortCallbackWrapper.call);
await pumpEventQueue();
final processingFuture = handleEventsCallback(events, mockAbortCallbackWrapper.call);
await pumpEventQueue();
expect(handler1Started, isTrue);
expect(handler1Started, isTrue);
// Signal cancellation while handler 1 is waiting
when(() => cancellationChecker()).thenReturn(true);
await pumpEventQueue();
// Signal cancellation while handler 1 is waiting
when(() => cancellationChecker()).thenReturn(true);
await pumpEventQueue();
processingCompleter.complete();
await processingFuture;
processingCompleter.complete();
await processingFuture;
verifyNever(() => mockSyncStreamRepo.updateUsersV1(any()));
verifyNever(() => mockSyncStreamRepo.updateUsersV1(any()));
verify(() => mockSyncApiRepo.ack(["2"])).called(1);
},
);
verify(() => mockSyncApiRepo.ack(["2"])).called(1);
});
test("processes memory sync events successfully", () async {
final events = [
@@ -289,15 +254,9 @@ void main() {
test("handles memory sync failure gracefully", () async {
when(() => mockSyncStreamRepo.updateMemoriesV1(any())).thenThrow(Exception("Memory sync failed"));
final events = [
SyncStreamStub.memoryV1,
SyncStreamStub.userV1Admin,
];
final events = [SyncStreamStub.memoryV1, SyncStreamStub.userV1Admin];
expect(
() async => await simulateEvents(events),
throwsA(isA<Exception>()),
);
expect(() async => await simulateEvents(events), throwsA(isA<Exception>()));
});
test("processes memory asset events with correct data types", () async {
@@ -89,9 +89,7 @@ void main() {
when(() => mockUserApiRepo.getMyUser()).thenAnswer((_) async => null);
final result = await sut.refreshMyUser();
verifyNever(
() => mockStoreService.put(StoreKey.currentUser, UserStub.admin),
);
verifyNever(() => mockStoreService.put(StoreKey.currentUser, UserStub.admin));
verifyNever(() => mockUserRepo.update(UserStub.admin));
expect(result, isNull);
});
@@ -103,10 +101,7 @@ void main() {
final updatedUser = UserStub.admin.copyWith(profileImagePath: profileImagePath);
when(
() => mockUserApiRepo.createProfileImage(
name: profileImagePath,
data: Uint8List(0),
),
() => mockUserApiRepo.createProfileImage(name: profileImagePath, data: Uint8List(0)),
).thenAnswer((_) async => profileImagePath);
when(() => mockStoreService.put(StoreKey.currentUser, updatedUser)).thenAnswer((_) async => true);
when(() => mockUserRepo.update(updatedUser)).thenAnswer((_) async => UserStub.admin);
@@ -123,16 +118,11 @@ void main() {
final updatedUser = UserStub.admin.copyWith(profileImagePath: profileImagePath);
when(
() => mockUserApiRepo.createProfileImage(
name: profileImagePath,
data: Uint8List(0),
),
() => mockUserApiRepo.createProfileImage(name: profileImagePath, data: Uint8List(0)),
).thenThrow(Exception('Failed to create profile image'));
final result = await sut.createProfileImage(profileImagePath, Uint8List(0));
verifyNever(
() => mockStoreService.put(StoreKey.currentUser, updatedUser),
);
verifyNever(() => mockStoreService.put(StoreKey.currentUser, updatedUser));
verifyNever(() => mockUserRepo.update(updatedUser));
expect(result, isNull);
});
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+15 -14
View File
@@ -42,20 +42,21 @@ final class AlbumStub {
endDate: DateTime(2023),
)..assets.addAll([AssetStub.image1]);
static final twoAsset = Album(
name: "album-with-two-assets",
localId: "album-with-two-assets-local",
remoteId: "album-with-two-assets-remote",
createdAt: DateTime(2001),
modifiedAt: DateTime(2010),
shared: false,
activityEnabled: false,
startDate: DateTime(2019),
endDate: DateTime(2020),
)
..assets.addAll([AssetStub.image1, AssetStub.image2])
..activityEnabled = true
..owner.value = User.fromDto(UserStub.admin);
static final twoAsset =
Album(
name: "album-with-two-assets",
localId: "album-with-two-assets-local",
remoteId: "album-with-two-assets-remote",
createdAt: DateTime(2001),
modifiedAt: DateTime(2010),
shared: false,
activityEnabled: false,
startDate: DateTime(2019),
endDate: DateTime(2020),
)
..assets.addAll([AssetStub.image1, AssetStub.image2])
..activityEnabled = true
..owner.value = User.fromDto(UserStub.admin);
static final create2020end2020Album = Album(
name: "create2020update2020Album",
+5 -27
View File
@@ -4,24 +4,12 @@ import 'package:openapi/api.dart';
abstract final class SyncStreamStub {
static final userV1Admin = SyncEvent(
type: SyncEntityType.userV1,
data: SyncUserV1(
deletedAt: DateTime(2020),
email: "admin@admin",
id: "1",
name: "Admin",
avatarColor: null,
),
data: SyncUserV1(deletedAt: DateTime(2020), email: "admin@admin", id: "1", name: "Admin", avatarColor: null),
ack: "1",
);
static final userV1User = SyncEvent(
type: SyncEntityType.userV1,
data: SyncUserV1(
deletedAt: DateTime(2021),
email: "user@user",
id: "5",
name: "User",
avatarColor: null,
),
data: SyncUserV1(deletedAt: DateTime(2021), email: "user@user", id: "5", name: "User", avatarColor: null),
ack: "5",
);
static final userDeleteV1 = SyncEvent(
@@ -32,11 +20,7 @@ abstract final class SyncStreamStub {
static final partnerV1 = SyncEvent(
type: SyncEntityType.partnerV1,
data: SyncPartnerV1(
inTimeline: true,
sharedById: "1",
sharedWithId: "2",
),
data: SyncPartnerV1(inTimeline: true, sharedById: "1", sharedWithId: "2"),
ack: "3",
);
static final partnerDeleteV1 = SyncEvent(
@@ -72,19 +56,13 @@ abstract final class SyncStreamStub {
static final memoryToAssetV1 = SyncEvent(
type: SyncEntityType.memoryToAssetV1,
data: SyncMemoryAssetV1(
assetId: "asset-1",
memoryId: "memory-1",
),
data: SyncMemoryAssetV1(assetId: "asset-1", memoryId: "memory-1"),
ack: "7",
);
static final memoryToAssetDeleteV1 = SyncEvent(
type: SyncEntityType.memoryToAssetDeleteV1,
data: SyncMemoryAssetDeleteV1(
assetId: "asset-2",
memoryId: "memory-1",
),
data: SyncMemoryAssetDeleteV1(assetId: "asset-2", memoryId: "memory-1"),
ack: "8",
);
}
@@ -12,48 +12,21 @@ void main() {
late MediumFactory mediumFactory;
setUp(() {
db = Drift(
DatabaseConnection(
NativeDatabase.memory(),
closeStreamsSynchronously: true,
),
);
db = Drift(DatabaseConnection(NativeDatabase.memory(), closeStreamsSynchronously: true));
mediumFactory = MediumFactory(db);
});
group('getAll', () {
test('sorts albums by backupSelection & isIosSharedAlbum', () async {
final localAlbumRepo = mediumFactory.getRepository<DriftLocalAlbumRepository>();
await localAlbumRepo.upsert(mediumFactory.localAlbum(id: '1', backupSelection: BackupSelection.none));
await localAlbumRepo.upsert(mediumFactory.localAlbum(id: '2', backupSelection: BackupSelection.excluded));
await localAlbumRepo.upsert(
mediumFactory.localAlbum(
id: '1',
backupSelection: BackupSelection.none,
),
);
await localAlbumRepo.upsert(
mediumFactory.localAlbum(
id: '2',
backupSelection: BackupSelection.excluded,
),
);
await localAlbumRepo.upsert(
mediumFactory.localAlbum(
id: '3',
backupSelection: BackupSelection.selected,
isIosSharedAlbum: true,
),
);
await localAlbumRepo.upsert(
mediumFactory.localAlbum(
id: '4',
backupSelection: BackupSelection.selected,
),
mediumFactory.localAlbum(id: '3', backupSelection: BackupSelection.selected, isIosSharedAlbum: true),
);
await localAlbumRepo.upsert(mediumFactory.localAlbum(id: '4', backupSelection: BackupSelection.selected));
final albums = await localAlbumRepo.getAll(
sortBy: {
SortLocalAlbumsBy.backupSelection,
SortLocalAlbumsBy.isIosSharedAlbum,
},
sortBy: {SortLocalAlbumsBy.backupSelection, SortLocalAlbumsBy.isIosSharedAlbum},
);
expect(albums.length, 4);
expect(albums[0].id, '4'); // selected
@@ -24,16 +24,8 @@ Future<void> _addStrStoreValue(Isar db, StoreKey key, String? value) async {
Future<void> _populateStore(Isar db) async {
await db.writeTxn(() async {
await _addIntStoreValue(
db,
StoreKey.colorfulInterface,
_kTestColorfulInterface ? 1 : 0,
);
await _addIntStoreValue(
db,
StoreKey.backupFailedSince,
_kTestBackupFailed.millisecondsSinceEpoch,
);
await _addIntStoreValue(db, StoreKey.colorfulInterface, _kTestColorfulInterface ? 1 : 0);
await _addIntStoreValue(db, StoreKey.backupFailedSince, _kTestBackupFailed.millisecondsSinceEpoch);
await _addStrStoreValue(db, StoreKey.accessToken, _kTestAccessToken);
await _addIntStoreValue(db, StoreKey.version, _kTestVersion);
});
@@ -143,21 +135,10 @@ void main() {
stream,
emitsInAnyOrder([
emits(const StoreDto<Object>(StoreKey.version, _kTestVersion)),
emits(
StoreDto<Object>(StoreKey.backupFailedSince, _kTestBackupFailed),
),
emits(
const StoreDto<Object>(StoreKey.accessToken, _kTestAccessToken),
),
emits(
const StoreDto<Object>(
StoreKey.colorfulInterface,
_kTestColorfulInterface,
),
),
emits(
const StoreDto<Object>(StoreKey.version, _kTestVersion + 10),
),
emits(StoreDto<Object>(StoreKey.backupFailedSince, _kTestBackupFailed)),
emits(const StoreDto<Object>(StoreKey.accessToken, _kTestAccessToken)),
emits(const StoreDto<Object>(StoreKey.colorfulInterface, _kTestColorfulInterface)),
emits(const StoreDto<Object>(StoreKey.version, _kTestVersion + 10)),
]),
);
await sut.update(StoreKey.version, _kTestVersion + 10);
@@ -63,14 +63,8 @@ void main() {
}
});
Future<void> streamChanges(
Function(List<SyncEvent>, Function() abort) onDataCallback,
) {
return sut.streamChanges(
onDataCallback,
batchSize: testBatchSize,
httpClient: mockHttpClient,
);
Future<void> streamChanges(Function(List<SyncEvent>, Function() abort) onDataCallback) {
return sut.streamChanges(onDataCallback, batchSize: testBatchSize, httpClient: mockHttpClient);
}
test('streamChanges stops processing stream when abort is called', () async {
@@ -96,11 +90,7 @@ void main() {
for (int i = 0; i < testBatchSize; i++) {
responseStreamController.add(
utf8.encode(
_createJsonLine(
SyncEntityType.userDeleteV1.toString(),
SyncUserDeleteV1(userId: "user$i").toJson(),
'ack$i',
),
_createJsonLine(SyncEntityType.userDeleteV1.toString(), SyncUserDeleteV1(userId: "user$i").toJson(), 'ack$i'),
),
);
}
@@ -108,11 +98,7 @@ void main() {
for (int i = testBatchSize; i < testBatchSize * 2; i++) {
responseStreamController.add(
utf8.encode(
_createJsonLine(
SyncEntityType.userDeleteV1.toString(),
SyncUserDeleteV1(userId: "user$i").toJson(),
'ack$i',
),
_createJsonLine(SyncEntityType.userDeleteV1.toString(), SyncUserDeleteV1(userId: "user$i").toJson(), 'ack$i'),
),
);
}
@@ -126,113 +112,91 @@ void main() {
verify(() => mockHttpClient.close()).called(1);
});
test(
'streamChanges does not process remaining lines in finally block if aborted',
() async {
int onDataCallCount = 0;
bool abortWasCalledInCallback = false;
test('streamChanges does not process remaining lines in finally block if aborted', () async {
int onDataCallCount = 0;
bool abortWasCalledInCallback = false;
onDataCallback(List<SyncEvent> events, Function() abort) {
onDataCallCount++;
if (onDataCallCount == 1) {
abort();
abortWasCalledInCallback = true;
} else {
fail("onData called more than once after abort was invoked");
}
onDataCallback(List<SyncEvent> events, Function() abort) {
onDataCallCount++;
if (onDataCallCount == 1) {
abort();
abortWasCalledInCallback = true;
} else {
fail("onData called more than once after abort was invoked");
}
}
final streamChangesFuture = streamChanges(onDataCallback);
final streamChangesFuture = streamChanges(onDataCallback);
await pumpEventQueue();
await pumpEventQueue();
for (int i = 0; i < testBatchSize; i++) {
responseStreamController.add(
utf8.encode(
_createJsonLine(
SyncEntityType.userDeleteV1.toString(),
SyncUserDeleteV1(userId: "user$i").toJson(),
'ack$i',
),
),
);
}
// emit a single event to skip batching and trigger finally
for (int i = 0; i < testBatchSize; i++) {
responseStreamController.add(
utf8.encode(
_createJsonLine(
SyncEntityType.userDeleteV1.toString(),
SyncUserDeleteV1(userId: "user100").toJson(),
'ack100',
),
_createJsonLine(SyncEntityType.userDeleteV1.toString(), SyncUserDeleteV1(userId: "user$i").toJson(), 'ack$i'),
),
);
}
await responseStreamController.close();
await expectLater(streamChangesFuture, completes);
// emit a single event to skip batching and trigger finally
responseStreamController.add(
utf8.encode(
_createJsonLine(SyncEntityType.userDeleteV1.toString(), SyncUserDeleteV1(userId: "user100").toJson(), 'ack100'),
),
);
expect(onDataCallCount, 1);
expect(abortWasCalledInCallback, isTrue);
verify(() => mockHttpClient.close()).called(1);
},
);
await responseStreamController.close();
await expectLater(streamChangesFuture, completes);
test(
'streamChanges processes remaining lines in finally block if not aborted',
() async {
int onDataCallCount = 0;
List<SyncEvent> receivedEventsBatch1 = [];
List<SyncEvent> receivedEventsBatch2 = [];
expect(onDataCallCount, 1);
expect(abortWasCalledInCallback, isTrue);
verify(() => mockHttpClient.close()).called(1);
});
onDataCallback(List<SyncEvent> events, Function() _) {
onDataCallCount++;
if (onDataCallCount == 1) {
receivedEventsBatch1 = events;
} else if (onDataCallCount == 2) {
receivedEventsBatch2 = events;
} else {
fail("onData called more than expected");
}
test('streamChanges processes remaining lines in finally block if not aborted', () async {
int onDataCallCount = 0;
List<SyncEvent> receivedEventsBatch1 = [];
List<SyncEvent> receivedEventsBatch2 = [];
onDataCallback(List<SyncEvent> events, Function() _) {
onDataCallCount++;
if (onDataCallCount == 1) {
receivedEventsBatch1 = events;
} else if (onDataCallCount == 2) {
receivedEventsBatch2 = events;
} else {
fail("onData called more than expected");
}
}
final streamChangesFuture = streamChanges(onDataCallback);
final streamChangesFuture = streamChanges(onDataCallback);
await pumpEventQueue();
await pumpEventQueue();
// Batch 1
for (int i = 0; i < testBatchSize; i++) {
responseStreamController.add(
utf8.encode(
_createJsonLine(
SyncEntityType.userDeleteV1.toString(),
SyncUserDeleteV1(userId: "user$i").toJson(),
'ack$i',
),
),
);
}
// Partial Batch 2
// Batch 1
for (int i = 0; i < testBatchSize; i++) {
responseStreamController.add(
utf8.encode(
_createJsonLine(
SyncEntityType.userDeleteV1.toString(),
SyncUserDeleteV1(userId: "user100").toJson(),
'ack100',
),
_createJsonLine(SyncEntityType.userDeleteV1.toString(), SyncUserDeleteV1(userId: "user$i").toJson(), 'ack$i'),
),
);
}
await responseStreamController.close();
await expectLater(streamChangesFuture, completes);
// Partial Batch 2
responseStreamController.add(
utf8.encode(
_createJsonLine(SyncEntityType.userDeleteV1.toString(), SyncUserDeleteV1(userId: "user100").toJson(), 'ack100'),
),
);
expect(onDataCallCount, 2);
expect(receivedEventsBatch1.length, testBatchSize);
expect(receivedEventsBatch2.length, 1);
verify(() => mockHttpClient.close()).called(1);
},
);
await responseStreamController.close();
await expectLater(streamChangesFuture, completes);
expect(onDataCallCount, 2);
expect(receivedEventsBatch1.length, testBatchSize);
expect(receivedEventsBatch2.length, 1);
verify(() => mockHttpClient.close()).called(1);
});
test('streamChanges handles stream error gracefully', () async {
final streamError = Exception("Network Error");
@@ -248,11 +212,7 @@ void main() {
responseStreamController.add(
utf8.encode(
_createJsonLine(
SyncEntityType.userDeleteV1.toString(),
SyncUserDeleteV1(userId: "user1").toJson(),
'ack1',
),
_createJsonLine(SyncEntityType.userDeleteV1.toString(), SyncUserDeleteV1(userId: "user1").toJson(), 'ack1'),
),
);
+3 -6
View File
@@ -40,12 +40,9 @@ class MockHttpOverrides extends HttpOverrides {
final cancelOnError = invocation.namedArguments[#cancelOnError] as bool;
return Stream<List<int>>.fromIterable([kTransparentImage.toList()]).listen(
onData,
onDone: onDone,
onError: onError,
cancelOnError: cancelOnError,
);
return Stream<List<int>>.fromIterable([
kTransparentImage.toList(),
]).listen(onData, onDone: onDone, onError: onError, cancelOnError: cancelOnError);
});
return client;
@@ -49,19 +49,8 @@ final _activities = [
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,
),
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() {
@@ -85,10 +74,7 @@ void main() {
mockCurrentAssetProvider = MockCurrentAssetProvider(AssetStub.image1);
activityMock = MockAlbumActivity(_activities);
overrides = [
albumActivityProvider(
AlbumStub.twoAsset.remoteId!,
AssetStub.image1.remoteId!,
).overrideWith(() => activityMock),
albumActivityProvider(AlbumStub.twoAsset.remoteId!, AssetStub.image1.remoteId!).overrideWith(() => activityMock),
currentAlbumProvider.overrideWith(() => mockCurrentAlbumProvider),
currentAssetProvider.overrideWith(() => mockCurrentAssetProvider),
];
@@ -108,147 +94,82 @@ void main() {
});
group("App bar", () {
testWidgets(
"No title when currentAsset != null",
(tester) async {
await tester.pumpConsumerWidget(
const ActivitiesPage(),
overrides: overrides,
);
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);
},
);
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();
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();
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);
},
);
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();
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(ActivityTextField)), findsOneWidget);
expect(
find.descendant(
of: find.byType(Stack),
matching: find.byType(ListView),
),
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();
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));
},
);
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();
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());
when(() => activityMock.addComment(any())).thenAnswer((_) => Future.value());
final textField = find.byType(TextField);
await tester.enterText(textField, 'Test comment');
await tester.testTextInput.receiveAction(TextInputAction.done);
final textField = find.byType(TextField);
await tester.enterText(textField, 'Test comment');
await tester.testTextInput.receiveAction(TextInputAction.done);
verify(() => activityMock.addComment('Test comment'));
},
);
verify(() => activityMock.addComment('Test comment'));
});
testWidgets(
"Owner can remove all activities",
(tester) async {
await tester.pumpConsumerWidget(
const ActivitiesPage(),
overrides: overrides,
);
await tester.pumpAndSettle();
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));
},
);
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();
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();
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,
),
);
},
);
final deletableActivityFinder = find.byWidgetPredicate(
(widget) => widget is DismissibleActivity && widget.onDismiss != null,
);
expect(deletableActivityFinder, findsNWidgets(_activities.where((a) => a.user == UserStub.user1).length));
});
});
}
@@ -26,19 +26,8 @@ final _activities = [
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,
),
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() {
@@ -70,11 +59,7 @@ void main() {
// Init and wait for providers future to complete
provider = albumActivityProvider('test-album', 'test-asset');
listener = ListenerMock();
container.listen(
provider,
listener.call,
fireImmediately: true,
);
container.listen(provider, listener.call, fireImmediately: true);
await container.read(provider.future);
});
@@ -83,18 +68,14 @@ void main() {
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)),
),
],
),
),
),
const AsyncLoading(),
any(
that: allOf([
isA<AsyncData<List<Activity>>>(),
predicate((AsyncData<List<Activity>> ad) => ad.requireValue.every((e) => _activities.contains(e))),
]),
),
),
]);
verifyNoMoreInteractions(listener);
@@ -102,30 +83,15 @@ void main() {
group('addLike()', () {
test('Like successfully added', () async {
final like = Activity(
id: '5',
createdAt: DateTime(2023),
type: ActivityType.like,
user: UserStub.admin,
);
final like = Activity(id: '5', createdAt: DateTime(2023), type: ActivityType.like, user: UserStub.admin);
when(
() => activityMock.addActivity(
'test-album',
ActivityType.like,
assetId: 'test-asset',
),
() => 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',
),
);
verify(() => activityMock.addActivity('test-album', ActivityType.like, assetId: 'test-asset'));
final activities = await container.read(provider.future);
expect(activities, hasLength(5));
@@ -136,31 +102,14 @@ void main() {
});
test('Like failed', () async {
final like = Activity(
id: '5',
createdAt: DateTime(2023),
type: ActivityType.like,
user: UserStub.admin,
);
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),
);
() => 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',
),
);
verify(() => activityMock.addActivity('test-album', ActivityType.like, assetId: 'test-asset'));
final activities = await container.read(provider.future);
expect(activities, hasLength(4));
@@ -174,16 +123,11 @@ void main() {
await container.read(provider.notifier).removeActivity('3');
verify(
() => activityMock.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'))),
);
expect(activities, isNot(anyElement(predicate((Activity a) => a.id == '3'))));
verifyNever(() => activityStatisticsMock.removeActivity());
});
@@ -195,10 +139,7 @@ void main() {
final activities = await container.read(provider.future);
expect(activities, hasLength(4));
expect(
activities,
anyElement(predicate((Activity a) => a.id == '3')),
);
expect(activities, anyElement(predicate((Activity a) => a.id == '3')));
});
test('Comment successfully removed', () async {
@@ -207,10 +148,7 @@ void main() {
await container.read(provider.notifier).removeActivity('1');
final activities = await container.read(provider.future);
expect(
activities,
isNot(anyElement(predicate((Activity a) => a.id == '1'))),
);
expect(activities, isNot(anyElement(predicate((Activity a) => a.id == '1'))));
verify(() => activityStatisticsMock.removeActivity());
});
@@ -281,11 +219,7 @@ void main() {
);
when(
() => activityMock.addActivity(
'test-album',
ActivityType.comment,
comment: 'Test-Comment',
),
() => 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]);
@@ -294,12 +228,7 @@ void main() {
await container.read(albumProvider.notifier).addComment('Test-Comment');
verify(
() => activityMock.addActivity(
'test-album',
ActivityType.comment,
assetId: null,
comment: 'Test-Comment',
),
() => activityMock.addActivity('test-album', ActivityType.comment, assetId: null, comment: 'Test-Comment'),
);
final activities = await container.read(albumProvider.future);
@@ -327,9 +256,7 @@ void main() {
assetId: 'test-asset',
comment: 'Test-Comment',
),
).thenAnswer(
(_) async => AsyncError(Exception('Error'), StackTrace.current),
);
).thenAnswer((_) async => AsyncError(Exception('Error'), StackTrace.current));
await container.read(provider.notifier).addComment('Test-Comment');
@@ -15,11 +15,7 @@ void main() {
setUp(() async {
activityMock = ActivityServiceMock();
container = TestUtils.createContainer(
overrides: [
activityServiceProvider.overrideWith((ref) => activityMock),
],
);
container = TestUtils.createContainer(overrides: [activityServiceProvider.overrideWith((ref) => activityMock)]);
listener = ListenerMock();
});
@@ -31,34 +27,21 @@ void main() {
// 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,
);
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),
]);
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));
when(() => activityMock.getStatistics('test-album')).thenAnswer((_) async => const ActivityStats(comments: 10));
final provider = activityStatisticsProvider('test-album');
container.listen(
provider,
listener.call,
fireImmediately: true,
);
container.listen(provider, listener.call, fireImmediately: true);
// Sleep for the getStatistics future to resolve
await Future.delayed(const Duration(milliseconds: 1));
@@ -75,11 +58,7 @@ void main() {
).thenAnswer((_) async => const ActivityStats(comments: 10));
final provider = activityStatisticsProvider('new-album', 'test-asset');
container.listen(
provider,
listener.call,
fireImmediately: true,
);
container.listen(provider, listener.call, fireImmediately: true);
// Sleep for the getStatistics future to resolve
await Future.delayed(const Duration(milliseconds: 1));
@@ -49,12 +49,7 @@ void main() {
});
testWidgets('Returns an Input text field', (tester) async {
await tester.pumpConsumerWidget(
ActivityTextField(
onSubmit: (_) {},
),
overrides: overrides,
);
await tester.pumpConsumerWidget(ActivityTextField(onSubmit: (_) {}), overrides: overrides);
expect(find.byType(TextField), findsOneWidget);
});
@@ -63,76 +58,38 @@ void main() {
final userProvider = MockCurrentUserProvider();
await tester.pumpConsumerWidget(
ActivityTextField(
onSubmit: (_) {},
),
overrides: [
currentUserProvider.overrideWith((ref) => userProvider),
...overrides,
],
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,
);
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 {
testWidgets('Filled icon if likedId != null', (tester) async {
await tester.pumpConsumerWidget(
ActivityTextField(
onSubmit: (_) {},
),
ActivityTextField(onSubmit: (_) {}, likeId: '1'),
overrides: overrides,
);
expect(
find.widgetWithIcon(IconButton, Icons.favorite_border_rounded),
findsOneWidget,
);
expect(
find.widgetWithIcon(IconButton, Icons.favorite_rounded),
findsNothing,
);
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,
);
await tester.pumpConsumerWidget(ActivityTextField(onSubmit: (_) {}), overrides: overrides);
when(() => activityMock.addLike()).thenAnswer((_) => Future.value());
@@ -144,10 +101,7 @@ void main() {
testWidgets('Removes like if already liked', (tester) async {
await tester.pumpConsumerWidget(
ActivityTextField(
onSubmit: (_) {},
likeId: 'test-suffix',
),
ActivityTextField(onSubmit: (_) {}, likeId: 'test-suffix'),
overrides: overrides,
);
@@ -163,10 +117,7 @@ void main() {
String? receivedText;
await tester.pumpConsumerWidget(
ActivityTextField(
onSubmit: (text) => receivedText = text,
likeId: 'test-suffix',
),
ActivityTextField(onSubmit: (text) => receivedText = text, likeId: 'test-suffix'),
overrides: overrides,
);
@@ -180,11 +131,7 @@ void main() {
String? receviedText;
await tester.pumpConsumerWidget(
ActivityTextField(
onSubmit: (text) => receviedText = text,
isEnabled: false,
likeId: 'test-suffix',
),
ActivityTextField(onSubmit: (text) => receviedText = text, isEnabled: false, likeId: 'test-suffix'),
overrides: overrides,
);
@@ -43,14 +43,7 @@ void main() {
testWidgets('Returns a ListTile', (tester) async {
await tester.pumpConsumerWidget(
ActivityTile(
Activity(
id: '1',
createdAt: DateTime(100),
type: ActivityType.like,
user: UserStub.admin,
),
),
ActivityTile(Activity(id: '1', createdAt: DateTime(100), type: ActivityType.like, user: UserStub.admin)),
overrides: overrides,
);
@@ -59,14 +52,7 @@ void main() {
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,
),
),
ActivityTile(Activity(id: '1', createdAt: DateTime(100), type: ActivityType.like, user: UserStub.admin)),
overrides: overrides,
);
@@ -77,13 +63,7 @@ void main() {
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',
),
Activity(id: '1', createdAt: DateTime(100), type: ActivityType.like, user: UserStub.admin, assetId: '1'),
),
overrides: overrides,
);
@@ -96,13 +76,7 @@ void main() {
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',
),
Activity(id: '1', createdAt: DateTime(100), type: ActivityType.like, user: UserStub.admin, assetId: '1'),
),
overrides: overrides,
);
@@ -115,37 +89,23 @@ void main() {
});
group('Like Activity', () {
final activity = Activity(
id: '1',
createdAt: DateTime(100),
type: ActivityType.like,
user: UserStub.admin,
);
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,
);
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,
);
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,
);
await tester.pumpConsumerWidget(ActivityTile(activity), overrides: overrides);
final listTile = tester.widget<ListTile>(find.byType(ListTile));
@@ -153,10 +113,7 @@ void main() {
});
testWidgets('No subtitle for likes', (tester) async {
await tester.pumpConsumerWidget(
ActivityTile(activity),
overrides: overrides,
);
await tester.pumpConsumerWidget(ActivityTile(activity), overrides: overrides);
final listTile = tester.widget<ListTile>(find.byType(ListTile));
@@ -174,10 +131,7 @@ void main() {
);
testWidgets('Comment contains User Circle Avatar as leading', (tester) async {
await tester.pumpConsumerWidget(
ActivityTile(activity),
overrides: overrides,
);
await tester.pumpConsumerWidget(ActivityTile(activity), overrides: overrides);
final userAvatarFinder = find.byType(UserCircleAvatar);
expect(userAvatarFinder, findsOneWidget);
@@ -192,10 +146,7 @@ void main() {
});
testWidgets('Comment title is top aligned', (tester) async {
await tester.pumpConsumerWidget(
ActivityTile(activity),
overrides: overrides,
);
await tester.pumpConsumerWidget(ActivityTile(activity), overrides: overrides);
final listTile = tester.widget<ListTile>(find.byType(ListTile));
@@ -203,21 +154,12 @@ void main() {
});
testWidgets('Contains comment text as subtitle', (tester) async {
await tester.pumpConsumerWidget(
ActivityTile(activity),
overrides: overrides,
);
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,
);
expect(find.descendant(of: find.byType(ListTile), matching: find.text(activity.comment!)), findsOneWidget);
});
});
}
@@ -15,12 +15,7 @@ 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,
);
final activity = Activity(id: '1', createdAt: DateTime(100), type: ActivityType.like, user: UserStub.admin);
void main() {
late MockCurrentAssetProvider assetProvider;
@@ -34,10 +29,7 @@ void main() {
});
testWidgets('Returns a Dismissible', (tester) async {
await tester.pumpConsumerWidget(
DismissibleActivity('1', ActivityTile(activity)),
overrides: overrides,
);
await tester.pumpConsumerWidget(DismissibleActivity('1', ActivityTile(activity)), overrides: overrides);
expect(find.byType(Dismissible), findsOneWidget);
});
@@ -58,11 +50,7 @@ void main() {
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,
),
DismissibleActivity('1', ActivityTile(activity), onDismiss: (id) => receivedActivityId = id),
overrides: overrides,
);
@@ -91,10 +79,7 @@ void main() {
});
testWidgets('No delete dialog if onDismiss is not set', (tester) async {
await tester.pumpConsumerWidget(
DismissibleActivity('1', ActivityTile(activity)),
overrides: overrides,
);
await tester.pumpConsumerWidget(DismissibleActivity('1', ActivityTile(activity)), overrides: overrides);
final dismissible = find.byType(Dismissible);
await tester.drag(dismissible, const Offset(500, 0));
@@ -104,10 +89,7 @@ void main() {
});
testWidgets('No icon for background if onDismiss is not set', (tester) async {
await tester.pumpConsumerWidget(
DismissibleActivity('1', ActivityTile(activity)),
overrides: overrides,
);
await tester.pumpConsumerWidget(DismissibleActivity('1', ActivityTile(activity)), overrides: overrides);
final dismissible = find.byType(Dismissible);
await tester.drag(dismissible, const Offset(-500, 0));
@@ -22,12 +22,7 @@ void main() {
db = await TestUtils.initIsar();
});
final albums = [
AlbumStub.emptyAlbum,
AlbumStub.sharedWithUser,
AlbumStub.oneAsset,
AlbumStub.twoAsset,
];
final albums = [AlbumStub.emptyAlbum, AlbumStub.sharedWithUser, AlbumStub.oneAsset, AlbumStub.twoAsset];
setUp(() {
db.writeTxnSync(() {
@@ -48,23 +43,13 @@ void main() {
const created = AlbumSortMode.created;
test("Created time - ASC", () {
final sorted = created.sortFn(albums, false);
final sortedList = [
AlbumStub.emptyAlbum,
AlbumStub.twoAsset,
AlbumStub.oneAsset,
AlbumStub.sharedWithUser,
];
final sortedList = [AlbumStub.emptyAlbum, AlbumStub.twoAsset, AlbumStub.oneAsset, AlbumStub.sharedWithUser];
expect(sorted, orderedEquals(sortedList));
});
test("Created time - DESC", () {
final sorted = created.sortFn(albums, true);
final sortedList = [
AlbumStub.sharedWithUser,
AlbumStub.oneAsset,
AlbumStub.twoAsset,
AlbumStub.emptyAlbum,
];
final sortedList = [AlbumStub.sharedWithUser, AlbumStub.oneAsset, AlbumStub.twoAsset, AlbumStub.emptyAlbum];
expect(sorted, orderedEquals(sortedList));
});
});
@@ -73,23 +58,13 @@ void main() {
const assetCount = AlbumSortMode.assetCount;
test("Asset Count - ASC", () {
final sorted = assetCount.sortFn(albums, false);
final sortedList = [
AlbumStub.emptyAlbum,
AlbumStub.sharedWithUser,
AlbumStub.oneAsset,
AlbumStub.twoAsset,
];
final sortedList = [AlbumStub.emptyAlbum, AlbumStub.sharedWithUser, AlbumStub.oneAsset, AlbumStub.twoAsset];
expect(sorted, orderedEquals(sortedList));
});
test("Asset Count - DESC", () {
final sorted = assetCount.sortFn(albums, true);
final sortedList = [
AlbumStub.twoAsset,
AlbumStub.oneAsset,
AlbumStub.sharedWithUser,
AlbumStub.emptyAlbum,
];
final sortedList = [AlbumStub.twoAsset, AlbumStub.oneAsset, AlbumStub.sharedWithUser, AlbumStub.emptyAlbum];
expect(sorted, orderedEquals(sortedList));
});
});
@@ -98,23 +73,13 @@ void main() {
const lastModified = AlbumSortMode.lastModified;
test("Last modified - ASC", () {
final sorted = lastModified.sortFn(albums, false);
final sortedList = [
AlbumStub.twoAsset,
AlbumStub.emptyAlbum,
AlbumStub.sharedWithUser,
AlbumStub.oneAsset,
];
final sortedList = [AlbumStub.twoAsset, AlbumStub.emptyAlbum, AlbumStub.sharedWithUser, AlbumStub.oneAsset];
expect(sorted, orderedEquals(sortedList));
});
test("Last modified - DESC", () {
final sorted = lastModified.sortFn(albums, true);
final sortedList = [
AlbumStub.oneAsset,
AlbumStub.sharedWithUser,
AlbumStub.emptyAlbum,
AlbumStub.twoAsset,
];
final sortedList = [AlbumStub.oneAsset, AlbumStub.sharedWithUser, AlbumStub.emptyAlbum, AlbumStub.twoAsset];
expect(sorted, orderedEquals(sortedList));
});
});
@@ -123,23 +88,13 @@ void main() {
const created = AlbumSortMode.created;
test("Created - ASC", () {
final sorted = created.sortFn(albums, false);
final sortedList = [
AlbumStub.emptyAlbum,
AlbumStub.twoAsset,
AlbumStub.oneAsset,
AlbumStub.sharedWithUser,
];
final sortedList = [AlbumStub.emptyAlbum, AlbumStub.twoAsset, AlbumStub.oneAsset, AlbumStub.sharedWithUser];
expect(sorted, orderedEquals(sortedList));
});
test("Created - DESC", () {
final sorted = created.sortFn(albums, true);
final sortedList = [
AlbumStub.sharedWithUser,
AlbumStub.oneAsset,
AlbumStub.twoAsset,
AlbumStub.emptyAlbum,
];
final sortedList = [AlbumStub.sharedWithUser, AlbumStub.oneAsset, AlbumStub.twoAsset, AlbumStub.emptyAlbum];
expect(sorted, orderedEquals(sortedList));
});
});
@@ -148,15 +103,12 @@ void main() {
const mostRecent = AlbumSortMode.mostRecent;
test("Most Recent - DESC", () {
final sorted = mostRecent.sortFn(
[
AlbumStub.create2020end2020Album,
AlbumStub.create2020end2022Album,
AlbumStub.create2020end2024Album,
AlbumStub.create2020end2026Album,
],
false,
);
final sorted = mostRecent.sortFn([
AlbumStub.create2020end2020Album,
AlbumStub.create2020end2022Album,
AlbumStub.create2020end2024Album,
AlbumStub.create2020end2026Album,
], false);
final sortedList = [
AlbumStub.create2020end2026Album,
AlbumStub.create2020end2024Album,
@@ -167,15 +119,12 @@ void main() {
});
test("Most Recent - ASC", () {
final sorted = mostRecent.sortFn(
[
AlbumStub.create2020end2020Album,
AlbumStub.create2020end2022Album,
AlbumStub.create2020end2024Album,
AlbumStub.create2020end2026Album,
],
true,
);
final sorted = mostRecent.sortFn([
AlbumStub.create2020end2020Album,
AlbumStub.create2020end2022Album,
AlbumStub.create2020end2024Album,
AlbumStub.create2020end2026Album,
], true);
final sortedList = [
AlbumStub.create2020end2020Album,
AlbumStub.create2020end2022Album,
@@ -191,23 +140,13 @@ void main() {
test("Most Oldest - ASC", () {
final sorted = mostOldest.sortFn(albums, false);
final sortedList = [
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);
final sortedList = [
AlbumStub.sharedWithUser,
AlbumStub.oneAsset,
AlbumStub.emptyAlbum,
AlbumStub.twoAsset,
];
final sortedList = [AlbumStub.sharedWithUser, AlbumStub.oneAsset, AlbumStub.emptyAlbum, AlbumStub.twoAsset];
expect(sorted, orderedEquals(sortedList));
});
});
@@ -221,67 +160,43 @@ void main() {
setUp(() async {
settingsMock = MockAppSettingsService();
container = TestUtils.createContainer(
overrides: [
appSettingsServiceProvider.overrideWith((ref) => settingsMock),
],
overrides: [appSettingsServiceProvider.overrideWith((ref) => settingsMock)],
);
when(
() => settingsMock.setSetting<bool>(
AppSettingsEnum.selectedAlbumSortReverse,
any(),
),
() => settingsMock.setSetting<bool>(AppSettingsEnum.selectedAlbumSortReverse, any()),
).thenAnswer((_) async => {});
when(
() => settingsMock.setSetting<int>(
AppSettingsEnum.selectedAlbumSortOrder,
any(),
),
() => settingsMock.setSetting<int>(AppSettingsEnum.selectedAlbumSortOrder, any()),
).thenAnswer((_) async => {});
});
test('Returns the default sort mode when none set', () {
// Returns the default value when nothing is set
when(
() => settingsMock.getSetting(AppSettingsEnum.selectedAlbumSortOrder),
).thenReturn(0);
when(() => settingsMock.getSetting(AppSettingsEnum.selectedAlbumSortOrder)).thenReturn(0);
expect(container.read(albumSortByOptionsProvider), AlbumSortMode.created);
});
test('Returns the correct sort mode with index from Store', () {
// Returns the default value when nothing is set
when(
() => settingsMock.getSetting(AppSettingsEnum.selectedAlbumSortOrder),
).thenReturn(3);
when(() => settingsMock.getSetting(AppSettingsEnum.selectedAlbumSortOrder)).thenReturn(3);
expect(
container.read(albumSortByOptionsProvider),
AlbumSortMode.lastModified,
);
expect(container.read(albumSortByOptionsProvider), AlbumSortMode.lastModified);
});
test('Properly saves the correct store index of sort mode', () {
container.read(albumSortByOptionsProvider.notifier).changeSortMode(AlbumSortMode.mostOldest);
verify(
() => settingsMock.setSetting(
AppSettingsEnum.selectedAlbumSortOrder,
AlbumSortMode.mostOldest.storeIndex,
),
() => settingsMock.setSetting(AppSettingsEnum.selectedAlbumSortOrder, AlbumSortMode.mostOldest.storeIndex),
);
});
test('Notifies listeners on state change', () {
when(
() => settingsMock.getSetting(AppSettingsEnum.selectedAlbumSortOrder),
).thenReturn(0);
when(() => settingsMock.getSetting(AppSettingsEnum.selectedAlbumSortOrder)).thenReturn(0);
final listener = ListenerMock<AlbumSortMode>();
container.listen(
albumSortByOptionsProvider,
listener.call,
fireImmediately: true,
);
container.listen(albumSortByOptionsProvider, listener.call, fireImmediately: true);
// Created -> Most Oldest
container.read(albumSortByOptionsProvider.notifier).changeSortMode(AlbumSortMode.mostOldest);
@@ -309,28 +224,18 @@ void main() {
setUp(() async {
settingsMock = MockAppSettingsService();
container = TestUtils.createContainer(
overrides: [
appSettingsServiceProvider.overrideWith((ref) => settingsMock),
],
overrides: [appSettingsServiceProvider.overrideWith((ref) => settingsMock)],
);
when(
() => settingsMock.setSetting<bool>(
AppSettingsEnum.selectedAlbumSortReverse,
any(),
),
() => settingsMock.setSetting<bool>(AppSettingsEnum.selectedAlbumSortReverse, any()),
).thenAnswer((_) async => {});
when(
() => settingsMock.setSetting<int>(
AppSettingsEnum.selectedAlbumSortOrder,
any(),
),
() => settingsMock.setSetting<int>(AppSettingsEnum.selectedAlbumSortOrder, any()),
).thenAnswer((_) async => {});
});
test('Returns the default sort order when none set - false', () {
when(
() => settingsMock.getSetting(AppSettingsEnum.selectedAlbumSortReverse),
).thenReturn(false);
when(() => settingsMock.getSetting(AppSettingsEnum.selectedAlbumSortReverse)).thenReturn(false);
expect(container.read(albumSortOrderProvider), isFalse);
});
@@ -338,25 +243,14 @@ void main() {
test('Properly saves the correct order', () {
container.read(albumSortOrderProvider.notifier).changeSortDirection(true);
verify(
() => settingsMock.setSetting(
AppSettingsEnum.selectedAlbumSortReverse,
true,
),
);
verify(() => settingsMock.setSetting(AppSettingsEnum.selectedAlbumSortReverse, true));
});
test('Notifies listeners on state change', () {
when(
() => settingsMock.getSetting(AppSettingsEnum.selectedAlbumSortReverse),
).thenReturn(false);
when(() => settingsMock.getSetting(AppSettingsEnum.selectedAlbumSortReverse)).thenReturn(false);
final listener = ListenerMock<bool>();
container.listen(
albumSortOrderProvider,
listener.call,
fireImmediately: true,
);
container.listen(albumSortOrderProvider, listener.call, fireImmediately: true);
// false -> true
container.read(albumSortOrderProvider.notifier).changeSortDirection(true);
@@ -5,21 +5,11 @@ import 'package:immich_mobile/extensions/asset_extensions.dart';
import 'package:timezone/data/latest.dart';
import 'package:timezone/timezone.dart';
ExifInfo makeExif({
DateTime? dateTimeOriginal,
String? timeZone,
}) {
return ExifInfo(
dateTimeOriginal: dateTimeOriginal,
timeZone: timeZone,
);
ExifInfo makeExif({DateTime? dateTimeOriginal, String? timeZone}) {
return ExifInfo(dateTimeOriginal: dateTimeOriginal, timeZone: timeZone);
}
Asset makeAsset({
required String id,
required DateTime createdAt,
ExifInfo? exifInfo,
}) {
Asset makeAsset({required String id, required DateTime createdAt, ExifInfo? exifInfo}) {
return Asset(
checksum: '',
localId: id,
@@ -79,10 +69,7 @@ void main() {
test('Returns dateTimeOriginal in UTC from exifInfo with invalid timezone', () {
final createdAt = DateTime.parse("2023-01-27T14:00:00-0500");
final dateTimeOriginal = DateTime.parse("2022-01-27T14:00:00+0530");
final e = makeExif(
dateTimeOriginal: dateTimeOriginal,
timeZone: "#_#",
); // Invalid timezone
final e = makeExif(dateTimeOriginal: dateTimeOriginal, timeZone: "#_#"); // Invalid timezone
final a = makeAsset(id: '1', createdAt: createdAt, exifInfo: e);
final (dt, tz) = a.getTZAdjustedTimeAndOffset();
@@ -5,10 +5,7 @@ import 'package:immich_mobile/extensions/string_extensions.dart';
void main() {
group('Test toDuration', () {
test('ok', () {
expect(
"1:02:33".toDuration(),
const Duration(hours: 1, minutes: 2, seconds: 33),
);
expect("1:02:33".toDuration(), const Duration(hours: 1, minutes: 2, seconds: 33));
});
test('malformed', () {
expect("".toDuration(), isNull);
@@ -45,9 +42,7 @@ void main() {
test('withKey', () {
final a = ["a", "bb", "cc", "ddd"];
expect(
a.uniqueConsecutive(
compare: (s1, s2) => s1.length.compareTo(s2.length),
),
a.uniqueConsecutive(compare: (s1, s2) => s1.length.compareTo(s2.length)),
orderedEquals(["a", "bb", "ddd"]),
);
});
@@ -58,10 +58,7 @@ void main() {
group('Test grouped', () {
test('test grouped check months', () async {
final renderList = await RenderList.fromAssets(
assets,
GroupAssetsBy.day,
);
final renderList = await RenderList.fromAssets(assets, GroupAssetsBy.day);
// Oct
// Day 1
@@ -75,33 +72,18 @@ void main() {
// Day 1
// 5 Assets => 2 Rows
expect(renderList.elements, hasLength(4));
expect(
renderList.elements[0].type,
RenderAssetGridElementType.monthTitle,
);
expect(renderList.elements[0].type, RenderAssetGridElementType.monthTitle);
expect(renderList.elements[0].date.month, 1);
expect(
renderList.elements[1].type,
RenderAssetGridElementType.groupDividerTitle,
);
expect(renderList.elements[1].type, RenderAssetGridElementType.groupDividerTitle);
expect(renderList.elements[1].date.month, 1);
expect(
renderList.elements[2].type,
RenderAssetGridElementType.monthTitle,
);
expect(renderList.elements[2].type, RenderAssetGridElementType.monthTitle);
expect(renderList.elements[2].date.month, 2);
expect(
renderList.elements[3].type,
RenderAssetGridElementType.monthTitle,
);
expect(renderList.elements[3].type, RenderAssetGridElementType.monthTitle);
expect(renderList.elements[3].date.month, 10);
});
test('test grouped check types', () async {
final renderList = await RenderList.fromAssets(
assets,
GroupAssetsBy.day,
);
final renderList = await RenderList.fromAssets(assets, GroupAssetsBy.day);
// Oct
// Day 1
@@ -67,9 +67,7 @@ void main() {
overrides: overrides,
);
mapStateNotifier.state = mapState.copyWith(
darkStyleFetched: const AsyncError("Error", StackTrace.empty),
);
mapStateNotifier.state = mapState.copyWith(darkStyleFetched: const AsyncError("Error", StackTrace.empty));
await tester.pumpAndSettle();
expect(mapStyle?.hasError, isTrue);
});
@@ -86,10 +84,7 @@ void main() {
overrides: overrides,
);
mapStateNotifier.state = mapState.copyWith(
themeMode: ThemeMode.light,
lightStyleFetched: const AsyncData("light"),
);
mapStateNotifier.state = mapState.copyWith(themeMode: ThemeMode.light, lightStyleFetched: const AsyncData("light"));
await tester.pumpAndSettle();
expect(mapStyle?.valueOrNull, "light");
});
@@ -66,13 +66,7 @@ void main() {
final MockPartnerRepository partnerRepository = MockPartnerRepository();
final MockUserService userService = MockUserService();
final owner = UserDto(
id: "1",
updatedAt: DateTime.now(),
email: "a@b.c",
name: "first last",
isAdmin: false,
);
final owner = UserDto(id: "1", updatedAt: DateTime.now(), email: "a@b.c", name: "first last", isAdmin: false);
late SyncService s;
setUpAll(() async {
WidgetsFlutterBinding.ensureInitialized();
@@ -81,10 +75,7 @@ void main() {
db.writeTxnSync(() => db.clearSync());
await StoreService.init(storeRepository: IsarStoreRepository(db));
await Store.put(StoreKey.currentUser, owner);
await LogService.init(
logRepository: IsarLogRepository(db),
storeRepository: IsarStoreRepository(db),
);
await LogService.init(logRepository: IsarLogRepository(db), storeRepository: IsarStoreRepository(db));
});
final List<Asset> initialAssets = [
makeAsset(checksum: "a", remoteId: "0-1"),
@@ -119,22 +110,20 @@ void main() {
when(() => userRepository.getAll(sortBy: SortUserBy.id)).thenAnswer((_) async => [owner]);
when(() => userRepository.getAll()).thenAnswer((_) async => [owner]);
when(
() => assetRepository.getAll(
ownerId: owner.id,
sortBy: AssetSort.checksum,
),
() => assetRepository.getAll(ownerId: owner.id, sortBy: AssetSort.checksum),
).thenAnswer((_) async => initialAssets);
when(() => assetRepository.getAllByOwnerIdChecksum(any(), any()))
.thenAnswer((_) async => [initialAssets[3], null, null]);
when(
() => assetRepository.getAllByOwnerIdChecksum(any(), any()),
).thenAnswer((_) async => [initialAssets[3], null, null]);
when(() => assetRepository.updateAll(any())).thenAnswer((_) async => []);
when(() => assetRepository.deleteByIds(any())).thenAnswer((_) async {});
when(() => exifInfoRepository.updateAll(any())).thenAnswer((_) async => []);
when(() => assetRepository.transaction<void>(any())).thenAnswer(
(call) => (call.positionalArguments.first as Function).call(),
);
when(() => assetRepository.transaction<Null>(any())).thenAnswer(
(call) => (call.positionalArguments.first as Function).call(),
);
when(
() => assetRepository.transaction<void>(any()),
).thenAnswer((call) => (call.positionalArguments.first as Function).call());
when(
() => assetRepository.transaction<Null>(any()),
).thenAnswer((call) => (call.positionalArguments.first as Function).call());
when(() => userApiRepository.getAll()).thenAnswer((_) async => [owner]);
registerFallbackValue(Direction.sharedByMe);
when(() => partnerApiRepository.getAll(any())).thenAnswer((_) async => []);
@@ -170,9 +159,7 @@ void main() {
);
expect(c1, isTrue);
final updatedAsset = initialAssets[3].updatedCopy(remoteAssets[3]);
verify(
() => assetRepository.updateAll([remoteAssets[4], remoteAssets[5], updatedAsset]),
);
verify(() => assetRepository.updateAll([remoteAssets[4], remoteAssets[5], updatedAsset]));
});
test('test syncing duplicate assets', () async {
@@ -191,10 +178,7 @@ void main() {
);
expect(c1, isTrue);
when(
() => assetRepository.getAll(
ownerId: owner.id,
sortBy: AssetSort.checksum,
),
() => assetRepository.getAll(ownerId: owner.id, sortBy: AssetSort.checksum),
).thenAnswer((_) async => remoteAssets);
final bool c2 = await s.syncRemoteAssetsToDb(
users: [owner],
@@ -204,10 +188,7 @@ void main() {
expect(c2, isFalse);
final currentState = [...remoteAssets];
when(
() => assetRepository.getAll(
ownerId: owner.id,
sortBy: AssetSort.checksum,
),
() => assetRepository.getAll(ownerId: owner.id, sortBy: AssetSort.checksum),
).thenAnswer((_) async => currentState);
remoteAssets.removeAt(4);
final bool c3 = await s.syncRemoteAssetsToDb(
@@ -228,18 +209,19 @@ void main() {
test('test efficient sync', () async {
when(
() => assetRepository.deleteAllByRemoteId(
[initialAssets[1].remoteId!, initialAssets[2].remoteId!],
state: AssetState.remote,
),
() => assetRepository.deleteAllByRemoteId([
initialAssets[1].remoteId!,
initialAssets[2].remoteId!,
], state: AssetState.remote),
).thenAnswer((_) async {
return;
});
when(
() => assetRepository.getAllByRemoteId(["2-1", "1-1"], state: AssetState.merged),
).thenAnswer((_) async => [initialAssets[2]]);
when(() => assetRepository.getAllByOwnerIdChecksum(any(), any()))
.thenAnswer((_) async => [initialAssets[0], null, null]); //afg
when(
() => assetRepository.getAllByOwnerIdChecksum(any(), any()),
).thenAnswer((_) async => [initialAssets[0], null, null]); //afg
final List<Asset> toUpsert = [
makeAsset(checksum: "a", remoteId: "0-1"), // changed
makeAsset(checksum: "f", remoteId: "0-2"), // new
@@ -262,18 +244,11 @@ void main() {
test('test upsert with EXIF data', () async {
final assets = [AssetStub.image1, AssetStub.image2];
expect(
assets.map((a) => a.exifInfo?.assetId),
List.filled(assets.length, null),
);
expect(assets.map((a) => a.exifInfo?.assetId), List.filled(assets.length, null));
await s.upsertAssetsWithExif(assets);
verify(
() => exifInfoRepository.updateAll(
any(
that: containsAll(
assets.map((a) => a.exifInfo!.copyWith(assetId: a.id)),
),
),
any(that: containsAll(assets.map((a) => a.exifInfo!.copyWith(assetId: a.id)))),
),
);
expect(assets.map((a) => a.exifInfo?.assetId), assets.map((a) => a.id));
@@ -282,8 +257,4 @@ void main() {
});
}
Future<(List<Asset>?, List<String>?)> _failDiff(
List<UserDto> user,
DateTime time,
) =>
Future.value((null, null));
Future<(List<Asset>?, List<String>?)> _failDiff(List<UserDto> user, DateTime time) => Future.value((null, null));
@@ -7,33 +7,13 @@ void main() {
AsyncMutex lock = AsyncMutex();
List<int> events = [];
expect(0, lock.enqueued);
lock.run(
() => Future.delayed(
const Duration(milliseconds: 10),
() => events.add(1),
),
);
lock.run(() => Future.delayed(const Duration(milliseconds: 10), () => events.add(1)));
expect(1, lock.enqueued);
lock.run(
() => Future.delayed(
const Duration(milliseconds: 3),
() => events.add(2),
),
);
lock.run(() => Future.delayed(const Duration(milliseconds: 3), () => events.add(2)));
expect(2, lock.enqueued);
lock.run(
() => Future.delayed(
const Duration(milliseconds: 1),
() => events.add(3),
),
);
lock.run(() => Future.delayed(const Duration(milliseconds: 1), () => events.add(3)));
expect(3, lock.enqueued);
await lock.run(
() => Future.delayed(
const Duration(milliseconds: 10),
() => events.add(4),
),
);
await lock.run(() => Future.delayed(const Duration(milliseconds: 10), () => events.add(4)));
expect(0, lock.enqueued);
expect(events, [1, 2, 3, 4]);
});
@@ -9,22 +9,12 @@ void main() {
final dateTimeString = DateFormat.yMMMMd().format(dateTime);
test('returns description if it has one', () {
final result = getAltText(
const ExifInfo(description: 'description'),
dateTime,
AssetType.image,
[],
);
final result = getAltText(const ExifInfo(description: 'description'), dateTime, AssetType.image, []);
expect(result, 'description');
});
test('returns image alt text with date if no location', () {
final (template, args) = getAltTextTemplate(
const ExifInfo(),
dateTime,
AssetType.image,
[],
);
final (template, args) = getAltTextTemplate(const ExifInfo(), dateTime, AssetType.image, []);
expect(template, "image_alt_text_date");
expect(args["isVideo"], "false");
expect(args["date"], dateTimeString);
@@ -45,12 +35,7 @@ void main() {
});
test('returns image alt text with date and some people', () {
final (template, args) = getAltTextTemplate(
const ExifInfo(),
dateTime,
AssetType.image,
["Alice", "Bob"],
);
final (template, args) = getAltTextTemplate(const ExifInfo(), dateTime, AssetType.image, ["Alice", "Bob"]);
expect(template, "image_alt_text_date_2_people");
expect(args["isVideo"], "false");
expect(args["date"], dateTimeString);
@@ -6,10 +6,7 @@ void main() {
String? result;
result = getVersionCompatibilityMessage(1, 0, 2, 0);
expect(
result,
'Your app major version is not compatible with the server!',
);
expect(result, 'Your app major version is not compatible with the server!');
result = getVersionCompatibilityMessage(1, 106, 1, 105);
expect(
+8 -32
View File
@@ -45,18 +45,11 @@ void main() {
final emptyTextSearch = isA<MetadataSearchDto>().having((s) => s.originalFileName, 'originalFileName', null);
testWidgets('contextual search with/without text', (tester) async {
await tester.pumpConsumerWidget(
const SearchPage(),
overrides: overrides,
);
await tester.pumpConsumerWidget(const SearchPage(), overrides: overrides);
await tester.pumpAndSettle();
expect(
find.byIcon(Icons.abc_rounded),
findsOneWidget,
reason: 'Should have contextual search icon',
);
expect(find.byIcon(Icons.abc_rounded), findsOneWidget, reason: 'Should have contextual search icon');
final searchField = find.byKey(const Key('search_text_field'));
expect(searchField, findsOneWidget);
@@ -64,14 +57,9 @@ void main() {
await tester.enterText(searchField, 'test');
await tester.testTextInput.receiveAction(TextInputAction.search);
var captured = verify(
() => mockSearchApi.searchSmart(captureAny()),
).captured;
var captured = verify(() => mockSearchApi.searchSmart(captureAny())).captured;
expect(
captured.first,
isA<SmartSearchDto>().having((s) => s.query, 'query', 'test'),
);
expect(captured.first, isA<SmartSearchDto>().having((s) => s.query, 'query', 'test'));
await tester.enterText(searchField, '');
await tester.testTextInput.receiveAction(TextInputAction.search);
@@ -81,10 +69,7 @@ void main() {
});
testWidgets('not contextual search with/without text', (tester) async {
await tester.pumpConsumerWidget(
const SearchPage(),
overrides: overrides,
);
await tester.pumpConsumerWidget(const SearchPage(), overrides: overrides);
await tester.pumpAndSettle();
@@ -92,11 +77,7 @@ void main() {
await tester.pumpAndSettle();
expect(
find.byIcon(Icons.image_search_rounded),
findsOneWidget,
reason: 'Should not have contextual search icon',
);
expect(find.byIcon(Icons.image_search_rounded), findsOneWidget, reason: 'Should not have contextual search icon');
final searchField = find.byKey(const Key('search_text_field'));
expect(searchField, findsOneWidget);
@@ -104,14 +85,9 @@ void main() {
await tester.enterText(searchField, 'test');
await tester.testTextInput.receiveAction(TextInputAction.search);
var captured = verify(
() => mockSearchApi.searchAssets(captureAny()),
).captured;
var captured = verify(() => mockSearchApi.searchAssets(captureAny())).captured;
expect(
captured.first,
isA<MetadataSearchDto>().having((s) => s.originalFileName, 'originalFileName', 'test'),
);
expect(captured.first, isA<MetadataSearchDto>().having((s) => s.originalFileName, 'originalFileName', 'test'));
await tester.enterText(searchField, '');
await tester.testTextInput.receiveAction(TextInputAction.search);
+27 -63
View File
@@ -33,12 +33,12 @@ void main() {
when(() => userService.getMyUser()).thenReturn(UserStub.user1);
when(() => albumRepository.transaction<void>(any())).thenAnswer(
(call) => (call.positionalArguments.first as Function).call(),
);
when(() => assetRepository.transaction<Null>(any())).thenAnswer(
(call) => (call.positionalArguments.first as Function).call(),
);
when(
() => albumRepository.transaction<void>(any()),
).thenAnswer((call) => (call.positionalArguments.first as Function).call());
when(
() => assetRepository.transaction<Null>(any()),
).thenAnswer((call) => (call.positionalArguments.first as Function).call());
sut = AlbumService(
syncService,
@@ -66,15 +66,14 @@ void main() {
test('one selected albums, two on device', () async {
when(() => backupRepository.getIdsBySelection(BackupSelection.exclude)).thenAnswer((_) async => []);
when(() => backupRepository.getIdsBySelection(BackupSelection.select))
.thenAnswer((_) async => [AlbumStub.oneAsset.localId!]);
when(
() => backupRepository.getIdsBySelection(BackupSelection.select),
).thenAnswer((_) async => [AlbumStub.oneAsset.localId!]);
when(() => albumMediaRepository.getAll()).thenAnswer((_) async => [AlbumStub.oneAsset, AlbumStub.twoAsset]);
when(() => syncService.syncLocalAlbumAssetsToDb(any(), any())).thenAnswer((_) async => true);
final result = await sut.refreshDeviceAlbums();
expect(result, true);
verify(
() => syncService.syncLocalAlbumAssetsToDb([AlbumStub.oneAsset], null),
).called(1);
verify(() => syncService.syncLocalAlbumAssetsToDb([AlbumStub.oneAsset], null)).called(1);
verifyNoMoreInteractions(syncService);
});
});
@@ -85,15 +84,12 @@ void main() {
when(() => syncService.syncUsersFromServer(any())).thenAnswer((_) async => true);
when(() => albumApiRepository.getAll(shared: true)).thenAnswer((_) async => [AlbumStub.sharedWithUser]);
when(() => albumApiRepository.getAll(shared: null))
.thenAnswer((_) async => [AlbumStub.oneAsset, AlbumStub.twoAsset]);
when(
() => albumApiRepository.getAll(shared: null),
).thenAnswer((_) async => [AlbumStub.oneAsset, AlbumStub.twoAsset]);
when(
() => syncService.syncRemoteAlbumsToDb([
AlbumStub.twoAsset,
AlbumStub.oneAsset,
AlbumStub.sharedWithUser,
]),
() => syncService.syncRemoteAlbumsToDb([AlbumStub.twoAsset, AlbumStub.oneAsset, AlbumStub.sharedWithUser]),
).thenAnswer((_) async => true);
final result = await sut.refreshRemoteAlbums();
expect(result, true);
@@ -102,13 +98,7 @@ void main() {
verify(() => albumApiRepository.getAll(shared: true)).called(1);
verify(() => albumApiRepository.getAll(shared: null)).called(1);
verify(
() => syncService.syncRemoteAlbumsToDb(
[
AlbumStub.twoAsset,
AlbumStub.oneAsset,
AlbumStub.sharedWithUser,
],
),
() => syncService.syncRemoteAlbumsToDb([AlbumStub.twoAsset, AlbumStub.oneAsset, AlbumStub.sharedWithUser]),
).called(1);
verifyNoMoreInteractions(userService);
verifyNoMoreInteractions(albumApiRepository);
@@ -130,9 +120,7 @@ void main() {
() => entityService.fillAlbumWithDatabaseEntities(AlbumStub.oneAsset),
).thenAnswer((_) async => AlbumStub.oneAsset);
when(
() => albumRepository.create(AlbumStub.oneAsset),
).thenAnswer((_) async => AlbumStub.twoAsset);
when(() => albumRepository.create(AlbumStub.oneAsset)).thenAnswer((_) async => AlbumStub.twoAsset);
final result = await sut.createAlbum("name", [AssetStub.image1], [UserStub.user1]);
expect(result, AlbumStub.twoAsset);
@@ -143,9 +131,7 @@ void main() {
sharedUserIds: [UserStub.user1.id],
),
).called(1);
verify(
() => entityService.fillAlbumWithDatabaseEntities(AlbumStub.oneAsset),
).called(1);
verify(() => entityService.fillAlbumWithDatabaseEntities(AlbumStub.oneAsset)).called(1);
});
});
@@ -153,29 +139,14 @@ void main() {
test('one added, one duplicate', () async {
when(
() => albumApiRepository.addAssets(AlbumStub.oneAsset.remoteId!, any()),
).thenAnswer(
(_) async => (added: [AssetStub.image2.remoteId!], duplicates: [AssetStub.image1.remoteId!]),
);
when(
() => albumRepository.get(AlbumStub.oneAsset.id),
).thenAnswer((_) async => AlbumStub.oneAsset);
when(
() => albumRepository.addAssets(AlbumStub.oneAsset, [AssetStub.image2]),
).thenAnswer((_) async {});
when(
() => albumRepository.removeAssets(AlbumStub.oneAsset, []),
).thenAnswer((_) async {});
when(
() => albumRepository.recalculateMetadata(AlbumStub.oneAsset),
).thenAnswer((_) async => AlbumStub.oneAsset);
when(
() => albumRepository.update(AlbumStub.oneAsset),
).thenAnswer((_) async => AlbumStub.oneAsset);
).thenAnswer((_) async => (added: [AssetStub.image2.remoteId!], duplicates: [AssetStub.image1.remoteId!]));
when(() => albumRepository.get(AlbumStub.oneAsset.id)).thenAnswer((_) async => AlbumStub.oneAsset);
when(() => albumRepository.addAssets(AlbumStub.oneAsset, [AssetStub.image2])).thenAnswer((_) async {});
when(() => albumRepository.removeAssets(AlbumStub.oneAsset, [])).thenAnswer((_) async {});
when(() => albumRepository.recalculateMetadata(AlbumStub.oneAsset)).thenAnswer((_) async => AlbumStub.oneAsset);
when(() => albumRepository.update(AlbumStub.oneAsset)).thenAnswer((_) async => AlbumStub.oneAsset);
final result = await sut.addAssets(
AlbumStub.oneAsset,
[AssetStub.image1, AssetStub.image2],
);
final result = await sut.addAssets(AlbumStub.oneAsset, [AssetStub.image1, AssetStub.image2]);
expect(result != null, true);
expect(result!.alreadyInAlbum, [AssetStub.image1.remoteId!]);
@@ -187,9 +158,7 @@ void main() {
test('one added', () async {
when(
() => albumApiRepository.addUsers(AlbumStub.emptyAlbum.remoteId!, any()),
).thenAnswer(
(_) async => AlbumStub.sharedWithUser,
);
).thenAnswer((_) async => AlbumStub.sharedWithUser);
when(
() => albumRepository.addUsers(
@@ -198,14 +167,9 @@ void main() {
),
).thenAnswer((_) async => AlbumStub.emptyAlbum);
when(
() => albumRepository.update(AlbumStub.emptyAlbum),
).thenAnswer((_) async => AlbumStub.emptyAlbum);
when(() => albumRepository.update(AlbumStub.emptyAlbum)).thenAnswer((_) async => AlbumStub.emptyAlbum);
final result = await sut.addUsers(
AlbumStub.emptyAlbum,
[UserStub.user2.id],
);
final result = await sut.addUsers(AlbumStub.emptyAlbum, [UserStub.user2.id]);
expect(result, true);
});
+3 -5
View File
@@ -81,9 +81,7 @@ void main() {
final upsertExifCallback = verify(() => syncService.upsertAssetsWithExif(captureAny()));
upsertExifCallback.called(1);
final receivedAssets = upsertExifCallback.captured.firstOrNull as List<Object>? ?? [];
final receivedDatetime = receivedAssets.cast<Asset>().map(
(a) => a.exifInfo?.dateTimeOriginal ?? DateTime(0),
);
final receivedDatetime = receivedAssets.cast<Asset>().map((a) => a.exifInfo?.dateTimeOriginal ?? DateTime(0));
expect(receivedDatetime.every((d) => d == dateTime), isTrue);
});
@@ -97,8 +95,8 @@ void main() {
upsertExifCallback.called(1);
final receivedAssets = upsertExifCallback.captured.firstOrNull as List<Object>? ?? [];
final receivedCoords = receivedAssets.cast<Asset>().map(
(a) => LatLng(a.exifInfo?.latitude ?? 0, a.exifInfo?.longitude ?? 0),
);
(a) => LatLng(a.exifInfo?.latitude ?? 0, a.exifInfo?.longitude ?? 0),
);
expect(receivedCoords.every((l) => l == latLng), isTrue);
});
});
+24 -60
View File
@@ -95,10 +95,7 @@ void main() {
when(() => apiService.resolveAndSetEndpoint(testUrl)).thenThrow(Exception('Invalid URL'));
expect(
() async => await sut.validateServerUrl(testUrl),
throwsA(isA<Exception>()),
);
expect(() async => await sut.validateServerUrl(testUrl), throwsA(isA<Exception>()));
verify(() => apiService.resolveAndSetEndpoint(testUrl)).called(1);
verifyNever(() => apiService.setDeviceInfoHeader());
@@ -109,10 +106,7 @@ void main() {
when(() => apiService.resolveAndSetEndpoint(testUrl)).thenThrow(Exception('Server is not reachable'));
expect(
() async => await sut.validateServerUrl(testUrl),
throwsA(isA<Exception>()),
);
expect(() async => await sut.validateServerUrl(testUrl), throwsA(isA<Exception>()));
verify(() => apiService.resolveAndSetEndpoint(testUrl)).called(1);
verifyNever(() => apiService.setDeviceInfoHeader());
@@ -126,10 +120,7 @@ void main() {
when(() => authRepository.clearLocalData()).thenAnswer((_) => Future.value(null));
when(() => uploadService.cancelBackup()).thenAnswer((_) => Future.value(1));
when(
() => appSettingsService.setSetting(
AppSettingsEnum.enableBackup,
false,
),
() => appSettingsService.setSetting(AppSettingsEnum.enableBackup, false),
).thenAnswer((_) => Future.value(null));
await sut.logout();
@@ -144,10 +135,7 @@ void main() {
when(() => authRepository.clearLocalData()).thenAnswer((_) => Future.value(null));
when(() => uploadService.cancelBackup()).thenAnswer((_) => Future.value(1));
when(
() => appSettingsService.setSetting(
AppSettingsEnum.enableBackup,
false,
),
() => appSettingsService.setSetting(AppSettingsEnum.enableBackup, false),
).thenAnswer((_) => Future.value(null));
await sut.logout();
@@ -176,8 +164,9 @@ void main() {
when(() => authRepository.getEndpointSwitchingFeature()).thenReturn(true);
when(() => authRepository.getPreferredWifiName()).thenReturn('TestWifi');
when(() => authRepository.getLocalEndpoint()).thenReturn('http://local.endpoint');
when(() => apiService.resolveAndSetEndpoint('http://local.endpoint'))
.thenAnswer((_) async => 'http://local.endpoint');
when(
() => apiService.resolveAndSetEndpoint('http://local.endpoint'),
).thenAnswer((_) async => 'http://local.endpoint');
final result = await sut.setOpenApiServiceEndpoint();
@@ -192,12 +181,9 @@ void main() {
test('Should set external endpoint if wifi name not matching', () async {
when(() => authRepository.getEndpointSwitchingFeature()).thenReturn(true);
when(() => authRepository.getPreferredWifiName()).thenReturn('DifferentWifi');
when(() => authRepository.getExternalEndpointList()).thenReturn([
const AuxilaryEndpoint(
url: 'https://external.endpoint',
status: AuxCheckStatus.valid,
),
]);
when(
() => authRepository.getExternalEndpointList(),
).thenReturn([const AuxilaryEndpoint(url: 'https://external.endpoint', status: AuxCheckStatus.valid)]);
when(
() => apiService.resolveAndSetEndpoint('https://external.endpoint'),
).thenAnswer((_) async => 'https://external.endpoint/api');
@@ -209,23 +195,15 @@ void main() {
verify(() => networkService.getWifiName()).called(1);
verify(() => authRepository.getPreferredWifiName()).called(1);
verify(() => authRepository.getExternalEndpointList()).called(1);
verify(
() => apiService.resolveAndSetEndpoint('https://external.endpoint'),
).called(1);
verify(() => apiService.resolveAndSetEndpoint('https://external.endpoint')).called(1);
});
test('Should set second external endpoint if the first throw any error', () async {
when(() => authRepository.getEndpointSwitchingFeature()).thenReturn(true);
when(() => authRepository.getPreferredWifiName()).thenReturn('DifferentWifi');
when(() => authRepository.getExternalEndpointList()).thenReturn([
const AuxilaryEndpoint(
url: 'https://external.endpoint',
status: AuxCheckStatus.valid,
),
const AuxilaryEndpoint(
url: 'https://external.endpoint2',
status: AuxCheckStatus.valid,
),
const AuxilaryEndpoint(url: 'https://external.endpoint', status: AuxCheckStatus.valid),
const AuxilaryEndpoint(url: 'https://external.endpoint2', status: AuxCheckStatus.valid),
]);
when(
@@ -242,23 +220,15 @@ void main() {
verify(() => networkService.getWifiName()).called(1);
verify(() => authRepository.getPreferredWifiName()).called(1);
verify(() => authRepository.getExternalEndpointList()).called(1);
verify(
() => apiService.resolveAndSetEndpoint('https://external.endpoint2'),
).called(1);
verify(() => apiService.resolveAndSetEndpoint('https://external.endpoint2')).called(1);
});
test('Should set second external endpoint if the first throw ApiException', () async {
when(() => authRepository.getEndpointSwitchingFeature()).thenReturn(true);
when(() => authRepository.getPreferredWifiName()).thenReturn('DifferentWifi');
when(() => authRepository.getExternalEndpointList()).thenReturn([
const AuxilaryEndpoint(
url: 'https://external.endpoint',
status: AuxCheckStatus.valid,
),
const AuxilaryEndpoint(
url: 'https://external.endpoint2',
status: AuxCheckStatus.valid,
),
const AuxilaryEndpoint(url: 'https://external.endpoint', status: AuxCheckStatus.valid),
const AuxilaryEndpoint(url: 'https://external.endpoint2', status: AuxCheckStatus.valid),
]);
when(
@@ -275,17 +245,16 @@ void main() {
verify(() => networkService.getWifiName()).called(1);
verify(() => authRepository.getPreferredWifiName()).called(1);
verify(() => authRepository.getExternalEndpointList()).called(1);
verify(
() => apiService.resolveAndSetEndpoint('https://external.endpoint2'),
).called(1);
verify(() => apiService.resolveAndSetEndpoint('https://external.endpoint2')).called(1);
});
test('Should handle error when setting local connection', () async {
when(() => authRepository.getEndpointSwitchingFeature()).thenReturn(true);
when(() => authRepository.getPreferredWifiName()).thenReturn('TestWifi');
when(() => authRepository.getLocalEndpoint()).thenReturn('http://local.endpoint');
when(() => apiService.resolveAndSetEndpoint('http://local.endpoint'))
.thenThrow(Exception('Local endpoint error'));
when(
() => apiService.resolveAndSetEndpoint('http://local.endpoint'),
).thenThrow(Exception('Local endpoint error'));
final result = await sut.setOpenApiServiceEndpoint();
@@ -300,12 +269,9 @@ void main() {
test('Should handle error when setting external connection', () async {
when(() => authRepository.getEndpointSwitchingFeature()).thenReturn(true);
when(() => authRepository.getPreferredWifiName()).thenReturn('DifferentWifi');
when(() => authRepository.getExternalEndpointList()).thenReturn([
const AuxilaryEndpoint(
url: 'https://external.endpoint',
status: AuxCheckStatus.valid,
),
]);
when(
() => authRepository.getExternalEndpointList(),
).thenReturn([const AuxilaryEndpoint(url: 'https://external.endpoint', status: AuxCheckStatus.valid)]);
when(
() => apiService.resolveAndSetEndpoint('https://external.endpoint'),
).thenThrow(Exception('External endpoint error'));
@@ -317,9 +283,7 @@ void main() {
verify(() => networkService.getWifiName()).called(1);
verify(() => authRepository.getPreferredWifiName()).called(1);
verify(() => authRepository.getExternalEndpointList()).called(1);
verify(
() => apiService.resolveAndSetEndpoint('https://external.endpoint'),
).called(1);
verify(() => apiService.resolveAndSetEndpoint('https://external.endpoint')).called(1);
});
});
}
+25 -29
View File
@@ -22,23 +22,22 @@ void main() {
group('fillAlbumWithDatabaseEntities', () {
test('remote album with owner, thumbnail, sharedUsers and assets', () async {
final Album album = Album(
name: "album-with-two-assets-and-two-users",
localId: "album-with-two-assets-and-two-users-local",
remoteId: "album-with-two-assets-and-two-users-remote",
createdAt: DateTime(2001),
modifiedAt: DateTime(2010),
shared: true,
activityEnabled: true,
startDate: DateTime(2019),
endDate: DateTime(2020),
)
..remoteThumbnailAssetId = AssetStub.image1.remoteId
..assets.addAll([AssetStub.image1, AssetStub.image1])
..owner.value = User.fromDto(UserStub.user1)
..sharedUsers.addAll(
[User.fromDto(UserStub.admin), User.fromDto(UserStub.admin)],
);
final Album album =
Album(
name: "album-with-two-assets-and-two-users",
localId: "album-with-two-assets-and-two-users-local",
remoteId: "album-with-two-assets-and-two-users-remote",
createdAt: DateTime(2001),
modifiedAt: DateTime(2010),
shared: true,
activityEnabled: true,
startDate: DateTime(2019),
endDate: DateTime(2020),
)
..remoteThumbnailAssetId = AssetStub.image1.remoteId
..assets.addAll([AssetStub.image1, AssetStub.image1])
..owner.value = User.fromDto(UserStub.user1)
..sharedUsers.addAll([User.fromDto(UserStub.admin), User.fromDto(UserStub.admin)]);
when(() => userRepository.getByUserId(any())).thenAnswer((_) async => UserStub.admin);
when(() => userRepository.getByUserId(any())).thenAnswer((_) async => UserStub.admin);
@@ -52,23 +51,20 @@ void main() {
await sut.fillAlbumWithDatabaseEntities(album);
expect(album.owner.value?.toDto(), UserStub.admin);
expect(album.thumbnail.value, AssetStub.image1);
expect(
album.remoteUsers.map((u) => u.toDto()).toSet(),
{UserStub.user1, UserStub.user2},
);
expect(album.remoteUsers.map((u) => u.toDto()).toSet(), {UserStub.user1, UserStub.user2});
expect(album.remoteAssets.toSet(), {AssetStub.image1, AssetStub.image2});
});
test('remote album without any info', () async {
makeEmptyAlbum() => Album(
name: "album-without-info",
localId: "album-without-info-local",
remoteId: "album-without-info-remote",
createdAt: DateTime(2001),
modifiedAt: DateTime(2010),
shared: false,
activityEnabled: false,
);
name: "album-without-info",
localId: "album-without-info-local",
remoteId: "album-without-info-remote",
createdAt: DateTime(2001),
modifiedAt: DateTime(2010),
shared: false,
activityEnabled: false,
);
final album = makeEmptyAlbum();
await sut.fillAlbumWithDatabaseEntities(album);
+29 -71
View File
@@ -31,15 +31,10 @@ void main() {
mockBackgroundService = MockBackgroundService();
mockDeviceAssetRepository = MockDeviceAssetRepository();
sut = HashService(
deviceAssetRepository: mockDeviceAssetRepository,
backgroundService: mockBackgroundService,
);
sut = HashService(deviceAssetRepository: mockDeviceAssetRepository, backgroundService: mockBackgroundService);
when(() => mockDeviceAssetRepository.transaction<Null>(any())).thenAnswer((_) async {
final capturedCallback = verify(
() => mockDeviceAssetRepository.transaction<Null>(captureAny()),
).captured;
final capturedCallback = verify(() => mockDeviceAssetRepository.transaction<Null>(captureAny())).captured;
// Invoke the transaction callback
await (capturedCallback.firstOrNull as Future<Null> Function()?)?.call();
});
@@ -53,17 +48,13 @@ void main() {
when(() => mockBackgroundService.digestFiles([file.path])).thenAnswer((_) async => [hash]);
// No DB entries for this asset
when(
() => mockDeviceAssetRepository.getByIds([AssetStub.image1.localId!]),
).thenAnswer((_) async => []);
when(() => mockDeviceAssetRepository.getByIds([AssetStub.image1.localId!])).thenAnswer((_) async => []);
final result = await sut.hashAssets([mockAsset]);
// Verify we stored the new hash in DB
when(() => mockDeviceAssetRepository.transaction<Null>(any())).thenAnswer((_) async {
final capturedCallback = verify(
() => mockDeviceAssetRepository.transaction<Null>(captureAny()),
).captured;
final capturedCallback = verify(() => mockDeviceAssetRepository.transaction<Null>(captureAny())).captured;
// Invoke the transaction callback
await (capturedCallback.firstOrNull as Future<Null> Function()?)?.call();
verify(
@@ -73,10 +64,7 @@ void main() {
).called(1);
verify(() => mockDeviceAssetRepository.deleteIds([])).called(1);
});
expect(
result,
[AssetStub.image1.copyWith(checksum: base64.encode(hash))],
);
expect(result, [AssetStub.image1.copyWith(checksum: base64.encode(hash))]);
});
});
@@ -84,15 +72,9 @@ void main() {
test("when the asset is not modified", () async {
final hash = utf8.encode("image1-hash");
when(
() => mockDeviceAssetRepository.getByIds([AssetStub.image1.localId!]),
).thenAnswer(
when(() => mockDeviceAssetRepository.getByIds([AssetStub.image1.localId!])).thenAnswer(
(_) async => [
DeviceAsset(
assetId: AssetStub.image1.localId!,
hash: hash,
modifiedTime: AssetStub.image1.fileModifiedAt,
),
DeviceAsset(assetId: AssetStub.image1.localId!, hash: hash, modifiedTime: AssetStub.image1.fileModifiedAt),
],
);
final result = await sut.hashAssets([AssetStub.image1]);
@@ -102,9 +84,7 @@ void main() {
verifyNever(() => mockDeviceAssetRepository.updateAll(any()));
verifyNever(() => mockDeviceAssetRepository.deleteIds(any()));
expect(result, [
AssetStub.image1.copyWith(checksum: base64.encode(hash)),
]);
expect(result, [AssetStub.image1.copyWith(checksum: base64.encode(hash))]);
});
test("hashed successful when asset is modified", () async {
@@ -118,9 +98,7 @@ void main() {
final result = await sut.hashAssets([mockAsset]);
when(() => mockDeviceAssetRepository.transaction<Null>(any())).thenAnswer((_) async {
final capturedCallback = verify(
() => mockDeviceAssetRepository.transaction<Null>(captureAny()),
).captured;
final capturedCallback = verify(() => mockDeviceAssetRepository.transaction<Null>(captureAny())).captured;
// Invoke the transaction callback
await (capturedCallback.firstOrNull as Future<Null> Function()?)?.call();
verify(
@@ -133,9 +111,7 @@ void main() {
verify(() => mockBackgroundService.digestFiles([file.path])).called(1);
expect(result, [
AssetStub.image1.copyWith(checksum: base64.encode(hash)),
]);
expect(result, [AssetStub.image1.copyWith(checksum: base64.encode(hash))]);
});
});
@@ -161,18 +137,14 @@ void main() {
verifyNever(() => mockBackgroundService.digestFiles(any()));
verifyNever(() => mockBackgroundService.digestFile(any()));
verifyNever(() => mockDeviceAssetRepository.updateAll(any()));
verify(
() => mockDeviceAssetRepository.deleteIds([AssetStub.image1.localId!]),
).called(1);
verify(() => mockDeviceAssetRepository.deleteIds([AssetStub.image1.localId!])).called(1);
expect(result, isEmpty);
});
test("cleanups DeviceAsset when hashing failed", () async {
when(() => mockDeviceAssetRepository.transaction<Null>(any())).thenAnswer((_) async {
final capturedCallback = verify(
() => mockDeviceAssetRepository.transaction<Null>(captureAny()),
).captured;
final capturedCallback = verify(() => mockDeviceAssetRepository.transaction<Null>(captureAny())).captured;
// Invoke the transaction callback
await (capturedCallback.firstOrNull as Future<Null> Function()?)?.call();
@@ -194,9 +166,7 @@ void main() {
// To avoid this, we capture the callback and execute it within the transaction stub itself
// and verify the results inside the transaction stub
verify(() => mockDeviceAssetRepository.updateAll([])).called(1);
verify(
() => mockDeviceAssetRepository.deleteIds([AssetStub.image1.localId!]),
).called(1);
verify(() => mockDeviceAssetRepository.deleteIds([AssetStub.image1.localId!])).called(1);
});
when(() => mockBackgroundService.digestFiles([file.path])).thenAnswer(
@@ -243,14 +213,11 @@ void main() {
verify(() => mockBackgroundService.digestFiles([file1.path, file2.path])).called(1);
verify(() => mockBackgroundService.digestFiles([file3.path])).called(1);
expect(
result,
[
AssetStub.image1.copyWith(checksum: base64.encode(hash1)),
AssetStub.image2.copyWith(checksum: base64.encode(hash2)),
AssetStub.image3.copyWith(checksum: base64.encode(hash3)),
],
);
expect(result, [
AssetStub.image1.copyWith(checksum: base64.encode(hash1)),
AssetStub.image2.copyWith(checksum: base64.encode(hash2)),
AssetStub.image3.copyWith(checksum: base64.encode(hash3)),
]);
});
test("processes assets in batches when file limit is reached", () async {
@@ -283,14 +250,11 @@ void main() {
verify(() => mockBackgroundService.digestFiles([file2.path])).called(1);
verify(() => mockBackgroundService.digestFiles([file3.path])).called(1);
expect(
result,
[
AssetStub.image1.copyWith(checksum: base64.encode(hash1)),
AssetStub.image2.copyWith(checksum: base64.encode(hash2)),
AssetStub.image3.copyWith(checksum: base64.encode(hash3)),
],
);
expect(result, [
AssetStub.image1.copyWith(checksum: base64.encode(hash1)),
AssetStub.image2.copyWith(checksum: base64.encode(hash2)),
AssetStub.image3.copyWith(checksum: base64.encode(hash3)),
]);
});
test("HashService: Sort & Process different states", () async {
@@ -345,15 +309,10 @@ void main() {
test("handles all file access failures", () async {
// No DB entries
when(
() => mockDeviceAssetRepository.getByIds(
[AssetStub.image1.localId!, AssetStub.image2.localId!],
),
() => mockDeviceAssetRepository.getByIds([AssetStub.image1.localId!, AssetStub.image2.localId!]),
).thenAnswer((_) async => []);
final result = await sut.hashAssets([
AssetStub.image1,
AssetStub.image2,
]);
final result = await sut.hashAssets([AssetStub.image1, AssetStub.image2]);
verifyNever(() => mockBackgroundService.digestFiles(any()));
verifyNever(() => mockDeviceAssetRepository.updateAll(any()));
@@ -363,9 +322,7 @@ void main() {
});
}
Future<(Asset, File, DeviceAsset, Uint8List)> _createAssetMock(
Asset asset,
) async {
Future<(Asset, File, DeviceAsset, Uint8List)> _createAssetMock(Asset asset) async {
final random = Random();
final hash = Uint8List.fromList(List.generate(20, (i) => random.nextInt(255)));
final mockAsset = MockAsset();
@@ -384,8 +341,9 @@ Future<(Asset, File, DeviceAsset, Uint8List)> _createAssetMock(
when(() => mockAsset.fileName).thenReturn(asset.fileName);
when(() => mockAsset.fileCreatedAt).thenReturn(asset.fileCreatedAt);
when(() => mockAsset.fileModifiedAt).thenReturn(asset.fileModifiedAt);
when(() => mockAsset.copyWith(checksum: any(named: "checksum")))
.thenReturn(asset.copyWith(checksum: base64.encode(hash)));
when(
() => mockAsset.copyWith(checksum: any(named: "checksum")),
).thenReturn(asset.copyWith(checksum: base64.encode(hash)));
when(() => mockAsset.local).thenAnswer((_) => mockAssetEntity);
when(() => mockAssetEntity.originFile).thenAnswer((_) async => file);
+11 -16
View File
@@ -73,11 +73,7 @@ abstract final class TestUtils {
List<Override> overrides = const [],
List<ProviderObserver>? observers,
}) {
final container = ProviderContainer(
parent: parent,
overrides: overrides,
observers: observers,
);
final container = ProviderContainer(parent: parent, overrides: overrides, observers: observers);
// Dispose on test end
addTearDown(container.dispose);
@@ -94,23 +90,22 @@ abstract final class TestUtils {
// Workaround till the following issue is resolved
// https://github.com/dart-lang/test/issues/2307
static T fakeAsync<T>(
Future<T> Function(FakeAsync _) callback, {
DateTime? initialTime,
}) {
static T fakeAsync<T>(Future<T> Function(FakeAsync _) callback, {DateTime? initialTime}) {
late final T result;
Object? error;
StackTrace? stack;
FakeAsync(initialTime: initialTime).run((FakeAsync async) {
bool shouldPump = true;
unawaited(
callback(async).then<void>(
(value) => result = value,
onError: (e, s) {
error = e;
stack = s;
},
).whenComplete(() => shouldPump = false),
callback(async)
.then<void>(
(value) => result = value,
onError: (e, s) {
error = e;
stack = s;
},
)
.whenComplete(() => shouldPump = false),
);
while (shouldPump) {
+1 -4
View File
@@ -18,10 +18,7 @@ extension PumpConsumerWidget on WidgetTester {
return pumpWidget(
ProviderScope(
overrides: overrides,
child: MaterialApp(
debugShowCheckedModeBanner: false,
home: Material(child: widget),
),
child: MaterialApp(debugShowCheckedModeBanner: false, home: Material(child: widget)),
),
duration: duration,
phase: phase,