Flutter Widget 测试:在测试中无法通过语义标签找到 Widget,尽管它存在于转储中

Flutter Widget Testing: Can't Find Widget by Semantics Label in Test, Though it's Present in Dump

正在测试“DaysContainer”。单击日期时,它会被选中。单击测试时工作正常,但无法为其编写测试。这是我的测试:


testWidgets('Days are clickable ',
        (WidgetTester tester) async {
      Finder findDay =
          find.bySemanticsLabel(DateHelper.semanticName(DateTime(2021, 6, 20)));
      Finder findOtherDay =
          find.bySemanticsLabel(DateHelper.semanticName(DateTime(2021, 6, 21)));
      Finder findSelectedDay =
          find.bySemanticsLabel("20 June 2021 Selected");
      Widget wrapper = MultiBlocProvider(providers: [
        BlocProvider<SelectedDateBloc>(create: (context) => SelectedDateBloc()),
        BlocProvider<SelectedIntervalBloc>(
            create: (context) => SelectedIntervalBloc()),
      ],
        child: Directionality(
          textDirection: TextDirection.ltr,
          child: DaysContainer(MonthData(startDate: DateTime(2021, 6))))
      );

      
      await tester.pumpWidget(wrapper);
      expect(findDay, findsOneWidget);
      expect(findOtherDay, findsOneWidget);
      await tester.tap(findDay);
      await tester.pump();
      expect(findOtherDay, findsOneWidget);
      
      expect(findDay, findsNothing);
      debugDumpApp();
      expect(findSelectedDay, findsOneWidget);
      
    });

