Files
Musify/musify_flutter/.agents/skills/flutter-testing/references/common-errors.md
T

12 KiB

Common Testing Errors

Overview

This guide covers frequently encountered Flutter testing errors and their solutions.

Layout Errors

'A RenderFlex overflowed...'

Error Message:

The following assertion was thrown during layout:
A RenderFlex overflowed by 1146 pixels on the right.

Cause: Yellow and black stripes indicate overflow when a child widget is too large for its parent (Row/Column).

Solution: Wrap the overflowing widget in Expanded or Flexible.

// Problem
Row(
  children: [
    Icon(Icons.message),
    Column(
      mainAxisSize: MainAxisSize.min,
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text('Title', style: Theme.of(context).textTheme.headlineMedium),
        Text('Very long text that overflows the available space...'),
      ],
    ),
  ],
)

// Solution
Row(
  children: [
    Icon(Icons.message),
    Expanded(
      child: Column(
        mainAxisSize: MainAxisSize.min,
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text('Title', style: Theme.of(context).textTheme.headlineMedium),
          Text('Very long text that fits in the available space...'),
        ],
      ),
    ),
  ],
)

'Vertical viewport was given unbounded height'

Error Message:

Vertical viewport was given unbounded height.
Viewports expand in the scrolling direction to fill their container.
This situation typically happens when a scrollable widget is nested inside another scrollable widget.

Cause: ListView or other scrollable widget inside Column without height constraints.

Solution: Wrap in Expanded or use shrinkWrap: true.

// Problem
Column(
  children: [
    Text('Header'),
    ListView(
      children: [
        ListTile(leading: Icon(Icons.map), title: Text('Map')),
        ListTile(leading: Icon(Icons.subway), title: Text('Subway')),
      ],
    ),
  ],
)

// Solution 1: Expanded
Column(
  children: [
    Text('Header'),
    Expanded(
      child: ListView(
        children: [
          ListTile(leading: Icon(Icons.map), title: Text('Map')),
          ListTile(leading: Icon(Icons.subway), title: Text('Subway')),
        ],
      ),
    ),
  ],
)

// Solution 2: shrinkWrap
Column(
  children: [
    Text('Header'),
    ListView(
      shrinkWrap: true,
      children: [
        ListTile(leading: Icon(Icons.map), title: Text('Map')),
        ListTile(leading: Icon(Icons.subway), title: Text('Subway')),
      ],
    ),
  ],
)

'An InputDecorator...cannot have an unbounded width'

Error Message:

An InputDecorator, which is typically created by a TextField, cannot have an unbounded width.
This happens when the parent widget does not provide a finite width constraint.

Cause: TextField or TextFormField inside Row without width constraints.

Solution: Wrap in Expanded or SizedBox.

// Problem
Row(
  children: [TextField()],
)

// Solution 1: Expanded
Row(
  children: [Expanded(child: TextFormField())],
)

// Solution 2: SizedBox
Row(
  children: [SizedBox(width: 200, child: TextFormField())],
)

Widget Lifecycle Errors

'setState called during build'

Error Message:

setState() or markNeedsBuild() called during build.
This Overlay widget cannot be marked as needing to build because the framework
is already in the process of building widgets.

Cause: Calling setState (or methods that call it like showDialog) directly in the build method.

Solution: Use WidgetsBinding.instance.addPostFrameCallback or Navigator API.

// Problem
Widget build(BuildContext context) {
  showDialog(
    context: context,
    builder: (context) => AlertDialog(title: Text('Error!')),
  );
  return Center(child: Text('Show Dialog'));
}

// Solution 1: Post-frame callback
Widget build(BuildContext context) {
  WidgetsBinding.instance.addPostFrameCallback((_) {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(title: Text('Success!')),
    );
  });
  return Center(child: Text('Show Dialog'));
}

// Solution 2: Navigator API (for initial navigation)
Widget build(BuildContext context) {
  return ElevatedButton(
    onPressed: () {
      Navigator.push(
        context,
        PageRouteBuilder(
          pageBuilder: (_, __, ___) => DialogScreen(),
        ),
      );
    },
    child: Text('Show Dialog'),
  );
}

Parent Widget Errors

'Incorrect use of ParentData widget'

Error Message:

The following assertion was thrown while looking for parent data:
Incorrect use of ParentDataWidget.
Usually, this indicates that at least one of the offending ParentDataWidgets
listed above is not placed directly inside a compatible ancestor widget.

Cause: Using widgets that require specific parent widgets in wrong context.

Common Solutions:

Widget Expected Parent Solution
Flexible Row, Column, Flex Ensure parent is Row/Column/Flex
Expanded Row, Column, Flex Ensure parent is Row/Column/Flex
Positioned Stack Wrap in Stack
TableCell Table Ensure parent is Table
// Problem
Column(
  children: [
    Expanded(child: Text('Wrong!')), // Expanded needs Row/Column parent, not Column children
  ],
)

// Solution
Column(
  children: [
    Row(
      children: [
        Expanded(child: Text('Correct!')),
      ],
    ),
  ],
)

Testing-Specific Errors

'WidgetTester.pumpWidget() called with a widget that doesn't include a MaterialApp'

Error Message:

WidgetTester.pumpWidget() called with a widget that doesn't include a MaterialApp.

Cause: Pumping widget without MaterialApp context.

Solution: Wrap widget in MaterialApp.

// Problem
testWidgets('test without MaterialApp', (tester) async {
  await tester.pumpWidget(MyWidget());
});

