alchemist icon indicating copy to clipboard operation
alchemist copied to clipboard

request: different screen size

Open Lyokone opened this issue 2 years ago • 6 comments

Is there an existing feature request for this?

  • [X] I have searched the existing issues.

Command

I would love if I could run my tests with a lot of screen sizes (iPhone / iPad / desktop)

Description

I would love if I could generate file the same way golden toolkit is doing

Screenshot 2022-03-22 at 16 17 53

Reasoning

Since variant is used for the platform, I cannot find a way to keep the device type properly organized with Alchemist. Variants are useful to get also a nice overview in IDEs.

Screenshot 2022-03-22 at 16 25 39

Additional context and comments

No response

Lyokone avatar Mar 22 '22 15:03 Lyokone

Hey @Lyokone,

I have written a GoldenTestDeviceScenario widget which takes a Device containing the device configuration to achieve this. The Device is basically the same class as the one from eBay's golden toolkit. See the following code:

golden_test_device_scenario.dart

import 'package:alchemist/alchemist.dart';
import 'package:flutter/material.dart';

import 'device.dart';

/// Wrapper for testing widgets (primarily screens) with device constraints
class GoldenTestDeviceScenario extends StatelessWidget {
  final String name;
  final Device device;
  final ValueGetter<Widget> builder;

  const GoldenTestDeviceScenario({
    required this.name,
    required this.builder,
    this.device = Device.iphone11,
    Key? key,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return GoldenTestScenario(
      name: '${this.name} (device: ${device.name})',
      child: ClipRect(
        child: MediaQuery(
          data: MediaQuery.of(context).copyWith(
            size: this.device.size,
            padding: this.device.safeArea,
            platformBrightness: this.device.brightness,
            devicePixelRatio: this.device.devicePixelRatio,
            textScaleFactor: this.device.textScaleFactor,
          ),
          child: SizedBox(
            height: this.device.size.height,
            width: this.device.size.width,
            child: builder(),
          ),
        ),
      ),
    );
  }
}

device.dart

// Copied and adapted from https://github.com/eBay/flutter_glove_box/blob/master/packages/golden_toolkit/lib/src/device.dart

import 'package:flutter/material.dart';

/// This [Device] is a configuration for golden test.
class Device {
  /// This [Device] is a configuration for golden test.
  const Device({
    required this.size,
    required this.name,
    this.devicePixelRatio = 1.0,
    this.textScaleFactor = 1.0,
    this.brightness = Brightness.light,
    this.safeArea = const EdgeInsets.all(0),
  });

  /// [smallPhone] one of the smallest phone screens
  static const Device smallPhone =
      Device(name: 'small_phone', size: Size(375, 667));

  /// [iphone11] matches specs of iphone11, but with lower DPI for performance
  static const Device iphone11 = Device(
    name: 'iphone11',
    size: Size(414, 896),
    devicePixelRatio: 1.0,
    safeArea: EdgeInsets.only(top: 44, bottom: 34),
  );

  static const Device iphone11Landscape = Device(
    name: 'iphone11_landscape',
    size: Size(896, 414),
    devicePixelRatio: 1.0,
    safeArea: EdgeInsets.only(left: 44, right: 34),
  );

  /// [tabletLandscape] example of tablet that in landscape mode
  static const Device tabletLandscape =
      Device(name: 'tablet_landscape', size: Size(1366, 1024));

  /// [tabletPortrait] example of tablet that in portrait mode
  static const Device tabletPortrait =
      Device(name: 'tablet_portrait', size: Size(1024, 1366));

  /// [name] specify device name. Ex: Phone, Tablet, Watch
  final String name;

  /// [size] specify device screen size. Ex: Size(1366, 1024))
  final Size size;

  /// [devicePixelRatio] specify device Pixel Ratio
  final double devicePixelRatio;

  /// [textScaleFactor] specify custom text scale factor
  final double textScaleFactor;

  /// [brightness] specify platform brightness
  final Brightness brightness;

  /// [safeArea] specify insets to define a safe area
  final EdgeInsets safeArea;

  /// [copyWith] convenience function for [Device] modification
  Device copyWith({
    Size? size,
    double? devicePixelRatio,
    String? name,
    double? textScale,
    Brightness? brightness,
    EdgeInsets? safeArea,
  }) {
    return Device(
      size: size ?? this.size,
      devicePixelRatio: devicePixelRatio ?? this.devicePixelRatio,
      name: name ?? this.name,
      textScaleFactor: textScale ?? this.textScaleFactor,
      brightness: brightness ?? this.brightness,
      safeArea: safeArea ?? this.safeArea,
    );
  }

  /// [dark] convenience method to copy the current device and apply dark theme
  Device dark() {
    return Device(
      size: size,
      devicePixelRatio: devicePixelRatio,
      textScaleFactor: textScaleFactor,
      brightness: Brightness.dark,
      safeArea: safeArea,
      name: '${name}_dark',
    );
  }

  @override
  String toString() {
    return 'Device: $name, '
        '${size.width}x${size.height} @ $devicePixelRatio, '
        'text: $textScaleFactor, $brightness, safe: $safeArea';
  }
}

And in the test which you want to run with different device configurations:

import 'package:alchemist/alchemist.dart';
import 'package:flutter/material.dart';

import 'device.dart';
import 'golden_test_device_scenario.dart';

void main() {
  Widget buildWidgetUnderTest() => MaterialApp(
        home: Scaffold(
          body: ListView.builder(
            itemBuilder: (context, i) => Text('$i'),
          ),
        ),
      );

  goldenTest(
    'golden test',
    fileName: 'foo_widget',
    builder: () => GoldenTestGroup(
      children: [
        GoldenTestDeviceScenario(
          device: Device.smallPhone,
          name: 'golden test FooWidget on small phone',
          builder: buildWidgetUnderTest,
        ),
        GoldenTestDeviceScenario(
          device: Device.tabletLandscape,
          name: 'golden test FooWidget on tablet',
          builder: buildWidgetUnderTest,
        ),
      ],
    ),
  );
}

This test will generate the following golden image foo_widget

An important thing to note is that you will run into trouble if your image ends up being larger than 2000 x 2000 pixels (e.g. if you run a lot of golden test scenarios with large devices in one GoldenTestGroup). In that case you have to pass the parameter constraints: BoxConstraints.loose(<expected size of your image>) to goldenTest.

@definitelyokay @Kirpal I can create a PR containing this code if you think it makes sense.

Giuspepe avatar Mar 24 '22 22:03 Giuspepe

Wow! Such an insightful response!! Thanks a lot 😍 I'll try that tomorrow on my main project! But I think it answers all my questions!

Lyokone avatar Mar 24 '22 22:03 Lyokone

@Giuspepe that's pretty cool! A pull request would definitely be appreciated. Do you think it's worth revisiting the resizing/constraints logic at the same time to avoid using hacky constraints values? I can't tell how hacky your workaround ends up being. What do you think?

Kirpal avatar Mar 25 '22 16:03 Kirpal

A PR is currently in the pipeline related to this functionality, #59.

@Giuspepe @Kirpal @Lyokone, Seeing as we can define scenario-specific constraints once that PR has merged, and you can already use custom widgets as @Giuspepe has shown, can we considered this issue fixed? 👀

If not, feel free to comment and let us know if there's anything you'd like us to have a look at!

jeroen-meijer avatar Jun 14 '22 08:06 jeroen-meijer

Hello, looks like everything needed is here 😁

Lyokone avatar Jun 15 '22 06:06 Lyokone

@Giuspepe you are awesome thank you for sharing this.

jeremiahlukus avatar Jul 19 '22 11:07 jeremiahlukus