From 3ffe3cb50fc83e8152dd9f2937dc42dcd6bfa77d Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sun, 10 Nov 2024 18:37:34 +0800 Subject: [PATCH] :sparkles: Editable post --- assets/translations/en-US.json | 4 +- assets/translations/zh-CN.json | 4 +- lib/router.dart | 9 ++ lib/screens/account.dart | 10 +- .../account/publishers/publisher_new.dart | 2 +- .../account/publishers/publishers.dart | 2 +- lib/screens/auth/login.dart | 14 +- lib/screens/auth/register.dart | 2 +- lib/screens/explore.dart | 37 +++-- lib/screens/post/post_editor.dart | 153 +++++++++++++++--- lib/types/post.dart | 7 + lib/types/post.freezed.dart | 8 +- lib/widgets/account/account_image.dart | 3 +- lib/widgets/post/post_item.dart | 49 +++++- lib/widgets/universal_image.dart | 5 +- pubspec.lock | 2 +- pubspec.yaml | 1 + test/widget_test.dart | 30 ---- 18 files changed, 249 insertions(+), 93 deletions(-) delete mode 100644 test/widget_test.dart diff --git a/assets/translations/en-US.json b/assets/translations/en-US.json index 5a069ab..022f6be 100644 --- a/assets/translations/en-US.json +++ b/assets/translations/en-US.json @@ -31,6 +31,7 @@ "preview": "Preview", "loading": "Loading...", "delete": "Delete", + "report": "Report", "fieldUsername": "Username", "fieldNickname": "Nickname", "fieldEmail": "Email address", @@ -72,5 +73,6 @@ "fieldPostContent": "What happened?!", "fieldPostTitle": "Title", "fieldPostDescription": "Description", - "postPublish": "Publish" + "postPublish": "Publish", + "postEditingNotice": "You're about to editing a post that posted {}." } diff --git a/assets/translations/zh-CN.json b/assets/translations/zh-CN.json index 162865f..11d8944 100644 --- a/assets/translations/zh-CN.json +++ b/assets/translations/zh-CN.json @@ -31,6 +31,7 @@ "create": "创建", "preview": "预览", "delete": "删除", + "report": "检举", "fieldUsername": "用户名", "fieldNickname": "显示名", "fieldEmail": "电子邮箱地址", @@ -72,5 +73,6 @@ "fieldPostContent": "发生什么事了?!", "fieldPostTitle": "标题", "fieldPostDescription": "描述", - "postPublish": "发布" + "postPublish": "发布", + "postEditingNotice": "你正在修改由 {} 发布的帖子。" } diff --git a/lib/router.dart b/lib/router.dart index 1131cd3..da0f991 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -46,6 +46,15 @@ final appRouter = GoRouter( name: 'postEditor', builder: (context, state) => PostEditorScreen( mode: state.pathParameters['mode']!, + postEditId: int.tryParse( + state.uri.queryParameters['editing'] ?? '', + ), + postReplyId: int.tryParse( + state.uri.queryParameters['replying'] ?? '', + ), + postRepostId: int.tryParse( + state.uri.queryParameters['reposting'] ?? '', + ), ), ), ], diff --git a/lib/screens/account.dart b/lib/screens/account.dart index eac66c0..53a2a9d 100644 --- a/lib/screens/account.dart +++ b/lib/screens/account.dart @@ -79,7 +79,7 @@ class _AuthorizedAccountScreen extends StatelessWidget { subtitle: Text('accountProfileEditSubtitle').tr(), contentPadding: const EdgeInsets.symmetric(horizontal: 24), leading: const Icon(Symbols.contact_page), - trailing: const Icon(Icons.chevron_right), + trailing: const Icon(Symbols.chevron_right), onTap: () { GoRouter.of(context).pushNamed('accountProfileEdit'); }, @@ -89,7 +89,7 @@ class _AuthorizedAccountScreen extends StatelessWidget { subtitle: Text('accountPublishersSubtitle').tr(), contentPadding: const EdgeInsets.symmetric(horizontal: 24), leading: const Icon(Symbols.face), - trailing: const Icon(Icons.chevron_right), + trailing: const Icon(Symbols.chevron_right), onTap: () { GoRouter.of(context).pushNamed('accountPublishers'); }, @@ -99,7 +99,7 @@ class _AuthorizedAccountScreen extends StatelessWidget { subtitle: Text('accountLogoutSubtitle').tr(), contentPadding: const EdgeInsets.symmetric(horizontal: 24), leading: const Icon(Symbols.logout), - trailing: const Icon(Icons.chevron_right), + trailing: const Icon(Symbols.chevron_right), onTap: () { context .showConfirmDialog( @@ -147,7 +147,7 @@ class _UnauthorizedAccountScreen extends StatelessWidget { subtitle: Text('screenAuthLoginSubtitle').tr(), contentPadding: const EdgeInsets.symmetric(horizontal: 24), leading: const Icon(Symbols.login), - trailing: const Icon(Icons.chevron_right), + trailing: const Icon(Symbols.chevron_right), onTap: () { GoRouter.of(context).pushNamed('authLogin'); }, @@ -157,7 +157,7 @@ class _UnauthorizedAccountScreen extends StatelessWidget { subtitle: Text('screenAuthRegisterSubtitle').tr(), contentPadding: const EdgeInsets.symmetric(horizontal: 24), leading: const Icon(Symbols.person_add), - trailing: const Icon(Icons.chevron_right), + trailing: const Icon(Symbols.chevron_right), onTap: () { GoRouter.of(context).pushNamed('authRegister'); }, diff --git a/lib/screens/account/publishers/publisher_new.dart b/lib/screens/account/publishers/publisher_new.dart index 9d52db2..a5d1339 100644 --- a/lib/screens/account/publishers/publisher_new.dart +++ b/lib/screens/account/publishers/publisher_new.dart @@ -121,7 +121,7 @@ class _PublisherNewPersonalState extends State<_PublisherNewPersonal> { width: double.infinity, child: ElevatedButton.icon( onPressed: _isBusy ? null : _performAction, - icon: const Icon(Icons.add), + icon: const Icon(Symbols.add), label: Text('create').tr(), ), ).padding(horizontal: 2), diff --git a/lib/screens/account/publishers/publishers.dart b/lib/screens/account/publishers/publishers.dart index 7ebb1f0..495448d 100644 --- a/lib/screens/account/publishers/publishers.dart +++ b/lib/screens/account/publishers/publishers.dart @@ -96,7 +96,7 @@ class _PublisherScreenState extends State { PopupMenuItem( child: Row( children: [ - const Icon(Icons.edit), + const Icon(Symbols.edit), const Gap(16), Text('edit').tr(), ], diff --git a/lib/screens/auth/login.dart b/lib/screens/auth/login.dart index ef0f51a..578ad19 100644 --- a/lib/screens/auth/login.dart +++ b/lib/screens/auth/login.dart @@ -12,8 +12,8 @@ import 'package:surface/widgets/dialog.dart'; import 'package:url_launcher/url_launcher_string.dart'; final Map _factorLabelMap = { - 0: ('authFactorPassword'.tr(), Icons.password, false), - 1: ('authFactorEmail'.tr(), Icons.email, true), + 0: ('authFactorPassword'.tr(), Symbols.password, false), + 1: ('authFactorEmail'.tr(), Symbols.email, true), }; class LoginScreen extends StatefulWidget { @@ -225,7 +225,7 @@ class _LoginCheckScreenState extends State<_LoginCheckScreen> { mainAxisSize: MainAxisSize.min, children: [ Text('next').tr(), - const Icon(Icons.chevron_right), + const Icon(Symbols.chevron_right), ], ), ), @@ -320,7 +320,7 @@ class _LoginPickerScreenState extends State<_LoginPickerScreen> { ), ), secondary: Icon( - _factorLabelMap[x.type]?.$2 ?? Icons.question_mark, + _factorLabelMap[x.type]?.$2 ?? Symbols.question_mark, ), title: Text( _factorLabelMap[x.type]?.$1 ?? 'unknown'.tr(), @@ -355,7 +355,7 @@ class _LoginPickerScreenState extends State<_LoginPickerScreen> { mainAxisSize: MainAxisSize.min, children: [ Text('next'.tr()), - const Icon(Icons.chevron_right), + const Icon(Symbols.chevron_right), ], ), ), @@ -505,7 +505,7 @@ class _LoginLookupScreenState extends State<_LoginLookupScreen> { mainAxisSize: MainAxisSize.min, children: [ Text('next').tr(), - const Icon(Icons.chevron_right), + const Icon(Symbols.chevron_right), ], ), ), @@ -538,7 +538,7 @@ class _LoginLookupScreenState extends State<_LoginLookupScreen> { children: [ Text('termAcceptLink'.tr()), const Gap(4), - const Icon(Icons.launch, size: 14), + const Icon(Symbols.launch, size: 14), ], ), onTap: () { diff --git a/lib/screens/auth/register.dart b/lib/screens/auth/register.dart index 09ac1df..b5d5047 100644 --- a/lib/screens/auth/register.dart +++ b/lib/screens/auth/register.dart @@ -146,7 +146,7 @@ class _RegisterScreenState extends State { mainAxisSize: MainAxisSize.min, children: [ Text('next').tr(), - const Icon(Icons.chevron_right), + const Icon(Symbols.chevron_right), ], ), ), diff --git a/lib/screens/explore.dart b/lib/screens/explore.dart index 4f8bb2f..fda1525 100644 --- a/lib/screens/explore.dart +++ b/lib/screens/explore.dart @@ -27,7 +27,7 @@ class _ExploreScreenState extends State { final List _posts = List.empty(growable: true); int? _postCount; - void _fetchPosts() async { + Future _fetchPosts() async { if (_postCount != null && _posts.length >= _postCount!) return; setState(() => _isBusy = true); @@ -75,9 +75,6 @@ class _ExploreScreenState extends State { @override Widget build(BuildContext context) { return AppScaffold( - appBar: AppBar( - title: Text('screenExplore').tr(), - ), floatingActionButtonLocation: ExpandableFab.location, floatingActionButton: ExpandableFab( key: _fabKey, @@ -155,15 +152,31 @@ class _ExploreScreenState extends State { ), ], ), - body: InfiniteList( - itemCount: _posts.length, - isLoading: _isBusy, - hasReachedMax: _postCount != null && _posts.length >= _postCount!, - onFetchData: _fetchPosts, - itemBuilder: (context, idx) { - return PostItem(data: _posts[idx]); + body: RefreshIndicator( + displacement: 40 + MediaQuery.of(context).padding.top, + onRefresh: () { + _posts.clear(); + return _fetchPosts(); }, - separatorBuilder: (context, index) => const Divider(), + child: CustomScrollView( + slivers: [ + SliverAppBar( + title: Text('screenExplore').tr(), + floating: true, + snap: true, + ), + SliverInfiniteList( + itemCount: _posts.length, + isLoading: _isBusy, + hasReachedMax: _postCount != null && _posts.length >= _postCount!, + onFetchData: _fetchPosts, + itemBuilder: (context, idx) { + return PostItem(data: _posts[idx]); + }, + separatorBuilder: (context, index) => const Divider(), + ) + ], + ), ), ); } diff --git a/lib/screens/post/post_editor.dart b/lib/screens/post/post_editor.dart index c6384e8..3aa1122 100644 --- a/lib/screens/post/post_editor.dart +++ b/lib/screens/post/post_editor.dart @@ -1,3 +1,4 @@ +import 'package:dio/dio.dart'; import 'package:dropdown_button2/dropdown_button2.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/gestures.dart'; @@ -12,7 +13,9 @@ import 'package:surface/providers/sn_network.dart'; import 'package:surface/types/attachment.dart'; import 'package:surface/types/post.dart'; import 'package:surface/widgets/account/account_image.dart'; +import 'package:surface/widgets/loading_indicator.dart'; import 'package:surface/widgets/navigation/app_scaffold.dart'; +import 'package:surface/widgets/post/post_item.dart'; import 'package:surface/widgets/post/post_media_pending_list.dart'; import 'package:surface/widgets/post/post_meta_editor.dart'; import 'package:surface/widgets/dialog.dart'; @@ -20,7 +23,16 @@ import 'package:provider/provider.dart'; class PostEditorScreen extends StatefulWidget { final String mode; - const PostEditorScreen({super.key, required this.mode}); + final int? postEditId; + final int? postReplyId; + final int? postRepostId; + const PostEditorScreen({ + super.key, + required this.mode, + required this.postEditId, + required this.postReplyId, + required this.postRepostId, + }); @override State createState() => _PostEditorScreenState(); @@ -33,6 +45,7 @@ class _PostEditorScreenState extends State { }; bool _isBusy = false; + bool _isLoading = false; SnPublisher? _publisher; List? _publishers; @@ -40,7 +53,7 @@ class _PostEditorScreenState extends State { final List _selectedMedia = List.empty(growable: true); final List _attachments = List.empty(growable: true); - void _fetchPublishers() async { + Future _fetchPublishers() async { final sn = context.read(); final resp = await sn.client.get('/cgi/co/publishers'); _publishers = List.from( @@ -51,6 +64,63 @@ class _PostEditorScreenState extends State { }); } + SnPost? _editingOg; + SnPost? _replyingTo; + SnPost? _repostingTo; + + Future _fetchRelatedPost() async { + final sn = context.read(); + final attach = context.read(); + + try { + setState(() => _isLoading = true); + + if (widget.postEditId != null) { + final resp = await sn.client.get('/cgi/co/posts/${widget.postEditId}'); + final post = SnPost.fromJson(resp.data); + final attachments = await attach + .getMultiple(post.body['attachments']?.cast() ?? []); + _title = post.body['title']; + _description = post.body['description']; + _contentController.text = post.body['content'] ?? ''; + _attachments.addAll(attachments); + + _editingOg = post.copyWith( + preload: SnPostPreload( + attachments: attachments, + ), + ); + } + + if (widget.postReplyId != null) { + final resp = await sn.client.get('/cgi/co/posts/${widget.postReplyId}'); + final post = SnPost.fromJson(resp.data); + _replyingTo = post.copyWith( + preload: SnPostPreload( + attachments: await attach + .getMultiple(post.body['attachments']?.cast() ?? []), + ), + ); + } + + if (widget.postRepostId != null) { + final resp = + await sn.client.get('/cgi/co/posts/${widget.postRepostId}'); + final post = SnPost.fromJson(resp.data); + _repostingTo = post.copyWith( + preload: SnPostPreload( + attachments: await attach + .getMultiple(post.body['attachments']?.cast() ?? []), + ), + ); + } + } catch (err) { + context.showErrorDialog(err); + } finally { + setState(() => _isLoading = false); + } + } + String? _title; String? _description; @@ -110,24 +180,35 @@ class _PostEditorScreenState extends State { // Posting the content try { final baseProgressVal = _progress!; - await sn.client.post('/cgi/co/${widget.mode}', data: { - 'publisher': _publisher!.id, - 'content': _contentController.value.text, - 'title': _title, - 'description': _description, - 'attachments': _attachments.map((e) => e.rid).toList(), - }, onSendProgress: (count, total) { - setState(() { - _progress = - baseProgressVal + (count / total) * (kPostingProgressWeight / 2); - }); - }, onReceiveProgress: (count, total) { - setState(() { - _progress = baseProgressVal + - (kPostingProgressWeight / 2) + - (count / total) * (kPostingProgressWeight / 2); - }); - }); + await sn.client.request( + [ + '/cgi/co/${widget.mode}', + if (widget.postEditId != null) '${widget.postEditId}', + ].join('/'), + data: { + 'publisher': _publisher!.id, + 'content': _contentController.value.text, + 'title': _title, + 'description': _description, + 'attachments': _attachments.map((e) => e.rid).toList(), + }, + onSendProgress: (count, total) { + setState(() { + _progress = baseProgressVal + + (count / total) * (kPostingProgressWeight / 2); + }); + }, + onReceiveProgress: (count, total) { + setState(() { + _progress = baseProgressVal + + (kPostingProgressWeight / 2) + + (count / total) * (kPostingProgressWeight / 2); + }); + }, + options: Options( + method: widget.postEditId != null ? 'PUT' : 'POST', + ), + ); if (!mounted) return; Navigator.pop(context, true); } catch (err) { @@ -177,6 +258,7 @@ class _PostEditorScreenState extends State { context.showErrorDialog('Unknown post type'); Navigator.pop(context); } + _fetchRelatedPost(); _fetchPublishers(); } @@ -221,6 +303,7 @@ class _PostEditorScreenState extends State { items: >[ ...(_publishers?.map( (item) => DropdownMenuItem( + enabled: _editingOg == null, value: item, child: Row( children: [ @@ -302,9 +385,36 @@ class _PostEditorScreenState extends State { const Divider(height: 1), Expanded( child: SingleChildScrollView( - padding: const EdgeInsets.symmetric(vertical: 8), + padding: EdgeInsets.only( + top: _editingOg == null ? 8 : 0, + bottom: 8, + ), child: Column( children: [ + // Editing Notice + if (_editingOg != null) + Column( + children: [ + Theme( + data: Theme.of(context) + .copyWith(dividerColor: Colors.transparent), + child: ExpansionTile( + minTileHeight: 48, + leading: + const Icon(Symbols.edit_note).padding(left: 4), + title: Text('postEditingNotice') + .fontSize(15) + .tr(args: ['@${_editingOg!.publisher.name}']), + children: [ + PostItem(data: _editingOg!), + ], + ), + ), + const Divider(height: 1), + const Gap(8) + ], + ), + // Content Input Area TextField( controller: _contentController, maxLines: null, @@ -338,6 +448,7 @@ class _PostEditorScreenState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + LoadingIndicator(isActive: _isBusy), if (_isBusy && _progress != null) TweenAnimationBuilder( tween: Tween(begin: 0, end: 1), diff --git a/lib/types/post.dart b/lib/types/post.dart index 8e864e6..5f7551d 100644 --- a/lib/types/post.dart +++ b/lib/types/post.dart @@ -6,6 +6,8 @@ part 'post.g.dart'; @freezed class SnPost with _$SnPost { + const SnPost._(); + const factory SnPost({ required int id, required DateTime createdAt, @@ -44,6 +46,11 @@ class SnPost with _$SnPost { }) = _SnPost; factory SnPost.fromJson(Map json) => _$SnPostFromJson(json); + + String get typePlural => switch (type) { + 'story' => 'stories', + _ => '${type}s', + }; } @freezed diff --git a/lib/types/post.freezed.dart b/lib/types/post.freezed.dart index eb03e9a..5bed330 100644 --- a/lib/types/post.freezed.dart +++ b/lib/types/post.freezed.dart @@ -577,7 +577,7 @@ class __$$SnPostImplCopyWithImpl<$Res> /// @nodoc @JsonSerializable() -class _$SnPostImpl implements _SnPost { +class _$SnPostImpl extends _SnPost { const _$SnPostImpl( {required this.id, required this.createdAt, @@ -615,7 +615,8 @@ class _$SnPostImpl implements _SnPost { this.preload}) : _body = body, _tags = tags, - _categories = categories; + _categories = categories, + super._(); factory _$SnPostImpl.fromJson(Map json) => _$$SnPostImplFromJson(json); @@ -827,7 +828,7 @@ class _$SnPostImpl implements _SnPost { } } -abstract class _SnPost implements SnPost { +abstract class _SnPost extends SnPost { const factory _SnPost( {required final int id, required final DateTime createdAt, @@ -863,6 +864,7 @@ abstract class _SnPost implements SnPost { required final SnPublisher publisher, required final SnMetric metric, final SnPostPreload? preload}) = _$SnPostImpl; + const _SnPost._() : super._(); factory _SnPost.fromJson(Map json) = _$SnPostImpl.fromJson; diff --git a/lib/widgets/account/account_image.dart b/lib/widgets/account/account_image.dart index d5aaa22..342d381 100644 --- a/lib/widgets/account/account_image.dart +++ b/lib/widgets/account/account_image.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:material_symbols_icons/symbols.dart'; import 'package:provider/provider.dart'; import 'package:surface/providers/sn_network.dart'; import 'package:surface/widgets/universal_image.dart'; @@ -39,7 +40,7 @@ class AccountImage extends StatelessWidget { child: (content?.isEmpty ?? true) ? (fallbackWidget ?? Icon( - Icons.account_circle, + Symbols.account_circle, size: radius != null ? radius! * 1.2 : 24, color: foregroundColor, )) diff --git a/lib/widgets/post/post_item.dart b/lib/widgets/post/post_item.dart index d0c8d92..cd2975d 100644 --- a/lib/widgets/post/post_item.dart +++ b/lib/widgets/post/post_item.dart @@ -1,4 +1,6 @@ +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:relative_time/relative_time.dart'; import 'package:styled_widget/styled_widget.dart'; @@ -52,13 +54,48 @@ class _PostContentHeader extends StatelessWidget { ], ), ), - IconButton( - onPressed: () {}, - visualDensity: const VisualDensity(horizontal: -4, vertical: -4), - icon: const Icon( - Symbols.more_horiz, - size: 16, + PopupMenuButton( + icon: const Icon(Symbols.more_horiz), + style: const ButtonStyle( + visualDensity: VisualDensity(horizontal: -4, vertical: -4), ), + itemBuilder: (BuildContext context) => [ + PopupMenuItem( + child: Row( + children: [ + const Icon(Symbols.edit), + const Gap(16), + Text('edit').tr(), + ], + ), + onTap: () { + GoRouter.of(context).pushNamed( + 'postEditor', + pathParameters: {'mode': data.typePlural}, + queryParameters: {'editing': data.id.toString()}, + ); + }, + ), + PopupMenuItem( + child: Row( + children: [ + const Icon(Symbols.delete), + const Gap(16), + Text('delete').tr(), + ], + ), + ), + const PopupMenuDivider(), + PopupMenuItem( + child: Row( + children: [ + const Icon(Symbols.flag), + const Gap(16), + Text('report').tr(), + ], + ), + ), + ], ), ], ).padding(horizontal: 12, vertical: 8); diff --git a/lib/widgets/universal_image.dart b/lib/widgets/universal_image.dart index 09da323..d709f29 100644 --- a/lib/widgets/universal_image.dart +++ b/lib/widgets/universal_image.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:material_symbols_icons/symbols.dart'; import 'package:styled_widget/styled_widget.dart'; import 'package:flutter_animate/flutter_animate.dart'; @@ -67,7 +68,7 @@ class UniversalImage extends StatelessWidget { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - AnimateWidgetExtensions(Icon(Icons.close, size: 24)) + AnimateWidgetExtensions(Icon(Symbols.close, size: 24)) .animate(onPlay: (e) => e.repeat(reverse: true)) .fade(duration: 500.ms), Text( @@ -123,7 +124,7 @@ class UniversalImage extends StatelessWidget { child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - AnimateWidgetExtensions(Icon(Icons.close, size: 24)) + AnimateWidgetExtensions(Icon(Symbols.close, size: 24)) .animate(onPlay: (e) => e.repeat(reverse: true)) .fade(duration: 500.ms), Text( diff --git a/pubspec.lock b/pubspec.lock index 8be4507..0b10651 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1123,7 +1123,7 @@ packages: source: hosted version: "0.28.0" shared_preferences: - dependency: transitive + dependency: "direct main" description: name: shared_preferences sha256: "95f9997ca1fb9799d494d0cb2a780fd7be075818d59f00c43832ed112b158a82" diff --git a/pubspec.yaml b/pubspec.yaml index 4503c3b..880a886 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -70,6 +70,7 @@ dependencies: dismissible_page: ^1.0.2 uuid: ^4.5.1 photo_view: ^0.15.0 + shared_preferences: ^2.3.3 dev_dependencies: flutter_test: diff --git a/test/widget_test.dart b/test/widget_test.dart deleted file mode 100644 index 77cdd6c..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:surface/main.dart'; - -void main() { - testWidgets('Counter increments smoke test', (WidgetTester tester) async { - // Build our app and trigger a frame. - await tester.pumpWidget(const SolianApp()); - - // 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); - }); -}