@@ -71,6 +71,10 @@ void main() {
7171 ..appendChild (InspectorTreeNode ()));
7272 }
7373
74+ List <InspectorTreeNode > visibleNodes (InspectorTreeController controller) {
75+ return controller.rowsInTree.value.map ((row) => row! .node).toList ();
76+ }
77+
7478 group ('InspectorTreeController' , () {
7579 testWidgets ('Row with negative index regression test' , (
7680 WidgetTester tester,
@@ -363,5 +367,212 @@ void main() {
363367 expect (capturedNotify, isTrue);
364368 },
365369 );
370+
371+ testWidgets (
372+ 'arrowLeft key collapses selected node without removing previous rows' ,
373+ (WidgetTester tester) async {
374+ final treeController = buildTreeController (
375+ onSelectionChange: ({bool notifyFlutterInspector = false }) {},
376+ );
377+
378+ final root = treeController.root! ;
379+ final previousSibling = root.children.first;
380+ final selectedSibling = root.children.last
381+ ..appendChild (InspectorTreeNode ())
382+ ..appendChild (InspectorTreeNode ());
383+ final selectedSiblingFirstChild = selectedSibling.children.first;
384+ final selectedSiblingSecondChild = selectedSibling.children.last;
385+ treeController.root = root;
386+ treeController.setSelectedNode (selectedSibling);
387+
388+ await pumpInspectorTree (tester, treeController: treeController);
389+
390+ expect (visibleNodes (treeController), [
391+ root,
392+ previousSibling,
393+ selectedSibling,
394+ selectedSiblingFirstChild,
395+ selectedSiblingSecondChild,
396+ ]);
397+
398+ await tester.sendKeyEvent (LogicalKeyboardKey .arrowLeft);
399+ await tester.pump ();
400+
401+ expect (selectedSibling.isExpanded, isFalse);
402+ expect (treeController.selection, selectedSibling);
403+ expect (visibleNodes (treeController), [
404+ root,
405+ previousSibling,
406+ selectedSibling,
407+ ]);
408+ },
409+ );
410+
411+ testWidgets (
412+ 'arrowLeft key on collapsed child selects parent without changing rows' ,
413+ (WidgetTester tester) async {
414+ final treeController = buildTreeController (
415+ onSelectionChange: ({bool notifyFlutterInspector = false }) {},
416+ );
417+
418+ final root = treeController.root! ;
419+ final previousSibling = root.children.first;
420+ final parent = root.children.last
421+ ..appendChild (InspectorTreeNode ())
422+ ..appendChild (InspectorTreeNode ());
423+ final child = parent.children.first..isExpanded = false ;
424+ final nextSibling = parent.children.last;
425+ treeController.root = root;
426+ treeController.setSelectedNode (child);
427+
428+ await pumpInspectorTree (tester, treeController: treeController);
429+
430+ final rowsBeforeArrowLeft = visibleNodes (treeController);
431+ expect (rowsBeforeArrowLeft, [
432+ root,
433+ previousSibling,
434+ parent,
435+ child,
436+ nextSibling,
437+ ]);
438+
439+ await tester.sendKeyEvent (LogicalKeyboardKey .arrowLeft);
440+ await tester.pump ();
441+
442+ expect (treeController.selection, parent);
443+ expect (visibleNodes (treeController), rowsBeforeArrowLeft);
444+ },
445+ );
446+
447+ testWidgets ('arrowLeft key does not put the tree into the loading state' , (
448+ WidgetTester tester,
449+ ) async {
450+ final treeController = buildTreeController (
451+ onSelectionChange: ({bool notifyFlutterInspector = false }) {},
452+ );
453+
454+ await pumpInspectorTree (tester, treeController: treeController);
455+
456+ final root = treeController.root! ;
457+ treeController.setSelectedNode (root);
458+ await tester.pump ();
459+
460+ await tester.sendKeyEvent (LogicalKeyboardKey .arrowLeft);
461+ await tester.pump ();
462+
463+ // After collapsing the root, only the root row remains visible. The
464+ // tree must still render that row instead of a loading indicator.
465+ expect (find.byType (CenteredCircularProgressIndicator ), findsNothing);
466+ expect (visibleNodes (treeController), [root]);
467+ });
468+
469+ testWidgets (
470+ 'onSelectNode does not re-expand collapsed siblings of the clicked node' ,
471+ (WidgetTester tester) async {
472+ // Regression test: clicking a still-visible row used to call
473+ // expandPath on the clicked node, which re-expanded the clicked node
474+ // itself and undid any subtree collapses the user had just performed
475+ // via the arrow-left key.
476+ final treeController = buildTreeController (
477+ onSelectionChange: ({bool notifyFlutterInspector = false }) {},
478+ );
479+
480+ final root = treeController.root! ;
481+ final firstChild = root.children.first
482+ ..appendChild (InspectorTreeNode ())
483+ ..appendChild (InspectorTreeNode ());
484+ final secondChild = root.children.last;
485+ treeController.root = root;
486+ treeController.setSelectedNode (firstChild);
487+
488+ await pumpInspectorTree (tester, treeController: treeController);
489+
490+ // Collapse [firstChild] so its grandchildren are hidden.
491+ await tester.sendKeyEvent (LogicalKeyboardKey .arrowLeft);
492+ await tester.pump ();
493+ expect (firstChild.isExpanded, isFalse);
494+ expect (visibleNodes (treeController), [root, firstChild, secondChild]);
495+
496+ // Selecting another visible row must not re-expand [firstChild].
497+ treeController.onSelectNode (secondChild);
498+ await tester.pump ();
499+
500+ expect (firstChild.isExpanded, isFalse);
501+ expect (visibleNodes (treeController), [root, firstChild, secondChild]);
502+ },
503+ );
504+
505+ testWidgets (
506+ 'onSelectNode does not re-expand a node the user just collapsed by '
507+ 'clicking it' ,
508+ (WidgetTester tester) async {
509+ // Regression test: clicking a row used to call expandPath on the
510+ // clicked node itself, so a user could not select a node in its
511+ // collapsed state.
512+ final treeController = buildTreeController (
513+ onSelectionChange: ({bool notifyFlutterInspector = false }) {},
514+ );
515+
516+ final root = treeController.root! ;
517+ final firstChild = root.children.first
518+ ..appendChild (InspectorTreeNode ())
519+ ..isExpanded = false ;
520+ treeController.root = root;
521+
522+ await pumpInspectorTree (tester, treeController: treeController);
523+
524+ treeController.onSelectNode (firstChild);
525+ await tester.pump ();
526+
527+ expect (treeController.selection, firstChild);
528+ expect (firstChild.isExpanded, isFalse);
529+ },
530+ );
531+ });
532+
533+ group ('InspectorTree loading indicator' , () {
534+ testWidgets (
535+ 'shows a loading indicator while the initial tree load is in progress' ,
536+ (WidgetTester tester) async {
537+ // Before the first inspector tree load completes, a tree with at most
538+ // a single row represents the "still loading" state and should render
539+ // a progress indicator instead of the bare row.
540+ inspectorController.firstInspectorTreeLoadCompleted = false ;
541+
542+ final treeController = InspectorTreeController ()
543+ ..config = InspectorTreeConfig (
544+ onNodeAdded: (_, _) {},
545+ onClientActiveChange: (_) {},
546+ )
547+ ..root = InspectorTreeNode ();
548+
549+ await pumpInspectorTree (tester, treeController: treeController);
550+
551+ expect (find.byType (CenteredCircularProgressIndicator ), findsOneWidget);
552+ },
553+ );
554+
555+ testWidgets (
556+ 'renders a single-row tree (no spinner) after the initial load has '
557+ 'completed' ,
558+ (WidgetTester tester) async {
559+ // Regression test: collapsing the root via the arrow-left key shrinks
560+ // the visible rows down to a single row. Before the fix, the
561+ // [InspectorTree] widget treated a one-row tree as "loading" and
562+ // showed a spinner, hiding the user's [root] row.
563+ inspectorController.firstInspectorTreeLoadCompleted = true ;
564+
565+ final treeController = InspectorTreeController ()
566+ ..config = InspectorTreeConfig (
567+ onNodeAdded: (_, _) {},
568+ onClientActiveChange: (_) {},
569+ )
570+ ..root = InspectorTreeNode ();
571+
572+ await pumpInspectorTree (tester, treeController: treeController);
573+
574+ expect (find.byType (CenteredCircularProgressIndicator ), findsNothing);
575+ },
576+ );
366577 });
367578}
0 commit comments