Betterment / alchemist

A Flutter tool that makes golden testing easy.
MIT License
268 stars 36 forks source link

fix: Build errors during screenshot-table of tested widgets creation #96

Open timokz opened 1 year ago

timokz commented 1 year ago

Is there an existing issue for this?

Version

0.6.1

Description

During the creation of golden tests for our existing app, I've encountered following error executing said tests:

══╡ EXCEPTION CAUGHT BY RENDERING LIBRARY ╞═════════════════════════════════════════════════════════
The following assertion was thrown during performLayout():
LayoutBuilder does not support returning intrinsic dimensions.
Calculating the intrinsic dimensions would require running the layout callback speculatively, which
might mutate the live render object tree.

The relevant error-causing widget was:
  Table
  Table:~/Pub/Cache/hosted/pub.dev/alchemist-0.6.1/lib/src/golden_test_group.dart:117:14

When the exception was thrown, this was the stack:
#0      _RenderLayoutBuilder._debugThrowIfNotCheckingIntrinsics.<anonymous closure> (package:flutter/src/widgets/layout_builder.dart:345:9)
#1      _RenderLayoutBuilder._debugThrowIfNotCheckingIntrinsics (package:flutter/src/widgets/layout_builder.dart:352:6)
#2      _RenderLayoutBuilder.computeMaxIntrinsicHeight (package:flutter/src/widgets/layout_builder.dart:297:12)
#3      RenderBox._computeIntrinsicDimension.<anonymous closure> (package:flutter/src/rendering/box.dart:1409:23)
#4      _LinkedHashMapMixin.putIfAbsent (dart:collection-patch/compact_hash.dart:535:23)
#5      RenderBox._computeIntrinsicDimension (package:flutter/src/rendering/box.dart:1407:57)
#6      RenderBox.getMaxIntrinsicHeight (package:flutter/src/rendering/box.dart:1746:12)
#7      RenderFlex.computeMaxIntrinsicHeight.<anonymous closure> (package:flutter/src/rendering/flex.dart:615:60)
#8      RenderFlex._getIntrinsicSize (package:flutter/src/rendering/flex.dart:573:58)
#9      RenderFlex.computeMaxIntrinsicHeight (package:flutter/src/rendering/flex.dart:612:12)
#10     RenderBox._computeIntrinsicDimension.<anonymous closure> (package:flutter/src/rendering/box.dart:1409:23)
#11     _LinkedHashMapMixin.putIfAbsent (dart:collection-patch/compact_hash.dart:535:23)
#12     RenderBox._computeIntrinsicDimension (package:flutter/src/rendering/box.dart:1407:57)
#13     RenderBox.getMaxIntrinsicHeight (package:flutter/src/rendering/box.dart:1746:12)
#14     RenderFlex._getIntrinsicSize (package:flutter/src/rendering/flex.dart:554:32)
#15     RenderFlex.computeMaxIntrinsicWidth (package:flutter/src/rendering/flex.dart:594:12)
#16     RenderBox._computeIntrinsicDimension.<anonymous closure> (package:flutter/src/rendering/box.dart:1409:23)
#17     _LinkedHashMapMixin.putIfAbsent (dart:collection-patch/compact_hash.dart:535:23)
#18     RenderBox._computeIntrinsicDimension (package:flutter/src/rendering/box.dart:1407:57)
#19     RenderBox.getMaxIntrinsicWidth (package:flutter/src/rendering/box.dart:1595:12)
#20     RenderProxyBoxMixin.computeMaxIntrinsicWidth (package:flutter/src/rendering/proxy_box.dart:82:21)
#21     RenderConstrainedBox.computeMaxIntrinsicWidth (package:flutter/src/rendering/proxy_box.dart:259:32)
#22     RenderBox._computeIntrinsicDimension.<anonymous closure> (package:flutter/src/rendering/box.dart:1409:23)
#23     _LinkedHashMapMixin.putIfAbsent (dart:collection-patch/compact_hash.dart:535:23)
#24     RenderBox._computeIntrinsicDimension (package:flutter/src/rendering/box.dart:1407:57)
#25     RenderBox.getMaxIntrinsicWidth (package:flutter/src/rendering/box.dart:1595:12)
#26     RenderProxyBoxMixin.computeMaxIntrinsicWidth (package:flutter/src/rendering/proxy_box.dart:82:21)
#27     RenderConstrainedBox.computeMaxIntrinsicWidth (package:flutter/src/rendering/proxy_box.dart:259:32)
#28     RenderBox._computeIntrinsicDimension.<anonymous closure> (package:flutter/src/rendering/box.dart:1409:23)
#29     _LinkedHashMapMixin.putIfAbsent (dart:collection-patch/compact_hash.dart:535:23)
#30     RenderBox._computeIntrinsicDimension (package:flutter/src/rendering/box.dart:1407:57)
#31     RenderBox.getMaxIntrinsicWidth (package:flutter/src/rendering/box.dart:1595:12)
#32     RenderFlex.computeMaxIntrinsicWidth.<anonymous closure> (package:flutter/src/rendering/flex.dart:597:60)
#33     RenderFlex._getIntrinsicSize (package:flutter/src/rendering/flex.dart:555:36)
#34     RenderFlex.computeMaxIntrinsicWidth (package:flutter/src/rendering/flex.dart:594:12)
#35     RenderBox._computeIntrinsicDimension.<anonymous closure> (package:flutter/src/rendering/box.dart:1409:23)
#36     _LinkedHashMapMixin.putIfAbsent (dart:collection-patch/compact_hash.dart:535:23)
#37     RenderBox._computeIntrinsicDimension (package:flutter/src/rendering/box.dart:1407:57)
#38     RenderBox.getMaxIntrinsicWidth (package:flutter/src/rendering/box.dart:1595:12)
#39     RenderPadding.computeMaxIntrinsicWidth (package:flutter/src/rendering/shifted_box.dart:178:21)
#40     RenderBox._computeIntrinsicDimension.<anonymous closure> (package:flutter/src/rendering/box.dart:1409:23)
#41     _LinkedHashMapMixin.putIfAbsent (dart:collection-patch/compact_hash.dart:535:23)
#42     RenderBox._computeIntrinsicDimension (package:flutter/src/rendering/box.dart:1407:57)
#43     RenderBox.getMaxIntrinsicWidth (package:flutter/src/rendering/box.dart:1595:12)
#44     IntrinsicColumnWidth.maxIntrinsicWidth (package:flutter/src/rendering/table.dart:116:38)
#45     RenderTable._computeColumnWidths (package:flutter/src/rendering/table.dart:877:52)
#46     RenderTable.performLayout (package:flutter/src/rendering/table.dart:1085:33)
#47     RenderObject.layout (package:flutter/src/rendering/object.dart:2395:7)
#48     RenderBox.layout (package:flutter/src/rendering/box.dart:2386:11)
#49     RenderPadding.performLayout (package:flutter/src/rendering/shifted_box.dart:238:12)
#50     RenderObject.layout (package:flutter/src/rendering/object.dart:2395:7)
#51     RenderBox.layout (package:flutter/src/rendering/box.dart:2386:11)
#52     RenderPositionedBox.performLayout (package:flutter/src/rendering/shifted_box.dart:438:14)
#53     RenderObject.layout (package:flutter/src/rendering/object.dart:2395:7)
#54     RenderBox.layout (package:flutter/src/rendering/box.dart:2386:11)
#55     RenderConstrainedOverflowBox.performLayout (package:flutter/src/rendering/shifted_box.dart:631:14)
#56     RenderObject.layout (package:flutter/src/rendering/object.dart:2395:7)
#57     RenderBox.layout (package:flutter/src/rendering/box.dart:2386:11)
#58     RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:122:14)
#59     RenderObject.layout (package:flutter/src/rendering/object.dart:2395:7)
#60     RenderBox.layout (package:flutter/src/rendering/box.dart:2386:11)
#61     RenderPositionedBox.performLayout (package:flutter/src/rendering/shifted_box.dart:438:14)
#62     RenderObject.layout (package:flutter/src/rendering/object.dart:2395:7)
#63     RenderBox.layout (package:flutter/src/rendering/box.dart:2386:11)
#64     RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:122:14)
#65     RenderObject.layout (package:flutter/src/rendering/object.dart:2395:7)
#66     RenderBox.layout (package:flutter/src/rendering/box.dart:2386:11)
#67     RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:122:14)
#68     RenderCustomPaint.performLayout (package:flutter/src/rendering/custom_paint.dart:554:11)
#69     RenderObject.layout (package:flutter/src/rendering/object.dart:2395:7)
#70     RenderBox.layout (package:flutter/src/rendering/box.dart:2386:11)
#71     RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:122:14)
#72     _RenderCustomClip.performLayout (package:flutter/src/rendering/proxy_box.dart:1449:11)
#73     RenderObject.layout (package:flutter/src/rendering/object.dart:2395:7)
#74     RenderBox.layout (package:flutter/src/rendering/box.dart:2386:11)
#75     RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:122:14)
#76     RenderObject.layout (package:flutter/src/rendering/object.dart:2395:7)
#77     RenderBox.layout (package:flutter/src/rendering/box.dart:2386:11)
#78     RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:122:14)
#79     RenderObject.layout (package:flutter/src/rendering/object.dart:2395:7)
#80     RenderBox.layout (package:flutter/src/rendering/box.dart:2386:11)
#81     RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:122:14)
#82     RenderObject.layout (package:flutter/src/rendering/object.dart:2395:7)
#83     RenderBox.layout (package:flutter/src/rendering/box.dart:2386:11)
#84     RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:122:14)
#85     RenderObject.layout (package:flutter/src/rendering/object.dart:2395:7)
#86     RenderBox.layout (package:flutter/src/rendering/box.dart:2386:11)
#87     RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:122:14)
#88     RenderObject.layout (package:flutter/src/rendering/object.dart:2395:7)
#89     RenderBox.layout (package:flutter/src/rendering/box.dart:2386:11)
#90     RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:122:14)
#91     RenderObject.layout (package:flutter/src/rendering/object.dart:2395:7)
#92     RenderBox.layout (package:flutter/src/rendering/box.dart:2386:11)
#93     RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:122:14)
#94     RenderObject.layout (package:flutter/src/rendering/object.dart:2395:7)
#95     RenderBox.layout (package:flutter/src/rendering/box.dart:2386:11)
#96     RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:122:14)
#97     RenderObject.layout (package:flutter/src/rendering/object.dart:2395:7)
#98     RenderBox.layout (package:flutter/src/rendering/box.dart:2386:11)
#99     RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:122:14)
#100    RenderObject.layout (package:flutter/src/rendering/object.dart:2395:7)
#101    RenderBox.layout (package:flutter/src/rendering/box.dart:2386:11)
#102    RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:122:14)
#103    RenderOffstage.performLayout (package:flutter/src/rendering/proxy_box.dart:3751:13)
#104    RenderObject.layout (package:flutter/src/rendering/object.dart:2395:7)
#105    RenderBox.layout (package:flutter/src/rendering/box.dart:2386:11)
#106    RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:122:14)
#107    RenderObject.layout (package:flutter/src/rendering/object.dart:2395:7)
#108    RenderBox.layout (package:flutter/src/rendering/box.dart:2386:11)
#109    _RenderTheaterMixin.performLayout (package:flutter/src/widgets/overlay.dart:832:15)
#110    RenderObject.layout (package:flutter/src/rendering/object.dart:2395:7)
#111    RenderBox.layout (package:flutter/src/rendering/box.dart:2386:11)
#112    RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:122:14)
#113    RenderObject.layout (package:flutter/src/rendering/object.dart:2395:7)
#114    RenderBox.layout (package:flutter/src/rendering/box.dart:2386:11)
#115    RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:122:14)
#116    RenderObject.layout (package:flutter/src/rendering/object.dart:2395:7)
#117    RenderBox.layout (package:flutter/src/rendering/box.dart:2386:11)
#118    RenderProxyBoxMixin.performLayout (package:flutter/src/rendering/proxy_box.dart:122:14)
#119    RenderObject.layout (package:flutter/src/rendering/object.dart:2395:7)
#120    RenderBox.layout (package:flutter/src/rendering/box.dart:2386:11)
#121    RenderView.performLayout (package:flutter/src/rendering/view.dart:173:14)
#122    RenderObject._layoutWithoutResize (package:flutter/src/rendering/object.dart:2234:7)
#123    PipelineOwner.flushLayout (package:flutter/src/rendering/object.dart:1016:18)
#124    AutomatedTestWidgetsFlutterBinding.drawFrame (package:flutter_test/src/binding.dart:1388:23)
#125    RendererBinding._handlePersistentFrameCallback (package:flutter/src/rendering/binding.dart:358:5)
#126    SchedulerBinding._invokeFrameCallback (package:flutter/src/scheduler/binding.dart:1284:15)
#127    SchedulerBinding.handleDrawFrame (package:flutter/src/scheduler/binding.dart:1214:9)
#128    AutomatedTestWidgetsFlutterBinding.pump.<anonymous closure> (package:flutter_test/src/binding.dart:1236:9)
#131    TestAsyncUtils.guard (package:flutter_test/src/test_async_utils.dart:68:41)
#132    AutomatedTestWidgetsFlutterBinding.pump (package:flutter_test/src/binding.dart:1222:27)
#133    WidgetTester._pumpWidget (package:flutter_test/src/widget_tester.dart:592:20)
#134    WidgetTester.pumpWidget.<anonymous closure> (package:flutter_test/src/widget_tester.dart:577:14)
#137    TestAsyncUtils.guard (package:flutter_test/src/test_async_utils.dart:68:41)
#138    WidgetTester.pumpWidget (package:flutter_test/src/widget_tester.dart:576:27)
#139    onlyPumpWidget (package:alchemist/src/pumps.dart:74:17)
#140    FlutterGoldenTestAdapter.pumpGoldenTest (package:alchemist/src/golden_test_adapter.dart:238:21)
#141    FlutterGoldenTestRunner.run (package:alchemist/src/golden_test_runner.dart:78:31)
#142    goldenTest.<anonymous closure> (package:alchemist/src/golden_test.dart:169:30)
<asynchronous suspension>
<asynchronous suspension>
(elided 5 frames from dart:async and package:stack_trace)

