diff --git a/app/lib/main.dart b/app/lib/main.dart index 4e8e18f..ab70fea 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -3,6 +3,8 @@ import 'package:flutter/services.dart'; import 'package:scoped_model/scoped_model.dart'; import 'models/current_user_model.dart'; +import 'models/post.dart'; +import 'models/post_mock.dart'; import 'pages/home_page.dart'; import 'pages/register_page.dart'; import 'theme.dart'; @@ -22,7 +24,10 @@ class MyApp extends StatelessWidget { debugShowCheckedModeBanner: false, title: 'Birb', theme: buildThemeData(), - home: const HomePage(title: 'Birb'), + home: HomePage( + title: 'Birb', + posts: _loadPosts(context), + ), routes: { RegisterPage.routeName: (BuildContext context) => const RegisterPage(), @@ -30,4 +35,16 @@ class MyApp extends StatelessWidget { ), ); } + + Stream> _loadPosts(BuildContext context) { + final List> mockSnapshot = >[ + List.generate(10, (int index) => mockPostData(index: index)) + ]; + return Stream>.fromIterable(mockSnapshot) + .map(_convertToPosts); + } + + List _convertToPosts(List data) { + return data.map((dynamic item) => Post.fromMap(item)).toList(); + } } diff --git a/app/lib/pages/home_page.dart b/app/lib/pages/home_page.dart index 0fa9d47..1087ba6 100644 --- a/app/lib/pages/home_page.dart +++ b/app/lib/pages/home_page.dart @@ -3,16 +3,20 @@ import 'package:scoped_model/scoped_model.dart'; import '../models/current_user_model.dart'; import '../models/post.dart'; -import '../models/post_mock.dart'; import '../posts_list.dart'; import '../sign_in_fab.dart'; import '../sign_out_action.dart'; class HomePage extends StatefulWidget { - const HomePage({Key key, this.title}) : super(key: key); + const HomePage({ + Key key, + @required this.title, + @required this.posts, + }) : super(key: key); static const String routeName = '/'; final String title; + final Stream> posts; @override _HomePageState createState() => _HomePageState(); @@ -30,7 +34,7 @@ class _HomePageState extends State { SignOutAction(), ], ), - body: PostsList(_loadPosts(context)), + body: PostsList(widget.posts), floatingActionButton: _floatingActionButton(), ); } @@ -45,16 +49,4 @@ class _HomePageState extends State { model.user == null ? const SignInFab() : Container(), ); } - - Stream> _loadPosts(BuildContext context) { - final List> mockSnapshot = >[ - List.generate(10, (int index) => mockPostData(index: index)) - ]; - return Stream>.fromIterable(mockSnapshot) - .map(_convertToPosts); - } - - List _convertToPosts(List data) { - return data.map((dynamic item) => Post.fromMap(item)).toList(); - } } diff --git a/app/lib/pages/post_page.dart b/app/lib/pages/post_page.dart new file mode 100644 index 0000000..dce906d --- /dev/null +++ b/app/lib/pages/post_page.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; + +import '../models/post.dart'; +import '../post_item.dart'; + +class PostPage extends StatelessWidget { + const PostPage({ + Key key, + @required this.post, + }) : super(key: key); + + final Post post; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Post'), + centerTitle: true, + elevation: 0.0, + ), + body: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.fromLTRB(16.0, 16.0, 16.0, 8.0), + child: PostItem(post), + ), + ), + ); + } +} diff --git a/app/lib/post_item.dart b/app/lib/post_item.dart index 9ca89a4..f77eda4 100644 --- a/app/lib/post_item.dart +++ b/app/lib/post_item.dart @@ -12,9 +12,12 @@ class PostItem extends StatelessWidget { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - ClipRRect( - child: Image.network(post.imageUrl), - borderRadius: BorderRadius.circular(10.0), + Hero( + tag: post.id, + child: ClipRRect( + child: Image.network(post.imageUrl), + borderRadius: BorderRadius.circular(10.0), + ), ), const SizedBox(height: 8.0), Text( diff --git a/app/lib/posts_list.dart b/app/lib/posts_list.dart index 9730c6d..b327e52 100644 --- a/app/lib/posts_list.dart +++ b/app/lib/posts_list.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'models/post.dart'; import 'no_content.dart'; +import 'pages/post_page.dart'; import 'post_item.dart'; class PostsList extends StatelessWidget { @@ -25,20 +26,48 @@ class PostsList extends StatelessWidget { if (snapshot.data.isEmpty) { return const NoContent(); } - return _itemList(snapshot.data); + return _itemList(context, snapshot.data); } }, ); } - ListView _itemList(List items) { + ListView _itemList(BuildContext context, List items) { return ListView( children: items.map((Post post) { return Container( padding: const EdgeInsets.fromLTRB(16.0, 16.0, 16.0, 8.0), - child: PostItem(post), + child: InkWell( + onTap: () => _navigateToPost(context, post), + child: PostItem(post), + ), ); }).toList(), ); } + + void _navigateToPost(BuildContext context, Post post) { + Navigator.of(context).push( + PageRouteBuilder( + pageBuilder: ( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + ) { + return PostPage(post: post); + }, + transitionsBuilder: ( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + Widget child, + ) { + return FadeTransition( + opacity: animation, + child: child, + ); + }, + ), + ); + } } diff --git a/app/test/pages/home_page_test.dart b/app/test/pages/home_page_test.dart index bee6f19..149d552 100644 --- a/app/test/pages/home_page_test.dart +++ b/app/test/pages/home_page_test.dart @@ -1,20 +1,28 @@ +import 'package:birb/models/current_user_model.dart'; +import 'package:birb/models/post_mock.dart'; import 'package:birb/pages/home_page.dart'; import 'package:birb/posts_list.dart'; import 'package:birb/sign_in_fab.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:scoped_model/scoped_model.dart'; import '../mocks/app_mock.dart'; import '../mocks/current_user_model_mock.dart'; void main() { - final dynamic app = appMock( - child: const HomePage(title: 'Awesome'), - mock: CurrentUserModelMock(), - ); + ScopedModel app({int count}) { + return appMock( + child: HomePage( + title: 'Awesome', + posts: mockPosts(count: 5), + ), + mock: CurrentUserModelMock(), + ); + } testWidgets('Renders list of posts', (WidgetTester tester) async { // Build our app and trigger a frame. - await tester.pumpWidget(app); + await tester.pumpWidget(app()); expect(find.text('Awesome'), findsOneWidget); expect(find.byType(PostsList), findsOneWidget); @@ -22,7 +30,7 @@ void main() { testWidgets('Renders sign in FAB', (WidgetTester tester) async { // Build our app and trigger a frame. - await tester.pumpWidget(app); + await tester.pumpWidget(app()); expect(find.byType(SignInFab), findsOneWidget); }); diff --git a/app/test/pages/post_page_test.dart b/app/test/pages/post_page_test.dart new file mode 100644 index 0000000..586e653 --- /dev/null +++ b/app/test/pages/post_page_test.dart @@ -0,0 +1,30 @@ +import 'package:birb/models/current_user_model.dart'; +import 'package:birb/models/post_mock.dart'; +import 'package:birb/pages/post_page.dart'; +import 'package:birb/post_item.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:image_test_utils/image_test_utils.dart'; +import 'package:scoped_model/scoped_model.dart'; + +import '../mocks/app_mock.dart'; +import '../mocks/current_user_model_mock.dart'; + +void main() { + ScopedModel app({int count}) { + return appMock( + child: PostPage( + post: mockPost(), + ), + mock: CurrentUserModelMock(), + ); + } + + testWidgets('Renders a post', (WidgetTester tester) async { + provideMockedNetworkImages(() async { + await tester.pumpWidget(app()); + + expect(find.text('Post'), findsOneWidget); + expect(find.byType(PostItem), findsOneWidget); + }); + }); +} diff --git a/app/test/post_item_test.dart b/app/test/post_item_test.dart index 091be48..98aaf72 100644 --- a/app/test/post_item_test.dart +++ b/app/test/post_item_test.dart @@ -9,12 +9,15 @@ void main() { testWidgets('Renders a post', (WidgetTester tester) async { provideMockedNetworkImages(() async { final Post post = mockPost(); - // Build our app and trigger a frame. await tester.pumpWidget(MaterialApp( home: PostItem(post), )); + final Finder hero = find.byType(Hero); + expect(hero, findsOneWidget); + expect(find.byType(ClipRRect), findsOneWidget); expect(find.byType(Image), findsOneWidget); + expect(tester.widget(hero).tag, post.id); expect(find.text(post.username), findsOneWidget); expect(find.text(post.text), findsOneWidget); }); diff --git a/app/test/posts_list_test.dart b/app/test/posts_list_test.dart index 9110b14..0266795 100644 --- a/app/test/posts_list_test.dart +++ b/app/test/posts_list_test.dart @@ -3,43 +3,64 @@ import 'package:birb/models/post_mock.dart'; import 'package:birb/no_content.dart'; import 'package:birb/post_item.dart'; import 'package:birb/posts_list.dart'; +import 'package:birb/pages/post_page.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:image_test_utils/image_test_utils.dart'; +import 'package:birb/models/current_user_model.dart'; +import 'package:scoped_model/scoped_model.dart'; + +import 'mocks/app_mock.dart'; +import 'mocks/current_user_model_mock.dart'; void main() { + ScopedModel app({int count}) { + return appMock( + child: PostsList(mockPosts(count: count)), + mock: CurrentUserModelMock(), + ); + } + group('PostsList', () { testWidgets('renders list of PostItems', (WidgetTester tester) async { provideMockedNetworkImages(() async { - // Build our app and trigger a frame. - await tester.pumpWidget(MaterialApp( - home: PostsList(mockPosts(count: 5)), - )); + await tester.pumpWidget(app(count: 5)); expect(find.text('Loading...'), findsOneWidget); - await tester.pump(Duration.zero); + await tester.pumpAndSettle(); expect(find.byType(PostItem), findsNWidgets(5)); }); }); + testWidgets('Opens post page when item is tapped', + (WidgetTester tester) async { + provideMockedNetworkImages(() async { + // TODO(abraham): get the post ID and check for Hero tag before/after + await tester.pumpWidget(app(count: 5)); + await tester.pumpAndSettle(); + await tester.tap(find.byType(PostItem).first); + await tester.pumpAndSettle(); + + expect(find.byType(PostPage), findsOneWidget); + }); + }); + testWidgets('renders NoContent widget', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(MaterialApp( - home: PostsList(mockPosts(count: 0)), - )); - await tester.pump(Duration.zero); + await tester.pumpWidget(app(count: 0)); + await tester.pumpAndSettle(); expect(find.byType(NoContent), findsOneWidget); }); testWidgets('renders error text', (WidgetTester tester) async { - // Build our app and trigger a frame. await tester.pumpWidget(MaterialApp( - home: PostsList(Future>.error('Bad Connection').asStream()), + home: PostsList( + Future>.error('Bad Connection').asStream(), + ), )); - await tester.pump(Duration.zero); + await tester.pumpAndSettle(); expect(find.text('Error: Bad Connection'), findsOneWidget); });