// 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 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter/src/foundation/constants.dart';
import 'package:flutter_test/flutter_test.dart';

class _TestSliverPersistentHeaderDelegate extends SliverPersistentHeaderDelegate {
  _TestSliverPersistentHeaderDelegate({
    required this.minExtent,
    required this.maxExtent,
    required this.child,
    this.vsync = const TestVSync(),
  });

  final Widget child;

  @override
  final double maxExtent;

  @override
  final double minExtent;

  @override
  final TickerProvider? vsync;

  @override
  final PersistentHeaderShowOnScreenConfiguration showOnScreenConfiguration =
      const PersistentHeaderShowOnScreenConfiguration();

  @override
  Widget build(BuildContext context, double shrinkOffset, bool overlapsContent) => child;

  @override
  bool shouldRebuild(_TestSliverPersistentHeaderDelegate oldDelegate) => true;
}

void main() {
  const TextStyle textStyle = TextStyle();
  const Color cursorColor = Color.fromARGB(0xFF, 0xFF, 0x00, 0x00);

  late TextEditingController controller;
  late FocusNode focusNode;

  setUp(() {
    controller = TextEditingController();
    focusNode = FocusNode();
  });

  tearDown(() {
    controller.dispose();
    focusNode.dispose();
  });

  testWidgets('tapping on a partly visible editable brings it fully on screen', (
    WidgetTester tester,
  ) async {
    final ScrollController scrollController = ScrollController();
    addTearDown(scrollController.dispose);

    await tester.pumpWidget(
      MaterialApp(
        home: Center(
          child: SizedBox(
            height: 300.0,
            child: ListView(
              controller: scrollController,
              children: <Widget>[
                EditableText(
                  backgroundCursorColor: Colors.grey,
                  controller: controller,
                  focusNode: focusNode,
                  style: textStyle,
                  cursorColor: cursorColor,
                ),
                const SizedBox(height: 350.0),
              ],
            ),
          ),
        ),
      ),
    );

    // Scroll the EditableText half off screen.
    final RenderBox render = tester.renderObject(find.byType(EditableText));
    scrollController.jumpTo(render.size.height / 2);
    await tester.pumpAndSettle();
    expect(scrollController.offset, render.size.height / 2);

    await tester.showKeyboard(find.byType(EditableText));
    await tester.pumpAndSettle();
    expect(scrollController.offset, 0.0);
  });

  testWidgets('tapping on a partly visible editable brings it fully on screen with scrollInsets', (
    WidgetTester tester,
  ) async {
    final ScrollController scrollController = ScrollController();
    addTearDown(scrollController.dispose);

    await tester.pumpWidget(
      MaterialApp(
        home: Center(
          child: SizedBox(
            height: 300.0,
            child: ListView(
              controller: scrollController,
              children: <Widget>[
                const SizedBox(height: 200.0),
                EditableText(
                  backgroundCursorColor: Colors.grey,
                  scrollPadding: const EdgeInsets.all(50.0),
                  controller: controller,
                  focusNode: focusNode,
                  style: textStyle,
                  cursorColor: cursorColor,
                ),
                const SizedBox(height: 850.0),
              ],
            ),
          ),
        ),
      ),
    );

    // Scroll the EditableText half off screen.
    final RenderBox render = tester.renderObject(find.byType(EditableText));
    scrollController.jumpTo(200 + render.size.height / 2);
    await tester.pumpAndSettle();
    expect(scrollController.offset, 200 + render.size.height / 2);

    await tester.showKeyboard(find.byType(EditableText));
    await tester.pumpAndSettle();
    // Container above the text is 200 in height, the scrollInsets are 50
    // Tolerance of 5 units (The actual value was 152.0 in the current tests instead of 150.0)
    expect(scrollController.offset, lessThan(200.0 - 50.0 + 5.0));
    expect(scrollController.offset, greaterThan(200.0 - 50.0 - 5.0));
  });

  testWidgets('editable comes back on screen when entering text while it is off-screen', (
    WidgetTester tester,
  ) async {
    final ScrollController scrollController = ScrollController(initialScrollOffset: 100.0);
    addTearDown(scrollController.dispose);

    await tester.pumpWidget(
      MaterialApp(
        home: Center(
          child: SizedBox(
            height: 300.0,
            child: ListView(
              controller: scrollController,
              children: <Widget>[
                const SizedBox(height: 350.0),
                EditableText(
                  backgroundCursorColor: Colors.grey,
                  controller: controller,
                  focusNode: focusNode,
                  style: textStyle,
                  cursorColor: cursorColor,
                ),
                const SizedBox(height: 350.0),
              ],
            ),
          ),
        ),
      ),
    );

    // Focus the EditableText and scroll it off screen.
    await tester.showKeyboard(find.byType(EditableText));
    await tester.pumpAndSettle();
    expect(focusNode.hasFocus, isTrue);
    scrollController.jumpTo(0.0);
    await tester.pumpAndSettle();
    expect(scrollController.offset, 0.0);
    expect(find.byType(EditableText), findsNothing);

    // Entering text brings it back on screen.
    tester.testTextInput.enterText('Hello');
    await tester.pumpAndSettle();
    expect(scrollController.offset, greaterThan(0.0));
    expect(find.byType(EditableText), findsOneWidget);
  });

  testWidgets('entering text does not scroll when scrollPhysics.allowImplicitScrolling = false', (
    WidgetTester tester,
  ) async {
    // regression test for https://github.com/flutter/flutter/issues/19523
    final ScrollController scrollController = ScrollController(initialScrollOffset: 100.0);
    addTearDown(scrollController.dispose);

    await tester.pumpWidget(
      MaterialApp(
        home: Center(
          child: SizedBox(
            height: 300.0,
            child: ListView(
              physics: const NoImplicitScrollPhysics(),
              controller: scrollController,
              children: <Widget>[
                const SizedBox(height: 350.0),
                EditableText(
                  backgroundCursorColor: Colors.grey,
                  controller: controller,
                  focusNode: focusNode,
                  style: textStyle,
                  cursorColor: cursorColor,
                ),
                const SizedBox(height: 350.0),
              ],
            ),
          ),
        ),
      ),
    );

    // Focus the EditableText and scroll it off screen.
    await tester.showKeyboard(find.byType(EditableText));
    await tester.pumpAndSettle();
    expect(focusNode.hasFocus, isTrue);
    scrollController.jumpTo(0.0);
    await tester.pumpAndSettle();
    expect(scrollController.offset, 0.0);
    expect(find.byType(EditableText), findsNothing);

    // Entering text brings it not back on screen.
    tester.testTextInput.enterText('Hello');
    await tester.pumpAndSettle();
    expect(scrollController.offset, 0.0);
    expect(find.byType(EditableText), findsNothing);
  });

  testWidgets('entering text does not scroll a surrounding PageView', (WidgetTester tester) async {
    // regression test for https://github.com/flutter/flutter/issues/19523
    final PageController pageController = PageController(initialPage: 1);
    addTearDown(pageController.dispose);

    await tester.pumpWidget(
      MaterialApp(
        home: MediaQuery(
          data: const MediaQueryData(),
          child: Directionality(
            textDirection: TextDirection.ltr,
            child: Material(
              child: PageView(
                controller: pageController,
                children: <Widget>[
                  Container(color: Colors.red),
                  ColoredBox(
                    color: Colors.green,
                    child: TextField(controller: controller),
                  ),
                  Container(color: Colors.red),
                ],
              ),
            ),
          ),
        ),
      ),
    );

    await tester.showKeyboard(find.byType(EditableText));
    await tester.pumpAndSettle();
    expect(controller.text, '');
    tester.testTextInput.enterText('H');
    final int frames = await tester.pumpAndSettle();

    // The text input should not trigger any animations, which would indicate
    // that the surrounding PageView is incorrectly scrolling back-and-forth.
    expect(frames, 1);

    expect(controller.text, 'H');
  });

  testWidgets('focused multi-line editable scrolls caret back into view when typing', (
    WidgetTester tester,
  ) async {
    final ScrollController scrollController = ScrollController();
    addTearDown(scrollController.dispose);
    controller.text = "Start${'\n' * 39}End";

    await tester.pumpWidget(
      MaterialApp(
        home: Center(
          child: SizedBox(
            height: 300.0,
            child: ListView(
              controller: scrollController,
              children: <Widget>[
                EditableText(
                  backgroundCursorColor: Colors.grey,
                  maxLines: null, // multiline
                  controller: controller,
                  focusNode: focusNode,
                  style: textStyle,
                  cursorColor: cursorColor,
                ),
              ],
            ),
          ),
        ),
      ),
    );

    // Bring keyboard up and verify that end of EditableText is not on screen.
    await tester.showKeyboard(find.byType(EditableText));
    await tester.pumpAndSettle();
    scrollController.jumpTo(0.0);
    await tester.pumpAndSettle();
    final RenderBox render = tester.renderObject(find.byType(EditableText));
    expect(render.size.height, greaterThan(500.0));
    expect(scrollController.offset, 0.0);

    // Enter text at end, which is off-screen.
    final String textToEnter = '${controller.text} HELLO';
    tester.testTextInput.updateEditingValue(
      TextEditingValue(
        text: textToEnter,
        selection: TextSelection.collapsed(offset: textToEnter.length),
      ),
    );
    await tester.pumpAndSettle();

    // Caret scrolls into view.
    expect(find.byType(EditableText), findsOneWidget);
    expect(render.size.height, greaterThan(500.0));
    expect(scrollController.offset, greaterThan(0.0));
  });

  testWidgets(
    'focused multi-line editable does not scroll to old position when non-collapsed selection set',
    (WidgetTester tester) async {
      final ScrollController scrollController = ScrollController();
      addTearDown(scrollController.dispose);
      final String text = "Start${'\n' * 39}End";
      controller.value = TextEditingValue(
        text: text,
        selection: TextSelection.collapsed(offset: text.length - 3),
      );

      await tester.pumpWidget(
        MaterialApp(
          home: Center(
            child: SizedBox(
              height: 300.0,
              child: ListView(
                controller: scrollController,
                children: <Widget>[
                  EditableText(
                    backgroundCursorColor: Colors.grey,
                    maxLines: null, // multiline
                    controller: controller,
                    focusNode: focusNode,
                    style: textStyle,
                    cursorColor: cursorColor,
                  ),
                ],
              ),
            ),
          ),
        ),
      );

      // Bring keyboard up and verify that end of EditableText is not on screen.
      await tester.showKeyboard(find.byType(EditableText));
      await tester.pumpAndSettle();

      scrollController.jumpTo(0.0);
      await tester.pumpAndSettle();
      final RenderBox render = tester.renderObject(find.byType(EditableText));
      expect(render.size.height, greaterThan(500.0));
      expect(scrollController.offset, 0.0);

      // Change selection to non-collapsed so that cursor isn't shown
      // and the location requires a bit of scroll.
      tester.testTextInput.updateEditingValue(
        TextEditingValue(
          text: text,
          selection: const TextSelection(baseOffset: 26, extentOffset: 27),
        ),
      );
      await tester.pumpAndSettle();

      // Selection extent scrolls into view.
      expect(find.byType(EditableText), findsOneWidget);
      expect(render.size.height, greaterThan(500.0));
      expect(scrollController.offset, 28.0);
    },
  );

  testWidgets('scrolls into view with scrollInserts after the keyboard pops up', (
    WidgetTester tester,
  ) async {
    final ScrollController scrollController = ScrollController();
    addTearDown(scrollController.dispose);
    const Key container = Key('container');

    await tester.pumpWidget(
      MaterialApp(
        home: Align(
          alignment: Alignment.bottomCenter,
          child: SizedBox(
            height: 300.0,
            child: ListView(
              controller: scrollController,
              children: <Widget>[
                const SizedBox(key: container, height: 200.0),
                EditableText(
                  backgroundCursorColor: Colors.grey,
                  scrollPadding: const EdgeInsets.only(bottom: 300.0),
                  controller: controller,
                  focusNode: focusNode,
                  style: textStyle,
                  cursorColor: cursorColor,
                ),
                const SizedBox(height: 400.0),
              ],
            ),
          ),
        ),
      ),
    );

    expect(scrollController.offset, 0.0);

    await tester.showKeyboard(find.byType(EditableText));
    await tester.pumpAndSettle();
    expect(scrollController.offset, greaterThan(0.0));
    expect(find.byKey(container), findsNothing);
  });

  testWidgets(
    'A pinned persistent header should not scroll when its descendant EditableText gains focus',
    (WidgetTester tester) async {
      // Regression test for https://github.com/flutter/flutter/issues/25507.
      final ScrollController scrollController = ScrollController();
      addTearDown(scrollController.dispose);
      const Key headerKey = Key('header');

      await tester.pumpWidget(
        MaterialApp(
          home: Center(
            child: SizedBox(
              height: 600.0,
              width: 600.0,
              child: CustomScrollView(
                controller: scrollController,
                slivers: List<Widget>.generate(50, (int i) {
                  return i == 10
                      ? SliverPersistentHeader(
                          pinned: true,
                          delegate: _TestSliverPersistentHeaderDelegate(
                            minExtent: 50,
                            maxExtent: 50,
                            child: Container(
                              alignment: Alignment.topCenter,
                              child: EditableText(
                                key: headerKey,
                                backgroundCursorColor: Colors.grey,
                                controller: controller,
                                focusNode: focusNode,
                                style: textStyle,
                                cursorColor: cursorColor,
                              ),
                            ),
                          ),
                        )
                      : SliverToBoxAdapter(child: SizedBox(height: 100.0, child: Text('Tile $i')));
                }),
              ),
            ),
          ),
        ),
      );

      // The persistent header should now be pinned at the top.
      scrollController.jumpTo(100.0 * 15);
      await tester.pumpAndSettle();
      expect(scrollController.offset, 100.0 * 15);

      focusNode.requestFocus();
      await tester.pumpAndSettle();
      // The scroll offset should remain the same.
      expect(scrollController.offset, 100.0 * 15);
    },
  );

  testWidgets(
    'A pinned persistent header should not scroll when its descendant EditableText gains focus (no animation)',
    (WidgetTester tester) async {
      // Regression test for https://github.com/flutter/flutter/issues/25507.
      final ScrollController scrollController = ScrollController();
      addTearDown(scrollController.dispose);

      const Key headerKey = Key('header');
      await tester.pumpWidget(
        MaterialApp(
          home: Center(
            child: SizedBox(
              height: 600.0,
              width: 600.0,
              child: CustomScrollView(
                controller: scrollController,
                slivers: List<Widget>.generate(50, (int i) {
                  return i == 10
                      ? SliverPersistentHeader(
                          pinned: true,
                          delegate: _TestSliverPersistentHeaderDelegate(
                            minExtent: 50,
                            maxExtent: 50,
                            vsync: null,
                            child: Container(
                              alignment: Alignment.topCenter,
                              child: EditableText(
                                key: headerKey,
                                backgroundCursorColor: Colors.grey,
                                controller: controller,
                                focusNode: focusNode,
                                style: textStyle,
                                cursorColor: cursorColor,
                              ),
                            ),
                          ),
                        )
                      : SliverToBoxAdapter(child: SizedBox(height: 100.0, child: Text('Tile $i')));
                }),
              ),
            ),
          ),
        ),
      );

      // The persistent header should now be pinned at the top.
      scrollController.jumpTo(100.0 * 15);
      await tester.pumpAndSettle();
      expect(scrollController.offset, 100.0 * 15);

      focusNode.requestFocus();
      await tester.pumpAndSettle();
      // The scroll offset should remain the same.
      expect(scrollController.offset, 100.0 * 15);
    },
  );

  void testShowCaretOnScreen({required bool readOnly}) {
    group('EditableText._showCaretOnScreen, readOnly=$readOnly', () {
      final TextInputFormatter rejectEverythingFormatter = TextInputFormatter.withFunction(
        (TextEditingValue old, TextEditingValue value) => old,
      );

      bool isCaretOnScreen(WidgetTester tester) {
        final EditableTextState state = tester.state<EditableTextState>(
          find.byType(EditableText, skipOffstage: false),
        );
        final RenderEditable renderEditable = state.renderEditable;
        final Rect localRect = renderEditable.getLocalRectForCaret(
          state.textEditingValue.selection.base,
        );
        final Offset caretOrigin = renderEditable.localToGlobal(localRect.topLeft);
        final Rect caretRect = caretOrigin & localRect.size;
        return const Rect.fromLTWH(0, 0, 800, 600).intersect(caretRect) == caretRect;
      }

      Widget buildEditableText({
        required bool rejectUserInputs,
        ScrollController? scrollController,
        ScrollController? editableScrollController,
      }) {
        return MaterialApp(
          home: Scaffold(
            body: ListView(
              controller: scrollController,
              cacheExtent: 1000,
              children: <Widget>[
                // The text field is not fully visible.
                const SizedBox(height: 599),
                EditableText(
                  backgroundCursorColor: Colors.grey,
                  controller: controller,
                  scrollController: editableScrollController,
                  inputFormatters: <TextInputFormatter>[
                    if (rejectUserInputs) rejectEverythingFormatter,
                  ],
                  focusNode: focusNode,
                  style: textStyle,
                  cursorColor: cursorColor,
                  readOnly: readOnly,
                ),
              ],
            ),
          ),
        );
      }

      testWidgets('focus-triggered showCaretOnScreen', (WidgetTester tester) async {
        controller.text = 'a' * 100;
        controller.selection = const TextSelection.collapsed(offset: 100);
        final ScrollController scrollController = ScrollController();
        addTearDown(scrollController.dispose);
        final ScrollController editableScrollController = ScrollController();
        addTearDown(editableScrollController.dispose);

        await tester.pumpWidget(
          buildEditableText(
            rejectUserInputs: false,
            scrollController: scrollController,
            editableScrollController: editableScrollController,
          ),
        );

        focusNode.requestFocus();
        await tester.pumpAndSettle();

        if (kIsWeb) {
          await tester.sendKeyEvent(LogicalKeyboardKey.arrowRight);
          await tester.pump();
        }

        // On web, the entire field is selected, and only part of that selection
        // is visible on the screen.
        expect(isCaretOnScreen(tester), !readOnly && !kIsWeb);
        expect(scrollController.offset, readOnly ? 0.0 : greaterThan(0.0));
        expect(editableScrollController.offset, readOnly ? 0.0 : greaterThan(0.0));
      });

      testWidgets('selection-triggered showCaretOnScreen: virtual keyboard', (
        WidgetTester tester,
      ) async {
        controller.text = 'a' * 100;
        controller.selection = const TextSelection.collapsed(offset: 80);
        final ScrollController scrollController = ScrollController();
        addTearDown(scrollController.dispose);
        final ScrollController editableScrollController = ScrollController();
        addTearDown(editableScrollController.dispose);

        await tester.pumpWidget(
          buildEditableText(
            rejectUserInputs: false,
            scrollController: scrollController,
            editableScrollController: editableScrollController,
          ),
        );

        focusNode.requestFocus();
        await tester.pumpAndSettle();

        // Ensure the caret is not fully visible and the text field is focused.
        scrollController.jumpTo(0);
        editableScrollController.jumpTo(0);
        await tester.pumpAndSettle();
        expect(isCaretOnScreen(tester), isFalse);

        final EditableTextState state = tester.state<EditableTextState>(
          find.byType(EditableText, skipOffstage: false),
        );

        // Change the selection. Show caret on screen when readyOnly is true,
        // as a read-only text field rejects everything from the software
        // keyboard (except for web).
        state.updateEditingValue(
          state.textEditingValue.copyWith(selection: const TextSelection.collapsed(offset: 90)),
        );
        await tester.pumpAndSettle();
        expect(isCaretOnScreen(tester), !readOnly || kIsWeb);
        expect(scrollController.offset, readOnly && !kIsWeb ? 0.0 : greaterThan(0.0));
        expect(editableScrollController.offset, readOnly && !kIsWeb ? 0.0 : greaterThan(0.0));

        // Reject user input.
        await tester.pumpWidget(
          buildEditableText(
            rejectUserInputs: true,
            scrollController: scrollController,
            editableScrollController: editableScrollController,
          ),
        );

        // Ensure the caret is not fully visible and the text field is focused.
        scrollController.jumpTo(0);
        editableScrollController.jumpTo(0);
        await tester.pumpAndSettle();
        expect(isCaretOnScreen(tester), isFalse);

        state.updateEditingValue(
          state.textEditingValue.copyWith(selection: const TextSelection.collapsed(offset: 100)),
        );
        await tester.pumpAndSettle();
        expect(isCaretOnScreen(tester), !readOnly || kIsWeb);
        expect(scrollController.offset, readOnly && !kIsWeb ? 0.0 : greaterThan(0.0));
        expect(editableScrollController.offset, readOnly && !kIsWeb ? 0.0 : greaterThan(0.0));
      });

      testWidgets('selection-triggered showCaretOnScreen: text selection delegate', (
        WidgetTester tester,
      ) async {
        controller.text = 'a' * 100;
        controller.selection = const TextSelection.collapsed(offset: 80);
        final ScrollController scrollController = ScrollController();
        addTearDown(scrollController.dispose);
        final ScrollController editableScrollController = ScrollController();
        addTearDown(editableScrollController.dispose);

        await tester.pumpWidget(
          buildEditableText(
            rejectUserInputs: false,
            scrollController: scrollController,
            editableScrollController: editableScrollController,
          ),
        );

        focusNode.requestFocus();
        await tester.pumpAndSettle();

        // Ensure the caret is not fully visible and the text field is focused.
        scrollController.jumpTo(0);
        editableScrollController.jumpTo(0);
        await tester.pumpAndSettle();
        expect(isCaretOnScreen(tester), isFalse);

        final EditableTextState state = tester.state<EditableTextState>(
          find.byType(EditableText, skipOffstage: false),
        );

        // Change the selection. Show caret on screen even when readyOnly is
        // false.
        state.userUpdateTextEditingValue(
          state.textEditingValue.copyWith(selection: const TextSelection.collapsed(offset: 90)),
          null,
        );
        await tester.pumpAndSettle();
        expect(isCaretOnScreen(tester), isTrue);
        expect(scrollController.offset, greaterThan(0.0));
        expect(editableScrollController.offset, greaterThan(0.0));

        // Rejects user input.
        await tester.pumpWidget(
          buildEditableText(
            rejectUserInputs: true,
            scrollController: scrollController,
            editableScrollController: editableScrollController,
          ),
        );

        // Ensure the caret is not fully visible and the text field is focused.
        scrollController.jumpTo(0);
        editableScrollController.jumpTo(0);
        await tester.pumpAndSettle();
        expect(isCaretOnScreen(tester), isFalse);

        state.userUpdateTextEditingValue(
          state.textEditingValue.copyWith(selection: const TextSelection.collapsed(offset: 100)),
          null,
        );
        await tester.pumpAndSettle();
        expect(isCaretOnScreen(tester), isTrue);
        expect(scrollController.offset, greaterThan(0.0));
        expect(editableScrollController.offset, greaterThan(0.0));
      });

      // Regression text for https://github.com/flutter/flutter/pull/74722.
      testWidgets('does NOT randomly trigger when cursor blinks', (WidgetTester tester) async {
        controller.text = 'a' * 100;
        controller.selection = const TextSelection.collapsed(offset: 0);
        final ScrollController editableScrollController = ScrollController();
        addTearDown(editableScrollController.dispose);
        final bool deterministicCursor = EditableText.debugDeterministicCursor;
        EditableText.debugDeterministicCursor = false;

        await tester.pumpWidget(
          MaterialApp(
            home: Scaffold(
              body: EditableText(
                backgroundCursorColor: Colors.grey,
                controller: controller,
                scrollController: editableScrollController,
                focusNode: focusNode,
                style: textStyle,
                cursorColor: cursorColor,
              ),
            ),
          ),
        );

        final EditableTextState state = tester.state<EditableTextState>(
          find.byType(EditableText, skipOffstage: false),
        );

        // Ensure the text was initially visible.
        expect(isCaretOnScreen(tester), true);
        expect(editableScrollController.offset, 0.0);

        // Change the text but keep the cursor location.
        state.updateEditingValue(controller.value.copyWith(text: 'a' * 101));

        await tester.pumpAndSettle();

        // The caret should stay where it was, since the selection didn't change.
        expect(isCaretOnScreen(tester), true);
        expect(editableScrollController.offset, 0.0);

        // Now move to hide the cursor.
        editableScrollController.jumpTo(100.0);

        // Does not trigger showCaretOnScreen.
        await tester.pump();
        await tester.pumpAndSettle();
        expect(editableScrollController.offset, 100.0);
        expect(isCaretOnScreen(tester), isFalse);

        EditableText.debugDeterministicCursor = deterministicCursor;
      });
    });
  }

  testShowCaretOnScreen(readOnly: true);
  testShowCaretOnScreen(readOnly: false);
}

class NoImplicitScrollPhysics extends AlwaysScrollableScrollPhysics {
  const NoImplicitScrollPhysics({super.parent});

  @override
  bool get allowImplicitScrolling => false;

  @override
  NoImplicitScrollPhysics applyTo(ScrollPhysics? ancestor) {
    return NoImplicitScrollPhysics(parent: buildParent(ancestor));
  }
}