The following RenderObject was being processed when the exception was fired: RenderTable#1ea5e relayoutBoundary=up3 NEEDS-PAINT NEEDS-COMPOSITING-BITS-UPDATE:
  creator: Table ← GoldenTestScenarioConstraints ← GoldenTestGroup ← Padding-[#04929] ← Center ←
    OverflowBox ← ColoredBox ← Builder ← Align ← DefaultTextStyle ← AnimatedDefaultTextStyle ←
    _InkFeatures-[GlobalKey#ae861 ink renderer] ← ⋯
  parentData: offset=Offset(8.0, 8.0) (can use size)
  constraints: BoxConstraints(unconstrained)
  size: MISSING
  border: TableBorder(BorderSide(width: 0.0, style: none), BorderSide(width: 0.0, style: none),
    BorderSide(width: 0.0, style: none), BorderSide(width: 0.0, style: none), BorderSide(color:
    Color(0x4d000000)), BorderSide(color: Color(0x4d000000)), BorderRadius.zero)
  default column width: IntrinsicColumnWidth(flex: null)
  table size: 1×1
  column offsets: unknown
  row offsets: []
This RenderObject had the following descendants (showing up to depth 5):
    child (0, 0): RenderPadding#950d5 NEEDS-LAYOUT NEEDS-PAINT NEEDS-COMPOSITING-BITS-UPDATE
      child: RenderFlex#f9517 NEEDS-LAYOUT NEEDS-PAINT NEEDS-COMPOSITING-BITS-UPDATE
        child 1: RenderParagraph#f1a93 NEEDS-LAYOUT NEEDS-PAINT
          text: TextSpan
        child 2: RenderConstrainedBox#7de36 NEEDS-LAYOUT NEEDS-PAINT
        child 3: RenderConstrainedBox#eaeab NEEDS-LAYOUT NEEDS-PAINT NEEDS-COMPOSITING-BITS-UPDATE
          child: RenderConstrainedBox#aaa9b NEEDS-LAYOUT NEEDS-PAINT NEEDS-COMPOSITING-BITS-UPDATE
            child: RenderFlex#64667 NEEDS-LAYOUT NEEDS-PAINT NEEDS-COMPOSITING-BITS-UPDATE
════════════════════════════════════════════════════════════════════════════════════════════════════

I've tried going down the tree of the tested components and identifying the root cause of these errors, but after isolating the affected widgets in widget-tests without using alchemist, I don't encounter them anymore. Also, the affected RenderObject seems to be within the alchemist source.

I've tried to produce a POC that enables the reproduction of the error, the possibility of a fault within my test structure I didn't notice is also very real.

Alchemist golden test:

void main() {
  group('LegendItem Golden Tests', () {
    goldenTest(
      'LegendItem scenarios render correctly',
      fileName: 'legend_item',
      tags: ['golden'],
      builder: () => GoldenTestGroup(
        children: [
          GoldenTestScenario(
            name: 'scenario 1',
            child: const ProviderScope(
              child: LegendItem(
                color: Colors.red,
                text: 'alchemist text',
                valueLabel: 'betterment value',
                percentLabel: 'percentLabel',
              ),
            ),
          ),
        ],
      ),
    );
  });
}

Code for the tested widget:

class LegendItem extends StatelessWidget {
  const LegendItem({
    super.key,
    required this.color,
    required this.text,
    this.size = 16,
    this.textColor = const Color(0xff505050),
    required this.valueLabel,
    required this.percentLabel,
  });

  /// Legend item color
  final Color color;

  /// Legend indicator size
  final double size;

  /// Item description
  final String text;

  /// Color of [text]
  final Color textColor;

  /// Value label
  final String valueLabel;

  /// Label describing the percentage of the total value
  final String percentLabel;

  static const kSpacing = 2.0;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Row(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: [
            Container(
              width: size,
              height: size,
              decoration: BoxDecoration(
                shape: BoxShape.circle,
                color: color,
              ),
            ),
            const SizedBox(width: kSpacing),
            Flexible(
              child: AutoSizeText(
                text,
                style: Theme.of(context).textTheme.labelSmall,
                textAlign: TextAlign.start,
              ),
            ),
          ],
        ),
        Row(
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
          children: [
            Flexible(child: AutoSizeText(percentLabel)),
            const SizedBox(width: kSpacing),
            Flexible(
              child: AutoSizeText(
                valueLabel,
                style: Theme.of(context).textTheme.titleSmall,
                textAlign: TextAlign.start,
              ),
            ),
          ],
        ),
      ],
    );
  }
}

