From 280c63d53a9fbdc172617306c65effd3c2e7ad56 Mon Sep 17 00:00:00 2001 From: Luke Walton Date: Fri, 23 Aug 2024 09:38:43 +0100 Subject: [PATCH] fix(UX-1171): ExtendedTopAppBar padding without leading (#157) test: Add basic tests for ExtendedTopAppBar ci: Update flutter-code-quality action to use main --- .github/workflows/pull-request.yml | 6 +- .../pages/components/top_app_bar_example.dart | 20 +-- .../top_app_bar/extended_top_app_bar.dart | 40 +++-- .../extended_top_app_bar_test.dart | 169 ++++++++++++++++++ .../golden/extended_app_bar_shrinks.png | Bin 0 -> 2230 bytes ...tended_app_bar_shrinks_with_no_leading.png | Bin 0 -> 2206 bytes 6 files changed, 206 insertions(+), 29 deletions(-) create mode 100644 test/src/components/top_app_bar/extended_top_app_bar_test.dart create mode 100644 test/src/components/top_app_bar/golden/extended_app_bar_shrinks.png create mode 100644 test/src/components/top_app_bar/golden/extended_app_bar_shrinks_with_no_leading.png diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 6fa5706a..65bd4d26 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -21,9 +21,11 @@ jobs: with: cache: true - run: dart run build_runner build --delete-conflicting-outputs - - uses: ZebraDevs/flutter-code-quality@v1.0.8 + - uses: ZebraDevs/flutter-code-quality@main with: - token: ${{secrets.GITHUB_TOKEN}} + token: ${{secrets.GITHUB_TOKEN}} + coverage-pass-score: '1' + check-secret: runs-on: ubuntu-latest diff --git a/example/lib/pages/components/top_app_bar_example.dart b/example/lib/pages/components/top_app_bar_example.dart index 7cf3f5cf..2837ae13 100644 --- a/example/lib/pages/components/top_app_bar_example.dart +++ b/example/lib/pages/components/top_app_bar_example.dart @@ -43,7 +43,7 @@ class _TopAppBarExampleState extends State { return ExampleScaffold( name: TopAppBarExample.name, child: ColoredBox( - color: colors.surfaceSecondary, + color: colors.surfaceWarm, child: SingleChildScrollView( child: Column( children: [ @@ -57,7 +57,7 @@ class _TopAppBarExampleState extends State { children: [ ZetaAvatar(size: ZetaAvatarSize.xs, image: image), Padding( - padding: const EdgeInsets.only(left: ZetaSpacing.medium), + padding: EdgeInsets.only(left: ZetaSpacing.medium), child: Text("Title"), ), ], @@ -154,7 +154,7 @@ class _TopAppBarExampleState extends State { children: [ ZetaAvatar(size: ZetaAvatarSize.xs, image: image), Padding( - padding: const EdgeInsets.only(left: ZetaSpacing.medium), + padding: EdgeInsets.only(left: ZetaSpacing.medium), child: Text("Title"), ), ], @@ -178,9 +178,9 @@ class _TopAppBarExampleState extends State { child: Container( width: 800, height: 800, - color: Zeta.of(context).colors.surfaceSecondary, + color: Zeta.of(context).colors.surfaceSelectedHover, child: CustomPaint( - painter: Painter(colors: colors), + painter: Painter(zeta: Zeta.of(context)), size: Size(800, 800), ), ), @@ -220,9 +220,9 @@ class _TopAppBarExampleState extends State { child: Container( width: 800, height: 800, - color: Zeta.of(context).colors.surfaceSecondary, + color: Zeta.of(context).colors.surfaceSelectedHover, child: CustomPaint( - painter: Painter(colors: colors), + painter: Painter(zeta: Zeta.of(context)), size: Size(800, 800), ), ), @@ -239,9 +239,9 @@ class _TopAppBarExampleState extends State { } class Painter extends CustomPainter { - final ZetaColors colors; + final Zeta zeta; - Painter({super.repaint, required this.colors}); + Painter({super.repaint, required this.zeta}); @override void paint(Canvas canvas, Size size) { @@ -249,7 +249,7 @@ class Painter extends CustomPainter { var p1 = Offset(i, -10); var p2 = Offset(800 + i, 810); var paint = Paint() - ..color = colors.primary + ..color = zeta.colors.surfaceDefault ..strokeWidth = ZetaSpacing.minimum; canvas.drawLine(p1, p2, paint); } diff --git a/lib/src/components/top_app_bar/extended_top_app_bar.dart b/lib/src/components/top_app_bar/extended_top_app_bar.dart index decb0983..4398852d 100644 --- a/lib/src/components/top_app_bar/extended_top_app_bar.dart +++ b/lib/src/components/top_app_bar/extended_top_app_bar.dart @@ -2,15 +2,6 @@ import 'package:flutter/material.dart'; import '../../../zeta_flutter.dart'; -const _searchBarOffsetTop = ZetaSpacing.minimum * 1.5; -const _searchBarOffsetRight = ZetaSpacing.minimum * 22; -const _maxExtent = ZetaSpacing.minimum * 26; -const _minExtent = ZetaSpacing.xl_9; -const _leftMin = ZetaSpacing.large; -const _leftMax = ZetaSpacingBase.x12_5; -const _topMin = ZetaSpacing.xl_1; -const _topMax = ZetaSpacing.minimum * 15; - /// Delegate for creating an extended app bar, that grows and shrinks when scrolling. /// {@category Components} class ZetaExtendedAppBarDelegate extends SliverPersistentHeaderDelegate { @@ -38,26 +29,41 @@ class ZetaExtendedAppBarDelegate extends SliverPersistentHeaderDelegate { /// If `ZetaTopAppBarType.extend` shrinks. Does not affect other types of app bar. final bool shrinks; + static const double _maxExtent = 104; + static const double _minExtent = 52; + @override Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) { + const searchBarOffsetTop = ZetaSpacing.minimum * 1.5; + const searchBarOffsetRight = ZetaSpacing.minimum * 22; + const maxExtent = ZetaSpacing.minimum * 26; + const leftMin = ZetaSpacing.large; + + const topMin = ZetaSpacing.xl_1; + const topMax = ZetaSpacing.minimum * 15; + + /// If there is no leading widget, the left margin should not change + /// If there is a leading widget, the left margin should be the same as the leading widget's width plus padding + final leftMax = leading == null ? leftMin : _minExtent + ZetaSpacing.small; + return ConstrainedBox( - constraints: const BoxConstraints(minHeight: ZetaSpacing.xl_9, maxHeight: _maxExtent), + constraints: const BoxConstraints(minHeight: ZetaSpacing.xl_9, maxHeight: maxExtent), child: ColoredBox( color: Zeta.of(context).colors.surfacePrimary, child: Stack( children: [ Positioned( top: shrinks - ? (_topMax + (-1 * shrinkOffset)).clamp( - _topMin - + ? (topMax + (-1 * shrinkOffset)).clamp( + topMin - (searchController != null && searchController!.isEnabled - ? _searchBarOffsetTop + ? searchBarOffsetTop : ZetaSpacing.none), - _topMax, + topMax, ) - : _topMax, - left: shrinks ? ((shrinkOffset / _maxExtent) * ZetaSpacingBase.x50).clamp(_leftMin, _leftMax) : _leftMin, - right: searchController != null && searchController!.isEnabled ? _searchBarOffsetRight : ZetaSpacing.none, + : topMax, + left: shrinks ? ((shrinkOffset / maxExtent) * ZetaSpacingBase.x50).clamp(leftMin, leftMax) : leftMin, + right: searchController != null && searchController!.isEnabled ? searchBarOffsetRight : ZetaSpacing.none, child: title, ), if (leading != null) Positioned(top: ZetaSpacing.medium, left: ZetaSpacing.small, child: leading!), diff --git a/test/src/components/top_app_bar/extended_top_app_bar_test.dart b/test/src/components/top_app_bar/extended_top_app_bar_test.dart new file mode 100644 index 00000000..379b2a14 --- /dev/null +++ b/test/src/components/top_app_bar/extended_top_app_bar_test.dart @@ -0,0 +1,169 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:path/path.dart'; +import 'package:zeta_flutter/zeta_flutter.dart'; + +import '../../../test_utils/test_app.dart'; +import '../../../test_utils/tolerant_comparator.dart'; +import '../../../test_utils/utils.dart'; + +void main() { + setUpAll(() { + final testUri = Uri.parse(getCurrentPath('top_app_bar')); + goldenFileComparator = TolerantComparator(testUri, tolerance: 0.01); + }); + + testWidgets('ZetaExtendedAppBarDelegate builds correctly', (WidgetTester tester) async { + const title = Text('Title'); + final actions = [IconButton(icon: const Icon(Icons.search), onPressed: () {})]; + final leading = IconButton(icon: const Icon(Icons.menu), onPressed: () {}); + const boxKey = Key('box'); + tester.view.devicePixelRatio = 1.0; + tester.view.physicalSize = const Size(481, 480); + + await tester.pumpWidget( + TestApp( + home: Builder( + builder: (context) { + return SizedBox( + child: CustomScrollView( + slivers: [ + ZetaTopAppBar.extended(leading: leading, title: title, actions: actions), + SliverToBoxAdapter( + child: Container( + width: 800, + height: 700, + color: Zeta.of(context).colors.surfaceSelectedHover, + key: boxKey, + ), + ), + ], + ), + ); + }, + ), + ), + ); + + final boxFinder = find.byKey(boxKey); + expect(boxFinder, findsOneWidget); + + await tester.drag(boxFinder.first, const Offset(0, -100)); + await tester.pumpAndSettle(); + + final appBarFinder = find.byType(ZetaTopAppBar); + expect(appBarFinder, findsOneWidget); + + final titleFinder = find.descendant(of: appBarFinder, matching: find.byWidget(title)); + expect(titleFinder, findsOneWidget); + + final actionsFinder = find.descendant(of: appBarFinder, matching: find.byWidget(actions[0])); + expect(actionsFinder, findsOneWidget); + + final leadingFinder = find.descendant(of: appBarFinder, matching: find.byWidget(leading)); + expect(leadingFinder, findsOneWidget); + + await expectLater( + appBarFinder, + matchesGoldenFile(join(getCurrentPath('top_app_bar'), 'extended_app_bar_shrinks.png')), + ); + }); + + testWidgets('ZetaExtendedAppBarDelegate shrinks correctly with padding', (WidgetTester tester) async { + const title = Text('Title'); + final actions = [IconButton(icon: const Icon(Icons.search), onPressed: () {})]; + final leading = IconButton(icon: const Icon(Icons.menu), onPressed: () {}); + const boxKey = Key('box'); + tester.view.devicePixelRatio = 1.0; + tester.view.physicalSize = const Size(481, 480); + + await tester.pumpWidget( + TestApp( + home: Builder( + builder: (context) { + return SizedBox( + child: CustomScrollView( + slivers: [ + ZetaTopAppBar.extended(leading: leading, title: title, actions: actions), + SliverToBoxAdapter( + child: Container( + width: 800, + height: 700, + color: Zeta.of(context).colors.surfaceSelectedHover, + key: boxKey, + ), + ), + ], + ), + ); + }, + ), + ), + ); + + final boxFinder = find.byKey(boxKey); + expect(boxFinder, findsOneWidget); + + await tester.drag(boxFinder.first, const Offset(0, -100)); + await tester.pumpAndSettle(); + + final appBarFinder = find.byType(ZetaTopAppBar); + expect(appBarFinder, findsOneWidget); + + final positionedFinder = find.descendant(of: appBarFinder, matching: find.byType(Positioned)); + + final positionedWidget = tester.widget(positionedFinder.first); + expect(positionedWidget.left, 60); + }); + testWidgets('ZetaExtendedAppBarDelegate shrinks correctly with padding and no leading', (WidgetTester tester) async { + const title = Text('Title'); + final actions = [IconButton(icon: const Icon(Icons.search), onPressed: () {})]; + + const boxKey = Key('box'); + tester.view.devicePixelRatio = 1.0; + tester.view.physicalSize = const Size(481, 480); + + await tester.pumpWidget( + TestApp( + home: Builder( + builder: (context) { + return SizedBox( + child: CustomScrollView( + slivers: [ + ZetaTopAppBar.extended(title: title, actions: actions), + SliverToBoxAdapter( + child: Container( + width: 800, + height: 700, + color: Zeta.of(context).colors.surfaceSelectedHover, + key: boxKey, + ), + ), + ], + ), + ); + }, + ), + ), + ); + + final boxFinder = find.byKey(boxKey); + expect(boxFinder, findsOneWidget); + + await tester.drag(boxFinder.first, const Offset(0, -100)); + await tester.pumpAndSettle(); + + final appBarFinder = find.byType(ZetaTopAppBar); + expect(appBarFinder, findsOneWidget); + + final positionedFinder = find.descendant(of: appBarFinder, matching: find.byType(Positioned)); + + final positionedWidget = tester.widget(positionedFinder.first); + expect(positionedWidget.left, 16); + + await expectLater( + appBarFinder, + matchesGoldenFile(join(getCurrentPath('top_app_bar'), 'extended_app_bar_shrinks_with_no_leading.png')), + ); + }); +} diff --git a/test/src/components/top_app_bar/golden/extended_app_bar_shrinks.png b/test/src/components/top_app_bar/golden/extended_app_bar_shrinks.png new file mode 100644 index 0000000000000000000000000000000000000000..6a0c1237cf93e0ebf43cfba27e66b14b132d73f0 GIT binary patch literal 2230 zcmeAS@N?(olHy`uVBq!ia0y~yV0;L~4>;I>B9p|ft_D(!#X;^)4C~IxyaaMsik&<| zIDnvrBc+3Zfg{1w#WAE}&f7bddBK4q4G&-QL@P|tQIws`!h71|ppr&}qVI%57nUwg z?3l7-iSHtJ-bD!&-`*d&A0B<&z3Aiq>fB00-^(Tp4O4Hg5@%@eRXxNYAehL|(P6;g z;v&JIq{PD@D9F~(!NKg{(!!{qM4GhqSEkh^wYA26{{HE+%ZrPRk45yneOoy%)hzd( zM6!&WT;JTeZ*M+hyrb5gIcr%e&%Rx|q~6=t%t^iW*q#6QyLa#E&+pu^L&kre-TYMb zXCL1^sd)4D?&|9d?`D+dTzIeZR;8x0(z35F?wrx-TyzK=I5Wx z$jARrUc0s^t}$MxM&*Fen&jrXbYQ55=rc0RK4GhmH>BUx{Eb`Bz%cuyC&jTrSzM0l uCMO4shT3STQLQ)~O-!SSiOz}X1pm$-DUWVs3+Dja9t@tYelF{r5}E)O;vUuj literal 0 HcmV?d00001 diff --git a/test/src/components/top_app_bar/golden/extended_app_bar_shrinks_with_no_leading.png b/test/src/components/top_app_bar/golden/extended_app_bar_shrinks_with_no_leading.png new file mode 100644 index 0000000000000000000000000000000000000000..87b9ce7d2c2ca3abc62ef24ba8d4e6ad95f1847a GIT binary patch literal 2206 zcmeAS@N?(olHy`uVBq!ia0y~yV0;L~4>;I>B9p|ft_D(!#X;^)4C~IxyaaMsik&<| zIDnvrBc+3Zfy2Yo#WAE}&f7b-ej$Mp4G)bCavVjbIXsV7>OH-nKu~1e1vX&^Yb$A2 zp#@u5i`M3JblMkxH2q-n^^ShL(b4MjGgHJaoA@#_{CIxt6)VGog+hr89UTS?E-n%b zN=iHof`V)f9UROKE-j1-N{1K(1WA&f_*9EOKK_2sjDNmaJR1?%2on z{>^8q{=Ip5F~=e96Ds4uCkwEXt%-Wj9Ue-G~7xOcDZ={2|is6k!*I)mZe zUi+OvcGlL?>&xQr|MWZTDZyYxgs1AGIS=eDuPCvRn_u^L?fH50=imQnXAcaO@Av-w zyIs{fzetuB6fzI)Md~~|eS6O4yy%x!Rk^u__tnlMJ=A8Dh>rF@HvRnkSEhBDhQ60W z^cflWCjy;=FJL3*)vXR^XlOqPbR*>fPg%5$>Lw?#kA~W4s8Oxt98FB4iHV+x=|s8E XDtCLmyLr{XHU@*ItDnm{r-UW|xYrGi literal 0 HcmV?d00001