diff --git a/lib/api/movie_api.dart b/lib/api/movie_api.dart index 3d088b5..3df063c 100644 --- a/lib/api/movie_api.dart +++ b/lib/api/movie_api.dart @@ -1,7 +1,11 @@ import 'package:release_schedule/model/movie.dart'; -abstract class MovieApi { - Future> getUpcomingMovies(DateTime startDate, [int count]); - Future> searchForMovies(String searchTerm); - Future addMovieDetails(List movies); +class MovieApi { + Future> getUpcomingMovies(DateTime startDate, + [int count = 10]) async => + []; + + Future> searchForMovies(String searchTerm) async => []; + + Future addMovieDetails(List movies) async {} } diff --git a/lib/model/date_format.dart b/lib/model/date_format.dart index 427d208..bdbeb0e 100644 --- a/lib/model/date_format.dart +++ b/lib/model/date_format.dart @@ -14,7 +14,8 @@ String _durationToRelativeDateString(Duration duration) { return "Yesterday"; } if (duration.isNegative) { - return "${_durationApproximatedInWords(-duration)} ago"; + String result = _durationApproximatedInWords(-duration); + return "${result[0].toUpperCase()}${result.substring(1)} ago"; } else if (duration == Duration.zero) { return "Today"; } else { diff --git a/lib/model/movie.dart b/lib/model/movie.dart index d9de369..7f6db88 100644 --- a/lib/model/movie.dart +++ b/lib/model/movie.dart @@ -95,12 +95,12 @@ class MovieData extends ChangeNotifier { "releaseDates": releaseDatesByCountry, "genres": genres, "titles": titlesByCountry, - "reviews": reviews, + "reviews": reviews?.map((review) => review.toJsonEncodable()).toList(), }; } bool same(MovieData other) { - return title == other.title && releaseDate == other.releaseDate; + return title == other.title && releaseDate.date == other.releaseDate.date; } MovieData.fromJsonEncodable(Map json) @@ -112,13 +112,13 @@ class MovieData extends ChangeNotifier { ?.map((genre) => genre as String) .toList(), releaseDates: json["releaseDates"] != null - ? (json["releaseDates"] as List>) + ? (json["releaseDates"] as List) .map((release) => DateWithPrecisionAndCountry.fromJsonEncodable(release)) .toList() : null, reviews: json["reviews"] != null - ? (json["reviews"] as List>) + ? (json["reviews"] as List) .map((review) => Review.fromJsonEncodable(review)) .toList() : null, @@ -155,7 +155,9 @@ class DateWithPrecisionAndCountry { @override String toString() { String dateString = switch (precision) { - DatePrecision.decade || DatePrecision.year => date.year.toString(), + DatePrecision.decade => + "${DateFormat("yyyy").format(date).substring(0, 3)}0s", + DatePrecision.year => date.year.toString(), DatePrecision.month => DateFormat("MMMM yyyy").format(date), DatePrecision.day => DateFormat("MMMM d, yyyy").format(date), DatePrecision.hour => DateFormat("MMMM d, yyyy, HH").format(date), diff --git a/test/model/date_format_test.dart b/test/model/date_format_test.dart index 25e2aa1..facc529 100644 --- a/test/model/date_format_test.dart +++ b/test/model/date_format_test.dart @@ -21,16 +21,64 @@ void main() { expect(result, 'Yesterday'); }); - test('returns "in X days" for future dates', () { + test('returns "In 5 days" for a date 5 days in the future', () { final futureDate = DateTime.now().add(const Duration(days: 5)); final result = dateRelativeToNow(futureDate); expect(result, 'In 5 days'); }); - test('returns "X days ago" for past dates', () { + test('returns "5 days ago" for a date 5 days in the past', () { final pastDate = DateTime.now().subtract(const Duration(days: 5)); final result = dateRelativeToNow(pastDate); expect(result, '5 days ago'); }); + + test('returns "a week" for a date 7 days in the future', () { + final futureDate = DateTime.now().add(const Duration(days: 7)); + final result = dateRelativeToNow(futureDate); + expect(result, 'In a week'); + }); + + test('returns "a week" for a date 7 days in the past', () { + final pastDate = DateTime.now().subtract(const Duration(days: 7)); + final result = dateRelativeToNow(pastDate); + expect(result, 'A week ago'); + }); + + test('returns "a month" for a date 30 days in the future', () { + final futureDate = DateTime.now().add(const Duration(days: 30)); + final result = dateRelativeToNow(futureDate); + expect(result, 'In a month'); + }); + + test('returns "a month" for a date 30 days in the past', () { + final pastDate = DateTime.now().subtract(const Duration(days: 30)); + final result = dateRelativeToNow(pastDate); + expect(result, 'A month ago'); + }); + + test('returns "a year" for a date 365 days in the future', () { + final futureDate = DateTime.now().add(const Duration(days: 365)); + final result = dateRelativeToNow(futureDate); + expect(result, 'In a year'); + }); + + test('returns "a year" for a date 365 days in the past', () { + final pastDate = DateTime.now().subtract(const Duration(days: 365)); + final result = dateRelativeToNow(pastDate); + expect(result, 'A year ago'); + }); + + test('returns "a century" for a date 100 years in the future', () { + final futureDate = DateTime.now().add(const Duration(days: 365 * 100)); + final result = dateRelativeToNow(futureDate); + expect(result, 'In a century'); + }); + + test('returns "a century" for a date 100 years in the past', () { + final pastDate = DateTime.now().subtract(const Duration(days: 365 * 100)); + final result = dateRelativeToNow(pastDate); + expect(result, 'A century ago'); + }); }); } diff --git a/test/model/movie_manager_test.dart b/test/model/movie_manager_test.dart new file mode 100644 index 0000000..c949991 --- /dev/null +++ b/test/model/movie_manager_test.dart @@ -0,0 +1,139 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:release_schedule/api/movie_api.dart'; +import 'package:release_schedule/model/local_movie_storage.dart'; +import 'package:release_schedule/model/movie.dart'; +import 'package:release_schedule/model/movie_manager.dart'; + +void main() { + group('MovieManager', () { + late MovieManager movieManager; + + setUp(() { + movieManager = MovieManager( + MovieApi(), + LocalMovieStorage(), + ); + }); + + test('addMovies should add movies to the list', () { + final movies = [ + MovieData( + 'The Matrix', + DateWithPrecisionAndCountry(DateTime(1999, 3, 31), DatePrecision.day, + 'United States of America'), + ), + MovieData( + 'The Matrix Reloaded', + DateWithPrecisionAndCountry(DateTime(2003, 5, 7), DatePrecision.day, + 'United States of America'), + ), + ]; + + movieManager.addMovies(movies); + + expect(movieManager.movies, equals(movies)); + }); + + test('addMovies should add new movies', () { + final movies = [ + MovieData( + 'The Matrix', + DateWithPrecisionAndCountry(DateTime(1999, 3, 31), DatePrecision.day, + 'United States of America'), + ), + MovieData( + 'The Matrix Reloaded', + DateWithPrecisionAndCountry(DateTime(2003, 5, 7), DatePrecision.day, + 'United States of America'), + ), + ]; + + movieManager.addMovies(movies); + + final newMovies = [ + MovieData( + 'The Matrix Revolutions', + DateWithPrecisionAndCountry(DateTime(2003, 11, 5), DatePrecision.day, + 'United States of America'), + ), + ]; + + movieManager.addMovies(newMovies); + + expect(movieManager.movies, equals([...movies, ...newMovies])); + }); + + test("addMovies should sort movies by their release dates", () { + final movies = [ + MovieData( + 'The Matrix Reloaded', + DateWithPrecisionAndCountry(DateTime(2003, 5, 7), DatePrecision.day, + 'United States of America'), + ), + MovieData( + 'The Matrix', + DateWithPrecisionAndCountry(DateTime(1999, 3, 31), DatePrecision.day, + 'United States of America'), + ), + ]; + + movieManager.addMovies(movies); + + expect(movieManager.movies, equals([...movies.reversed])); + }); + + test( + 'when a movie is modified and it\'s date is changed the movies should be resorted', + () async { + final movies = [ + MovieData( + 'The Matrix Reloaded', + DateWithPrecisionAndCountry(DateTime(1998, 5, 7), DatePrecision.day, + 'United States of America'), + ), + MovieData( + 'The Matrix', + DateWithPrecisionAndCountry(DateTime(1999, 3, 31), DatePrecision.day, + 'United States of America'), + ), + ]; + + movieManager.addMovies(movies); + + final movie = movieManager.movies.first; + movie.setDetails( + releaseDate: DateWithPrecisionAndCountry(DateTime(2003, 5, 7), + DatePrecision.day, 'United States of America'), + ); + await Future.delayed(const Duration(milliseconds: 100)); + + expect(movieManager.movies, equals([...movies.reversed])); + }); + + test('removeMoviesWhere should remove movies from the list', () { + final movies = [ + MovieData( + 'The Matrix', + DateWithPrecisionAndCountry(DateTime(1999, 3, 31), DatePrecision.day, + 'United States of America'), + ), + MovieData( + 'The Matrix Reloaded', + DateWithPrecisionAndCountry(DateTime(2003, 5, 7), DatePrecision.day, + 'United States of America'), + ), + ]; + MovieData notRemoved = MovieData( + 'Harry Potter and the Philosopher\'s Stone', + DateWithPrecisionAndCountry( + DateTime(2001, 11, 4), DatePrecision.day, 'United Kingdom'), + ); + + movieManager.addMovies([...movies, notRemoved]); + + movieManager.removeMoviesWhere((movie) => movie.title.contains('Matrix')); + + expect(movieManager.movies, equals([notRemoved])); + }); + }); +} diff --git a/test/model/movie_test.dart b/test/model/movie_test.dart new file mode 100644 index 0000000..590fdc1 --- /dev/null +++ b/test/model/movie_test.dart @@ -0,0 +1,157 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:release_schedule/model/movie.dart'; + +void main() { + group('MovieData', () { + test('updateWithNew() updates all fields', () { + final movie1 = MovieData('Title 1', + DateWithPrecisionAndCountry(DateTime.now(), DatePrecision.day, 'US')); + final movie2 = MovieData('Title 2', + DateWithPrecisionAndCountry(DateTime.now(), DatePrecision.day, 'UK')); + movie2.setDetails(releaseDates: [ + DateWithPrecisionAndCountry(DateTime.now(), DatePrecision.day, 'US') + ], genres: [ + 'Action', + 'Adventure' + ], titles: [ + (title: 'Title 2', language: 'en') + ], reviews: [ + Review('8.5', 'John Doe', DateTime.now(), 100) + ]); + movie1.updateWithNew(movie2); + expect(movie1.title, equals('Title 2')); + expect(movie1.releaseDate.country, equals('UK')); + expect(movie1.releaseDates!.length, equals(1)); + expect(movie1.releaseDates![0].country, equals('US')); + expect(movie1.genres!.length, equals(2)); + expect(movie1.genres![0], equals('Action')); + expect(movie1.genres![1], equals('Adventure')); + expect(movie1.titles!.length, equals(1)); + expect(movie1.titles![0].title, equals('Title 2')); + expect(movie1.titles![0].language, equals('en')); + expect(movie1.reviews!.length, equals(1)); + expect(movie1.reviews![0].score, equals('8.5')); + expect(movie1.reviews![0].by, equals('John Doe')); + expect(movie1.reviews![0].count, equals(100)); + }); + + test('same() returns true for same title and release date', () { + final movie1 = MovieData( + 'Title 1', + DateWithPrecisionAndCountry( + DateTime(2023, 1, 1), DatePrecision.day, 'US')); + final movie2 = MovieData( + 'Title 1', + DateWithPrecisionAndCountry( + DateTime(2023, 1, 1), DatePrecision.day, 'US')); + expect(movie1.same(movie2), isTrue); + }); + + test('same() returns false for different title', () { + final movie1 = MovieData( + 'Title 1', + DateWithPrecisionAndCountry( + DateTime(2023, 1, 1), DatePrecision.day, 'US')); + final movie2 = MovieData( + 'Title 2', + DateWithPrecisionAndCountry( + DateTime(2023, 1, 1), DatePrecision.day, 'US')); + expect(movie1.same(movie2), isFalse); + }); + + test('same() returns false for different release date', () { + final movie1 = MovieData( + 'Title 1', + DateWithPrecisionAndCountry( + DateTime(2023, 1, 1), DatePrecision.day, 'US')); + final movie2 = MovieData( + 'Title 1', + DateWithPrecisionAndCountry( + DateTime(2023, 1, 2), DatePrecision.day, 'US')); + expect(movie1.same(movie2), isFalse); + }); + test('can be encoded to JSON and back', () { + final movie = MovieData('Title 1', + DateWithPrecisionAndCountry(DateTime.now(), DatePrecision.day, 'US')); + movie.setDetails(releaseDates: [ + DateWithPrecisionAndCountry(DateTime.now(), DatePrecision.day, 'US') + ], genres: [ + 'Action', + 'Adventure' + ], titles: [ + (title: 'Title 2', language: 'en') + ], reviews: [ + Review('8.5', 'John Doe', DateTime.now(), 100) + ]); + final json = movie.toJsonEncodable(); + final movie2 = MovieData.fromJsonEncodable(json); + expect(movie2.title, equals('Title 1')); + expect(movie2.releaseDate.country, equals('US')); + expect(movie2.releaseDates!.length, equals(1)); + expect(movie2.releaseDates![0].country, equals('US')); + expect(movie2.genres!.length, equals(2)); + expect(movie2.genres![0], equals('Action')); + expect(movie2.genres![1], equals('Adventure')); + expect(movie2.titles!.length, equals(1)); + expect(movie2.titles![0].title, equals('Title 2')); + expect(movie2.titles![0].language, equals('en')); + expect(movie2.reviews!.length, equals(1)); + expect(movie2.reviews![0].score, equals('8.5')); + expect(movie2.reviews![0].by, equals('John Doe')); + expect(movie2.reviews![0].count, equals(100)); + }); + + test('toString()', () { + final movie = MovieData('Title 1', + DateWithPrecisionAndCountry(DateTime.now(), DatePrecision.day, 'US')); + movie.setDetails(releaseDates: [ + DateWithPrecisionAndCountry(DateTime.now(), DatePrecision.day, 'US') + ], genres: [ + 'Action', + 'Adventure' + ], titles: [ + (title: 'Title 2', language: 'en') + ], reviews: [ + Review('8.5', 'John Doe', DateTime.now(), 100) + ]); + expect(movie.toString(), + equals('Title 1 (November 16, 2023 (US); Action, Adventure)')); + }); + }); + + group('DateWithPrecisionAndCountry', () { + test('can be encoded to JSON and back', () { + final date = + DateWithPrecisionAndCountry(DateTime.now(), DatePrecision.day, 'US'); + final json = date.toJsonEncodable(); + final date2 = DateWithPrecisionAndCountry.fromJsonEncodable(json); + expect(date2.date, equals(date.date)); + expect(date2.precision, equals(date.precision)); + expect(date2.country, equals(date.country)); + }); + + test('toString()', () { + final date = DateWithPrecisionAndCountry( + DateTime(2023, 1, 1), DatePrecision.day, 'US'); + expect(date.toString(), equals('January 1, 2023 (US)')); + }); + + test('toString() with month precision', () { + final date = DateWithPrecisionAndCountry( + DateTime(2023, 1, 1), DatePrecision.month, 'US'); + expect(date.toString(), equals('January 2023 (US)')); + }); + + test('toString() with year precision', () { + final date = DateWithPrecisionAndCountry( + DateTime(2023, 1, 1), DatePrecision.year, 'US'); + expect(date.toString(), equals('2023 (US)')); + }); + + test('toString() with decade precision', () { + final date = DateWithPrecisionAndCountry( + DateTime(2023, 1, 1), DatePrecision.decade, 'US'); + expect(date.toString(), equals('2020s (US)')); + }); + }); +} diff --git a/test/widget_test.dart b/test/widget_test.dart deleted file mode 100644 index 6756ba9..0000000 --- a/test/widget_test.dart +++ /dev/null @@ -1,30 +0,0 @@ -// This is a basic Flutter widget test. -// -// To perform an interaction with a widget in your test, use the WidgetTester -// utility in the flutter_test package. For example, you can send tap and scroll -// gestures. You can also use WidgetTester to find child widgets in the widget -// tree, read text, and verify that the values of widget properties are correct. - -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; - -import 'package:release_schedule/main.dart'; - -void main() { - testWidgets('Counter increments smoke test', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(const MyApp()); - - // Verify that our counter starts at 0. - expect(find.text('0'), findsOneWidget); - expect(find.text('1'), findsNothing); - - // Tap the '+' icon and trigger a frame. - await tester.tap(find.byIcon(Icons.add)); - await tester.pump(); - - // Verify that our counter has incremented. - expect(find.text('0'), findsNothing); - expect(find.text('1'), findsOneWidget); - }); -}