multiGolden function

Future<void> multiGolden(
  1. WidgetTester tester,
  2. String name,
  3. Map<String, GoldenWidgetBuilder> widgets, {
  4. List<LdFrameOptions> frameScenarios = const [LdFrameOptions()],
  5. bool performWidgetTreeTests = true,
  6. List<LdThemeSize> themeSizeScenarios = LdThemeSize.values,
  7. List<Brightness> brightnessScenarios = Brightness.values,
  8. List<Orientation> orientationScenarios = const [Orientation.portrait],
  9. bool clipScreenToRadius = true,
})

Helper function to generate golden tests for multiple themes and sizes for multiple widgets of the same scope (e.g. one screen with different states).

Implementation

Future<void> multiGolden(
  /// The [tester] instance to use for the tests.
  WidgetTester tester,

  /// The name of the golden test.
  String name,

  /// A map of widget builders for various scenarios (e.g. "Default", "Error").
  Map<String, GoldenWidgetBuilder> widgets, {
  /// The [LdFrameOptions] to use for the tests. Now a list, defaults to one entry.
  List<LdFrameOptions> frameScenarios = const [LdFrameOptions()],

  /// Whether to perform widget tree tests as well.
  bool performWidgetTreeTests = true,

  /// The [ThemeSize] scenarios to test.
  List<LdThemeSize> themeSizeScenarios = LdThemeSize.values,

  /// The [Brightness] scenarios to test.
  List<Brightness> brightnessScenarios = Brightness.values,

  /// The [Orientation] scenarios to test.
  List<Orientation> orientationScenarios = const [Orientation.portrait],

  /// Whether to clip the screen to the screen radius.
  bool clipScreenToRadius = true,
}) async {
  // Save global state to restore on exit (prevents test contamination)
  final savedDebugDisableShadows = debugDisableShadows;
  final savedLdDisableAnimations = ldDisableAnimations;
  final savedLdIncludeFontPackage = ldIncludeFontPackage;

  try {
    debugDisableShadows = false;
    ldDisableAnimations = true;
    ldIncludeFontPackage = false;
    await loadAppFonts();

    // Track if any test fails
    List<String> failureMessages = [];

    // For each frame options, scenario, theme size, and brightness ...
    for (final ldFrameOptions in frameScenarios) {
      final frameLabel = ldFrameOptions.label;
      for (final entry in widgets.entries) {
        for (final themeSize in themeSizeScenarios) {
          for (final brightness in brightnessScenarios) {
            for (final orientation in orientationScenarios) {
              final slug = "${entry.key}/${[
                if (themeSizeScenarios.length > 1) themeSize.label,
                if (brightnessScenarios.length > 1) brightness.label,
                if (frameScenarios.length > 1) frameLabel,
                if (orientationScenarios.length > 1) orientation.label,
              ].join("_")}";

              // Apply device pixel ratio from ldFrameOptions
              tester.view.devicePixelRatio = ldFrameOptions.devicePixelRatio;

              // Apply target platform from ldFrameOptions
              if (ldFrameOptions.platform != null) {
                debugDefaultTargetPlatformOverride =
                    ldFrameOptions.targetPlatform;
              }

              // If we dont have a specified height, we start as a square
              Size size = Size(
                ldFrameOptions.width,
                ldFrameOptions.height ?? ldFrameOptions.width,
              );

              if (orientation == Orientation.landscape) {
                size = Size(size.height, size.width);
              }

              await tester.binding.setSurfaceSize(
                Size(size.width, size.height),
              );

              tester.view.physicalSize = Size(
                size.width,
                (size.height),
              );

              final key = ValueKey(slug);

              // Place the widget
              await entry.value(tester, (widget) async {
                final frame = RepaintBoundary(
                  key: key,
                  child: ClipRRect(
                    borderRadius: BorderRadius.circular(
                      clipScreenToRadius ? ldFrameOptions.screenRadius ?? 0 : 0,
                    ),
                    child: ldFrame(
                      child: widget,
                      size: themeSize,
                      ldFrameOptions: ldFrameOptions,
                      orientation: orientation,
                      brightnessMode: switch (brightness) {
                        Brightness.light => LdThemeBrightnessMode.light,
                        Brightness.dark => LdThemeBrightnessMode.dark,
                      },
                    ),
                  ),
                );

                // If we dont have a specified height, we need to wrap the frame in a
                // SingleChildScrollView to allow the frame to grow. We will
                // automatically detect the size of the widget later.
                if (ldFrameOptions.height == null) {
                  await tester.pumpWidget(
                    SingleChildScrollView(
                      child: IntrinsicWidth(
                        child: frame,
                      ),
                    ),
                    duration: Duration(milliseconds: 100),
                  );
                } else {
                  await tester.pumpWidget(
                    frame,
                    duration: Duration(milliseconds: 100),
                  );
                }

                if (performWidgetTreeTests) {
                  try {
                    await widgetTreeMatchesGolden(
                      tester,
                      widget: widget,
                      options: WidgetTreeOptions(goldenName: '$name/$slug'),
                    );
                  } catch (e) {
                    // Capture screenshot for visual debugging when XML mismatches
                    try {
                      await tester.pumpAndSettle();
                      final element = tester.element(find.byKey(key));
                      final renderObject = element.renderObject;
                      if (renderObject is RenderRepaintBoundary) {
                        final image = await renderObject.toImage(
                          pixelRatio: tester.view.devicePixelRatio,
                        );
                        final byteData = await image.toByteData(
                          format: ui.ImageByteFormat.png,
                        );
                        if (byteData != null) {
                          const failurePath =
                              'test/failures/golden_widget_trees';
                          final pngFile = File(
                            path.join(
                              failurePath,
                              name,
                              '$slug.png',
                            ),
                          );
                          pngFile.parent.createSync(recursive: true);
                          pngFile.writeAsBytesSync(
                            byteData.buffer.asUint8List(),
                          );
                        }
                      }
                    } catch (_) {
                      // Ignore screenshot failures; the XML failure is the main error
                    }
                    failureMessages.add(
                      'Widget tree test failed for $name/$slug: ${e.toString()}',
                    );
                  }
                }
              });

              await tester.pumpAndSettle();

              debugDefaultTargetPlatformOverride = null;
            }
          }
        }
      }
    }

    // After all tests have been executed, fail if any test failed
    if (failureMessages.isNotEmpty) {
      throw Exception(
        'One or more golden tests failed:\n${failureMessages.join('\n')}',
      );
    }
  } finally {
    // Restore global state and reset tester to prevent test contamination
    debugDisableShadows = savedDebugDisableShadows;
    ldDisableAnimations = savedLdDisableAnimations;
    ldIncludeFontPackage = savedLdIncludeFontPackage;
    debugDefaultTargetPlatformOverride = null;
    await resetTester(tester);
  }
}