// Solution
testWidgets('test with MaterialApp', (tester) async {
  await tester.pumpWidget(
    MaterialApp(
      home: MyWidget(),
    ),
  );
});

'No Finder found'

Error Message:

No Finder found.
Test failed: No elements found matching the finder.

Cause: Widget not found in tree at time of test.

Common Solutions:

// 1. Wrong text/content
expect(find.text('Wrong Text'), findsNothing); // Error

// Solution: Use correct text
expect(find.text('Correct Text'), findsOneWidget);

// 2. Widget not built yet
testWidgets('widget not built', (tester) async {
  await tester.pumpWidget(MyWidget());
  expect(find.text('Async Text'), findsOneWidget); // Error
});

// Solution: Pump and settle for async operations
testWidgets('widget not built', (tester) async {
  await tester.pumpWidget(MyWidget());
  await tester.pumpAndSettle(); // Wait for async operations
  expect(find.text('Async Text'), findsOneWidget);
});

// 3. Wrong finder type
testWidgets('wrong finder', (tester) async {
  await tester.pumpWidget(TextButton(child: Text('Button')));
  await tester.tap(find.byType(ElevatedButton)); // Error
});

// Solution: Use correct type or use Text finder
testWidgets('correct finder', (tester) async {
  await tester.pumpWidget(TextButton(child: Text('Button')));
  await tester.tap(find.byType(TextButton)); // Correct
});

Plugin/Platform Errors

'MissingPluginException'

Error Message:

MissingPluginException(No implementation found for method MethodName on channel channel.name)

Cause: Plugin not registered or mock not set up in tests.

Solution: Mock the method channel in tests.

import 'package:flutter/services.dart';

void main() {
  TestWidgetsFlutterBinding.ensureInitialized();
  
  setUp(() {
    TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
        .setMockMethodCallHandler(
      const MethodChannel('your.plugin.channel'),
      (MethodCall methodCall) async {
        if (methodCall.method == 'getPlatformVersion') {
          return 'Android 12';
        }
        throw MissingPluginException();
      },
    );
  });
  
  tearDown(() {
    TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
        .setMockMethodCallHandler(
      const MethodChannel('your.plugin.channel'),
      null,
    );
  });
}

Async Errors

'TimeoutException'

Error Message:

TimeoutException after 0:00:05.000000: Test timed out after 5 seconds.

Cause: Async operation not completing or pumpAndSettle() waiting indefinitely.

Solution: Set timeout or fix infinite loops.

// Solution 1: Increase timeout
testWidgets(
  'long-running test',
  (tester) async {
    await tester.pumpWidget(MyWidget());
    await Future.delayed(const Duration(seconds: 10));
  },
  timeout: const Timeout(Duration(minutes: 1)),
);

// Solution 2: Fix infinite pumpAndSettle
testWidgets('fix infinite settle', (tester) async {
  await tester.pumpWidget(MyWidget());
  await tester.pump(); // Use pump() instead of pumpAndSettle() if animation loops
});

Data Errors

'RangeError'

Error Message:

RangeError: Index out of range: index should be less than 5, but is 5

Cause: Accessing list/array with invalid index.

Solution: Check bounds before access or handle empty cases.

// Problem
testWidgets('range error', (tester) async {
  final items = [1, 2, 3, 4, 5];
  expect(items[5], 6); // Error: index 5 out of range
});

// Solution 1: Check bounds
testWidgets('safe access', (tester) async {
  final items = [1, 2, 3, 4, 5];
  if (items.length > 5) {
    expect(items[5], 6);
  }
});

// Solution 2: Handle empty lists
testWidgets('handle empty list', (tester) async {
  final items = <int>[];
  if (items.isEmpty) {
    expect(find.text('No items'), findsOneWidget);
  } else {
    expect(items.first, 1);
  }
});

Debugging Tips

Enable Verbose Logging

flutter test --verbose

Take Screenshots in Integration Tests

import 'package:integration_test/integration_test.dart';

testWidgets('debug with screenshot', (tester) async {
  final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized()
      as IntegrationTestWidgetsFlutterBinding;

  await tester.pumpWidget(MyWidget());
  await binding.convertFlutterSurfaceToImage();
  await tester.pump();
  await binding.takeScreenshot('debug-state');
});

Print Widget Tree

testWidgets('print tree', (tester) async {
  await tester.pumpWidget(MyWidget());
  debugDumpApp();
});

Use Debug Flags

testWidgets('debug flags', (tester) async {
  debugPrint('Current state: ...');
  await tester.pumpWidget(MyWidget());
  debugPrint('Widget tree: ${tester.widgetList(find.byType(MyWidget))}');
});

Prevention

Use Type Safety

// Good: Use type-safe access
final text = tester.widget<Text>(find.byType(Text));

// Avoid: Unsafe casts
final text = find.byType(Text).evaluate().first.widget as Text;

Check Before Actions

testWidgets('safe tap', (tester) async {
  await tester.pumpWidget(MyWidget());
  
  final button = find.byType(ElevatedButton);
  if (button.evaluate().isNotEmpty) {
    await tester.tap(button);
  } else {
    fail('Button not found');
  }
});

Handle All Cases

testWidgets('comprehensive', (tester) async {
  await tester.pumpWidget(MyWidget());
  
  // Check all possible states
  if (find.text('Loading').evaluate().isNotEmpty) {
    // Handle loading state
  } else if (find.text('Error').evaluate().isNotEmpty) {
    // Handle error state
  } else {
    // Handle success state
  }
});