flutterdartflutterdriver

Cannot Locate text Widget with 3rd party Library


Im writing Integration Test using Flutter Driver for the app that utilizes CoachMark library (https://pub.dev/packages/tutorial_coach_mark)

i want to click the text to close the CoachMark, but when i tried to inspect it using VSCode's widget inspector, the text didn't show up on the widget tree, when i hover the inspector to that text, it points to MaterialApp root widget (see screenshot)

what i want to click is a text with OKE text

These are methods i tried to locate them with no luck:

  1. find.byType('Text')
  2. find.text('OKE')
  3. find.byType('RichText')
  4. and even this nested, confusing finder

return find.descendant(of: find.byType('Align'), matching: find.descendant(of:find.byType('SafeArea'), matching: find.descendant(of:find.byType('AnimatedOpacity'), matching: find.descendant(of:find.byType('InkWell'), matching: find.descendant(of: find.byType('Padding'),matching: find.text('OKE'))))));

The reason why i tried using the 4th method is when i tried to dive into the library code itself, it builds the widget something like this

  Widget _buildSkip() {
    if (widget.hideSkip) {
      return SizedBox.shrink();
    }
    return Align(
      alignment: widget.alignSkip,
      child: SafeArea(
        child: AnimatedOpacity(
          opacity: showContent ? 1 : 0,
          duration: Duration(milliseconds: 300),
          child: InkWell(
            onTap: widget.clickSkip,
            child: Padding(
              padding: const EdgeInsets.all(20.0),
              child: Text(
                widget.textSkip,
                style: widget.textStyleSkip
              ),
            ),
          ),
        ),
      ),
    );
  }

i attached one more screenshot to show what i want to click (the text on the bottom right corner on the screen)enter image description here

any suggestion?

Edit
this is home screen:

class HomeSearchBarWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BaseWidget<HomeSearchBarViewModel>(
      model: HomeSearchBarViewModel(
        Provider.of<TrackingService>(context),
        Provider.of<ErrorReportingService>(context),
      ),
      onModelReady: (model) => model.initModel(),
      builder: (context, model, child) {
        return Container(
          padding: EdgeInsets.only(bottom: 5.0),
          margin: EdgeInsets.only(top: 8, left: 16, right: 8),
          alignment: Alignment.centerLeft,
          width: MediaQuery.of(context).size.width,
          child: Column(
            children: <Widget>[
              Row(
                children: <Widget>[
                  ImageHelper.logo,
                  UIHelper.horzSpace(16),
                  Expanded(
                    child: TextFormField(
                      readOnly: true,
                      decoration: new InputDecoration(
                        contentPadding: EdgeInsets.all(14),
                        labelStyle: PinTextStyles.styleBody2(
                          PinColorsV2.neutral500,
                        ),
                        prefixIcon: Icon(
                          Icons.search,
                          color: PinColorsV2.neutral500,
                        ),
                        border: UIHelper.inputBorder,
                        hintText: "Lokasi atau nama proyek",
                        hintStyle: PinTextStyles.styleBody2(
                          PinColorsV2.neutral200,
                        ),
                        enabledBorder: OutlineInputBorder(
                          borderSide: BorderSide(
                            color: PinColorsV2.neutral200,
                          ),
                        ),
                        focusedBorder: OutlineInputBorder(
                          borderSide: BorderSide(
                            color: PinColorsV2.neutral200,
                          ),
                        ),
                      ),
                      onTap: () async {
                        await model.trackLogEvent(
                          HomeSearchBarTrackingKeys.clickSearch,
                        );
                        Navigator.pushNamed(
                          context,
                          RoutePaths.ProjectSearch,
                          arguments: {
                            "keyword": "",
                          },
                        );
                      },
                    ),
                  ),
                  UIHelper.horzSpace(12),
                  CoachMarkWidget(
                    targets: model.targets,
                    keyTarget: model.helpCenterKey,
                    targetIdentify: "Help-Center",
                    title: "Punya pertanyaan terkait penggunaan aplikasi?",
                    description:
                    "Temukan semua solusinya dengan tap ikon tanda tanya di sudut kanan atas.",
                    onFinish: () => model.hideCoachMark(),
                    isVisible: model.coachMark != null && model.coachMark.value,
                    focusWidget: HelpCenterIconWidget(
                      page: HelpCenterPage.homePage,
                      iconColor: PinColorsV2.neutral500,
                      screenNameFrom: HomeViewTrackingKeys.open,
                    ),
                  ),
                ],
              ),
            ],
          ),
        );
      },
    );
  }
}

class HomeSearchBarTrackingKeys {
  static const String clickSearch = "landing_click_search";
}

<br? and this is the code for building the widget:

class CoachMarkWidget extends StatelessWidget {
  final List<TargetFocus> targets;
  final GlobalKey keyTarget;
  final String targetIdentify;
  final String title;
  final String description;
  final bool isVisible;
  final Function() onFinish;
  final Widget focusWidget;

  CoachMarkWidget({
    this.targets,
    this.keyTarget,
    this.targetIdentify,
    this.title,
    this.description,
    this.isVisible = true,
    this.onFinish,
    this.focusWidget, 
  });