当我点击测试时小部件工作正常,除了最后的预期之外一切都通过了,它失败并显示没有找到小部件的消息。这很有趣,因为当我在 debugDumpApp() 的输出中搜索“20 June 2021 Selected”(我通过查找器的确切字符串)时,我发现:

 └Text("20", inherit: true, color: MaterialColor(primary value: Color(0xff2196f3)), family: Maven-500, size: 27.0, semanticsLabel: "20 June 2021 Selected", dependencies: [DefaultTextStyle])
                            │                                       │         └Semantics(container: false, properties: SemanticsProperties, label: "20 June 2021 Selected", value: null, hint: null, hintOverrides: null, dependencies: [Directionality], renderObject: RenderSemanticsAnnotations#49cc1 relayoutBoundary=up2)

所以标签似乎存在。

这是当天未找到标签的整个子树,尽管标签存在:

├KeyedSubtree
                            │└AutomaticKeepAlive(state: _AutomaticKeepAliveState#df970(keeping subtree alive, handles: 1 active client))
                            │ └KeepAlive(keepAlive: false)
                            │  └NotificationListener<KeepAliveNotification>
                            │   └IndexedSemantics(index: 21, renderObject: RenderIndexedSemantics#c0550)
                            │    └RepaintBoundary(renderObject: RenderRepaintBoundary#7655d)
                            │     └Day
                            │      └BlocBuilder<SelectedDateBloc, DateTime>(state: _BlocBuilderBaseState<SelectedDateBloc, DateTime>#88248)
                            │       └BlocListener<SelectedDateBloc, DateTime>(state: _BlocListenerBaseState<SelectedDateBloc, DateTime>#85e73)
                            │        └BlocBuilder<SelectedIntervalBloc, IntervalState>(state: _BlocBuilderBaseState<SelectedIntervalBloc, IntervalState>#822c1)
                            │         └BlocListener<SelectedIntervalBloc, IntervalState>(state: _BlocListenerBaseState<SelectedIntervalBloc, IntervalState>#00299)
                            │          └Theme(ThemeData#645d4)
                            │           └_InheritedTheme
                            │            └CupertinoTheme(brightness: light, primaryColor: MaterialColor(primary value: Color(0xff2196f3)), primaryContrastingColor: Color(0xffffffff), scaffoldBackgroundColor: Color(0xfffafafa), actionTextStyle: TextStyle(inherit: false, color: MaterialColor(primary value: Color(0xff2196f3)), family: .SF Pro Text, size: 17.0, letterSpacing: -0.4, decoration: TextDecoration.none), navActionTextStyle: TextStyle(inherit: false, color: MaterialColor(primary value: Color(0xff2196f3)), family: .SF Pro Text, size: 17.0, letterSpacing: -0.4, decoration: TextDecoration.none))
                            │             └_InheritedCupertinoTheme
                            │              └IconTheme(color: MaterialColor(primary value: Color(0xff2196f3)))
                            │               └IconTheme(color: Color(0xdd000000))
                            │                └Material(type: canvas, color: MaterialColor(primary value: Color(0xff2196f3)), dependencies: [_InheritedTheme], state: _MaterialState#d72d9(tickers: tracking 2 tickers))
                            │                 └AnimatedPhysicalModel(duration: 200ms, shape: rectangle, borderRadius: BorderRadius.zero, elevation: 0.0, color: MaterialColor(primary value: Color(0xff2196f3)), animateColor: false, shadowColor: Color(0xff000000), animateShadowColor: true, state: _AnimatedPhysicalModelState#c95b8(ticker inactive))
                            │                  └PhysicalModel(shape: rectangle, borderRadius: BorderRadius.zero, elevation: 0.0, color: MaterialColor(primary value: Color(0xff2196f3)), shadowColor: Color(0xff000000), renderObject: RenderPhysicalModel#e9b7d)
                            │                   └NotificationListener<LayoutChangedNotification>
                            │                    └_InkFeatures-[GlobalKey#74a8e ink renderer](renderObject: _RenderInkFeatures#06556)
                            │                     └AnimatedDefaultTextStyle(duration: 200ms, debugLabel: (englishLike body1 2014).merge(blackMountainView bodyText2), inherit: false, color: Color(0xdd000000), family: Roboto, size: 14.0, weight: 400, baseline: alphabetic, decoration: TextDecoration.none, softWrap: wrapping at box width, overflow: clip, state: _AnimatedDefaultTextStyleState#f1c50(ticker inactive))
                            │                      └DefaultTextStyle(debugLabel: (englishLike body1 2014).merge(blackMountainView bodyText2), inherit: false, color: Color(0xdd000000), family: Roboto, size: 14.0, weight: 400, baseline: alphabetic, decoration: TextDecoration.none, softWrap: wrapping at box width, overflow: clip)
                            │                       └Container
                            │                        └InkWell
                            │                         └_InkResponseStateWidget(gestures: [tap], mouseCursor: null, clipped to BoxShape.rectangle, dependencies: [Directionality, _InheritedTheme], state: _InkResponseState#3829f)
                            │                          └_ParentInkResponseProvider
                            │                           └Actions(dispatcher: null, actions: {ActivateIntent: CallbackAction<ActivateIntent>#e99b1, ButtonActivateIntent: CallbackAction<ButtonActivateIntent>#6dc7a}, state: _ActionsState#68d98)
                            │                            └_ActionsMarker
                            │                             └Focus(state: _FocusState#092e0)
                            │                              └_FocusMarker
                            │                               └Semantics(container: false, properties: SemanticsProperties, label: null, value: null, hint: null, hintOverrides: null, renderObject: RenderSemanticsAnnotations#b8d20)
                            │                                └MouseRegion(listeners: [enter, exit], cursor: SystemMouseCursor(click), state: _MouseRegionState#fb175)
                            │                                 └_RawMouseRegion(renderObject: RenderMouseRegion#a08f1)
                            │                                  └Semantics(container: false, properties: SemanticsProperties, label: null, value: null, hint: null, hintOverrides: null, renderObject: RenderSemanticsAnnotations#132eb)
                            │                                   └GestureDetector(startBehavior: start)
                            │                                    └RawGestureDetector(state: RawGestureDetectorState#ffca9(gestures: [tap], excludeFromSemantics: true, behavior: opaque))
                            │                                     └Listener(listeners: [down], behavior: opaque, renderObject: RenderPointerListener#85889)
                            │                                      └Stack(alignment: Alignment.center, fit: loose, dependencies: [Directionality], renderObject: RenderStack#90c1c)
                            │                                       ├Positioned
                            │                                       │└Center(alignment: Alignment.center, dependencies: [Directionality], renderObject: RenderPositionedBox#04e9c relayoutBoundary=up1)
                            │                                       │ └SelectedCircle(dependencies: [_InheritedTheme])
                            │                                       │  └Container(bg: BoxDecoration(color: Color(0xffdadbdc), border: Border.all(BorderSide(Color(0xff000000), 0.0, BorderStyle.none)), borderRadius: BorderRadius.circular(71.4)), constraints: BoxConstraints(w=71.4, h=71.4))
                            │                                       │   └ConstrainedBox(BoxConstraints(w=71.4, h=71.4), renderObject: RenderConstrainedBox#e92d5 relayoutBoundary=up2)
                            │                                       │    └DecoratedBox(bg: BoxDecoration(color: Color(0xffdadbdc), border: Border.all(BorderSide(Color(0xff000000), 0.0, BorderStyle.none)), borderRadius: BorderRadius.circular(71.4)), dependencies: [Directionality], renderObject: RenderDecoratedBox#fde37)
                            │                                       │     └Padding(padding: EdgeInsets.zero, dependencies: [Directionality], renderObject: RenderPadding#9721c)
                            │                                       │      └Center(alignment: Alignment.center, dependencies: [Directionality], renderObject: RenderPositionedBox#5dbba)
                            │                                       │       └Column(direction: vertical, mainAxisAlignment: start, mainAxisSize: min, crossAxisAlignment: center, renderObject: RenderFlex#e8cc5 relayoutBoundary=up1)
                            │                                       │        ├Container(constraints: BoxConstraints(0.0<=w<=Infinity, h=20.0))
                            │                                       │        │└ConstrainedBox(BoxConstraints(0.0<=w<=Infinity, h=20.0), renderObject: RenderConstrainedBox#970ba relayoutBoundary=up2)
                            │                                       │        │ └Stack(alignment: Alignment.topCenter, fit: loose, clipBehavior: none, dependencies: [Directionality], renderObject: RenderStack#888b5 relayoutBoundary=up3)
                            │                                       │        │  └Positioned(bottom: -2.0)
                            │                                       │        │   └Text("Jun", inherit: true, color: MaterialColor(primary value: Color(0xff2196f3)), family: Maven-700, size: 20.0, dependencies: [DefaultTextStyle])
                            │                                       │        │    └RichText(softWrap: wrapping at box width, maxLines: unlimited, text: "Jun", dependencies: [Directionality], renderObject: RenderParagraph#0b9b9 relayoutBoundary=up4)
                            │                                       │        └Text("20", inherit: true, color: MaterialColor(primary value: Color(0xff2196f3)), family: Maven-500, size: 27.0, semanticsLabel: "20 June 2021 Selected", dependencies: [DefaultTextStyle])
                            │                                       │         └Semantics(container: false, properties: SemanticsProperties, label: "20 June 2021 Selected", value: null, hint: null, hintOverrides: null, dependencies: [Directionality], renderObject: RenderSemanticsAnnotations#49cc1 relayoutBoundary=up2)
                            │                                       │          └ExcludeSemantics(excluding: true, renderObject: RenderExcludeSemantics#252f6 relayoutBoundary=up3)
                            │                                       │           └RichText(softWrap: wrapping at box width, maxLines: unlimited, text: "20", dependencies: [Directionality], renderObject: RenderParagraph#9870d relayoutBoundary=up4)
                            │                                       └Positioned
                            │                                        └Container
                            │                                         └LimitedBox(maxWidth: 0.0, maxHeight: 0.0, renderObject: RenderLimitedBox#62493 relayoutBoundary=up1)
                            │                                          └ConstrainedBox(BoxConstraints(biggest), renderObject: RenderConstrainedBox#1168b relayoutBoundary=up2)

为了比较,这里是测试中提到的“the other day”的子树,测试正在其中找到语义标签:

├KeyedSubtree
                            │└AutomaticKeepAlive(state: _AutomaticKeepAliveState#bdfc2(handles: no notifications ever received))
                            │ └KeepAlive(keepAlive: false)
                            │  └NotificationListener<KeepAliveNotification>
                            │   └IndexedSemantics(index: 22, renderObject: RenderIndexedSemantics#47a09)
                            │    └RepaintBoundary(renderObject: RenderRepaintBoundary#1fd7b)
                            │     └Day
                            │      └BlocBuilder<SelectedDateBloc, DateTime>(state: _BlocBuilderBaseState<SelectedDateBloc, DateTime>#fbd45)
                            │       └BlocListener<SelectedDateBloc, DateTime>(state: _BlocListenerBaseState<SelectedDateBloc, DateTime>#5921f)
                            │        └BlocBuilder<SelectedIntervalBloc, IntervalState>(state: _BlocBuilderBaseState<SelectedIntervalBloc, IntervalState>#b6ec0)
                            │         └BlocListener<SelectedIntervalBloc, IntervalState>(state: _BlocListenerBaseState<SelectedIntervalBloc, IntervalState>#403c1)
                            │          └Theme(ThemeData#645d4)
                            │           └_InheritedTheme
                            │            └CupertinoTheme(brightness: light, primaryColor: MaterialColor(primary value: Color(0xff2196f3)), primaryContrastingColor: Color(0xffffffff), scaffoldBackgroundColor: Color(0xfffafafa), actionTextStyle: TextStyle(inherit: false, color: MaterialColor(primary value: Color(0xff2196f3)), family: .SF Pro Text, size: 17.0, letterSpacing: -0.4, decoration: TextDecoration.none), navActionTextStyle: TextStyle(inherit: false, color: MaterialColor(primary value: Color(0xff2196f3)), family: .SF Pro Text, size: 17.0, letterSpacing: -0.4, decoration: TextDecoration.none))
                            │             └_InheritedCupertinoTheme
                            │              └IconTheme(color: MaterialColor(primary value: Color(0xff2196f3)))
                            │               └IconTheme(color: Color(0xdd000000))
                            │                └Material(type: canvas, color: MaterialColor(primary value: Color(0xff2196f3)), dependencies: [_InheritedTheme], state: _MaterialState#d1e21)
                            │                 └AnimatedPhysicalModel(duration: 200ms, shape: rectangle, borderRadius: BorderRadius.zero, elevation: 0.0, color: MaterialColor(primary value: Color(0xff2196f3)), animateColor: false, shadowColor: Color(0xff000000), animateShadowColor: true, state: _AnimatedPhysicalModelState#86b21(ticker inactive))
                            │                  └PhysicalModel(shape: rectangle, borderRadius: BorderRadius.zero, elevation: 0.0, color: MaterialColor(primary value: Color(0xff2196f3)), shadowColor: Color(0xff000000), renderObject: RenderPhysicalModel#bef63)
                            │                   └NotificationListener<LayoutChangedNotification>
                            │                    └_InkFeatures-[GlobalKey#26e0b ink renderer](renderObject: _RenderInkFeatures#9766c)
                            │                     └AnimatedDefaultTextStyle(duration: 200ms, debugLabel: (englishLike body1 2014).merge(blackMountainView bodyText2), inherit: false, color: Color(0xdd000000), family: Roboto, size: 14.0, weight: 400, baseline: alphabetic, decoration: TextDecoration.none, softWrap: wrapping at box width, overflow: clip, state: _AnimatedDefaultTextStyleState#45f1f(ticker inactive))
                            │                      └DefaultTextStyle(debugLabel: (englishLike body1 2014).merge(blackMountainView bodyText2), inherit: false, color: Color(0xdd000000), family: Roboto, size: 14.0, weight: 400, baseline: alphabetic, decoration: TextDecoration.none, softWrap: wrapping at box width, overflow: clip)
                            │                       └Container
                            │                        └InkWell
                            │                         └_InkResponseStateWidget(gestures: [tap], mouseCursor: null, clipped to BoxShape.rectangle, state: _InkResponseState#54ed4)
                            │                          └_ParentInkResponseProvider
                            │                           └Actions(dispatcher: null, actions: {ActivateIntent: CallbackAction<ActivateIntent>#79fe6, ButtonActivateIntent: CallbackAction<ButtonActivateIntent>#20143}, state: _ActionsState#19552)
                            │                            └_ActionsMarker
                            │                             └Focus(state: _FocusState#761fa)
                            │                              └_FocusMarker
                            │                               └Semantics(container: false, properties: SemanticsProperties, label: null, value: null, hint: null, hintOverrides: null, renderObject: RenderSemanticsAnnotations#a7425)
                            │                                └MouseRegion(listeners: [enter, exit], cursor: SystemMouseCursor(click), state: _MouseRegionState#fe85d)
                            │                                 └_RawMouseRegion(renderObject: RenderMouseRegion#7ec13)
                            │                                  └Semantics(container: false, properties: SemanticsProperties, label: null, value: null, hint: null, hintOverrides: null, renderObject: RenderSemanticsAnnotations#ee1e2)
                            │                                   └GestureDetector(startBehavior: start)
                            │                                    └RawGestureDetector(state: RawGestureDetectorState#c4d93(gestures: [tap], excludeFromSemantics: true, behavior: opaque))
                            │                                     └Listener(listeners: [down], behavior: opaque, renderObject: RenderPointerListener#0304e)
                            │                                      └Stack(alignment: Alignment.center, fit: loose, dependencies: [Directionality], renderObject: RenderStack#9dc90)
                            │                                       ├Positioned
                            │                                       │└Center(alignment: Alignment.center, dependencies: [Directionality], renderObject: RenderPositionedBox#524d5 relayoutBoundary=up1)
                            │                                       │ └Column(direction: vertical, mainAxisAlignment: center, crossAxisAlignment: center, renderObject: RenderFlex#80171 relayoutBoundary=up2)
                            │                                       │  └Text("21", inherit: true, color: Color(0xffdadbdc), family: Maven-300, size: 27.0, semanticsLabel: "21 June 2021", dependencies: [DefaultTextStyle])
                            │                                       │   └Semantics(container: false, properties: SemanticsProperties, label: "21 June 2021", value: null, hint: null, hintOverrides: null, dependencies: [Directionality], renderObject: RenderSemanticsAnnotations#4553d relayoutBoundary=up3)
                            │                                       │    └ExcludeSemantics(excluding: true, renderObject: RenderExcludeSemantics#4c3a9 relayoutBoundary=up4)
                            │                                       │     └RichText(softWrap: wrapping at box width, maxLines: unlimited, text: "21", dependencies: [Directionality], renderObject: RenderParagraph#66a01 relayoutBoundary=up5)
                            │                                       └Positioned
                            │                                        └Container
                            │                                         └LimitedBox(maxWidth: 0.0, maxHeight: 0.0, renderObject: RenderLimitedBox#8ee5b relayoutBoundary=up1)
                            │                                          └ConstrainedBox(BoxConstraints(biggest), renderObject: RenderConstrainedBox#8ae45 relayoutBoundary=up2)

这是包含的子树:

[root](renderObject: RenderView#cafff)
└MultiBlocProvider
 └_NestedHook
  └BlocProvider<SelectedDateBloc>
   └InheritedProvider<SelectedDateBloc>(value: Instance of 'SelectedDateBloc', listening to value)
    └_InheritedProviderScope<SelectedDateBloc>(value: Instance of 'SelectedDateBloc', listening to value)
     └_NestedHook
      └BlocProvider<SelectedIntervalBloc>
       └InheritedProvider<SelectedIntervalBloc>(value: Instance of 'SelectedIntervalBloc', listening to value)
        └_InheritedProviderScope<SelectedIntervalBloc>(value: Instance of 'SelectedIntervalBloc', listening to value)
         └Directionality(textDirection: ltr)
          └DaysContainer
           └GridView(scrollDirection: vertical, primary: using primary controller, NeverScrollableScrollPhysics, shrinkWrap: shrink-wrapping)
            └Scrollable(axisDirection: down, physics: NeverScrollableScrollPhysics, restorationId: null, state: ScrollableState#3e2d3(position: ScrollPositionWithSingleContext#7eb89(offset: 0.0, range: 0.0..0.0, viewport: 600.0, ScrollableState, NeverScrollableScrollPhysics -> ClampingScrollPhysics -> RangeMaintainingScrollPhysics, IdleScrollActivity#94415, ScrollDirection.idle), effective physics: NeverScrollableScrollPhysics -> ClampingScrollPhysics -> RangeMaintainingScrollPhysics))
             └GlowingOverscrollIndicator(axisDirection: down, show: both sides, Color(0xffffffff), state: _GlowingOverscrollIndicatorState#e8bae(tickers: tracking 4 tickers))
              └NotificationListener<ScrollNotification>
               └RepaintBoundary(renderObject: RenderRepaintBoundary#24771)
                └CustomPaint(renderObject: RenderCustomPaint#9cfc1)
                 └RepaintBoundary(renderObject: RenderRepaintBoundary#56689)
                  └_ScrollSemantics-[GlobalKey#4792d](renderObject: _RenderScrollSemantics#0fcb6)
                   └_ScrollableScope
                    └Listener(listeners: [signal], behavior: deferToChild, renderObject: RenderPointerListener#519c9)
                     └RawGestureDetector-[LabeledGlobalKey<RawGestureDetectorState>#f52e9](state: RawGestureDetectorState#288f7(gestures: <none>, behavior: opaque))
                      └_GestureSemantics(renderObject: RenderSemanticsGestureHandler#4018a)
                       └Listener(listeners: [down], behavior: opaque, renderObject: RenderPointerListener#e6a2d)
                        └Semantics(container: false, properties: SemanticsProperties, label: null, value: null, hint: null, hintOverrides: null, renderObject: RenderSemanticsAnnotations#f5a78)
                         └IgnorePointer-[GlobalKey#ba652](ignoring: false, ignoringSemantics: false, renderObject: RenderIgnorePointer#ca4b4)
                          └ShrinkWrappingViewport(axisDirection: down, offset: ScrollPositionWithSingleContext#7eb89(offset: 0.0, range: 0.0..0.0, viewport: 600.0, ScrollableState, NeverScrollableScrollPhysics -> ClampingScrollPhysics -> RangeMaintainingScrollPhysics, IdleScrollActivity#94415, ScrollDirection.idle), dependencies: [Directionality], renderObject: RenderShrinkWrappingViewport#31d07)
                           └SliverGrid(delegate: SliverChildListDelegate#5b367(estimated child count: 28), renderObject: RenderSliverGrid#18f22 relayoutBoundary=up1)

这是完整的错误:


══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK ╞════════════════════════════════════════════════════
The following TestFailure object was thrown running a test:
  Expected: exactly one matching node in the widget tree
  Actual: _ElementPredicateFinder:<zero widgets with element matching predicate (Closure: (Element)
=> bool) (ignoring offstage widgets)>
   Which: means none were found but one was expected

When the exception was thrown, this was the stack:
#4      main.<anonymous closure>.<anonymous closure> (file:///myTestPath:46:7)
<asynchronous suspension>
<asynchronous suspension>
(elided one frame from package:stack_trace)
...

This was caught by the test expectation on the following line:
  file:///myTestPath line 46
The test description was:
  Days are clickable
════════════════════════════════════════════════════════════════════════════════════════════════════
00:09 +0 -1: /myTestPath: DaysContainer Days are clickable  [E]
  Test failed. See exception logs above.
  The test description was: Days are clickable 
  
00:09 +0 -1: Some tests failed.                                                                                               

我写了一个测试来找到自己渲染的SelectedCircle。此测试通过。

来自docs:“框架可能会在某些情况下组合语义标签,例如当多个文本小部件位于一个 MaterialButton 小部件中时。在这种情况下,最好通过正则表达式进行匹配."

在这种情况下,所选圆有一个上标。

将finder中的字符串替换为正则表达式,测试通过

      Finder findSelectedDay = find.bySemanticsLabel(RegExp(r"20 June 2021 Selected"));

除了现有的答案之外,如果您使用 Semantics() 作为容器小部件,那么您应该在语义中使用 container: true,如下面的代码。

Semantics(
    container: true,
    child: /// put the child widget
)