// 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/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart'; // Vertical position at which to anchor the toolbar for testing. const double _kAnchor = 200; // Amount for toolbar to overlap bottom padding for testing. const double _kTestToolbarOverlap = 10; void main() { TestWidgetsFlutterBinding.ensureInitialized(); /// Builds test button items for each of the suggestions provided. List<ContextMenuButtonItem> buildSuggestionButtons(List<String> suggestions) { final List<ContextMenuButtonItem> buttonItems = <ContextMenuButtonItem>[]; for (final String suggestion in suggestions) { buttonItems.add(ContextMenuButtonItem( onPressed: () {}, label: suggestion, )); } final ContextMenuButtonItem deleteButton = ContextMenuButtonItem( onPressed: () {}, type: ContextMenuButtonType.delete, label: 'DELETE', ); buttonItems.add(deleteButton); return buttonItems; } /// Finds the container of the [SpellCheckSuggestionsToolbar] so that /// the position of the toolbar itself may be determined. Finder findSpellCheckSuggestionsToolbar() { return find.descendant( of: find.byType(MaterialApp), matching: find.byWidgetPredicate( (Widget w) => '${w.runtimeType}' == '_SpellCheckSuggestionsToolbarContainer'), ); } testWidgetsWithLeakTracking('positions toolbar below anchor when it fits above bottom view padding', (WidgetTester tester) async { // We expect the toolbar to be positioned right below the anchor with padding accounted for. await tester.pumpWidget( MaterialApp( home: Scaffold( body: SpellCheckSuggestionsToolbar( anchor: const Offset(0.0, _kAnchor), buttonItems: buildSuggestionButtons(<String>['hello', 'yellow', 'yell']), ), ), ), ); final double toolbarY = tester.getTopLeft(findSpellCheckSuggestionsToolbar()).dy; expect(toolbarY, equals(_kAnchor)); }); testWidgetsWithLeakTracking('re-positions toolbar higher below anchor when it does not fit above bottom view padding', (WidgetTester tester) async { // We expect the toolbar to be positioned _kTestToolbarOverlap pixels above the anchor. const double expectedToolbarY = _kAnchor - _kTestToolbarOverlap; await tester.pumpWidget( MaterialApp( home: Scaffold( body: SpellCheckSuggestionsToolbar( anchor: const Offset(0.0, _kAnchor - _kTestToolbarOverlap), buttonItems: buildSuggestionButtons(<String>['hello', 'yellow', 'yell']), ), ), ), ); final double toolbarY = tester.getTopLeft(findSpellCheckSuggestionsToolbar()).dy; expect(toolbarY, equals(expectedToolbarY)); }); testWidgetsWithLeakTracking('more than three suggestions throws an error', (WidgetTester tester) async { Future<void> pumpToolbar(List<String> suggestions) async { await tester.pumpWidget( MaterialApp( home: Scaffold( body: SpellCheckSuggestionsToolbar( anchor: const Offset(0.0, _kAnchor - _kTestToolbarOverlap), buttonItems: buildSuggestionButtons(suggestions), ), ), ), ); } await pumpToolbar(<String>['hello', 'yellow', 'yell']); expect(() async { await pumpToolbar(<String>['hello', 'yellow', 'yell', 'yeller']); }, throwsAssertionError); }, skip: kIsWeb, // [intended] ); test('buildSuggestionButtons only considers the first three suggestions', () { final _FakeEditableTextState editableTextState = _FakeEditableTextState( suggestions: <String>[ 'hello', 'yellow', 'yell', 'yeller', ], ); final List<ContextMenuButtonItem>? buttonItems = SpellCheckSuggestionsToolbar.buildButtonItems(editableTextState); expect(buttonItems, isNotNull); final Iterable<String?> labels = buttonItems!.map((ContextMenuButtonItem buttonItem) { return buttonItem.label; }); expect(labels, hasLength(4)); expect(labels, contains('hello')); expect(labels, contains('yellow')); expect(labels, contains('yell')); expect(labels, contains(null)); // For the delete button. expect(labels, isNot(contains('yeller'))); }); test('buildButtonItems builds only a delete button when no suggestions', () { final _FakeEditableTextState editableTextState = _FakeEditableTextState(); final List<ContextMenuButtonItem>? buttonItems = SpellCheckSuggestionsToolbar.buildButtonItems(editableTextState); expect(buttonItems, hasLength(1)); expect(buttonItems!.first.type, ContextMenuButtonType.delete); }); } class _FakeEditableTextState extends EditableTextState { _FakeEditableTextState({ this.suggestions, }); final List<String>? suggestions; @override TextEditingValue get currentTextEditingValue => TextEditingValue.empty; @override SuggestionSpan? findSuggestionSpanAtCursorIndex(int cursorIndex) { return SuggestionSpan( const TextRange( start: 0, end: 0, ), suggestions ?? <String>[], ); } }