From c904641fb54cb182586d0459191c359a3ce9b79b Mon Sep 17 00:00:00 2001 From: Matt Carroll Date: Sat, 5 Jul 2025 12:56:47 -0700 Subject: [PATCH 01/10] FIX: Make use of given 'spacing' in GridGoldenSceneLayout --- lib/src/scenes/layouts/grid_layout.dart | 59 +++++++++---------------- 1 file changed, 21 insertions(+), 38 deletions(-) diff --git a/lib/src/scenes/layouts/grid_layout.dart b/lib/src/scenes/layouts/grid_layout.dart index 8efb2e2..13232bb 100644 --- a/lib/src/scenes/layouts/grid_layout.dart +++ b/lib/src/scenes/layouts/grid_layout.dart @@ -1,3 +1,4 @@ +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart' show Colors; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -71,14 +72,20 @@ 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: _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, + ), ), ), ); @@ -96,37 +103,13 @@ class GridGoldenScene extends StatelessWidget { child: GoldenSceneBounds( child: ColoredBox( color: Colors.white, - child: Table( - defaultColumnWidth: IntrinsicColumnWidth(), - children: rows, + child: Padding( + padding: spacing.around, + 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, - // ), - // ), - // ], - // ), - // ), ), ), ); From 6b4cdb7232129ff55db6c3d638a0e49d078460b5 Mon Sep 17 00:00:00 2001 From: Matt Carroll Date: Sat, 5 Jul 2025 16:59:33 -0700 Subject: [PATCH 02/10] FIX/ADJUSTMENT: Don't paint to Canvas before taking photo with FlutterCamera - this broke nuanced render object and layer behavior in follow_the_leader. Instead, use toImageSync() on the RepaintBoundary. --- lib/src/flutter/flutter_camera.dart | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) 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); From ae42541d715860f4882e8d30217a4210a9d677de Mon Sep 17 00:00:00 2001 From: Matt Carroll Date: Sat, 5 Jul 2025 17:09:50 -0700 Subject: [PATCH 03/10] ADJUSTMENT: Changed GridSceneLayout to use default item decorator and background to match existing behavior of FlexSceneLayout. --- lib/src/scenes/layouts/grid_layout.dart | 70 ++++++++++++++----------- 1 file changed, 40 insertions(+), 30 deletions(-) diff --git a/lib/src/scenes/layouts/grid_layout.dart b/lib/src/scenes/layouts/grid_layout.dart index 13232bb..b834ec3 100644 --- a/lib/src/scenes/layouts/grid_layout.dart +++ b/lib/src/scenes/layouts/grid_layout.dart @@ -1,5 +1,4 @@ import 'package:flutter/cupertino.dart'; -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'; @@ -59,6 +58,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 = []; @@ -77,16 +95,18 @@ class GridGoldenScene extends StatelessWidget { top: row > 0 ? defaultGridSpacing.between : 0, left: col > 0 ? defaultGridSpacing.between : 0, ), - child: _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, - ), - ), + 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, + ), + ); + }), ), ); } @@ -98,28 +118,18 @@ class GridGoldenScene extends StatelessWidget { ); } - return DefaultTextStyle( - style: GoldenSceneTheme.current.defaultTextStyle, - child: GoldenSceneBounds( - child: ColoredBox( - color: Colors.white, - child: Padding( - padding: spacing.around, - child: Table( - defaultColumnWidth: IntrinsicColumnWidth(), - children: rows, - ), - ), - ), - ), + 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); } } From a1965571eeb4f45af0a5f96815faf43d40f52d33 Mon Sep 17 00:00:00 2001 From: Matt Carroll Date: Sat, 5 Jul 2025 18:22:11 -0700 Subject: [PATCH 04/10] FEATURE: Add itemSetup to Gallery so that a single setup can be applied to all items. --- lib/src/scenes/gallery.dart | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/src/scenes/gallery.dart b/lib/src/scenes/gallery.dart index acda8ff..6ecc3f0 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 = {}; @@ -353,7 +361,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); From e4af9ce61d2274e780980bd1fe26a1faa9409e55 Mon Sep 17 00:00:00 2001 From: Matt Carroll Date: Sun, 6 Jul 2025 13:34:29 -0700 Subject: [PATCH 05/10] * Gallery sets its window size to match the item constraints for tests that need a window boundary * Timeline lets you specify a * Timeline now offers a truly minimal item scaffold with no theming, background color, or padding --- lib/src/scenes/gallery.dart | 8 ++++++ lib/src/scenes/timeline.dart | 47 ++++++++++++++++++++++++++++++++---- 2 files changed, 50 insertions(+), 5 deletions(-) diff --git a/lib/src/scenes/gallery.dart b/lib/src/scenes/gallery.dart index 6ecc3f0..7ef0ad9 100644 --- a/lib/src/scenes/gallery.dart +++ b/lib/src/scenes/gallery.dart @@ -337,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); diff --git a/lib/src/scenes/timeline.dart b/lib/src/scenes/timeline.dart index 6f903d3..04a820c 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; @@ -59,7 +63,11 @@ class Timeline { 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; } @@ -73,6 +81,8 @@ class Timeline { } _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} @@ -566,7 +586,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 +608,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, + ); +} From 41560f13898a459447a1800f4376c92142aa2f8c Mon Sep 17 00:00:00 2001 From: Matt Carroll Date: Sun, 6 Jul 2025 16:06:44 -0700 Subject: [PATCH 06/10] * Added a row break policy to animation timeline layout * Switched out map of goldens in layout API for a SceneLayoutContent data structure that includes goldens, description, etc - so that scene layouts can make more decisions about info to include in the final render --- doc/marketing_goldens/shadcn_test_tools.dart | 8 +- lib/src/flutter/flutter_golden_matcher.dart | 1 - lib/src/scenes/failure_scene.dart | 2 +- lib/src/scenes/gallery.dart | 23 +- .../layouts/animation_timeline_layout.dart | 255 ++++++++++++++---- lib/src/scenes/layouts/grid_layout.dart | 4 +- lib/src/scenes/layouts/magazine_layout.dart | 4 +- .../scenes/layouts/row_and_column_layout.dart | 4 +- lib/src/scenes/scene_layout.dart | 39 +-- lib/src/scenes/timeline.dart | 21 +- 10 files changed, 261 insertions(+), 100 deletions(-) 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/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 7ef0ad9..c276a97 100644 --- a/lib/src/scenes/gallery.dart +++ b/lib/src/scenes/gallery.dart @@ -492,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 @@ -503,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)), @@ -522,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 b834ec3..68588e4 100644 --- a/lib/src/scenes/layouts/grid_layout.dart +++ b/lib/src/scenes/layouts/grid_layout.dart @@ -24,13 +24,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, ); } } 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 04a820c..a6d9277 100644 --- a/lib/src/scenes/timeline.dart +++ b/lib/src/scenes/timeline.dart @@ -299,7 +299,10 @@ class Timeline { final sceneMetadata = await _layoutPhotos( tester, photos, - renderablePhotos, + SceneLayoutContent( + description: _description, + goldens: renderablePhotos, + ), _layout, goldenBackground: _goldenBackground, ); @@ -338,7 +341,7 @@ class Timeline { Future _layoutPhotos( WidgetTester tester, List photos, - Map renderablePhotos, + SceneLayoutContent content, SceneLayout layout, { GoldenSceneBackground? goldenBackground, }) async { @@ -356,7 +359,7 @@ class Timeline { final timeline = _buildTimeline( tester, contentKey, - renderablePhotos, + content, galleryKey: galleryKey, goldenBackground: goldenBackground, ); @@ -364,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)), @@ -377,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!, ), ], ); @@ -406,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); }); } From 97ca9ab191a17024bb80cddd125f822ee0e13ffc Mon Sep 17 00:00:00 2001 From: Matt Carroll Date: Sun, 6 Jul 2025 19:51:26 -0700 Subject: [PATCH 07/10] Renamed Timeline 'pump' method to 'builder' because its not actually a pumper --- lib/src/scenes/timeline.dart | 10 +++++----- test_goldens/flutter/buttons/buttons_test.dart | 10 +++++----- test_goldens/flutter/list_tile/list_tile_test.dart | 2 +- test_goldens/flutter/textfield/textfield_tests.dart | 2 +- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/src/scenes/timeline.dart b/lib/src/scenes/timeline.dart index a6d9277..20e8de7 100644 --- a/lib/src/scenes/timeline.dart +++ b/lib/src/scenes/timeline.dart @@ -57,7 +57,7 @@ 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."); @@ -72,10 +72,10 @@ class Timeline { 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."); } @@ -565,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); 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( From 323926211fdad603c1cd206eaa0919367b657f87 Mon Sep 17 00:00:00 2001 From: Matt Carroll Date: Sun, 6 Jul 2025 19:52:06 -0700 Subject: [PATCH 08/10] Regenerated a golden that changed slightly when window size control was added to Gallery --- .../gallery/gallery_item_sizes.png | Bin 23490 -> 23527 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/test_goldens/scene_types/gallery/gallery_item_sizes.png b/test_goldens/scene_types/gallery/gallery_item_sizes.png index aa075c13ce4860b4ac65d84eacd84b927b4f1f38..2c62b4d767d7196a8e2e326b4995e559b7512991 100644 GIT binary patch literal 23527 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 aw-KhMhNOl_T9L{`>a>k!G8fTuu1*^ literal 23490 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 hdXesYsvQuyN+j4}S$_*?`qP)7Ayph6HE@;1g zL0}HK69^6X5(syp@qRINVXNO3Kb}ZlKHmTp z#SX=_%HtL{p?s}K8Bx*Xd8at35Bc-X{kify@D7+*OixeGpF`oaXl%(={S#U<6ZqQl8dJ!xvgu#g+!l6O=3eaiM%tWL>{)#MC@53Y+3i*RNp$Ja6M@xt zHhPSb7Rf=^Y#QemMv2$2cnEE7Uw&4!Hgo;@A$4_H9U>&)<+}1F1t#@m!%C6cU-92R zv{*J$E}`7YYfjCh={e2t$@k)Tsav1&kM?G8ngs=crRHUoAlXPJL*LZR-L z?_nGr9H5sKdDnwC(;7KBkI(y`rqC0;aym*EdIS3|k=az=j) z<;Q{%7q3J=rSzLMx2MG>ny zx&C%#%gKE|s`Pc!oHg@R*T>)<{-+$TU4P6vUU;A<`#d?vOuFt-yjMAT?lLI2o=VG( z1@Aw&-I|}GoszVEqjhz2W4O`a@!2$mxVWU~SFc{JFjuR)sI!KMa!_)rJOT%5rUuIZ z>TGdP(fu+qi!zJ+F*?dd$@ycl@4L`T@I^vz0_QIJ<1pGamaX;%&h&Sv&F~QMcE5An zV5Gv+e)Xs72g7%3Hghv|u5@=_tY3YYlf#O9*$yRObOSp8vd}j8TdADyy2s-h3up&X z7db`^+1;Tv!-Mo&6E&;*3w$C$GMjQDIE+tu*k^-+^$>(Luc1>{m{EhLI||i+hD}#4 zzEODt{pIuLg38Jc)nvVrBGKDzkxyczLBWx(T#9_Txv-uLMY-=hA?hPyvUX0csCWfPXQfZ|^OfZK z+tq5TYiJ^JmNT@pcHQrL`@q*<0bZS9e}61L_PN5fW=wc55lT=%j#p6c5FL^RIgfthSnS>b|qf>x|YgPa>0eVV_F zVi)8eit%!7m6N@B`?fESJ8?ML#K?#iZr-zxdAuTk!m%l~G|og&s)Ux64gc0re#g9H zidgjaQq-G@EhhUy6|rk-C8EJ^cW8SOebnJ6E`Y=S^SO63$7aE3u9q zk+-w6`<$oDI-VcGA3#cd)Z-K~5eK}MG}A4wf*qfFyj1u_gU6QsBtL^&P6Y)W$^6np z^_yHX#bXV)i$JA7WRR=m`P2)x_w|v2qUF8n?ph{TF5aQ%I!DX#{&G_MbA<&eYky`n-tEH%=cOV3(c^em(v5H;8ZN*^fvQy%21>qbTq-|ybEIoMgfc7g8Ye0?PaJ&Xy=MScD2 z+TD{tdjqbeYddROYV)E`5zhFR(Xd5~LUE8VKR;pm#FIpvP`0>~I2{47qnP~A z3@-Z{&`Ib&2UYvbE-^r@Lm$3|o(Ai|&YIiy6_oKrxX<*5OR zYRa=`&qkgJ{^XWOO+p8g&-QdYr>KaaYxjQiUfafazkyc(9akM}$w{Ey^XLgC9-6{u zZ%M0uzUTy0J%|047sh5Ced4D%}Fm)fyD^p91P85|I3WU8NpR+)1&Y z8r#kVP^4Wp669R=N#!%>U}Ze981IZP0}llD~6NB!0o9;=KW9NLlrCDlKXWtoI3 zM~^~YeY9iWPuyulx+joEfnUEm{2od6-;H|$ie;XS?E@AlvN1m#Ejeb`SKT1p?fUtX zQSYX(8U=C<@A-Kr;ad&F(7vMd_y~$aACNOd|9Ukh-sQpqozwRHT-QkeQ0c675RE@(vrw}CL*g%ia@CZgti0W^lqbx!N9dw&nO zA$O0C4s62C+n`yI>Jjd?{+uPTFw!iRzoU6(a$&Q0XlK(3NzWuDC1Hh?IoPE-Yi+tV zphehbvw8iTfV|-j8+WCsg?>*F-&sUq158_AUNH^Ruog5wvQY$j;Yp5`&6w>-!!%1i zwB;o^OHM>jygP&+65eK(2l-nT$Dh1pN4sTeXP z1#~o!K|I1M=AW}wSv$DYz`RJdT}&@9h?<3NY;+~wliW;h$cA9NiA3yOU6XIDx@vx53h8;F%@$B=ZJ#KW zw($@uWq+m*d5}nhsHvsdmSpXQ2{axM!*3YD9U0GJGPQ}qk-lb8QHm~h%kbiF#1?O| zgkes~bsiGF4me`D#6V_1S65|TE`|EkyG$aGEI?|1N`-tMT^dkU5gcmnCqmjZFSj^` zJ)6K@tamA?t)&7k-nURsmV+-v2s1^GY>yMKm3SJ z&xQG$HWI#gGA>8N85z3{{Y4Z-FaM}C9C}F#6Hde8_Gv$6!yOqiAsdt z6;G%T5AFIVKpvX}-+@%JtE&qi$n2NUdqU=-Xam2j1vt|Tz(;cTmDm^}h$g?WTfPZs z)ZdZ1GasLju+f-uLYmNl?|wKd0gl5K`koFQZz^_T8YDrkfe5a2bi^Nu@^=n6Qpw!= z13zLCw&Fbab+qkvY*LPFd!HlKagaO#FjLUlIKAA{paFwQyN=2sEg3*fzcDu?J($wi z4C^(6R?5CSt%q}Sb2el9yEd|c7y8ySxcH-kNJv<5#*x9zP7V%HFk_zOa$zQx z|Ers`k7?=(!}z5N8xz!u%Q_J2u2UQ$DIg3H6x3mavFfmh)>^<8+d^AxF9Ta)K+!pM zBN)MrWiV$nd|22Vpn#ivj5%SM$PiF0mTX4kv(>4H0(*{Gvg{*E_ScsEb5ppzx9>go z+;h))-{14N)L*(KIKAx)R+oDFv7q;^M~BAS<$DOH-`-u(*w#&Tl}}_Nxz>Qbhs&Xg zdQZl=FI|cP#0(UR0*li#uBo(M`p$X4SuPcZvLx|Rnzw|q$((E(K79y@&A7hCeP9Xm zf{YO*<3H4Kpam5Q55X#BA*8|3v-L~k4z5sXd97C+ztmZqLpE;>30al3)-c-=LEW99 zBJc-fC+^mQ>0pYFBzLq;cA^4|CI5R6oM-Rb*U%ZiB>r-Np+wC$6t3Yv2vlU(-AdW* z|77KKJ6rSJyzmeFl+HJ9+#q5_nc{9X_`Ac$jr@|$G@epo`j!5~-|lDbf!u}j`urQ& z3)lO9iskJYxAqfi*`mdZ7QFO|&AS`y&!sPP0g{k~2{tJu1JAwY)bsrQDDV9bt{n|q zMEn4poD86#zP@;I?eUE<*-hWpS|%IPqu*IMnvft62*w_s?9jy;i;L@vH=Te)UPhd` zX3m^>cuIAGO%q+Xu4wOt_+Kzp&dCA{hwMzX3kk2e6>8>7CZXvNt)~E^)5s*B}Ye z-rk;ce?Wle;_8^WO8 zBz`{(ax7Rqb`)WVy7~Rf^c-Rf`JdP*vr+IMqN3{R>LiBwx?EJZ5jQEtggJ+^v-AC4 zfD41=TefUz>gjO-Es}aGZwoLQLqS17Xxa!8UZkeL@m0({Z8V~`nhBFUU5-7H+Aiwz zHbS8gD6LI%b8|%P8|&h{DjkukwgFc{%oxZm_V?!i2!UN>jAV&gA3ks(I3{KRv_=#)W@(z`Mb!M`17-Zc zC`>OQJ%bF#0Yv&V3bj(ZQ?GxR1zQFrg6zwpjExU(zC8o$>#bjU%n2UkfH0{VkNIK0zJ!?&*ys} zM~vn$KHdXOJZQZ&ux%h!3)o!|M(4uWA3mZ2iId@P+#&eL^Q*HD-0xF5KYHzB^t9d7SE|15PSpwKq zAiUY7rKO*xPv~GHLab?&7HdsS4fbc`=CX;C;lRLv&W2<-F%AM?q9|l-8_-Qi4~A6$ zCbb=pQ_)3;X-ePlu%k$1(sf}HXMrTHb7Bl*RPtp{*>}nGoSfszdLpocQlN5(xG|qmtNr{zY7K|BV`-%F0Tj&m6j5RW>pKD;sOCel!Ga zTUAvxHuhr<`4cZMmasap0d)(=uBQhE5XqT?Ut%}ZJR?`r4!5*~QS}%OERSv%{fNpu zGW+w1ChrCDUN)dEc?SuOKdK0hi=)v^i*y+O9jDnj)T1M?+3ZXN1cy(e{y*)fZ>ZdoT%6G|d2z~(HO7r+NBJaPA$eV|VeE)Go zuloENk+&Z{{W&C@_IDwX2!9L7=kG&8BjNae+DN2ro;H%N_g}V>_wQTjX@qHMqO_mf S?(QYGdBbaOtSes=ll2RsWIG}N From 907a6df9687313e7abf7804be62374e9e4b0d490 Mon Sep 17 00:00:00 2001 From: Matt Carroll Date: Sun, 6 Jul 2025 20:17:38 -0700 Subject: [PATCH 09/10] Removed unnecessary cupertino import --- lib/src/scenes/layouts/grid_layout.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/src/scenes/layouts/grid_layout.dart b/lib/src/scenes/layouts/grid_layout.dart index 68588e4..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/cupertino.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test_goldens/flutter_test_goldens.dart'; From f936df2dbde27300813cf5e220f9753cd81938af Mon Sep 17 00:00:00 2001 From: Matt Carroll Date: Sun, 6 Jul 2025 20:23:23 -0700 Subject: [PATCH 10/10] FIX: A website typo and a bad CSS code configuration --- doc/website/source/index.md | 2 +- doc/website/source/styles/docs_page_layout.scss | 16 +++++++++------- 2 files changed, 10 insertions(+), 8 deletions(-) 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 {