From be7fee433ca083854ac4676b2b659894b758094d Mon Sep 17 00:00:00 2001 From: Lockie Richter Date: Wed, 29 Jan 2025 20:17:53 +1030 Subject: [PATCH 1/5] Fix broken tests --- test/repositories/user_settings_repository_test.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/repositories/user_settings_repository_test.dart b/test/repositories/user_settings_repository_test.dart index 663b238..5adb1ae 100644 --- a/test/repositories/user_settings_repository_test.dart +++ b/test/repositories/user_settings_repository_test.dart @@ -73,7 +73,7 @@ void main() async { test('Then the byStartDate timeline sort strategy is returned', () { final timelineSortStrategy = userSettingsRepository.getTimelineSortStrategy(); - expect(timelineSortStrategy, TimelineSortStrategy.byStartDate.name); + expect(timelineSortStrategy, TimelineSortStrategy.byStartDate); }); }); }); @@ -84,7 +84,7 @@ void main() async { ); final timelineSortStrategy = userSettingsRepository.getTimelineSortStrategy(); - expect(timelineSortStrategy, TimelineSortStrategy.byStartDate.name); + expect(timelineSortStrategy, TimelineSortStrategy.byStartDate); }); }); }); From 338ae2ac276a0ab19125fd693e5387d7724a4663 Mon Sep 17 00:00:00 2001 From: Lockie Richter Date: Wed, 29 Jan 2025 21:01:14 +1030 Subject: [PATCH 2/5] Add some missing providers tests --- assets/data/test-repository.json | 5 + ios/Podfile.lock | 19 ---- lib/providers/book_label.dart | 11 +- test/providers/book_label_test.dart | 105 ++++++++++++++++++ test/providers/recommendations_test.dart | 57 ++++++++++ .../providers/recommendations_test.mocks.dart | 74 ++++++++++++ 6 files changed, 250 insertions(+), 21 deletions(-) create mode 100644 test/providers/book_label_test.dart create mode 100644 test/providers/recommendations_test.dart create mode 100644 test/providers/recommendations_test.mocks.dart diff --git a/assets/data/test-repository.json b/assets/data/test-repository.json index d280e31..83df2e4 100644 --- a/assets/data/test-repository.json +++ b/assets/data/test-repository.json @@ -1648,6 +1648,11 @@ "hexColor": "#ffe91e63", "id": "-1", "title": "Cosmere" + }, + { + "hexColor": "ff7d20ff", + "id": "-NpiIcSZtSUTijwQSriX", + "title": "purple" } ], "language": "en", diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 23a9e01..0ffa96b 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -5,9 +5,6 @@ PODS: - AppAuth/Core (1.7.6) - AppAuth/ExternalUserAgent (1.7.6): - AppAuth/Core - - cupertino_http (0.0.1): - - Flutter - - FlutterMacOS - device_info_plus (0.0.1): - Flutter - DKImagePickerController/Core (4.3.9): @@ -240,8 +237,6 @@ PODS: - nanopb/encode (= 3.30910.0) - nanopb/decode (3.30910.0) - nanopb/encode (3.30910.0) - - objective_c (0.0.1): - - Flutter - package_info_plus (0.4.5): - Flutter - path_provider_foundation (0.0.1): @@ -250,8 +245,6 @@ PODS: - PromisesObjC (2.4.0) - PromisesSwift (2.4.0): - PromisesObjC (= 2.4.0) - - realm (20.0.1): - - Flutter - RecaptchaInterop (100.0.0) - SDWebImage (5.20.0): - SDWebImage/Core (= 5.20.0) @@ -269,7 +262,6 @@ PODS: - Flutter DEPENDENCIES: - - cupertino_http (from `.symlinks/plugins/cupertino_http/darwin`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - file_picker (from `.symlinks/plugins/file_picker/ios`) - firebase_analytics (from `.symlinks/plugins/firebase_analytics/ios`) @@ -282,10 +274,8 @@ DEPENDENCIES: - flutter_barcode_scanner (from `.symlinks/plugins/flutter_barcode_scanner/ios`) - google_sign_in_ios (from `.symlinks/plugins/google_sign_in_ios/darwin`) - image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`) - - objective_c (from `.symlinks/plugins/objective_c/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - - realm (from `.symlinks/plugins/realm/ios`) - share_plus (from `.symlinks/plugins/share_plus/ios`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`) @@ -326,8 +316,6 @@ SPEC REPOS: - SwiftyGif EXTERNAL SOURCES: - cupertino_http: - :path: ".symlinks/plugins/cupertino_http/darwin" device_info_plus: :path: ".symlinks/plugins/device_info_plus/ios" file_picker: @@ -352,14 +340,10 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/google_sign_in_ios/darwin" image_picker_ios: :path: ".symlinks/plugins/image_picker_ios/ios" - objective_c: - :path: ".symlinks/plugins/objective_c/ios" package_info_plus: :path: ".symlinks/plugins/package_info_plus/ios" path_provider_foundation: :path: ".symlinks/plugins/path_provider_foundation/darwin" - realm: - :path: ".symlinks/plugins/realm/ios" share_plus: :path: ".symlinks/plugins/share_plus/ios" shared_preferences_foundation: @@ -371,7 +355,6 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: AppAuth: d4f13a8fe0baf391b2108511793e4b479691fb73 - cupertino_http: 94ac07f5ff090b8effa6c5e2c47871d48ab7c86c device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 @@ -409,12 +392,10 @@ SPEC CHECKSUMS: image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a leveldb-library: cc8b8f8e013647a295ad3f8cd2ddf49a6f19be19 nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 - objective_c: 89e720c30d716b036faf9c9684022048eee1eee2 package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851 - realm: 6ccbb161b166bb539c3204f40e58763c293e4802 RecaptchaInterop: 7d1a4a01a6b2cb1610a47ef3f85f0c411434cb21 SDWebImage: 73c6079366fea25fa4bb9640d5fb58f0893facd8 share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a diff --git a/lib/providers/book_label.dart b/lib/providers/book_label.dart index 5e47d50..6011dd1 100644 --- a/lib/providers/book_label.dart +++ b/lib/providers/book_label.dart @@ -2,6 +2,7 @@ import 'package:dantex/models/book.dart'; import 'package:dantex/models/book_label.dart'; import 'package:dantex/providers/book.dart'; import 'package:dantex/providers/repository.dart'; +import 'package:dantex/providers/service.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -11,11 +12,17 @@ part 'book_label.g.dart'; List booksWithLabel(Ref ref, String labelId) { return ref.watch(allBooksProvider).when( data: (books) { - return books + final thing = books .where((book) => book.labels.map((e) => e.id).contains(labelId)) .toList(); + return thing; + }, + error: (e, s) { + ref + .read(loggerProvider) + .e('Failed to get all books', error: e, stackTrace: s); + return []; }, - error: (e, s) => [], loading: () => [], ); } diff --git a/test/providers/book_label_test.dart b/test/providers/book_label_test.dart new file mode 100644 index 0000000..cb1b8c9 --- /dev/null +++ b/test/providers/book_label_test.dart @@ -0,0 +1,105 @@ +import 'package:dantex/providers/auth.dart'; +import 'package:dantex/providers/book.dart'; +import 'package:dantex/providers/book_label.dart'; +import 'package:dantex/providers/firebase.dart'; +import 'package:firebase_auth_mocks/firebase_auth_mocks.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../test_utilities.dart'; + +void main() async { + group('Given an allBookLabelsProvider', () { + group('When the repo has book labels', () { + test('Then the provider returns all book labels', () async { + final mockDatabase = await getMockDatabase(); + + final mockUser = MockUser( + uid: 'userId', + email: 'bob@somedomain.com', + displayName: 'Bob', + ); + + final container = createContainer( + overrides: [ + authStateChangesProvider.overrideWith( + (ref) => Stream.value(mockUser), + ), + firebaseDatabaseProvider.overrideWithValue(mockDatabase), + ], + ); + + final subscription = container.listen( + allBookLabelsProvider.future, + (_, __) {}, + ); + await container.pump(); + + final labels = await subscription.read(); + expect(labels, hasLength(1)); + }); + }); + }); + group('Given a booksWithLabelProvider', () { + group("When the label doesn't exist in the repo", () { + test('Then the provider returns an empty list', () async { + final mockBook = getMockBook().copyWith( + labels: [ + getMockBookLabel().copyWith(id: 'book-label-id'), + ], + ); + final container = createContainer( + overrides: [ + allBooksProvider.overrideWith((ref) => Stream.value([mockBook])), + ], + ); + final subscription = container.listen( + booksWithLabelProvider('non-existent-label-id'), + (_, __) {}, + ); + await container.pump(); + + final books = subscription.read(); + expect(books, isEmpty); + }); + }); + group('When the label does exist in the repo', () { + test('Then the provider returns the books with that label', () async { + final mockBook = getMockBook().copyWith( + labels: [ + getMockBookLabel().copyWith(id: 'book-label-id'), + ], + ); + final container = createContainer( + overrides: [ + allBooksProvider.overrideWith((ref) => Stream.value([mockBook])), + ], + ); + final subscription = container.listen( + booksWithLabelProvider('book-label-id'), + (_, __) {}, + ); + await container.pump(); + + final books = subscription.read(); + expect(books, hasLength(1)); + }); + }); + group('When the allBooksProvider throws and exception', () { + test('Then the provider returns an empty list', () async { + final container = createContainer( + overrides: [ + allBooksProvider.overrideWith((ref) => throw Exception()), + ], + ); + final subscription = container.listen( + booksWithLabelProvider('book-label-id'), + (_, __) {}, + ); + await container.pump(); + + final books = subscription.read(); + expect(books, isEmpty); + }); + }); + }); +} diff --git a/test/providers/recommendations_test.dart b/test/providers/recommendations_test.dart new file mode 100644 index 0000000..2aa294c --- /dev/null +++ b/test/providers/recommendations_test.dart @@ -0,0 +1,57 @@ +import 'package:dantex/models/book_recommendation.dart'; +import 'package:dantex/providers/recommendations.dart'; +import 'package:dantex/providers/repository.dart'; +import 'package:dantex/repositories/recommendations_repository.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import '../test_utilities.dart'; +import 'recommendations_test.mocks.dart'; + +@GenerateMocks([RecommendationsRepository]) +void main() async { + group('Given a recommendationsProvider', () { + group('When the repo has recommendations', () { + test('Then the provider returns all recommendations', () async { + final mockRepository = MockRecommendationsRepository(); + when(mockRepository.loadRecommendations()).thenAnswer( + (_) async => List.generate( + 3, + (index) => BookRecommendation( + suggestionId: 'suggestion$index', + suggester: BookRecommender(name: 'Suggester $index'), + suggestion: RecommendedBook( + title: 'Title $index', + subTitle: 'SubTitle $index', + author: 'Author $index', + state: 'State $index', + pageCount: 100 + index, + publishedDate: DateTime.now().toString(), + isbn: 'ISBN $index', + language: 'Language $index', + ), + recommendation: 'Recommendation $index', + ), + ), + ); + + final container = createContainer( + overrides: [ + recommendationsRepositoryProvider + .overrideWith((ref) => mockRepository), + ], + ); + + final subscription = container.listen( + recommendationsProvider.future, + (_, __) {}, + ); + await container.pump(); + + final recommendations = await subscription.read(); + expect(recommendations, hasLength(3)); + }); + }); + }); +} diff --git a/test/providers/recommendations_test.mocks.dart b/test/providers/recommendations_test.mocks.dart new file mode 100644 index 0000000..5ef6cb9 --- /dev/null +++ b/test/providers/recommendations_test.mocks.dart @@ -0,0 +1,74 @@ +// Mocks generated by Mockito 5.4.5 from annotations +// in dantex/test/providers/recommendations_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i3; + +import 'package:dantex/models/book.dart' as _i4; +import 'package:dantex/models/book_recommendation.dart' as _i5; +import 'package:dantex/repositories/recommendations_repository.dart' as _i2; +import 'package:mockito/mockito.dart' as _i1; + +// ignore_for_file: type=lint +// ignore_for_file: avoid_redundant_argument_values +// ignore_for_file: avoid_setters_without_getters +// ignore_for_file: comment_references +// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use_from_same_package +// ignore_for_file: implementation_imports +// ignore_for_file: invalid_use_of_visible_for_testing_member +// ignore_for_file: must_be_immutable +// ignore_for_file: prefer_const_constructors +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: camel_case_types +// ignore_for_file: subtype_of_sealed_class + +/// A class which mocks [RecommendationsRepository]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockRecommendationsRepository extends _i1.Mock + implements _i2.RecommendationsRepository { + MockRecommendationsRepository() { + _i1.throwOnMissingStub(this); + } + + @override + _i3.Future recommendBook( + _i4.Book? book, + String? recommendation, + ) => + (super.noSuchMethod( + Invocation.method( + #recommendBook, + [ + book, + recommendation, + ], + ), + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) as _i3.Future); + + @override + _i3.Future> loadRecommendations() => + (super.noSuchMethod( + Invocation.method( + #loadRecommendations, + [], + ), + returnValue: _i3.Future>.value( + <_i5.BookRecommendation>[]), + ) as _i3.Future>); + + @override + _i3.Future reportRecommendation(String? recommendationId) => + (super.noSuchMethod( + Invocation.method( + #reportRecommendation, + [recommendationId], + ), + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) as _i3.Future); +} From f24b6b744e174cfffdc1c633f41315b233f7fd76 Mon Sep 17 00:00:00 2001 From: Lockie Richter Date: Wed, 29 Jan 2025 21:50:34 +1030 Subject: [PATCH 3/5] Add more missing tests --- lib/providers/statistics.dart | 6 +- test/providers/statistics_test.dart | 109 ++++++++++++++++++++++++++++ 2 files changed, 113 insertions(+), 2 deletions(-) create mode 100644 test/providers/statistics_test.dart diff --git a/lib/providers/statistics.dart b/lib/providers/statistics.dart index 0a61bb5..ded16ee 100644 --- a/lib/providers/statistics.dart +++ b/lib/providers/statistics.dart @@ -11,10 +11,11 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'statistics.g.dart'; +typedef BookCount = ({int readLaterCount, int readingCount, int readCount}); const zeroBookCount = (readLaterCount: 0, readingCount: 0, readCount: 0); @riverpod -({int readLaterCount, int readingCount, int readCount}) bookCounts(Ref ref) { +BookCount bookCounts(Ref ref) { final books = ref.watch(allBooksProvider); return books.when( @@ -37,10 +38,11 @@ const zeroBookCount = (readLaterCount: 0, readingCount: 0, readCount: 0); ); } +typedef PageCount = ({int pagesWaiting, int pagesRead}); const zeroPageCount = (pagesWaiting: 0, pagesRead: 0); @riverpod -({int pagesWaiting, int pagesRead}) pageCounts(Ref ref) { +PageCount pageCounts(Ref ref) { final books = ref.watch(allBooksProvider); return books.when( diff --git a/test/providers/statistics_test.dart b/test/providers/statistics_test.dart new file mode 100644 index 0000000..39284d9 --- /dev/null +++ b/test/providers/statistics_test.dart @@ -0,0 +1,109 @@ +import 'package:dantex/models/book_state.dart'; +import 'package:dantex/providers/book.dart'; +import 'package:dantex/providers/statistics.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../test_utilities.dart'; + +void main() { + group('Given a bookCountsProvider', () { + group('When the allBooksProvider has data', () { + test('Then the correct book counts are returned', () async { + final mockBooks = List.generate( + 5, + (index) => getMockBook().copyWith( + id: 'book-$index', + state: index == 0 + ? BookState.wishlist + : index == 1 + ? BookState.readLater + : index == 2 + ? BookState.reading + : BookState.read, + ), + ); + final container = createContainer( + overrides: [ + allBooksProvider.overrideWith((ref) => Stream.value(mockBooks)), + ], + ); + final subscription = container.listen( + bookCountsProvider, + (_, __) {}, + ); + await container.pump(); + + final stats = subscription.read(); + expect(stats, (readLaterCount: 1, readingCount: 1, readCount: 2)); + }); + }); + group('When the allBooksDataProvider has an error', () { + test('Then zero book counts are returned', () async { + final container = createContainer( + overrides: [ + allBooksProvider.overrideWith((ref) => Stream.error('error')), + ], + ); + final subscription = container.listen( + bookCountsProvider, + (_, __) {}, + ); + await container.pump(); + + final stats = subscription.read(); + expect(stats, zeroBookCount); + }); + }); + }); + + group('Given a pageCountsProvider', () { + group('When the allBooksProvider has data', () { + test('Then the correct page counts are returned', () async { + final mockBooks = List.generate( + 5, + (index) => getMockBook().copyWith( + id: 'book-$index', + state: index == 0 + ? BookState.wishlist + : index == 1 + ? BookState.readLater + : index == 2 + ? BookState.reading + : BookState.read, + pageCount: 10, + ), + ); + final container = createContainer( + overrides: [ + allBooksProvider.overrideWith((ref) => Stream.value(mockBooks)), + ], + ); + final subscription = container.listen( + pageCountsProvider, + (_, __) {}, + ); + await container.pump(); + + final stats = subscription.read(); + expect(stats, (pagesWaiting: 10, pagesRead: 20)); + }); + }); + group('When the allBooksDataProvider has an error', () { + test('Then zero page counts are returned', () async { + final container = createContainer( + overrides: [ + allBooksProvider.overrideWith((ref) => Stream.error('error')), + ], + ); + final subscription = container.listen( + pageCountsProvider, + (_, __) {}, + ); + await container.pump(); + + final stats = subscription.read(); + expect(stats, zeroPageCount); + }); + }); + }); +} From d98f458b2bc47c5fc10851d7181cbd751a120930 Mon Sep 17 00:00:00 2001 From: Lockie Richter Date: Wed, 29 Jan 2025 21:51:47 +1030 Subject: [PATCH 4/5] Bump gradle version --- android/gradle/wrapper/gradle-wrapper.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 6b66533..fb5eb59 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-all.zip From 68691c8883756fbf3c7b8355cf8f4a02553b5461 Mon Sep 17 00:00:00 2001 From: Lockie Richter Date: Wed, 29 Jan 2025 23:15:59 +1030 Subject: [PATCH 5/5] Bump java version in android GitLab runner --- .github/workflows/main.yml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b4d9c81..dc642f9 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -11,13 +11,13 @@ jobs: - name: Setup Java uses: actions/setup-java@v2 with: - distribution: 'zulu' - java-version: '11' + distribution: "zulu" + java-version: "11" - name: Install Flutter uses: subosito/flutter-action@v2.12.0 with: - channel: 'stable' + channel: "stable" cache: true - name: Install dependencies @@ -46,13 +46,13 @@ jobs: - name: Setup Java uses: actions/setup-java@v2 with: - distribution: 'zulu' - java-version: '11' + distribution: "zulu" + java-version: "17" - name: Install Flutter uses: subosito/flutter-action@v2.12.0 with: - channel: 'stable' + channel: "stable" cache: true - name: Install dependencies @@ -81,13 +81,13 @@ jobs: - name: Setup Java uses: actions/setup-java@v2 with: - distribution: 'zulu' - java-version: '11' + distribution: "zulu" + java-version: "11" - name: Install Flutter uses: subosito/flutter-action@v2.12.0 with: - channel: 'stable' + channel: "stable" cache: true - name: Install dependencies