FlutterWidgetTestにおいてIndexedStackのような画面に描画されていないがOffstageには存在する要素の確認をする
はじめに
個人開発しているLazyLoadIndexedStack
というFlutterパッケージに新機能を追加して新バージョンをリリースした。
その際にテストを修正する過程で、IndexedStack
のchildren内に期待した要素が存在するかどうかの確認をしたかった。通常の画面であればIndexedStack
のchildren内にある要素のうち、指定したindex以外の要素は画面に描画されておらず、WidgetTestのFinder
を使ったexpect(find.text('expected text', findsOneWidget);
のようなテストはパスしない。
この記事では、画面に描画はされていないが存在は確認したいような場合のテストをどのように記述するかを残しておく。
テスト方法
結論としては、Finder
のメソッドの引数にskipOffstage: false
を渡す。
expect(find.text('expected text', skipOffstage: false), findsOneWidget);
以下、深掘りしていく。
skipOffstage
という引数について、Finder
のソースコードには以下のようにコメントされている。
If the
skipOffstage
argument is true (the default), then this skips nodes that are [Offstage] or that are from inactive [Route]s.
ref: https://github.com/flutter/flutter/blob/d0482116e77994122a7e8596f4a6b7078f08e190/packages/flutter_test/lib/src/finders.dart#L80C1-L81C68(拙訳)
skipOffstage
引数がtrueの場合(デフォルト)、これはOffstage
であるノードや、非アクティブなRoute
からのノードをスキップします。
Offstage
を知らなかったので調べてみた。
A widget that lays the child out as if it was in the tree, but without painting anything, without making the child available for hit testing, and without taking any room in the parent.
Offstage children are still active: they can receive focus and have keyboard input directed to them.
Animations continue to run in offstage children, and therefore use battery and CPU time, regardless of whether the animations end up being visible.
Offstage can be used to measure the dimensions of a widget without bringing it on screen (yet). To hide a widget from view while it is not needed, prefer removing the widget from the tree entirely rather than keeping it alive in an Offstage subtree.
要するに画面上には描画されていないが出番待ちの状態にあるWidgetがOffstage
に格納されており、テストの際はおそらくパフォーマンスの向上のために走査対象からスキップしているのだろう。
というわけでskipOffstage: false
というフラグを引数として渡すことで、Offstage
内にある要素もテストで確認できるということのようだ。
ちなみにこの引数に気づく前は以下のようなテストを書いていた。対象のIndexedStack
を見つけて直接childrenを見るやり方でこれはこれで明示的で悪くないが、記述量が多くて見通しが悪い。
final indexedStack = find.byType(IndexedStack);
expect(indexedStack, findsOneWidget);
final IndexedStack indexedStackWidget = tester.widget(indexedStack) as IndexedStack;
final children = indexedStackWidget.children;
expect(find.text('element1'), findsOneWidget);
bool hasElement2 = children.any((Widget widget) {
return widget is Center && widget.child is Text && (widget.child as Text).data == 'element2';
});
expect(hasElement2, isTrue);
おわり
FlutterのWidgetはまだまだ知らないことだらけだ。