From f190d6259f782b99ef02b2d206f68d1fad884800 Mon Sep 17 00:00:00 2001 From: Kate Lovett Date: Wed, 14 Feb 2024 14:48:36 -0600 Subject: [PATCH] Fix and test SemanticsController.simulatedAccessibilityTraversal (#143386) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes https://github.com/flutter/flutter/issues/143173 The `start` and `end` parameters of `SemanticsController.simulatedAccessibilityTraversal` were deprecated in https://github.com/flutter/flutter/issues/112413, but no tests were added that verified the new API. 😳 This change - fixes a typo in an error message - fixes the new `startNode` and `endNode` not being accounted for in setting the traversal range - adds dart fixes for the deprecations - adds tests for the new API that is meant to replace the deprecated one. - Filed https://github.com/flutter/flutter/issues/143405 to follow up on the new API not working in multiple views. --- .../fix_semantics_controller.yaml | 79 +++++++++++++ packages/flutter_test/lib/src/controller.dart | 77 ++++++++----- .../flutter_test/test/controller_test.dart | 104 ++++++++++++++++++ .../flutter_test/semantics_controller.dart | 19 ++++ .../semantics_controller.dart.expect | 19 ++++ 5 files changed, 270 insertions(+), 28 deletions(-) create mode 100644 packages/flutter_test/lib/fix_data/fix_flutter_test/fix_semantics_controller.yaml create mode 100644 packages/flutter_test/test_fixes/flutter_test/semantics_controller.dart create mode 100644 packages/flutter_test/test_fixes/flutter_test/semantics_controller.dart.expect diff --git a/packages/flutter_test/lib/fix_data/fix_flutter_test/fix_semantics_controller.yaml b/packages/flutter_test/lib/fix_data/fix_flutter_test/fix_semantics_controller.yaml new file mode 100644 index 000000000000..6ad37c5d7d25 --- /dev/null +++ b/packages/flutter_test/lib/fix_data/fix_flutter_test/fix_semantics_controller.yaml @@ -0,0 +1,79 @@ +# Copyright 2014 The Flutter Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +# For details regarding the *Flutter Fix* feature, see +# https://flutter.dev/docs/development/tools/flutter-fix + +# Please add new fixes to the top of the file, separated by one blank line +# from other fixes. In a comment, include a link to the PR where the change +# requiring the fix was made. + +# Every fix must be tested. See the +# flutter/packages/flutter_test/test_fixes/README.md file for instructions +# on testing these data driven fixes. + +# For documentation about this file format, see +# https://dart.dev/go/data-driven-fixes. + +# * Fixes in this file are for the flutter_test/controller.dart file. * + +version: 1 +transforms: + # Changes made in TBD + - title: "Migrate to startNode and endNode." + date: 2024-02-13 + element: + uris: [ 'flutter_test.dart' ] + method: simulatedAccessibilityTraversal + inClass: SemanticsController + oneOf: + - if: "start != '' && end != ''" + changes: + - kind: 'addParameter' + index: 2 + name: 'startNode' + style: optional_named + argumentValue: + expression: '{% start %}' + requiredIf: "start != '' && end != ''" + - kind: 'addParameter' + index: 3 + name: 'endNode' + style: optional_named + argumentValue: + expression: '{% end %}' + requiredIf: "start != '' && end != ''" + - kind: 'removeParameter' + name: 'start' + - kind: 'removeParameter' + name: 'end' + - if: "start != '' && end == ''" + changes: + - kind: 'addParameter' + index: 2 + name: 'startNode' + style: optional_named + argumentValue: + expression: '{% start %}' + requiredIf: "start != '' && end == ''" + - kind: 'removeParameter' + name: 'start' + - if: "start == '' && end != ''" + changes: + - kind: 'addParameter' + index: 2 + name: 'endNode' + style: optional_named + argumentValue: + expression: '{% end %}' + requiredIf: "start == '' && end != ''" + - kind: 'removeParameter' + name: 'end' + variables: + start: + kind: 'fragment' + value: 'arguments[start]' + end: + kind: 'fragment' + value: 'arguments[end]' diff --git a/packages/flutter_test/lib/src/controller.dart b/packages/flutter_test/lib/src/controller.dart index 602a1b94b698..5a3cedd87a0d 100644 --- a/packages/flutter_test/lib/src/controller.dart +++ b/packages/flutter_test/lib/src/controller.dart @@ -183,8 +183,14 @@ class SemanticsController { FlutterView? view, }) { TestAsyncUtils.guardSync(); - assert(start == null || startNode == null, 'Cannot provide both start and startNode. Prefer startNode as start is deprecated.'); - assert(end == null || endNode == null, 'Cannot provide both end and endNode. Prefer endNode as end is deprecated.'); + assert( + start == null || startNode == null, + 'Cannot provide both start and startNode. Prefer startNode as start is deprecated.', + ); + assert( + end == null || endNode == null, + 'Cannot provide both end and endNode. Prefer endNode as end is deprecated.', + ); FlutterView? startView; if (start != null) { @@ -197,8 +203,7 @@ class SemanticsController { 'Specified view: $view' ); } - } - if (startNode != null) { + } else if (startNode != null) { final SemanticsOwner owner = startNode.evaluate().single.owner!; final RenderView renderView = _controller.binding.renderViews.firstWhere( (RenderView render) => render.owner!.semanticsOwner == owner, @@ -206,9 +211,9 @@ class SemanticsController { startView = renderView.flutterView; if (view != null && startView != view) { throw StateError( - 'The end node is not part of the provided view.\n' + 'The start node is not part of the provided view.\n' 'Finder: ${startNode.toString(describeSelf: true)}\n' - 'View of end node: $startView\n' + 'View of start node: $startView\n' 'Specified view: $view' ); } @@ -225,8 +230,7 @@ class SemanticsController { 'Specified view: $view' ); } - } - if (endNode != null) { + } else if (endNode != null) { final SemanticsOwner owner = endNode.evaluate().single.owner!; final RenderView renderView = _controller.binding.renderViews.firstWhere( (RenderView render) => render.owner!.semanticsOwner == owner, @@ -261,31 +265,48 @@ class SemanticsController { traversal, ); - int startIndex = 0; - int endIndex = traversal.length - 1; + // Setting the range + SemanticsNode? node; + String? errorString; + int startIndex; if (start != null) { - final SemanticsNode startNode = find(start); - startIndex = traversal.indexOf(startNode); - if (startIndex == -1) { - throw StateError( - 'The expected starting node was not found.\n' - 'Finder: ${start.toString(describeSelf: true)}\n\n' - 'Expected Start Node: $startNode\n\n' - 'Traversal: [\n ${traversal.join('\n ')}\n]'); - } + node = find(start); + startIndex = traversal.indexOf(node); + errorString = start.toString(describeSelf: true); + } else if (startNode != null) { + node = startNode.evaluate().single; + startIndex = traversal.indexOf(node); + errorString = startNode.toString(describeSelf: true); + } else { + startIndex = 0; + } + if (startIndex == -1) { + throw StateError( + 'The expected starting node was not found.\n' + 'Finder: $errorString\n\n' + 'Expected Start Node: $node\n\n' + 'Traversal: [\n ${traversal.join('\n ')}\n]'); } + int endIndex; if (end != null) { - final SemanticsNode endNode = find(end); - endIndex = traversal.indexOf(endNode); - if (endIndex == -1) { - throw StateError( - 'The expected ending node was not found.\n' - 'Finder: ${end.toString(describeSelf: true)}\n\n' - 'Expected End Node: $endNode\n\n' - 'Traversal: [\n ${traversal.join('\n ')}\n]'); - } + node = find(end); + endIndex = traversal.indexOf(node); + errorString = end.toString(describeSelf: true); + } else if (endNode != null) { + node = endNode.evaluate().single; + endIndex = traversal.indexOf(node); + errorString = endNode.toString(describeSelf: true); + } else { + endIndex = traversal.length - 1; + } + if (endIndex == -1) { + throw StateError( + 'The expected ending node was not found.\n' + 'Finder: $errorString\n\n' + 'Expected End Node: $node\n\n' + 'Traversal: [\n ${traversal.join('\n ')}\n]'); } return traversal.getRange(startIndex, endIndex + 1); diff --git a/packages/flutter_test/test/controller_test.dart b/packages/flutter_test/test/controller_test.dart index 40c4d1c26e6e..4b08e5b06cfd 100644 --- a/packages/flutter_test/test/controller_test.dart +++ b/packages/flutter_test/test/controller_test.dart @@ -921,6 +921,30 @@ void main() { orderedEquals(expectedMatchers)); }); + testWidgets('starts traversal at semantics node for `startNode`', (WidgetTester tester) async { + await tester.pumpWidget(MaterialApp( + home: Center( + child: Column( + children: [ + for (int c = 0; c < 5; c++) + Semantics(container: true, child: Text('Child$c')), + ] + ), + ), + )); + expect( + tester.semantics.simulatedAccessibilityTraversal( + startNode: find.semantics.byLabel('Child1'), + ).map((SemanticsNode node) => node.label), + [ + 'Child1', + 'Child2', + 'Child3', + 'Child4', + ], + ); + }); + testWidgets('throws StateError if `start` not found in traversal', (WidgetTester tester) async { await tester.pumpWidget(const MaterialApp(home: _SemanticsTestWidget())); @@ -932,6 +956,23 @@ void main() { ); }); + testWidgets('throws StateError if `startNode` not found in traversal', (WidgetTester tester) async { + await tester.pumpWidget(MaterialApp( + home: Center( + child: Column( + children: [ + for (int c = 0; c < 5; c++) + Semantics(container: true, child: Text('Child$c')), + ] + ), + ), + )); + expect( + () => tester.semantics.simulatedAccessibilityTraversal(startNode: find.semantics.byLabel('Child20')), + throwsA(isA()), + ); + }); + testWidgets('ends traversal at semantics node for `end`', (WidgetTester tester) async { await tester.pumpWidget(const MaterialApp(home: _SemanticsTestWidget())); @@ -943,6 +984,28 @@ void main() { orderedEquals(expectedMatchers)); }); + testWidgets('ends traversal at semantics node for `endNode`', (WidgetTester tester) async { + await tester.pumpWidget(MaterialApp( + home: Center( + child: Column( + children: [ + for (int c = 0; c < 5; c++) + Semantics(container: true, child: Text('Child$c')), + ] + ), + ), + )); + expect( + tester.semantics.simulatedAccessibilityTraversal( + endNode: find.semantics.byLabel('Child1'), + ).map((SemanticsNode node) => node.label), + [ + 'Child0', + 'Child1', + ], + ); + }); + testWidgets('throws StateError if `end` not found in traversal', (WidgetTester tester) async { await tester.pumpWidget(const MaterialApp(home: _SemanticsTestWidget())); @@ -954,6 +1017,23 @@ void main() { ); }); + testWidgets('throws StateError if `endNode` not found in traversal', (WidgetTester tester) async { + await tester.pumpWidget(MaterialApp( + home: Center( + child: Column( + children: [ + for (int c = 0; c < 5; c++) + Semantics(container: true, child: Text('Child$c')), + ] + ), + ), + )); + expect( + () => tester.semantics.simulatedAccessibilityTraversal(endNode: find.semantics.byLabel('Child20')), + throwsA(isA()), + ); + }); + testWidgets('returns traversal between `start` and `end` if both are provided', (WidgetTester tester) async { await tester.pumpWidget(const MaterialApp(home: _SemanticsTestWidget())); @@ -968,6 +1048,30 @@ void main() { orderedEquals(expectedMatchers)); }); + testWidgets('returns traversal between `startNode` and `endNode` if both are provided', (WidgetTester tester) async { + await tester.pumpWidget(MaterialApp( + home: Center( + child: Column( + children: [ + for (int c = 0; c < 5; c++) + Semantics(container: true, child: Text('Child$c')), + ] + ), + ), + )); + expect( + tester.semantics.simulatedAccessibilityTraversal( + startNode: find.semantics.byLabel('Child1'), + endNode: find.semantics.byLabel('Child3'), + ).map((SemanticsNode node) => node.label), + [ + 'Child1', + 'Child2', + 'Child3', + ], + ); + }); + testWidgets('can do fuzzy traversal match with `containsAllInOrder`', (WidgetTester tester) async { await tester.pumpWidget(const MaterialApp(home: _SemanticsTestWidget())); diff --git a/packages/flutter_test/test_fixes/flutter_test/semantics_controller.dart b/packages/flutter_test/test_fixes/flutter_test/semantics_controller.dart new file mode 100644 index 000000000000..7dec14cc7c19 --- /dev/null +++ b/packages/flutter_test/test_fixes/flutter_test/semantics_controller.dart @@ -0,0 +1,19 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; + +void main() { + // Generic reference variables. + finders.FinderBase theStart; + finders.FinderBase theEnd; + + testWidgets('simulatedAccessibilityTraversal', (WidgetTester tester) async { + // Changes made in https://github.com/flutter/flutter/pull/143386 + tester.semantics.simulatedAccessibilityTraversal(); + tester.semantics.simulatedAccessibilityTraversal(start: theStart); + tester.semantics.simulatedAccessibilityTraversal(end: theEnd); + tester.semantics.simulatedAccessibilityTraversal(start: theStart, end: theEnd); + }); +} diff --git a/packages/flutter_test/test_fixes/flutter_test/semantics_controller.dart.expect b/packages/flutter_test/test_fixes/flutter_test/semantics_controller.dart.expect new file mode 100644 index 000000000000..d00687969b3e --- /dev/null +++ b/packages/flutter_test/test_fixes/flutter_test/semantics_controller.dart.expect @@ -0,0 +1,19 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_test/flutter_test.dart'; + +void main() { + // Generic reference variables. + finders.FinderBase theStart; + finders.FinderBase theEnd; + + testWidgets('simulatedAccessibilityTraversal', (WidgetTester tester) async { + // Changes made in https://github.com/flutter/flutter/pull/143386 + tester.semantics.simulatedAccessibilityTraversal(); + tester.semantics.simulatedAccessibilityTraversal(startNode: theStart); + tester.semantics.simulatedAccessibilityTraversal(endNode: theEnd); + tester.semantics.simulatedAccessibilityTraversal(startNode: theStart, endNode: theEnd); + }); +}