  void initTargetCoachMark() {
    return targets.add(
      TargetFocus(
        identify: targetIdentify,
        keyTarget: keyTarget,
        contents: [
          ContentTarget(
            align: AlignContent.bottom,
            child: Container(
              padding: EdgeInsets.symmetric(
                horizontal: 48,
              ),
              child: Column(
                children: <Widget>[
                  Text(
                    title,
                    style: PinTextStylesV2.styleHeadingXSmall(
                      color: PinColorsV2.neutralWhite,
                    ).merge(
                      TextStyle(
                        height: 1.22,
                      ),
                    ),
                  ),
                  Padding(
                    padding: EdgeInsets.only(top: 8),
                    child: Text(
                      description,
                      style: PinTextStylesV2.styleParagraphLarge(
                        color: PinColorsV2.neutralWhite,
                      ).merge(
                        TextStyle(
                          height: 1.5,
                        ),
                      ),
                    ),
                  ),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }

  showTutorial(
    BuildContext context,
  ) {
    return TutorialCoachMark(
      context,
      targets: targets,
      colorShadow: PinColorsV2.blue300,
      opacityShadow: 0.85,
      textSkip: "OKE",
      widgetKey: Key('OKE'),
      // key: Key("oke"),
      textStyleSkip: PinTextStylesV2.styleActionMedium(
        color: PinColorsV2.neutralWhite,
      ),
      onFinish: () async {
        await onFinish();
      },
      onClickTarget: (target) async {
        await onFinish();
      },
      onClickSkip: () async {
        await onFinish();
      },
    )..show();
  }

  @override
  Widget build(BuildContext context) {
    initTargetCoachMark();
    if (isVisible) {
      WidgetsBinding.instance.addPostFrameCallback(
        (_) {
          showTutorial(context);
        },
      );
    }
    return Container(
      key: keyTarget,
      child: focusWidget,
    );
  }
}

and here is the custom widget code
class TutorialCoachMark{
  final BuildContext _context;
  final List<TargetFocus> targets;
  final Function(TargetFocus) onClickTarget;
  final Function() onFinish;
  final double paddingFocus;
  final Function() onClickSkip;
  final AlignmentGeometry alignSkip;
  final String textSkip;
  final TextStyle textStyleSkip;
  final bool hideSkip;
  final Color colorShadow;
  final double opacityShadow;
  final GlobalKey<TutorialCoachMarkWidgetState> _widgetKey = GlobalKey();
  final Key widgetKey;

  OverlayEntry _overlayEntry;

  TutorialCoachMark(
    this._context, {
    this.targets,
    this.colorShadow = Colors.black,
    this.onClickTarget,
    this.onFinish,
    this.paddingFocus = 10,
    this.onClickSkip,
    this.alignSkip = Alignment.bottomRight,
    this.textSkip = "SKIP",
    this.textStyleSkip = const TextStyle(color: Colors.white),
    this.hideSkip = false,
    this.opacityShadow = 0.8,
    this.widgetKey
  }) : assert(targets != null, opacityShadow >= 0 && opacityShadow <= 1);

  OverlayEntry _buildOverlay() {
    return OverlayEntry(builder: (context) {
      return TutorialCoachMarkWidget(
        key: _widgetKey,
        // key: widgetKey,
        // text: widgetKey,
        // dua diatas ini tambahan (key nya tadinya pake yang line atas)
        targets: targets,
        clickTarget: onClickTarget,
        paddingFocus: paddingFocus,
        clickSkip: skip,
        alignSkip: alignSkip,
        textSkip: textSkip,
        textStyleSkip: textStyleSkip,
        hideSkip: hideSkip,
        colorShadow: colorShadow,
        opacityShadow: opacityShadow,
        finish: finish,
      );
    });
  }

  // @override
  // Widget build(BuildContext context){
  //   show();
  // }

  void show() {
    if (_overlayEntry == null) {
      _overlayEntry = _buildOverlay();
      Overlay.of(_context).insert(_overlayEntry);
    }
  }

  void finish() {
    if (onFinish != null) onFinish();
    _removeOverlay();
  }

  void skip() {
    if (onClickSkip != null) onClickSkip();
    _removeOverlay();
  }

  void next() => _widgetKey?.currentState?.next();
  void previous() => _widgetKey?.currentState?.previous();

  void _removeOverlay() {
    _overlayEntry?.remove();
    _overlayEntry = null;
  }
}

and here is the 'OKE' button that i want to click
class TutorialCoachMarkWidget extends StatefulWidget {
  const TutorialCoachMarkWidget({
    Key key,
    // this.key = Key('OKE'),
    this.targets,
    this.finish,
    this.paddingFocus = 10,
    this.clickTarget,
    this.alignSkip = Alignment.bottomRight,
    this.textSkip = "SKIP",
    this.clickSkip,
    this.colorShadow = Colors.black,
    this.opacityShadow = 0.8,
    this.textStyleSkip = const TextStyle(color: Colors.white),
    this.hideSkip,
  }) : super(key: key);

  final List<TargetFocus> targets;
  final Function(TargetFocus) clickTarget;
  final Function() finish;
  final Color colorShadow;
  final double opacityShadow;
  final double paddingFocus;
  final Function() clickSkip;
  final AlignmentGeometry alignSkip;
  final String textSkip;
  final TextStyle textStyleSkip;
  final bool hideSkip;
  // final Key key; ini diganti sama line 44

  @override
  TutorialCoachMarkWidgetState createState() => TutorialCoachMarkWidgetState();
}

class TutorialCoachMarkWidgetState extends State<TutorialCoachMarkWidget> {
  final GlobalKey<AnimatedFocusLightState> _focusLightKey = GlobalKey();
  final Key textKey = Key('OKE');
  bool showContent = false;
  TargetFocus currentTarget;

  @override
  Widget build(BuildContext context) {
    return Material(
      color: Colors.transparent,
      child: Stack(
        children: <Widget>[
          AnimatedFocusLight(
            key: _focusLightKey,
            targets: widget.targets,
            finish: widget.finish,
            paddingFocus: widget.paddingFocus,
            colorShadow: widget.colorShadow,
            opacityShadow: widget.opacityShadow,
            clickTarget: (target) {
              if (widget.clickTarget != null) widget.clickTarget(target);
            },
            focus: (target) {
              setState(() {
                currentTarget = target;
                showContent = true;
              });
            },
            removeFocus: () {
              setState(() {
                showContent = false;
              });
            },
          ),
          AnimatedOpacity(
            opacity: showContent ? 1 : 0,
            duration: Duration(milliseconds: 300),
            child: _buildContents(),
          ),
          _buildSkip()
        ],
      ),
    );
  }

  Widget _buildContents() {
    if (currentTarget == null) {
      return SizedBox.shrink();
    }

    List<Widget> children = List();

    TargetPosition target = getTargetCurrent(currentTarget);

    var positioned = Offset(
      target.offset.dx + target.size.width / 2,
      target.offset.dy + target.size.height / 2,
    );

    double haloWidth;
    double haloHeight;

    if (currentTarget.shape == ShapeLightFocus.Circle) {
      haloWidth = target.size.width > target.size.height
          ? target.size.width
          : target.size.height;
      haloHeight = haloWidth;
    } else {
      haloWidth = target.size.width;
      haloHeight = target.size.height;
    }

    haloWidth = haloWidth * 0.6 + widget.paddingFocus;
    haloHeight = haloHeight * 0.6 + widget.paddingFocus;

    double weight = 0.0;
    double top;
    double bottom;
    double left;

    children = currentTarget.contents.map<Widget>((i) {
      switch (i.align) {
        case AlignContent.bottom:
          {
            weight = MediaQuery.of(context).size.width;
            left = 0;
            top = positioned.dy + haloHeight;
            bottom = null;
          }
          break;
        case AlignContent.top:
          {
            weight = MediaQuery.of(context).size.width;
            left = 0;
            top = null;
            bottom = haloHeight +
                (MediaQuery.of(context).size.height - positioned.dy);
          }
          break;
        case AlignContent.left:
          {
            weight = positioned.dx - haloWidth;
            left = 0;
            top = positioned.dy - target.size.height / 2 - haloHeight;
            bottom = null;
          }
          break;
        case AlignContent.right:
          {
            left = positioned.dx + haloWidth;
            top = positioned.dy - target.size.height / 2 - haloHeight;
            bottom = null;
            weight = MediaQuery.of(context).size.width - left;
          }
          break;
        case AlignContent.custom:
          {
            left = i.customPosition.left;
            top = i.customPosition.top;
            bottom = i.customPosition.bottom;
            weight = MediaQuery.of(context).size.width;
          }
          break;
      }

      return Positioned(
        top: top,
        bottom: bottom,
        left: left,
        child: Container(
          width: weight,
          child: Padding(
            padding: const EdgeInsets.all(20.0),
            child: i.child,
          ),
        ),
      );
    }).toList();

    return Stack(
      children: children,
    );
  }

  Widget _buildSkip() {
    if (widget.hideSkip) {
      return SizedBox.shrink();
    }
    return Align(
      alignment: widget.alignSkip,
      child: SafeArea(
        child: AnimatedOpacity(
          opacity: showContent ? 1 : 0,
          duration: Duration(milliseconds: 300),
          child: InkWell(
            onTap: widget.clickSkip,
            child: Padding(
              padding: const EdgeInsets.all(20.0),
              child: Text(
                widget.textSkip,
                style: widget.textStyleSkip,
                key: textKey
              ),
            ),
          ),
        ),
      ),
    );
  }
  void next() => _focusLightKey?.currentState?.next();
  void previous() => _focusLightKey?.currentState?.previous();
}

is it because of the Coach Mark library creates something like full screen overlay so that i can't identify the widgets? if so what could i do?


Solution

  • i found the problem here.. flutter driver itself is frame synchronyzed, so in this COachMark library i have to wait until there are no pending frames..

    i changed my code from this

    await world.driver.tap(finderHere);
    

    to be something like this (i use runUnsynchronyzed to make sure that tere are no pending frames)

    await world.driver.runUnsynchronized(() async{
        await world.driver.tap(finderHere);
    });