Repository: flutter
Issue: `Failed assertion: line 1966 pos 12: 'hasSize'` error when `SelectionOverlay.showToolbar`
Labels: a: tests,framework,a: error message,has reproducible steps,P2,found in release: 3.10,found in release: 3.11,team-framework,triaged-framework

### Is there an existing issue for this?

- [X] I have searched the [existing issues](https://github.com/flutter/flutter/issues)
- [X] I have read the [guide to filing a bug](https://flutter.dev/docs/resources/bug-reports)

### Steps to reproduce

The current reproduction code, though seemingly long, is just "add HackyWidget into an existing Flutter test". See https://github.com/flutter/flutter/issues/126332#issuecomment-1555613297 for how I constructed it.

### Expected results

No errors.

### Actual results

```
══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═══════════════════════════════════════════════════════════
The following assertion was thrown building
_OverlayEntryWidget-[LabeledGlobalKey<_OverlayEntryWidgetState>#c504b](dirty, state:
_OverlayEntryWidgetState#dd11b):
RenderBox was not laid out: RenderFractionalTranslation#5b681 NEEDS-LAYOUT NEEDS-PAINT
NEEDS-COMPOSITING-BITS-UPDATE
'package:flutter/src/rendering/box.dart':
Failed assertion: line 1966 pos 12: 'hasSize'

Either the assertion indicates an error in the framework itself, or we should provide substantially
more information in this error message to help you determine and fix the underlying cause.
In either case, please report this assertion by filing a bug on GitHub:
  https://github.com/flutter/flutter/issues/new?template=2_bug.yml

The relevant error-causing widget was:
  MaterialApp MaterialApp:file:///Users/tom/RefCode/bug/test/a_test.dart:160:12

When the exception was thrown, this was the stack:
#2      RenderBox.size (package:flutter/src/rendering/box.dart:1966:12)
#3      RenderFractionalTranslation.applyPaintTransform (package:flutter/src/rendering/proxy_box.dart:2993:24)
#4      RenderObject.getTransformTo (package:flutter/src/rendering/object.dart:3181:24)
#5      RenderBox.localToGlobal (package:flutter/src/rendering/box.dart:2617:39)
#6      SelectionOverlay.showToolbar.<anonymous closure> (package:flutter/src/widgets/text_selection.dart:1356:30)
#7      ContextMenuController.show.<anonymous closure> (package:flutter/src/widgets/context_menu_controller.dart:65:54)
#8      _OverlayEntryWidgetState.build (package:flutter/src/widgets/overlay.dart:351:36)
#9      StatefulElement.build (package:flutter/src/widgets/framework.dart:5202:27)
#10     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:5090:15)
#11     StatefulElement.performRebuild (package:flutter/src/widgets/framework.dart:5255:11)
#12     Element.rebuild (package:flutter/src/widgets/framework.dart:4809:7)
#13     StatefulElement.update (package:flutter/src/widgets/framework.dart:5278:5)
#14     Element.updateChild (package:flutter/src/widgets/framework.dart:3690:15)
#15     RenderObjectElement.updateChildren (package:flutter/src/widgets/framework.dart:6097:32)
#16     MultiChildRenderObjectElement.update (package:flutter/src/widgets/framework.dart:6599:17)
#17     Element.updateChild (package:flutter/src/widgets/framework.dart:3690:15)
#18     ComponentElement.performRebuild (package:flutter/src/widgets/framework.dart:5115:16)
#19     StatefulElement.performRebuild (package:flutter/src/widgets/framework.dart:5255:11)
#20     Element.rebuild (package:flutter/src/widgets/framework.dart:4809:7)
...
```

### Code sample

<details><summary>Code sample</summary>

```dart
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:ui';

import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter_test/flutter_test.dart';

class HackyWidget extends StatefulWidget {
  final Widget child;

  const HackyWidget({Key? key, required this.child}) : super(key: key);

  @override
  State<HackyWidget> createState() => _HackyWidgetState();
}

class _HackyWidgetState extends State<HackyWidget> {
  final childKey = GlobalKey();

  // var enable = true;
  var enable = false;

  @override
  Widget build(BuildContext context) {
    print('$runtimeType.build enable=$enable');

    SchedulerBinding.instance.addPostFrameCallback((_) {
      if (mounted) setState(() => enable = !enable);
    });

    final inner = KeyedSubtree(key: childKey, child: widget.child);

    return enable
        ? FractionalTranslation(
      translation: Offset(0.1, 0.1),
      child: inner,
    )
        : inner;
  }
}


List<TextSelectionPoint> _globalize(
    Iterable<TextSelectionPoint> points, RenderBox box) {
  return points.map<TextSelectionPoint>((TextSelectionPoint point) {
    return TextSelectionPoint(
      box.localToGlobal(point.point),
      point.direction,
    );
  }).toList();
}

RenderEditable _findRenderEditable<T extends State<StatefulWidget>>(WidgetTester tester) {
  return (tester.state(find.byType(TextField))
  as TextSelectionGestureDetectorBuilderDelegate)
      .editableTextKey
      .currentState!
      .renderEditable;
}

Offset _textOffsetToPosition<T extends State<StatefulWidget>>(WidgetTester tester, int offset) {
  final RenderEditable renderEditable = _findRenderEditable(tester);

  final List<TextSelectionPoint> endpoints = renderEditable
      .getEndpointsForSelection(
    TextSelection.collapsed(offset: offset),
  )
      .map<TextSelectionPoint>((TextSelectionPoint point) => TextSelectionPoint(
    renderEditable.localToGlobal(point.point),
    point.direction,
  ))
      .toList();

  return endpoints[0].point + const Offset(0.0, -2.0);
}

void main() {
  const Duration durationBetweenActions = Duration(milliseconds: 20);
  const String defaultText = 'I am a magnifier, fear me!';

  Future<void> showMagnifier(WidgetTester tester, String characterToTapOn) async {
    final Offset tapOffset = _textOffsetToPosition(tester, defaultText.indexOf(characterToTapOn));

    print('hi 1');
    // Double tap 'Magnifier' word to show the selection handles.
    final TestGesture testGesture = await tester.startGesture(tapOffset);
    print('hi 2');
    await tester.pump(durationBetweenActions);
    print('hi 3');
    await testGesture.up();
    print('hi 4');
    await tester.pump(durationBetweenActions);
    print('hi 5');
    await testGesture.down(tapOffset);
    print('hi 6');
    await tester.pump(durationBetweenActions);
    print('hi 7');
    await testGesture.up();
    print('hi 8');
    // await tester.pumpAndSettle();
    for(var i=0;i<100;++i) {
      print('hi 9 - $i');
      await tester.pump(const Duration(milliseconds: 100));
    }

    final TextSelection selection = tester
        .firstWidget<TextField>(find.byType(TextField))
        .controller!
        .selection;

    final RenderEditable renderEditable = _findRenderEditable(tester);
    final List<TextSelectionPoint> endpoints = _globalize(
      renderEditable.getEndpointsForSelection(selection),
      renderEditable,
    );

    final Offset handlePos = endpoints.last.point + const Offset(10.0, 10.0);

    final TestGesture gesture = await tester.startGesture(handlePos);

    await gesture.moveTo(
      _textOffsetToPosition(
        tester,
        defaultText.length - 2,
      ),
    );
    await tester.pump();
  }

  testWidgets('should show custom magnifier on drag', (WidgetTester tester) async {
    // await tester.pumpWidget(const TextMagnifierExampleApp(text: defaultText));
    await tester.pumpWidget(HackyWidget(child: const TextMagnifierExampleApp(text: defaultText)));

    await showMagnifier(tester, 'e');
    expect(find.byType(CustomMagnifier), findsOneWidget);

    // await expectLater(
    //   find.byType(TextMagnifierExampleApp),
    //   matchesGoldenFile('text_magnifier.0_test.png'),
    // );
  });
}

class TextMagnifierExampleApp extends StatelessWidget {
  const TextMagnifierExampleApp({
    super.key,
    this.textDirection = TextDirection.ltr,
    required this.text,
  });

  final TextDirection textDirection;
  final String text;

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: Padding(
          padding: const EdgeInsets.symmetric(horizontal: 48.0),
          child: Center(
            child: TextField(
              textDirection: textDirection,
              // Create a custom magnifier configuration that
              // this `TextField` will use to build a magnifier with.
              magnifierConfiguration: TextMagnifierConfiguration(
                magnifierBuilder: (_, __, ValueNotifier<MagnifierInfo> magnifierInfo) => CustomMagnifier(
                  magnifierInfo: magnifierInfo,
                ),
              ),
              controller: TextEditingController(text: text),
            ),
          ),
        ),
      ),
    );
  }
}

class CustomMagnifier extends StatelessWidget {
  const CustomMagnifier({super.key, required this.magnifierInfo});

  static const Size magnifierSize = Size(200, 200);

  // This magnifier will consume some text data and position itself
  // based on the info in the magnifier.
  final ValueNotifier<MagnifierInfo> magnifierInfo;

  @override
  Widget build(BuildContext context) {
    // Use a value listenable builder because we want to rebuild
    // every time the text selection info changes.
    // `CustomMagnifier` could also be a `StatefulWidget` and call `setState`
    // when `magnifierInfo` updates. This would be useful for more complex
    // positioning cases.
    return ValueListenableBuilder<MagnifierInfo>(
        valueListenable: magnifierInfo,
        builder: (BuildContext context, MagnifierInfo currentMagnifierInfo, _) {
          // We want to position the magnifier at the global position of the gesture.
          Offset magnifierPosition = currentMagnifierInfo.globalGesturePosition;

          // You may use the `MagnifierInfo` however you'd like:
          // In this case, we make sure the magnifier never goes out of the current line bounds.
          magnifierPosition = Offset(
            clampDouble(
              magnifierPosition.dx,
              currentMagnifierInfo.currentLineBoundaries.left,
              currentMagnifierInfo.currentLineBoundaries.right,
            ),
            clampDouble(
              magnifierPosition.dy,
              currentMagnifierInfo.currentLineBoundaries.top,
              currentMagnifierInfo.currentLineBoundaries.bottom,
            ),
          );

          // Finally, align the magnifier to the bottom center. The initial anchor is
          // the top left, so subtract bottom center alignment.
          magnifierPosition -= Alignment.bottomCenter.alongSize(magnifierSize);

          return Positioned(
            left: magnifierPosition.dx,
            top: magnifierPosition.dy,
            child: RawMagnifier(
              magnificationScale: 2,
              // The focal point starts at the center of the magnifier.
              // We probably want to point below the magnifier, so
              // offset the focal point by half the magnifier height.
              focalPointOffset: Offset(0, magnifierSize.height / 2),
              // Decorate it however we'd like!
              decoration: const MagnifierDecoration(
                shape: StarBorder(
                  side: BorderSide(
                    color: Colors.green,
                    width: 2,
                  ),
                ),
              ),
              size: magnifierSize,
            ),
          );
        });
  }
}

```

</details>


### Screenshots or Video

<details>
<summary>Screenshots / Video demonstration</summary>

[Upload media here]

</details>


### Logs

<details><summary>Logs</summary>

```console
[Paste your logs here]
```

</details>


### Flutter Doctor output

<details><summary>Doctor output</summary>

```console
[Paste your output here]
```

</details>