diff --git a/doc/marketing_goldens/shadcn_test_tools.dart b/doc/marketing_goldens/shadcn_test_tools.dart index e0d590f..3b0226c 100644 --- a/doc/marketing_goldens/shadcn_test_tools.dart +++ b/doc/marketing_goldens/shadcn_test_tools.dart @@ -57,9 +57,9 @@ class ShadcnSingleShotSceneLayout implements SceneLayout { Widget build( WidgetTester tester, BuildContext context, - Map>> goldens, + SceneLayoutContent content, ) { - final golden = goldens.entries.first; + final golden = content.goldens.entries.first; return DefaultTextStyle( style: GoldenSceneTheme.current.defaultTextStyle.copyWith( @@ -129,9 +129,9 @@ class ShadcnGalleryLayout implements SceneLayout { Widget build( WidgetTester tester, BuildContext context, - Map>> goldens, + SceneLayoutContent content, ) { - final entries = goldens.entries.toList(); + final entries = content.goldens.entries.toList(); return DefaultTextStyle( style: GoldenSceneTheme.current.defaultTextStyle.copyWith( diff --git a/doc/website/source/index.md b/doc/website/source/index.md index 4c3535d..06a3364 100644 --- a/doc/website/source/index.md +++ b/doc/website/source/index.md @@ -12,7 +12,7 @@ how we solve them. * **Failure Files:** Flutter spreads a single test failure across four different files. It's frustrating to have to open up multiple files to cross reference. With - `flutter_test_goldes`, your failure output is painted to a single file for easy review. + `flutter_test_goldens`, your failure output is painted to a single file for easy review. * **Widget Galleries:** Flutter developers often want to verify multiple configurations of a single widget, or multiple related widgets, at the same time. With `flutter_test_goldens`, you can easily paint a variety of widgets into a gallery, diff --git a/doc/website/source/styles/docs_page_layout.scss b/doc/website/source/styles/docs_page_layout.scss index c0b4b12..78036ef 100644 --- a/doc/website/source/styles/docs_page_layout.scss +++ b/doc/website/source/styles/docs_page_layout.scss @@ -265,13 +265,15 @@ main.page-content { margin-bottom: 1.5em; } - code { - padding: 3px 6px; - background: #7f00a6; - border: 1px solid #a218cc; - border-radius: 4px; - - color: WHITE; + p, li > { + code { + padding: 3px 6px; + background: #7f00a6; + border: 1px solid #a218cc; + border-radius: 4px; + + color: WHITE; + } } li { diff --git a/lib/src/flutter/flutter_camera.dart b/lib/src/flutter/flutter_camera.dart index 259b511..f94f529 100644 --- a/lib/src/flutter/flutter_camera.dart +++ b/lib/src/flutter/flutter_camera.dart @@ -34,17 +34,7 @@ class FlutterCamera { ); } - final pictureRecorder = PictureRecorder(); - final canvas = Canvas(pictureRecorder); - final screenSize = fullscreenRenderObject.size; - - final paintingContext = TestRecordingPaintingContext(canvas); - fullscreenRenderObject.paint(paintingContext, Offset.zero); - - final fullscreenPhoto = await pictureRecorder.endRecording().toImage( - screenSize.width.round(), - screenSize.height.round(), - ); + final fullscreenPhoto = fullscreenRenderObject.toImageSync(); final contentFinder = finder ?? find.byType(GoldenImageBounds); expect(finder, findsOne); diff --git a/lib/src/flutter/flutter_golden_matcher.dart b/lib/src/flutter/flutter_golden_matcher.dart index e015c43..54be369 100644 --- a/lib/src/flutter/flutter_golden_matcher.dart +++ b/lib/src/flutter/flutter_golden_matcher.dart @@ -1,5 +1,4 @@ import 'dart:io'; -import 'dart:typed_data'; import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; diff --git a/lib/src/scenes/failure_scene.dart b/lib/src/scenes/failure_scene.dart index d5c487e..41ec9fc 100644 --- a/lib/src/scenes/failure_scene.dart +++ b/lib/src/scenes/failure_scene.dart @@ -186,7 +186,7 @@ Future<(Image, FailureSceneMetadata)> _layoutFailureScene( return layout.build( tester, context, - renderablePhotos, + SceneLayoutContent(goldens: renderablePhotos), ); }, ), diff --git a/lib/src/scenes/gallery.dart b/lib/src/scenes/gallery.dart index acda8ff..c276a97 100644 --- a/lib/src/scenes/gallery.dart +++ b/lib/src/scenes/gallery.dart @@ -32,12 +32,14 @@ class Gallery { BoxConstraints? itemConstraints, Finder? itemBoundsFinder, required SceneLayout layout, + GoldenSetup? itemSetup, }) : _fileName = fileName, _sceneDescription = sceneDescription, _itemScaffold = itemScaffold, _itemConstraints = itemConstraints, _itemBoundsFinder = itemBoundsFinder, - _layout = layout { + _layout = layout, + _itemSetup = itemSetup { _directory = directory ?? GoldenSceneTheme.current.directory; } @@ -84,6 +86,12 @@ class Gallery { /// 3. `find.byType(GoldenImageBounds)`. final Finder? _itemBoundsFinder; + /// An optional setup method that runs after pumping an item's tree, and just before the + /// item is screenshotted. + /// + /// This setup runs for every item in the scene unless an individual item overrides it. + final GoldenSetup? _itemSetup; + /// Requests for all screenshots within this scene, by their ID. final _requests = {}; @@ -329,6 +337,14 @@ class Gallery { final previousPlatform = debugDefaultTargetPlatformOverride; debugDefaultTargetPlatformOverride = item.platform ?? previousPlatform; + if (itemConstraints != null && itemConstraints.hasBoundedWidth && itemConstraints.hasBoundedHeight) { + // Some tests may want to control the size of the window. If we're given bounded + // constraints, make the window the biggest allowable size. + final previousSize = tester.view.physicalSize; + tester.view.physicalSize = itemConstraints.biggest; + addTearDown(() => tester.view.physicalSize = previousSize); + } + if (item.pumper != null) { // Defer to the `pumper` to pump the entire widget tree for this gallery item. await item.pumper!.call(tester, itemScaffold, item.description); @@ -353,7 +369,7 @@ class Gallery { } // Run the item's setup function, if there is one. - await item.setup?.call(tester); + await (item.setup ?? _itemSetup)?.call(tester); // Take a screenshot. expect(item.boundsFinder, findsOne); @@ -476,8 +492,11 @@ Image.memory( SceneLayout layout, Map goldenScreenshots, ) async { - final goldensAndGlobalKeys = Map.fromEntries( - goldenScreenshots.entries.map((entry) => MapEntry(entry.value, GlobalKey())), + final content = SceneLayoutContent( + description: _sceneDescription, + goldens: Map.fromEntries( + goldenScreenshots.entries.map((entry) => MapEntry(entry.value, GlobalKey())), + ), ); // Layout the gallery scene with the new goldens, check the intrinsic size of the @@ -487,13 +506,13 @@ Image.memory( // a corresponding `GlobalKey` already in the tree. Therefore, this layout pass inserts a // `GlobalKey` for every golden screenshot that we want to render. await tester.pumpWidgetAndAdjustWindow( - _buildGalleryLayout(tester, goldensAndGlobalKeys), + _buildGalleryLayout(tester, content), ); // Use Flutter's `precacheImage()` mechanism to get each golden screenshot bitmap to // render in this widget test. await tester.runAsync(() async { - for (final entry in goldensAndGlobalKeys.entries) { + for (final entry in content.goldens.entries) { await precacheImage( MemoryImage(entry.key.pngBytes), tester.element(find.byKey(entry.value)), @@ -506,21 +525,21 @@ Image.memory( return GoldenSceneMetadata( description: _sceneDescription, images: [ - for (final golden in goldensAndGlobalKeys.keys) + for (final golden in content.goldens.keys) GoldenImageMetadata( id: golden.id, metadata: golden.metadata, - topLeft: (goldensAndGlobalKeys[golden]!.currentContext!.findRenderObject() as RenderBox) - .localToGlobal(Offset.zero), - size: goldensAndGlobalKeys[golden]!.currentContext!.size!, + topLeft: + (content.goldens[golden]!.currentContext!.findRenderObject() as RenderBox).localToGlobal(Offset.zero), + size: content.goldens[golden]!.currentContext!.size!, ), ], ); } - Widget _buildGalleryLayout(WidgetTester tester, Map candidatesAndGlobalKeys) { + Widget _buildGalleryLayout(WidgetTester tester, SceneLayoutContent content) { return Builder(builder: (context) { - return _layout.build(tester, context, candidatesAndGlobalKeys); + return _layout.build(tester, context, content); }); } diff --git a/lib/src/scenes/layouts/animation_timeline_layout.dart b/lib/src/scenes/layouts/animation_timeline_layout.dart index 12030b9..6b7c3e1 100644 --- a/lib/src/scenes/layouts/animation_timeline_layout.dart +++ b/lib/src/scenes/layouts/animation_timeline_layout.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test_goldens/src/fonts/fonts.dart'; @@ -11,6 +13,7 @@ class AnimationTimelineSceneLayout implements SceneLayout { this.background = const GoldenSceneBackground.color(Color(0xff020817)), this.spacing = defaultGridSpacing, this.itemDecorator, + this.rowBreakPolicy, }); final GoldenSceneBackground? background; @@ -23,28 +26,67 @@ class AnimationTimelineSceneLayout implements SceneLayout { /// only impacts the final painted scene, after the screenshots have been taken. final GoldenSceneItemDecorator? itemDecorator; + /// An optional policy for where to break rows in the layout, or `null` to use a single row. + final AnimationTimelineRowBreak? rowBreakPolicy; + @override Widget build( WidgetTester tester, BuildContext context, - Map>> goldens, + SceneLayoutContent content, ) { return AnimationTimelineGoldenScene( background: background, spacing: spacing, itemDecorator: itemDecorator, - goldens: goldens, + content: content, + rowBreakPolicy: rowBreakPolicy, ); } } +/// Policy for where to break rows in an [AnimationTimeline]. +class AnimationTimelineRowBreak { + /// Breaks rows after a maximum number of columns of items. + const AnimationTimelineRowBreak.afterMaxColumnCount(this.maxColumnCount) + : beforeItemDescription = null, + afterItemDescription = null; + + /// Breaks rows before each item with the given description. + const AnimationTimelineRowBreak.beforeItemDescription(this.beforeItemDescription) + : maxColumnCount = null, + afterItemDescription = null; + + /// Breaks rows after each item with the given description. + const AnimationTimelineRowBreak.afterItemDescription(this.afterItemDescription) + : beforeItemDescription = null, + maxColumnCount = null; + + final int? maxColumnCount; + final String? beforeItemDescription; + final String? afterItemDescription; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is AnimationTimelineRowBreak && + runtimeType == other.runtimeType && + maxColumnCount == other.maxColumnCount && + beforeItemDescription == other.beforeItemDescription && + afterItemDescription == other.afterItemDescription; + + @override + int get hashCode => maxColumnCount.hashCode ^ beforeItemDescription.hashCode ^ afterItemDescription.hashCode; +} + class AnimationTimelineGoldenScene extends StatelessWidget { const AnimationTimelineGoldenScene({ super.key, this.background, this.spacing = defaultGridSpacing, this.itemDecorator, - required this.goldens, + required this.content, + this.rowBreakPolicy, }); final GridSpacing spacing; @@ -57,7 +99,9 @@ class AnimationTimelineGoldenScene extends StatelessWidget { /// only impacts the final painted scene, after the screenshots have been taken. final GoldenSceneItemDecorator? itemDecorator; - final Map goldens; + final SceneLayoutContent content; + + final AnimationTimelineRowBreak? rowBreakPolicy; @override Widget build(BuildContext context) { @@ -97,62 +141,165 @@ class AnimationTimelineGoldenScene extends StatelessWidget { letterSpacing: 4, ), ), - const SizedBox(height: 16), - Row( - mainAxisSize: MainAxisSize.min, - spacing: spacing.between, - children: [ - for (final entry in goldens.entries) // - Column( - mainAxisSize: MainAxisSize.min, - children: [ - IntrinsicWidth( - // ^ Intrinsic width is needed in case the following decorator has a `Column`, to not blow up - // when the `Flex` above is a row. - child: Builder(builder: (context) { - return _decorator( - context, - entry.key.metadata, - Image.memory( - key: entry.value, - entry.key.pngBytes, - width: entry.key.size.width.toDouble(), - height: entry.key.size.height.toDouble(), - ), - ); - }), - ), - Center( - child: Container( - width: 2, - height: 20, - color: _accentColor, - ), - ), - ], - ), - ], - ), - Divider(height: 2, thickness: 2, color: _accentColor), - const SizedBox(height: 16), - Row( - children: [ - Text( - "Start >", - style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), - ), - Spacer(), - Text( - "> End", - style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + if (content.description != null && content.description!.isNotEmpty) ...[ + const SizedBox(height: 4), + Text( + content.description!, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 18, + letterSpacing: 2, ), - ], - ), + ), + ], + const SizedBox(height: 24), + _buildRows(), ], ), ); } + Widget _buildRows() { + final itemRows = _breakDownRows(); + + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + spacing: spacing.between, + children: [ + for (final row in itemRows) // + _buildRow(row), + ], + ); + } + + List>> _breakDownRows() { + final rowBreakPolicy = this.rowBreakPolicy; + if (rowBreakPolicy == null) { + return [content.goldens.entries.toList()]; + } + + var allItems = content.goldens.entries.toList(growable: true); + final itemRows = >>[]; + + if (rowBreakPolicy.maxColumnCount != null) { + // Break after a max column count. + while (allItems.isNotEmpty) { + final end = min(rowBreakPolicy.maxColumnCount!, allItems.length); + itemRows.add( + allItems.sublist(0, end), + ); + if (end < allItems.length) { + allItems = allItems.sublist(end); + } else { + allItems = []; + } + } + return itemRows; + } + + final beforeItemDescription = rowBreakPolicy.beforeItemDescription; + if (beforeItemDescription != null) { + var row = >[]; + for (int i = 0; i < allItems.length; i += 1) { + final screenshot = allItems[i].key; + if (screenshot.metadata.description == beforeItemDescription && row.isNotEmpty) { + itemRows.add(row); + row = >[]; + } + row.add(allItems[i]); + } + if (row.isNotEmpty) { + // Add the final row to the list of rows. + itemRows.add(row); + } + + return itemRows; + } + + final afterItemDescription = rowBreakPolicy.afterItemDescription; + if (afterItemDescription != null) { + var row = >[]; + for (int i = 0; i < allItems.length; i += 1) { + row.add(allItems[i]); + + final screenshot = allItems[i].key; + if (screenshot.metadata.description == afterItemDescription && row.isNotEmpty) { + itemRows.add(row); + row = >[]; + } + } + if (row.isNotEmpty) { + // Add the final row to the list of rows. + itemRows.add(row); + } + + return itemRows; + } + + throw Exception("Unhandled row break policy: $rowBreakPolicy"); + } + + Widget _buildRow(List> items) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + spacing: spacing.between, + children: [ + for (final entry in items) // + Column( + mainAxisSize: MainAxisSize.min, + children: [ + IntrinsicWidth( + // ^ Intrinsic width is needed in case the following decorator has a `Column`, to not blow up + // when the `Flex` above is a row. + child: Builder(builder: (context) { + return _decorator( + context, + entry.key.metadata, + Image.memory( + key: entry.value, + entry.key.pngBytes, + width: entry.key.size.width.toDouble(), + height: entry.key.size.height.toDouble(), + ), + ); + }), + ), + Center( + child: Container( + width: 2, + height: 20, + color: _accentColor, + ), + ), + ], + ), + ], + ), + Divider(height: 2, thickness: 2, color: _accentColor), + const SizedBox(height: 16), + Row( + children: [ + Text( + "Start >", + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + Spacer(), + Text( + "> End", + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + ], + ), + ], + ); + } + Widget _decorator(BuildContext context, GoldenScreenshotMetadata metadata, Widget child) { // TODO: bring back configurable item decorator final itemDecorator = _itemDecorator; // this.itemDecorator ?? GoldenSceneTheme.current.itemDecorator; diff --git a/lib/src/scenes/layouts/grid_layout.dart b/lib/src/scenes/layouts/grid_layout.dart index 8efb2e2..8d88cef 100644 --- a/lib/src/scenes/layouts/grid_layout.dart +++ b/lib/src/scenes/layouts/grid_layout.dart @@ -1,4 +1,3 @@ -import 'package:flutter/material.dart' show Colors; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test_goldens/flutter_test_goldens.dart'; @@ -24,13 +23,13 @@ class GridGoldenSceneLayout implements SceneLayout { Widget build( WidgetTester tester, BuildContext context, - Map>> goldens, + SceneLayoutContent content, ) { return GridGoldenScene( background: background, spacing: spacing, itemDecorator: itemDecorator, - goldens: goldens, + goldens: content.goldens, ); } } @@ -58,6 +57,25 @@ class GridGoldenScene extends StatelessWidget { @override Widget build(BuildContext context) { + return DefaultTextStyle( + style: GoldenSceneTheme.current.defaultTextStyle, + child: GoldenSceneBounds( + child: Stack( + children: [ + Positioned.fill( + child: _buildBackground(context), + ), + Padding( + padding: spacing.around, + child: _buildGoldens(), + ), + ], + ), + ), + ); + } + + Widget _buildGoldens() { final entries = goldens.entries.toList(); final rows = []; @@ -71,15 +89,23 @@ class GridGoldenScene extends StatelessWidget { } items.add( - _buildItem( - context, - entries[index].key.metadata, - Image.memory( - key: entries[index].value, - entries[index].key.pngBytes, - width: entries[index].key.size.width, - height: entries[index].key.size.height, + Padding( + padding: EdgeInsets.only( + top: row > 0 ? defaultGridSpacing.between : 0, + left: col > 0 ? defaultGridSpacing.between : 0, ), + child: Builder(builder: (context) { + return _decorator( + context, + entries[index].key.metadata, + Image.memory( + key: entries[index].value, + entries[index].key.pngBytes, + width: entries[index].key.size.width, + height: entries[index].key.size.height, + ), + ); + }), ), ); } @@ -91,52 +117,18 @@ class GridGoldenScene extends StatelessWidget { ); } - return DefaultTextStyle( - style: GoldenSceneTheme.current.defaultTextStyle, - child: GoldenSceneBounds( - child: ColoredBox( - color: Colors.white, - child: Table( - defaultColumnWidth: IntrinsicColumnWidth(), - children: rows, - ), - // child: ConstrainedBox( - // constraints: BoxConstraints(maxWidth: maxWidth), - // // ^ We have to constrain the width due to the vertical scrolling viewport in the - // // the GridView. - // // TODO: Use some other grid implementation that doesn't include scrolling. - // child: GridView( - // gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - // mainAxisSpacing: 0, - // crossAxisCount: 3, - // crossAxisSpacing: 0, - // ), - // shrinkWrap: true, - // padding: const EdgeInsets.all(0), - // children: [ - // for (final entry in goldens.entries) - // ColoredBox( - // color: Colors.green, - // child: Image.memory( - // key: entry.value, - // entry.key.pngBytes, - // width: entry.key.size.width, - // height: entry.key.size.height, - // ), - // ), - // ], - // ), - // ), - ), - ), + return Table( + defaultColumnWidth: IntrinsicColumnWidth(), + children: rows, ); } - Widget _buildItem(BuildContext context, GoldenScreenshotMetadata metadata, Widget content) { - if (itemDecorator == null) { - return content; - } + Widget _decorator(BuildContext context, GoldenScreenshotMetadata metadata, Widget child) { + final itemDecorator = this.itemDecorator ?? GoldenSceneTheme.current.itemDecorator; + return itemDecorator(context, metadata, child); + } - return itemDecorator!(context, metadata, content); + Widget _buildBackground(BuildContext context) { + return (background ?? GoldenSceneTheme.current.background).build(context); } } diff --git a/lib/src/scenes/layouts/magazine_layout.dart b/lib/src/scenes/layouts/magazine_layout.dart index c2fb33f..6442e0a 100644 --- a/lib/src/scenes/layouts/magazine_layout.dart +++ b/lib/src/scenes/layouts/magazine_layout.dart @@ -41,9 +41,9 @@ class MagazineGoldenSceneLayout implements SceneLayout { Widget build( WidgetTester tester, BuildContext context, - Map>> goldens, + SceneLayoutContent content, ) { - final goldensList = goldens.entries.toList(); + final goldensList = content.goldens.entries.toList(); return MagazineGoldenScene( sceneBackground: sceneBackground, diff --git a/lib/src/scenes/layouts/row_and_column_layout.dart b/lib/src/scenes/layouts/row_and_column_layout.dart index f503301..8332f75 100644 --- a/lib/src/scenes/layouts/row_and_column_layout.dart +++ b/lib/src/scenes/layouts/row_and_column_layout.dart @@ -57,14 +57,14 @@ class FlexSceneLayout implements SceneLayout { Widget build( WidgetTester tester, BuildContext context, - Map>> goldens, + SceneLayoutContent content, ) { return FlexGoldenScene( direction: direction, background: background, spacing: spacing, itemDecorator: itemDecorator, - goldens: goldens, + goldens: content.goldens, ); } } diff --git a/lib/src/scenes/scene_layout.dart b/lib/src/scenes/scene_layout.dart index fa410cb..e4f985c 100644 --- a/lib/src/scenes/scene_layout.dart +++ b/lib/src/scenes/scene_layout.dart @@ -7,24 +7,33 @@ abstract interface class SceneLayout { Widget build( WidgetTester tester, BuildContext context, - // TODO: We need a data structure that represents all incoming info: - // - description of scene - // - each screenshot - // - pixels - // - description - // - WidgetTester simulated timestamp (for animation durations) - // - layers - // - GlobalKey - // - // Pretty much everything from GoldenSceneMetadata, minus the final bounds, - // plus GlobalKeys for reach screenshot. - // - // This way the scene can show the scene description, each golden description, - // the timestamp of each golden, log out the number of layers, etc. - Map goldens, + SceneLayoutContent content, ); } +// TODO: Add missing pieces to this data structure over time +// - each screenshot +// - pixels +// - description +// - WidgetTester simulated timestamp (for animation durations) +// - layers +// - GlobalKey +// +// Pretty much everything from GoldenSceneMetadata, minus the final bounds, +// plus GlobalKeys for reach screenshot. +// +// This way the scene can show the scene description, each golden description, +// the timestamp of each golden, log out the number of layers, etc. +class SceneLayoutContent { + const SceneLayoutContent({ + this.description, + required this.goldens, + }); + + final String? description; + final Map goldens; +} + const defaultGridSpacing = GridSpacing(around: EdgeInsets.all(48), between: 48); class GridSpacing { diff --git a/lib/src/scenes/timeline.dart b/lib/src/scenes/timeline.dart index 6f903d3..20e8de7 100644 --- a/lib/src/scenes/timeline.dart +++ b/lib/src/scenes/timeline.dart @@ -28,20 +28,24 @@ class Timeline { this._description, { Directory? directory, required String fileName, - GoldenSceneItemScaffold itemScaffold = minimalItemScaffold, + Size? windowSize, + GoldenSceneItemScaffold itemScaffold = standardTimelineItemScaffold, required SceneLayout layout, GoldenSceneBackground? goldenBackground, }) : _directory = directory, _fileName = fileName, + _windowSize = windowSize, + _itemScaffold = itemScaffold, _layout = layout, - _goldenBackground = goldenBackground, - _itemScaffold = itemScaffold; + _goldenBackground = goldenBackground; final String _description; late final Directory? _directory; final String _fileName; + final Size? _windowSize; + final GoldenSceneItemScaffold _itemScaffold; final GoldenSceneBackground? _goldenBackground; @@ -53,26 +57,32 @@ class Timeline { /// Setup the scene before taking any photos. /// /// If you only need to provide a widget tree, without taking other [WidgetTester] - /// actions, consider using [setupWithPump] for convenience. + /// actions, consider using [setupWithBuilder] for convenience. Timeline setup(TimelineSetupDelegate delegate) { if (_setup != null) { throw Exception("Timeline was already set up, but tried to call setup() again."); } - _setup = _TimelineSetup(delegate); + _setup = _TimelineSetup((tester) async { + _configureWindowSize(tester); + + await delegate(tester); + }); return this; } - /// Setup the scene before taking any photos, by pumping a widget tree. + /// Setup the scene before taking any photos, by building a widget tree. /// - /// If you need to take additional actions, beyond a single pump, use [setup] instead. - Timeline setupWithPump(TimelineSetupWithPumpFactory sceneBuilder) { + /// If you need to take additional actions, beyond a builder delegate, use [setup] instead. + Timeline setupWithBuilder(TimelineSetupBuilder sceneBuilder) { if (_setup != null) { throw Exception("Timeline was already set up, but tried to call setupWithPump() again."); } _setup = _TimelineSetup((tester) async { + _configureWindowSize(tester); + final widgetTree = _itemScaffold(tester, sceneBuilder()); await tester.pumpWidget(widgetTree); }); @@ -89,6 +99,8 @@ class Timeline { } _setup = _TimelineSetup((tester) async { + _configureWindowSize(tester); + final widgetTree = _itemScaffold(tester, widget); await tester.pumpWidget(widgetTree); }); @@ -96,6 +108,14 @@ class Timeline { return this; } + void _configureWindowSize(WidgetTester tester) { + if (_windowSize != null) { + final previousWindowSize = tester.view.physicalSize; + tester.view.physicalSize = _windowSize; + addTearDown(() => tester.view.physicalSize = previousWindowSize); + } + } + /// Take a golden photo screenshot of the current Flutter UI. /// /// {@template golden_image_bounds_default_finder} @@ -279,7 +299,10 @@ class Timeline { final sceneMetadata = await _layoutPhotos( tester, photos, - renderablePhotos, + SceneLayoutContent( + description: _description, + goldens: renderablePhotos, + ), _layout, goldenBackground: _goldenBackground, ); @@ -318,7 +341,7 @@ class Timeline { Future _layoutPhotos( WidgetTester tester, List photos, - Map renderablePhotos, + SceneLayoutContent content, SceneLayout layout, { GoldenSceneBackground? goldenBackground, }) async { @@ -336,7 +359,7 @@ class Timeline { final timeline = _buildTimeline( tester, contentKey, - renderablePhotos, + content, galleryKey: galleryKey, goldenBackground: goldenBackground, ); @@ -344,7 +367,7 @@ class Timeline { await tester.pumpWidgetAndAdjustWindow(timeline); await tester.runAsync(() async { - for (final entry in renderablePhotos.entries) { + for (final entry in content.goldens.entries) { await precacheImage( MemoryImage(entry.key.pngBytes), tester.element(find.byKey(entry.value)), @@ -357,13 +380,13 @@ class Timeline { return GoldenSceneMetadata( description: _description, images: [ - for (final golden in renderablePhotos.keys) + for (final golden in content.goldens.keys) GoldenImageMetadata( id: golden.id, metadata: golden.metadata, topLeft: - (renderablePhotos[golden]!.currentContext!.findRenderObject() as RenderBox).localToGlobal(Offset.zero), - size: renderablePhotos[golden]!.currentContext!.size!, + (content.goldens[golden]!.currentContext!.findRenderObject() as RenderBox).localToGlobal(Offset.zero), + size: content.goldens[golden]!.currentContext!.size!, ), ], ); @@ -386,12 +409,12 @@ class Timeline { Widget _buildTimeline( WidgetTester tester, GlobalKey contentKey, - Map renderablePhotos, { + SceneLayoutContent content, { Key? galleryKey, GoldenSceneBackground? goldenBackground, }) { return Builder(builder: (context) { - return _layout.build(tester, context, renderablePhotos); + return _layout.build(tester, context, content); }); } @@ -542,7 +565,7 @@ class _TimelineSetup { typedef TimelineSetupDelegate = Future Function(WidgetTester tester); -typedef TimelineSetupWithPumpFactory = Widget Function(); +typedef TimelineSetupBuilder = Widget Function(); class _TimelinePhotoRequest { const _TimelinePhotoRequest(this.photoBoundsFinder, this.description); @@ -566,7 +589,9 @@ class TimelineTestContext { final scratchPad = {}; } -Widget minimalItemScaffold(WidgetTester tester, Widget content) { +/// The standard [GoldenSceneItemScaffold] that wraps the content of a [Timeline], which +/// includes a dark theme, a dark background color, and some padding around the content. +Widget standardTimelineItemScaffold(WidgetTester tester, Widget content) { return MaterialApp( theme: ThemeData( brightness: Brightness.dark, @@ -586,3 +611,18 @@ Widget minimalItemScaffold(WidgetTester tester, Widget content) { debugShowCheckedModeBanner: false, ); } + +/// An absolute minimal [GoldenSceneItemScaffold] that wraps the content within a [Timeline]. +Widget minimalTimelineItemScaffold(WidgetTester tester, Widget content) { + return MaterialApp( + theme: ThemeData( + fontFamily: goldenBricks, + ), + home: Center( + child: GoldenImageBounds( + child: content, + ), + ), + debugShowCheckedModeBanner: false, + ); +} diff --git a/test_goldens/flutter/buttons/buttons_test.dart b/test_goldens/flutter/buttons/buttons_test.dart index 6cbf430..05d96d6 100644 --- a/test_goldens/flutter/buttons/buttons_test.dart +++ b/test_goldens/flutter/buttons/buttons_test.dart @@ -14,7 +14,7 @@ void main() { fileName: "button_elevated_interactions", layout: RowSceneLayout(), ) - .setupWithPump(() { + .setupWithBuilder(() { return FlutterWidgetScaffold( goldenKey: goldenKey, child: ElevatedButton( @@ -39,7 +39,7 @@ void main() { fileName: "button_text_interactions", layout: RowSceneLayout(), ) - .setupWithPump(() { + .setupWithBuilder(() { return FlutterWidgetScaffold( goldenKey: goldenKey, child: TextButton( @@ -64,7 +64,7 @@ void main() { fileName: "button_icon_interactions", layout: RowSceneLayout(), ) - .setupWithPump(() { + .setupWithBuilder(() { return MaterialApp( theme: ThemeData( fontFamily: goldenBricks, @@ -98,7 +98,7 @@ void main() { fileName: "button_fab_interactions", layout: RowSceneLayout(), ) - .setupWithPump(() { + .setupWithBuilder(() { return FlutterWidgetScaffold( goldenKey: goldenKey, child: FloatingActionButton( @@ -130,7 +130,7 @@ void main() { ), ), ) - .setupWithPump(() { + .setupWithBuilder(() { return FlutterWidgetScaffold( goldenKey: goldenKey, child: FloatingActionButton.extended( diff --git a/test_goldens/flutter/list_tile/list_tile_test.dart b/test_goldens/flutter/list_tile/list_tile_test.dart index 8ac3db5..d70258b 100644 --- a/test_goldens/flutter/list_tile/list_tile_test.dart +++ b/test_goldens/flutter/list_tile/list_tile_test.dart @@ -15,7 +15,7 @@ void main() { fileName: "list_tile_interactions", layout: ColumnSceneLayout(), ) - .setupWithPump(() { + .setupWithBuilder(() { return FlutterWidgetScaffold( goldenKey: goldenKey, child: SizedBox( diff --git a/test_goldens/flutter/textfield/textfield_tests.dart b/test_goldens/flutter/textfield/textfield_tests.dart index 882fc45..92c0220 100644 --- a/test_goldens/flutter/textfield/textfield_tests.dart +++ b/test_goldens/flutter/textfield/textfield_tests.dart @@ -13,7 +13,7 @@ void main() { fileName: "textfield_interactions", layout: ColumnSceneLayout(), ) - .setupWithPump(() { + .setupWithBuilder(() { return FlutterWidgetScaffold( goldenKey: goldenKey, child: SizedBox( diff --git a/test_goldens/scene_types/gallery/gallery_item_sizes.png b/test_goldens/scene_types/gallery/gallery_item_sizes.png index aa075c1..2c62b4d 100644 Binary files a/test_goldens/scene_types/gallery/gallery_item_sizes.png and b/test_goldens/scene_types/gallery/gallery_item_sizes.png differ