Finding a TextSpan to tap on with Flutter tests
Asked Answered
W

2

15

With a Flutter WidgetTester how can you tap on a TextSpan, like in the code below?

RichText(
  text: TextSpan(
    children: [
      TextSpan(text: 'aaa '),
      TextSpan(
        text: 'bbb ',
        recognizer: TapGestureRecognizer()
          ..onTap = () { 
            // How to reach this code in a widget test?
          },
      ),
      TextSpan(text: 'ccc'),
    ],
  ),
)
Wish answered 16/2, 2020 at 10:1 Comment(2)
this tutorial didn't help you?Takara
@P4yam, no because TextSpan is not a widget. But the accepted answer is great.Wish
C
29

CommonFinders byWidgetPredicate method

InlineSpan visitChildren method

Find the TextSpan:

final finder = find.byWidgetPredicate(
  (widget) => widget is RichText && tapTextSpan(widget, "bbb "),
);
bool findTextAndTap(InlineSpan visitor, String text) {
  if (visitor is TextSpan && visitor.text == text) {
    (visitor.recognizer as TapGestureRecognizer).onTap();

    return false;
  }

  return true;
}

bool tapTextSpan(RichText richText, String text) {
  final isTapped = !richText.text.visitChildren(
    (visitor) => findTextAndTap(visitor, text),
  );

  return isTapped;
}
Coping answered 16/2, 2020 at 10:18 Comment(4)
byWidgetPredicate method not found in FlutterDriver.Bladder
Not absolutely, but helped in understanding the flow.Constantan
I tried same, but worked for me was find the widget before tester .widget<RichText>(find.byKey(<key>)) and then call the suggested tapTextSpan(...) function.Sheedy
Here is a good workaround: github.com/flutter/flutter/issues/56023#issuecomment-764985456Katherinkatherina
P
0

There is a tapOnText() method of the WidgetTester class for taps (since maybe flutter 3.19?).

With that you can click on specific texts like this:

import 'package:flutter_test/flutter_test.dart';

void main() {
  testWidgets('tapping part of a text', (tester) async {
    // see my Example class below
    await tester.pumpWidget(const MaterialApp(home: Example()));

    await tester.tapOnText(find.textRange.ofSubstring('Tappable text'));
  });
}

In the original example above, this pattern:

TextSpan(text: 'bbb', recognizer: TapGestureRecognizer()..onTap = () {})

...may cause a memory leak. The recognizer property's docs say it does not dispose the provided gesture recognizer. So the right way is to use a StatefulWidget instead where you create the gesture recognizer instance(s) once and then you call .dispose() on them on the state's dispose() method, like this:

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

class Example extends StatefulWidget {
  const Example({super.key});

  @override
  State<Example> createState() => _ExampleState();
}

class _ExampleState extends State<Example> {
  late final TapGestureRecognizer _tapGestureRecognizer;

  @override
  void initState() {
    super.initState();
    _tapGestureRecognizer = TapGestureRecognizer()..onTap = _onTap;
  }

  @override
  void dispose() {
    _tapGestureRecognizer.dispose();
    super.dispose();
  }

  void _onTap() {
    // handle tap here
  }

  @override
  Widget build(BuildContext context) {
    return Text.rich(
      TextSpan(
        children: [
          const TextSpan(text: 'Normal text '),
          TextSpan(
            text: 'Tappable text',
            recognizer: _tapGestureRecognizer,
          ),
        ],
      ),
    );
  }
}
Portable answered 2/8 at 11:10 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.