🤯

TextFieldが含まれているCheckboxListTileのWidgetテストでハマった話

2025/03/04に公開

二つのListTileの動作
二つの CheckboxListTile の動作

このようなCheckboxListTileウィジェットがあったとします。

一つはチェックボックスとテキストがあるシンプルなものです。

もう一つはチェックボックスをオンにするとTextFieldが有効になり、テキストが入力できる仕様です。
アンケートフォームのような画面で、"その他"を選択するとフリーテキストで入力できるようになるイメージです。

https://dartpad.dev/?run=true&id=60166109704733995b2307b2bdd66b55

ではこのCheckboxListTileをタップした時、ちゃんとチェックボックスがオンオフに変化するかテストしてみましょう。

遭遇した問題

まず一つ目のシンプルなCheckboxListTileのテストは以下のように書けます。
テストは成功します。

testWidgets("CheckboxListTileがオンオフできること", (tester) async {
  await tester.pumpWidget(MyApp());

  final tileFinder = find.byKey(ValueKey("checkbox_list_tile"));

  // 初期値はfalse
  expect(tester.widget<CheckboxListTile>(tileFinder).value, isFalse);

  // タップ
  await tester.tap(tileFinder);
  await tester.pumpAndSettle();

  // 値がtrueになる
  expect(tester.widget<CheckboxListTile>(tileFinder).value, isTrue);

  // タップ
  await tester.tap(tileFinder);
  await tester.pumpAndSettle();

  // 値がfalseになる
  expect(tester.widget<CheckboxListTile>(tileFinder).value, isFalse);
});

続いてTextFieldが含まれているCheckboxListTileのテストを書いてみましょう。
なんということでしょう。チェックボックスをオフにする操作をしてもチェックボックスの値が変わらず、テストが失敗します。

testWidgets("TextField付きのCheckboxListTileがオンオフできること", (tester) async {
  await tester.pumpWidget(MyApp());

  final tileFinder = find.byKey(
    ValueKey("checkbox_list_tile_with_text_field"),
  );

  // 初期値はfalse
  expect(tester.widget<CheckboxListTile>(tileFinder).value, isFalse);

  // タップ
  await tester.tap(tileFinder);
  await tester.pumpAndSettle();

  // 値がtrueになる
  expect(tester.widget<CheckboxListTile>(tileFinder).value, isTrue);

  // タップ
  await tester.tap(tileFinder);
  await tester.pumpAndSettle();

  // **値がfalseにならない**
  expect(tester.widget<CheckboxListTile>(tileFinder).value, isFalse);
});

解法

タップすべきなのはCheckboxListTileではなく、その中に含まれるCheckboxのウィジェットです。
find.descendantを使って、CheckboxListTileの子のCheckboxを見つけてタップするようにしましょう。

testWidgets("TextField付きのCheckboxListTileがオンオフできること", (tester) async {
  await tester.pumpWidget(MyApp());

  final tileFinder = find.byKey(
    ValueKey("checkbox_list_tile_with_text_field"),
  );

+  final checkboxFinder = find.descendant(
+    of: tileFinder,
+    matching: find.byType(Checkbox),
+  );

  // 初期値はfalse
  expect(tester.widget<CheckboxListTile>(tileFinder).value, isFalse);

  // タップ
-  await tester.tap(tileFinder);
+  await tester.tap(checkboxFinder);
  await tester.pumpAndSettle();

  // 値がtrueになる
  expect(tester.widget<CheckboxListTile>(tileFinder).value, isTrue);

  // タップ
-  await tester.tap(tileFinder);
+  await tester.tap(checkboxFinder);
  await tester.pumpAndSettle();

  // 値がfalseになる
  expect(tester.widget<CheckboxListTile>(tileFinder).value, isFalse);
});

原因

どうやらCheckboxListTileをタップしているつもりでしたが、その中のTextFieldをタップしている扱いとなっているようです。
FocusNodeをセットしてみると、TextFieldがフォーカスされていることがわかります。

ちゃんと目的のウィジェットを明示してタップするようにしましょう。(半日吹き飛びました)

GitHubで編集を提案

Discussion