Steps to reproduce

  1. Wrap given LegendItem in alchemist golden test
  2. Try updating goldens using said test
  3. Encounter compile-time errors

Expected behavior

Expected behavior would include a successful compilation of the test as well as rendering of goldens for the current host platform.

Screenshots

No response

Additional context and comments

I can provide more information about the projects/my current setup if necessary. If the error is actually within alchemist, I would be happy to provide help regarding the identification and fixing of the bug.

Kirpal commented 1 year ago

Hi @timokz , thanks for submitting an issue!

I looked into it and it seems like this error is due to the LayoutBuilder within the AutoSizeText. It can't used inside a table that has IntrinsicColumnWidth, because the layout builder doesn't have an intrinsic size. We use IntrinsicColumnWidth by default inside the golden test group, but allow you to override this per column. So, I think the best way to fix this issue is to provide a fixed width for the columns within the group, ie columnWidthBuilder: (_) => FixedColumnWidth(600).

You could also provide constraints to the GoldenTestScenario, ie constraints: const BoxConstraints.tightFor(width: 600, height: 600).

timokz commented 1 year ago

Hey @Kirpal , thank you for taking a look! I've actually tried giving my scenarios constraints, but they also led to the same errors, depending on the given dimensions, so trial-and-erroring the right ones wasn't a solution imo. Giving the columns a fixed width solved my provided example, thanks for that!

I'm just wondering: did I miss a section in the documentation about this, or is this something one should know beforehand? I'd say I have an ok grasp of flutter, but the errors I encountered left me with no real leads on how to progress. Please let me know if you think extending the docs or adding an exception at the alchemist level would help.