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..eb737d283e20 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,32 +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); + } + 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]'); } + endIndex ??= traversal.length - 1; 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 00e1fc8736d2..afc10dd576f0 100644 --- a/packages/flutter_test/test/controller_test.dart +++ b/packages/flutter_test/test/controller_test.dart @@ -920,6 +920,42 @@ void main() { orderedEquals(expectedMatchers)); }); + testWidgets('simulatedAccessibilityTraversal end Index supports empty traversal', (WidgetTester tester) async { + await tester.pumpWidget(const MaterialApp( + home: Center( + child: Column(), // No nodes! + ), + )); + expect( + tester.semantics.simulatedAccessibilityTraversal().map((SemanticsNode node) => node.label), + [], + ); + }); + + 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())); @@ -931,6 +967,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())); @@ -942,6 +995,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())); @@ -953,6 +1028,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())); @@ -967,6 +1059,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); + }); +}