diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..6ea5cb2 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "dart.lineLength": 120 +} \ No newline at end of file diff --git a/lib/src/fonts/fonts.dart b/lib/src/fonts/fonts.dart index cbf41b8..dd0fa15 100644 --- a/lib/src/fonts/fonts.dart +++ b/lib/src/fonts/fonts.dart @@ -1,12 +1,19 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test_goldens/src/fonts/golden_toolkit_fonts.dart' as golden_toolkit; +/// Remember if fonts have already been loaded in this isolate. +bool _fontsLoaded = false; + /// Tools for working with fonts in tests. abstract class TestFonts { /// Load all fonts registered with the app and make them available /// to widget tests. static Future loadAppFonts() async { + if (_fontsLoaded) { + return; + } await golden_toolkit.loadAppFonts(); + _fontsLoaded = true; } static const openSans = "packages/flutter_test_goldens/OpenSans"; diff --git a/lib/src/scenes/gallery.dart b/lib/src/scenes/gallery.dart index c276a97..7885cf2 100644 --- a/lib/src/scenes/gallery.dart +++ b/lib/src/scenes/gallery.dart @@ -7,6 +7,7 @@ import 'package:flutter/material.dart' hide Image; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test_goldens/src/flutter/flutter_camera.dart'; import 'package:flutter_test_goldens/src/flutter/flutter_test_extensions.dart'; +import 'package:flutter_test_goldens/src/fonts/fonts.dart'; import 'package:flutter_test_goldens/src/goldens/golden_collections.dart'; import 'package:flutter_test_goldens/src/goldens/golden_comparisons.dart'; import 'package:flutter_test_goldens/src/goldens/golden_rendering.dart'; @@ -296,33 +297,42 @@ class Gallery { Future run(WidgetTester tester) async { FtgLog.pipeline.info("Rendering or comparing golden - $_sceneDescription"); - // Build each golden tree and take `FlutterScreenshot`s. - final camera = FlutterCamera(); - await _takeNewScreenshots(tester, camera); - - // Convert each `FlutterScreenshot` to a golden `GoldenSceneScreenshot`, which includes - // additional metadata, and multiple image representations. - final screenshots = await _convertFlutterScreenshotsToSceneScreenshots(tester, camera.photos); - - if (autoUpdateGoldenFiles) { - // Generate new goldens. - FtgLog.pipeline.finer("Generating new goldens..."); - // TODO: Return a success/failure report that we can publish to the test output. - await _updateGoldenScene( - tester, - _fileName, - screenshots, - ); - FtgLog.pipeline.finer("Done generating new goldens."); - } else { - // Compare to existing goldens. - FtgLog.pipeline.finer("Comparing existing goldens..."); - // TODO: Return a success/failure report that we can publish to the test output. - await _compareGoldens(tester, _fileName, screenshots); - FtgLog.pipeline.finer("Done comparing goldens."); + await TestFonts.loadAppFonts(); + + tester.view + ..devicePixelRatio = 1.0 + ..platformDispatcher.textScaleFactorTestValue = 1.0; + + try { + // Build each golden tree and take `FlutterScreenshot`s. + final camera = FlutterCamera(); + await _takeNewScreenshots(tester, camera); + + // Convert each `FlutterScreenshot` to a golden `GoldenSceneScreenshot`, which includes + // additional metadata, and multiple image representations. + final screenshots = await _convertFlutterScreenshotsToSceneScreenshots(tester, camera.photos); + + if (autoUpdateGoldenFiles) { + // Generate new goldens. + FtgLog.pipeline.finer("Generating new goldens..."); + // TODO: Return a success/failure report that we can publish to the test output. + await _updateGoldenScene( + tester, + _fileName, + screenshots, + ); + FtgLog.pipeline.finer("Done generating new goldens."); + } else { + // Compare to existing goldens. + FtgLog.pipeline.finer("Comparing existing goldens..."); + // TODO: Return a success/failure report that we can publish to the test output. + await _compareGoldens(tester, _fileName, screenshots); + FtgLog.pipeline.finer("Done comparing goldens."); + } + FtgLog.pipeline.fine("Done with golden generation/comparison"); + } finally { + tester.view.reset(); } - - FtgLog.pipeline.fine("Done with golden generation/comparison"); } /// For each scene screenshot request, pumps its widget tree, and then screenshots it with diff --git a/lib/src/scenes/timeline.dart b/lib/src/scenes/timeline.dart index 20e8de7..b2d44ef 100644 --- a/lib/src/scenes/timeline.dart +++ b/lib/src/scenes/timeline.dart @@ -5,6 +5,7 @@ import 'dart:ui' as ui; import 'package:flutter/material.dart' hide Image; import 'package:flutter_test/flutter_test.dart'; +import 'package:flutter_test_goldens/flutter_test_goldens.dart'; import 'package:flutter_test_goldens/src/flutter/flutter_camera.dart'; import 'package:flutter_test_goldens/src/flutter/flutter_test_extensions.dart'; import 'package:flutter_test_goldens/src/goldens/golden_collections.dart'; @@ -223,119 +224,129 @@ class Timeline { "Can't render or compare golden file without a setup action. Please call setup() or setupWithPump()."); } - FtgLog.pipeline.info("Rendering or comparing golden - $_fileName"); + await TestFonts.loadAppFonts(); - // Always operate at a 1:1 logical-to-physical pixel ratio to help reduce - // anti-aliasing and other artifacts from fractional pixel offsets. - tester.view.devicePixelRatio = 1.0; + tester.view + ..devicePixelRatio = 1.0 + ..platformDispatcher.textScaleFactorTestValue = 1.0; - final camera = FlutterCamera(); - final testContext = TimelineTestContext(); + try { + FtgLog.pipeline.info("Rendering or comparing golden - $_fileName"); - // Setup the scene. - FtgLog.pipeline.info("Running any given setup delegate before running steps."); - await _setup!.setupDelegate(tester); + // Always operate at a 1:1 logical-to-physical pixel ratio to help reduce + // anti-aliasing and other artifacts from fractional pixel offsets. + tester.view.devicePixelRatio = 1.0; - // Take photos and modify scene over time. - for (int i = 0; i < _steps.length; i += 1) { - final step = _steps[i]; - FtgLog.pipeline.info("Running step: $step"); + final camera = FlutterCamera(); + final testContext = TimelineTestContext(); - if (step is _TimelineModifySceneAction) { - await step.delegate(tester, testContext); - continue; - } + // Setup the scene. + FtgLog.pipeline.info("Running any given setup delegate before running steps."); + await _setup!.setupDelegate(tester); - if (step is _TimelinePhotoRequest) { - expect(step.photoBoundsFinder, findsOne); + // Take photos and modify scene over time. + for (int i = 0; i < _steps.length; i += 1) { + final step = _steps[i]; + FtgLog.pipeline.info("Running step: $step"); - final renderObject = step.photoBoundsFinder.evaluate().first.findRenderObject(); - expect( - renderObject, - isNotNull, - reason: - "Failed to find a render object for photo '${step.description}', using finder '${step.photoBoundsFinder}'", - ); + if (step is _TimelineModifySceneAction) { + await step.delegate(tester, testContext); + continue; + } - await tester.runAsync(() async { - await camera.takePhoto(step.description, step.photoBoundsFinder); - }); + if (step is _TimelinePhotoRequest) { + expect(step.photoBoundsFinder, findsOne); - continue; - } + final renderObject = step.photoBoundsFinder.evaluate().first.findRenderObject(); + expect( + renderObject, + isNotNull, + reason: + "Failed to find a render object for photo '${step.description}', using finder '${step.photoBoundsFinder}'", + ); - throw Exception("Tried to run a step when rendering a Timeline, but we don't recognize this step type: $step"); - } + await tester.runAsync(() async { + await camera.takePhoto(step.description, step.photoBoundsFinder); + }); - // Lay out photos in a row. - final photos = camera.photos; - // TODO: cleanup the modeling of these photos vs renderable photos once things are working - final renderablePhotos = {}; - await tester.runAsync(() async { - for (final photo in photos) { - final byteData = (await photo.pixels.toByteData(format: ui.ImageByteFormat.png))!; - - final candidate = GoldenSceneScreenshot( - // FIXME: When I refactored image modeling to become FlutterScreenshot and GoldenImage, I changed - // how IDs and descriptions were stored. The new structure worked fine for Galleries, where - // we already had an ID and a description. But timeline didn't appear to have an explicit - // ID for a given screenshot, so I gave the description as the "photo ID", which is why it's - // now used in 2 places here. We should probably create a first-class concept of an ID for - // a given timeline screenshot (independent from step index). - photo.id, - GoldenScreenshotMetadata( - description: photo.id, - simulatedPlatform: photo.simulatedPlatform, - ), - decodePng(byteData.buffer.asUint8List())!, - byteData.buffer.asUint8List(), - ); + continue; + } - renderablePhotos[candidate] = GlobalKey(); + throw Exception("Tried to run a step when rendering a Timeline, but we don't recognize this step type: $step"); } - }); - - // Layout photos in the timeline. - final sceneMetadata = await _layoutPhotos( - tester, - photos, - SceneLayoutContent( - description: _description, - goldens: renderablePhotos, - ), - _layout, - goldenBackground: _goldenBackground, - ); - FtgLog.pipeline.finer("Running momentary delay for render flakiness"); - await tester.runAsync(() async { - // Without this delay, the screenshot loading is spotty. However, with - // this delay, we seem to always get screenshots displayed in the widget tree. - // FIXME: Root cause this render flakiness and see if we can fix it. - await Future.delayed(const Duration(milliseconds: 1)); - }); + // Lay out photos in a row. + final photos = camera.photos; + // TODO: cleanup the modeling of these photos vs renderable photos once things are working + final renderablePhotos = {}; + await tester.runAsync(() async { + for (final photo in photos) { + final byteData = (await photo.pixels.toByteData(format: ui.ImageByteFormat.png))!; + + final candidate = GoldenSceneScreenshot( + // FIXME: When I refactored image modeling to become FlutterScreenshot and GoldenImage, I changed + // how IDs and descriptions were stored. The new structure worked fine for Galleries, where + // we already had an ID and a description. But timeline didn't appear to have an explicit + // ID for a given screenshot, so I gave the description as the "photo ID", which is why it's + // now used in 2 places here. We should probably create a first-class concept of an ID for + // a given timeline screenshot (independent from step index). + photo.id, + GoldenScreenshotMetadata( + description: photo.id, + simulatedPlatform: photo.simulatedPlatform, + ), + decodePng(byteData.buffer.asUint8List())!, + byteData.buffer.asUint8List(), + ); - await tester.pumpAndSettle(); + renderablePhotos[candidate] = GlobalKey(); + } + }); - final relativeGoldenFilePath = "$_relativeGoldenDirectory/$_fileName.png"; - if (autoUpdateGoldenFiles) { - // Generate new goldens. - await _updateGoldenScene( + // Layout photos in the timeline. + final sceneMetadata = await _layoutPhotos( tester, - relativeGoldenFilePath, - sceneMetadata, - ); - } else { - // Compare to existing goldens. - await _compareGoldens( - tester, - sceneMetadata, - relativeGoldenFilePath, - find.byType(GoldenSceneBounds), + photos, + SceneLayoutContent( + description: _description, + goldens: renderablePhotos, + ), + _layout, + goldenBackground: _goldenBackground, ); - } - FtgLog.pipeline.finer("Done with golden generation/comparison"); + FtgLog.pipeline.finer("Running momentary delay for render flakiness"); + await tester.runAsync(() async { + // Without this delay, the screenshot loading is spotty. However, with + // this delay, we seem to always get screenshots displayed in the widget tree. + // FIXME: Root cause this render flakiness and see if we can fix it. + await Future.delayed(const Duration(milliseconds: 1)); + }); + + await tester.pumpAndSettle(); + + final relativeGoldenFilePath = "$_relativeGoldenDirectory/$_fileName.png"; + if (autoUpdateGoldenFiles) { + // Generate new goldens. + await _updateGoldenScene( + tester, + relativeGoldenFilePath, + sceneMetadata, + ); + } else { + // Compare to existing goldens. + await _compareGoldens( + tester, + sceneMetadata, + relativeGoldenFilePath, + find.byType(GoldenSceneBounds), + ); + } + + FtgLog.pipeline.finer("Done with golden generation/comparison"); + } finally { + tester.view.reset(); + } } Future _layoutPhotos( diff --git a/lib/src/test_runners.dart b/lib/src/test_runners.dart index 1629660..888f093 100644 --- a/lib/src/test_runners.dart +++ b/lib/src/test_runners.dart @@ -1,6 +1,5 @@ import 'package:flutter/foundation.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:flutter_test_goldens/src/fonts/fonts.dart'; import 'package:meta/meta.dart'; /// Annotation for tests that generate a golden scene, which allows them to be easily @@ -34,10 +33,6 @@ void testGoldenSceneOnIOS( (tester) async { debugDefaultTargetPlatformOverride = TargetPlatform.iOS; - tester.view - ..devicePixelRatio = 1.0 - ..platformDispatcher.textScaleFactorTestValue = 1.0; - try { await test(tester); } finally { @@ -76,10 +71,6 @@ void testGoldenSceneOnAndroid( (tester) async { debugDefaultTargetPlatformOverride = TargetPlatform.android; - tester.view - ..devicePixelRatio = 1.0 - ..platformDispatcher.textScaleFactorTestValue = 1.0; - try { await test(tester); } finally { @@ -118,10 +109,6 @@ void testGoldenSceneOnMac( (tester) async { debugDefaultTargetPlatformOverride = TargetPlatform.macOS; - tester.view - ..devicePixelRatio = 1.0 - ..platformDispatcher.textScaleFactorTestValue = 1.0; - try { await test(tester); } finally { @@ -160,10 +147,6 @@ void testGoldenSceneOnWindows( (tester) async { debugDefaultTargetPlatformOverride = TargetPlatform.windows; - tester.view - ..devicePixelRatio = 1.0 - ..platformDispatcher.textScaleFactorTestValue = 1.0; - try { await test(tester); } finally { @@ -202,10 +185,6 @@ void testGoldenSceneOnLinux( (tester) async { debugDefaultTargetPlatformOverride = TargetPlatform.linux; - tester.view - ..devicePixelRatio = 1.0 - ..platformDispatcher.textScaleFactorTestValue = 1.0; - try { await test(tester); } finally { @@ -238,6 +217,10 @@ void testGoldenSceneOnLinux( /// {@endtemplate} @isGoldenScene @isTest +@Deprecated( + 'Use testWidgets directly. Flutter Test Goldens now automatically ' + 'configures the test environment for golden scene tests.', +) void testGoldenScene( String description, WidgetTesterCallback test, { @@ -251,17 +234,7 @@ void testGoldenScene( testWidgets( description, (tester) async { - await TestFonts.loadAppFonts(); - - tester.view - ..devicePixelRatio = 1.0 - ..platformDispatcher.textScaleFactorTestValue = 1.0; - - try { - await test(tester); - } finally { - tester.view.reset(); - } + await test(tester); }, skip: skip, variant: variant,