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)
These are methods i tried to locate them with no luck:
find.byType('Text')
find.text('OKE')
find.byType('RichText')
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)
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,
);
}
}
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;
}
}
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?
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);
});