Unverified Commit 04854e8a authored by Kostia Sokolovskyi's avatar Kostia Sokolovskyi Committed by GitHub

Fix memory leak in _SelectableTextState (#135049)

parent 1bfd6a1f
...@@ -536,6 +536,7 @@ class _SelectableTextState extends State<SelectableText> implements TextSelectio ...@@ -536,6 +536,7 @@ class _SelectableTextState extends State<SelectableText> implements TextSelectio
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
if (widget.data != oldWidget.data || widget.textSpan != oldWidget.textSpan) { if (widget.data != oldWidget.data || widget.textSpan != oldWidget.textSpan) {
_controller.removeListener(_onControllerChanged); _controller.removeListener(_onControllerChanged);
_controller.dispose();
_controller = _TextSpanEditingController( _controller = _TextSpanEditingController(
textSpan: widget.textSpan ?? TextSpan(text: widget.data), textSpan: widget.textSpan ?? TextSpan(text: widget.data),
); );
......
...@@ -18,6 +18,7 @@ import 'package:flutter/material.dart'; ...@@ -18,6 +18,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:leak_tracker_flutter_testing/leak_tracker_flutter_testing.dart';
import '../widgets/clipboard_utils.dart'; import '../widgets/clipboard_utils.dart';
import '../widgets/editable_text_utils.dart' show textOffsetToPosition; import '../widgets/editable_text_utils.dart' show textOffsetToPosition;
...@@ -55,6 +56,7 @@ Widget overlay({ Widget? child }) { ...@@ -55,6 +56,7 @@ Widget overlay({ Widget? child }) {
); );
}, },
); );
addTearDown(() => entry..remove()..dispose());
return overlayWithEntry(entry); return overlayWithEntry(entry);
} }
...@@ -215,7 +217,7 @@ void main() { ...@@ -215,7 +217,7 @@ void main() {
expect(tester.takeException(), isNotNull); // side effect exception expect(tester.takeException(), isNotNull); // side effect exception
}); });
testWidgets('Do not crash when remove SelectableText during handle drag', (WidgetTester tester) async { testWidgetsWithLeakTracking('Do not crash when remove SelectableText during handle drag', (WidgetTester tester) async {
// Regression test https://github.com/flutter/flutter/issues/108242 // Regression test https://github.com/flutter/flutter/issues/108242
bool isShow = true; bool isShow = true;
late StateSetter setter; late StateSetter setter;
...@@ -283,7 +285,7 @@ void main() { ...@@ -283,7 +285,7 @@ void main() {
await tester.pump(); await tester.pump();
}); });
testWidgets('has expected defaults', (WidgetTester tester) async { testWidgetsWithLeakTracking('has expected defaults', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
boilerplate( boilerplate(
child: const SelectableText('selectable text'), child: const SelectableText('selectable text'),
...@@ -299,7 +301,7 @@ void main() { ...@@ -299,7 +301,7 @@ void main() {
expect(selectableText.enableInteractiveSelection, true); expect(selectableText.enableInteractiveSelection, true);
}); });
testWidgets('Rich selectable text has expected defaults', (WidgetTester tester) async { testWidgetsWithLeakTracking('Rich selectable text has expected defaults', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
const MediaQuery( const MediaQuery(
data: MediaQueryData(), data: MediaQueryData(),
...@@ -344,7 +346,7 @@ void main() { ...@@ -344,7 +346,7 @@ void main() {
expect(selectableText.enableInteractiveSelection, true); expect(selectableText.enableInteractiveSelection, true);
}); });
testWidgets('Rich selectable text supports WidgetSpan', (WidgetTester tester) async { testWidgetsWithLeakTracking('Rich selectable text supports WidgetSpan', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
const MediaQuery( const MediaQuery(
data: MediaQueryData(), data: MediaQueryData(),
...@@ -385,7 +387,7 @@ void main() { ...@@ -385,7 +387,7 @@ void main() {
expect(tester.takeException(), isNull); expect(tester.takeException(), isNull);
}); });
testWidgets('no text keyboard when widget is focused', (WidgetTester tester) async { testWidgetsWithLeakTracking('no text keyboard when widget is focused', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
overlay( overlay(
child: const SelectableText('selectable text'), child: const SelectableText('selectable text'),
...@@ -396,7 +398,7 @@ void main() { ...@@ -396,7 +398,7 @@ void main() {
expect(tester.testTextInput.hasAnyClients, false); expect(tester.testTextInput.hasAnyClients, false);
}); });
testWidgets('uses DefaultSelectionStyle for selection and cursor colors if provided', (WidgetTester tester) async { testWidgetsWithLeakTracking('uses DefaultSelectionStyle for selection and cursor colors if provided', (WidgetTester tester) async {
const Color selectionColor = Colors.orange; const Color selectionColor = Colors.orange;
const Color cursorColor = Colors.red; const Color cursorColor = Colors.red;
...@@ -417,7 +419,7 @@ void main() { ...@@ -417,7 +419,7 @@ void main() {
expect(state.widget.cursorColor, cursorColor); expect(state.widget.cursorColor, cursorColor);
}); });
testWidgets('Selectable Text has adaptive size', (WidgetTester tester) async { testWidgetsWithLeakTracking('Selectable Text has adaptive size', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
boilerplate( boilerplate(
child: const SelectableText('s'), child: const SelectableText('s'),
...@@ -439,7 +441,7 @@ void main() { ...@@ -439,7 +441,7 @@ void main() {
expect(longtextBox.size, const Size(199.0, 14.0)); expect(longtextBox.size, const Size(199.0, 14.0));
}); });
testWidgets('can scale with textScaleFactor', (WidgetTester tester) async { testWidgetsWithLeakTracking('can scale with textScaleFactor', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
boilerplate( boilerplate(
child: const SelectableText('selectable text'), child: const SelectableText('selectable text'),
...@@ -462,7 +464,7 @@ void main() { ...@@ -462,7 +464,7 @@ void main() {
expect(scaledBox.size.height, 27.0); expect(scaledBox.size.height, 27.0);
}); });
testWidgets('can switch between textWidthBasis', (WidgetTester tester) async { testWidgetsWithLeakTracking('can switch between textWidthBasis', (WidgetTester tester) async {
RenderBox findTextBox() => tester.renderObject(find.byType(SelectableText)); RenderBox findTextBox() => tester.renderObject(find.byType(SelectableText));
const String text = 'I can face roll keyboardkeyboardaszzaaaaszzaaaaszzaaaaszzaaaa'; const String text = 'I can face roll keyboardkeyboardaszzaaaaszzaaaaszzaaaaszzaaaa';
await tester.pumpWidget( await tester.pumpWidget(
...@@ -488,7 +490,7 @@ void main() { ...@@ -488,7 +490,7 @@ void main() {
expect(textBox.size, const Size(633.0, 28.0)); expect(textBox.size, const Size(633.0, 28.0));
}); });
testWidgets('can switch between textHeightBehavior', (WidgetTester tester) async { testWidgetsWithLeakTracking('can switch between textHeightBehavior', (WidgetTester tester) async {
const String text = 'selectable text'; const String text = 'selectable text';
const TextHeightBehavior textHeightBehavior = TextHeightBehavior( const TextHeightBehavior textHeightBehavior = TextHeightBehavior(
applyHeightToFirstAscent: false, applyHeightToFirstAscent: false,
...@@ -512,7 +514,7 @@ void main() { ...@@ -512,7 +514,7 @@ void main() {
expect(findRenderEditable(tester).textHeightBehavior, textHeightBehavior); expect(findRenderEditable(tester).textHeightBehavior, textHeightBehavior);
}); });
testWidgets('Cursor blinks when showCursor is true', (WidgetTester tester) async { testWidgetsWithLeakTracking('Cursor blinks when showCursor is true', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
overlay( overlay(
child: const SelectableText( child: const SelectableText(
...@@ -540,7 +542,7 @@ void main() { ...@@ -540,7 +542,7 @@ void main() {
expect(editableText.cursorCurrentlyVisible, equals(initialShowCursor)); expect(editableText.cursorCurrentlyVisible, equals(initialShowCursor));
}); });
testWidgets('selectable text selection toolbar renders correctly inside opacity', (WidgetTester tester) async { testWidgetsWithLeakTracking('selectable text selection toolbar renders correctly inside opacity', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
const MaterialApp( const MaterialApp(
home: Scaffold( home: Scaffold(
...@@ -571,7 +573,7 @@ void main() { ...@@ -571,7 +573,7 @@ void main() {
expect(find.text('Select all'), findsOneWidget); expect(find.text('Select all'), findsOneWidget);
}); });
testWidgets('Caret position is updated on tap', (WidgetTester tester) async { testWidgetsWithLeakTracking('Caret position is updated on tap', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
overlay( overlay(
child: const SelectableText('abc def ghi'), child: const SelectableText('abc def ghi'),
...@@ -591,7 +593,7 @@ void main() { ...@@ -591,7 +593,7 @@ void main() {
expect(editableText.controller.selection.extentOffset, tapIndex); expect(editableText.controller.selection.extentOffset, tapIndex);
}); });
testWidgets('enableInteractiveSelection = false, tap', (WidgetTester tester) async { testWidgetsWithLeakTracking('enableInteractiveSelection = false, tap', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
overlay( overlay(
child: const SelectableText( child: const SelectableText(
...@@ -614,7 +616,7 @@ void main() { ...@@ -614,7 +616,7 @@ void main() {
expect(editableText.controller.selection.extentOffset, -1); expect(editableText.controller.selection.extentOffset, -1);
}); });
testWidgets('enableInteractiveSelection = false, long-press', (WidgetTester tester) async { testWidgetsWithLeakTracking('enableInteractiveSelection = false, long-press', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
overlay( overlay(
child: const SelectableText( child: const SelectableText(
...@@ -639,7 +641,7 @@ void main() { ...@@ -639,7 +641,7 @@ void main() {
expect(editableText.controller.selection.extentOffset, -1); expect(editableText.controller.selection.extentOffset, -1);
}); });
testWidgets('Can long press to select', (WidgetTester tester) async { testWidgetsWithLeakTracking('Can long press to select', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
overlay( overlay(
child: const SelectableText('abc def ghi'), child: const SelectableText('abc def ghi'),
...@@ -668,7 +670,7 @@ void main() { ...@@ -668,7 +670,7 @@ void main() {
expect(editableText.controller.selection.baseOffset, 9); expect(editableText.controller.selection.baseOffset, 9);
}); });
testWidgets("Slight movements in longpress don't hide/show handles", (WidgetTester tester) async { testWidgetsWithLeakTracking("Slight movements in longpress don't hide/show handles", (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
overlay( overlay(
child: const SelectableText('abc def ghi'), child: const SelectableText('abc def ghi'),
...@@ -695,7 +697,7 @@ void main() { ...@@ -695,7 +697,7 @@ void main() {
expect(handle.opacity.value, equals(1.0)); expect(handle.opacity.value, equals(1.0));
}); });
testWidgets('Mouse long press is just like a tap', (WidgetTester tester) async { testWidgetsWithLeakTracking('Mouse long press is just like a tap', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
overlay( overlay(
child: const SelectableText('abc def ghi'), child: const SelectableText('abc def ghi'),
...@@ -717,7 +719,7 @@ void main() { ...@@ -717,7 +719,7 @@ void main() {
expect(editableText.controller.selection.extentOffset, eIndex); expect(editableText.controller.selection.extentOffset, eIndex);
}); });
testWidgets('selectable text basic', (WidgetTester tester) async { testWidgetsWithLeakTracking('selectable text basic', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
overlay( overlay(
child: const SelectableText('selectable'), child: const SelectableText('selectable'),
...@@ -749,7 +751,7 @@ void main() { ...@@ -749,7 +751,7 @@ void main() {
expect(find.text('Cut'), findsNothing); expect(find.text('Cut'), findsNothing);
}); });
testWidgets('selectable text can disable toolbar options', (WidgetTester tester) async { testWidgetsWithLeakTracking('selectable text can disable toolbar options', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
overlay( overlay(
child: const SelectableText( child: const SelectableText(
...@@ -769,7 +771,7 @@ void main() { ...@@ -769,7 +771,7 @@ void main() {
expect(find.text('Select all'), findsOneWidget); expect(find.text('Select all'), findsOneWidget);
}); });
testWidgets('Can select text by dragging with a mouse', (WidgetTester tester) async { testWidgetsWithLeakTracking('Can select text by dragging with a mouse', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
const MaterialApp( const MaterialApp(
home: Material( home: Material(
...@@ -797,7 +799,7 @@ void main() { ...@@ -797,7 +799,7 @@ void main() {
expect(controller.selection.extentOffset, 8); expect(controller.selection.extentOffset, 8);
}); });
testWidgets('Continuous dragging does not cause flickering', (WidgetTester tester) async { testWidgetsWithLeakTracking('Continuous dragging does not cause flickering', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
const MaterialApp( const MaterialApp(
home: Material( home: Material(
...@@ -849,7 +851,7 @@ void main() { ...@@ -849,7 +851,7 @@ void main() {
expect(controller.selection.extentOffset, 9); expect(controller.selection.extentOffset, 9);
}); });
testWidgets('Dragging in opposite direction also works', (WidgetTester tester) async { testWidgetsWithLeakTracking('Dragging in opposite direction also works', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
const MaterialApp( const MaterialApp(
home: Material( home: Material(
...@@ -877,7 +879,7 @@ void main() { ...@@ -877,7 +879,7 @@ void main() {
expect(controller.selection.extentOffset, 5); expect(controller.selection.extentOffset, 5);
}); });
testWidgets('Slow mouse dragging also selects text', (WidgetTester tester) async { testWidgetsWithLeakTracking('Slow mouse dragging also selects text', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
const MaterialApp( const MaterialApp(
home: Material( home: Material(
...@@ -904,7 +906,7 @@ void main() { ...@@ -904,7 +906,7 @@ void main() {
expect(controller.selection.extentOffset,8); expect(controller.selection.extentOffset,8);
}); });
testWidgets('Can drag handles to change selection', (WidgetTester tester) async { testWidgetsWithLeakTracking('Can drag handles to change selection', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
const MaterialApp( const MaterialApp(
home: Material( home: Material(
...@@ -966,7 +968,7 @@ void main() { ...@@ -966,7 +968,7 @@ void main() {
expect(controller.selection.extentOffset, 11); expect(controller.selection.extentOffset, 11);
}); });
testWidgets('Dragging handles calls onSelectionChanged', (WidgetTester tester) async { testWidgetsWithLeakTracking('Dragging handles calls onSelectionChanged', (WidgetTester tester) async {
TextSelection? newSelection; TextSelection? newSelection;
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
...@@ -1018,7 +1020,7 @@ void main() { ...@@ -1018,7 +1020,7 @@ void main() {
expect(newSelection!.extentOffset, 9); expect(newSelection!.extentOffset, 9);
}); });
testWidgets('Cannot drag one handle past the other', (WidgetTester tester) async { testWidgetsWithLeakTracking('Cannot drag one handle past the other', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
const MaterialApp( const MaterialApp(
home: Material( home: Material(
...@@ -1076,7 +1078,7 @@ void main() { ...@@ -1076,7 +1078,7 @@ void main() {
expect(controller.selection.extentOffset, 5); expect(controller.selection.extentOffset, 5);
}); });
testWidgets('Can use selection toolbar', (WidgetTester tester) async { testWidgetsWithLeakTracking('Can use selection toolbar', (WidgetTester tester) async {
const String testValue = 'abc def ghi'; const String testValue = 'abc def ghi';
await tester.pumpWidget( await tester.pumpWidget(
const MaterialApp( const MaterialApp(
...@@ -1118,7 +1120,7 @@ void main() { ...@@ -1118,7 +1120,7 @@ void main() {
expect(controller.selection.isCollapsed, true); expect(controller.selection.isCollapsed, true);
}); });
testWidgets('Selectable height with maxLine', (WidgetTester tester) async { testWidgetsWithLeakTracking('Selectable height with maxLine', (WidgetTester tester) async {
await tester.pumpWidget(selectableTextBuilder()); await tester.pumpWidget(selectableTextBuilder());
RenderBox findTextBox() => tester.renderObject(find.byType(SelectableText)); RenderBox findTextBox() => tester.renderObject(find.byType(SelectableText));
...@@ -1170,7 +1172,7 @@ void main() { ...@@ -1170,7 +1172,7 @@ void main() {
expect(textBox.size.height, greaterThan(fourLineInputSize.height)); expect(textBox.size.height, greaterThan(fourLineInputSize.height));
}); });
testWidgets('Can drag handles to change selection in multiline', (WidgetTester tester) async { testWidgetsWithLeakTracking('Can drag handles to change selection in multiline', (WidgetTester tester) async {
const String testValue = kThreeLines; const String testValue = kThreeLines;
await tester.pumpWidget( await tester.pumpWidget(
overlay( overlay(
...@@ -1258,7 +1260,7 @@ void main() { ...@@ -1258,7 +1260,7 @@ void main() {
expect(controller.selection.isCollapsed, true); expect(controller.selection.isCollapsed, true);
}); });
testWidgets('Can scroll multiline input', (WidgetTester tester) async { testWidgetsWithLeakTracking('Can scroll multiline input', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
overlay( overlay(
child: const SelectableText( child: const SelectableText(
...@@ -1352,7 +1354,7 @@ void main() { ...@@ -1352,7 +1354,7 @@ void main() {
expect(inputBox.hitTest(BoxHitTestResult(), position: inputBox.globalToLocal(newFourthPos)), isFalse); expect(inputBox.hitTest(BoxHitTestResult(), position: inputBox.globalToLocal(newFourthPos)), isFalse);
}); });
testWidgets('minLines cannot be greater than maxLines', (WidgetTester tester) async { testWidgetsWithLeakTracking('minLines cannot be greater than maxLines', (WidgetTester tester) async {
expect( expect(
() async { () async {
await tester.pumpWidget( await tester.pumpWidget(
...@@ -1376,7 +1378,7 @@ void main() { ...@@ -1376,7 +1378,7 @@ void main() {
); );
}); });
testWidgets('Selectable height with minLine', (WidgetTester tester) async { testWidgetsWithLeakTracking('Selectable height with minLine', (WidgetTester tester) async {
await tester.pumpWidget(selectableTextBuilder()); await tester.pumpWidget(selectableTextBuilder());
RenderBox findTextBox() => tester.renderObject(find.byType(SelectableText)); RenderBox findTextBox() => tester.renderObject(find.byType(SelectableText));
...@@ -1390,7 +1392,7 @@ void main() { ...@@ -1390,7 +1392,7 @@ void main() {
expect(textBox.size.height, emptyInputSize.height * 2); expect(textBox.size.height, emptyInputSize.height * 2);
}); });
testWidgets('Can align to center', (WidgetTester tester) async { testWidgetsWithLeakTracking('Can align to center', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
overlay( overlay(
child: const SizedBox( child: const SizedBox(
...@@ -1412,7 +1414,7 @@ void main() { ...@@ -1412,7 +1414,7 @@ void main() {
expect(topLeft.dx, equals(399.0)); expect(topLeft.dx, equals(399.0));
}); });
testWidgets('Can align to center within center', (WidgetTester tester) async { testWidgetsWithLeakTracking('Can align to center within center', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
overlay( overlay(
child: const SizedBox( child: const SizedBox(
...@@ -1436,9 +1438,11 @@ void main() { ...@@ -1436,9 +1438,11 @@ void main() {
expect(topLeft.dx, equals(399.0)); expect(topLeft.dx, equals(399.0));
}); });
testWidgets('Selectable text is skipped during focus traversal', (WidgetTester tester) async { testWidgetsWithLeakTracking('Selectable text is skipped during focus traversal', (WidgetTester tester) async {
final FocusNode firstFieldFocus = FocusNode(); final FocusNode firstFieldFocus = FocusNode();
addTearDown(firstFieldFocus.dispose);
final FocusNode lastFieldFocus = FocusNode(); final FocusNode lastFieldFocus = FocusNode();
addTearDown(lastFieldFocus.dispose);
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
...@@ -1474,7 +1478,7 @@ void main() { ...@@ -1474,7 +1478,7 @@ void main() {
expect(lastFieldFocus.hasFocus, isTrue); expect(lastFieldFocus.hasFocus, isTrue);
}); });
testWidgets('Selectable text identifies as text field in semantics', (WidgetTester tester) async { testWidgetsWithLeakTracking('Selectable text identifies as text field in semantics', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester); final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget( await tester.pumpWidget(
...@@ -1501,7 +1505,7 @@ void main() { ...@@ -1501,7 +1505,7 @@ void main() {
semantics.dispose(); semantics.dispose();
}); });
testWidgets('Selectable text rich text with spell out in semantics', (WidgetTester tester) async { testWidgetsWithLeakTracking('Selectable text rich text with spell out in semantics', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester); final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget( await tester.pumpWidget(
...@@ -1534,7 +1538,7 @@ void main() { ...@@ -1534,7 +1538,7 @@ void main() {
semantics.dispose(); semantics.dispose();
}); });
testWidgets('Selectable text rich text with locale in semantics', (WidgetTester tester) async { testWidgetsWithLeakTracking('Selectable text rich text with locale in semantics', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester); final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget( await tester.pumpWidget(
...@@ -1567,7 +1571,7 @@ void main() { ...@@ -1567,7 +1571,7 @@ void main() {
semantics.dispose(); semantics.dispose();
}); });
testWidgets('Selectable rich text with gesture recognizer has correct semantics', (WidgetTester tester) async { testWidgetsWithLeakTracking('Selectable rich text with gesture recognizer has correct semantics', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester); final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget( await tester.pumpWidget(
overlay( overlay(
...@@ -1623,6 +1627,7 @@ void main() { ...@@ -1623,6 +1627,7 @@ void main() {
Future<void> setupWidget(WidgetTester tester, String text) async { Future<void> setupWidget(WidgetTester tester, String text) async {
final FocusNode focusNode = FocusNode(); final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
home: Material( home: Material(
...@@ -1642,7 +1647,7 @@ void main() { ...@@ -1642,7 +1647,7 @@ void main() {
controller = editableTextWidget.controller; controller = editableTextWidget.controller;
} }
testWidgets('Shift test 1', (WidgetTester tester) async { testWidgetsWithLeakTracking('Shift test 1', (WidgetTester tester) async {
await setupWidget(tester, 'a big house'); await setupWidget(tester, 'a big house');
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift); await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
...@@ -1650,7 +1655,7 @@ void main() { ...@@ -1650,7 +1655,7 @@ void main() {
expect(controller.selection.extentOffset - controller.selection.baseOffset, -1); expect(controller.selection.extentOffset - controller.selection.baseOffset, -1);
}, variant: KeySimulatorTransitModeVariant.all()); }, variant: KeySimulatorTransitModeVariant.all());
testWidgets('Shift test 2', (WidgetTester tester) async { testWidgetsWithLeakTracking('Shift test 2', (WidgetTester tester) async {
await setupWidget(tester, 'abcdefghi'); await setupWidget(tester, 'abcdefghi');
controller.selection = const TextSelection.collapsed(offset: 3); controller.selection = const TextSelection.collapsed(offset: 3);
...@@ -1662,7 +1667,7 @@ void main() { ...@@ -1662,7 +1667,7 @@ void main() {
expect(controller.selection.extentOffset - controller.selection.baseOffset, 1); expect(controller.selection.extentOffset - controller.selection.baseOffset, 1);
}, variant: KeySimulatorTransitModeVariant.all()); }, variant: KeySimulatorTransitModeVariant.all());
testWidgets('Control Shift test', (WidgetTester tester) async { testWidgetsWithLeakTracking('Control Shift test', (WidgetTester tester) async {
await setupWidget(tester, 'their big house'); await setupWidget(tester, 'their big house');
await tester.sendKeyDownEvent(LogicalKeyboardKey.control); await tester.sendKeyDownEvent(LogicalKeyboardKey.control);
...@@ -1674,7 +1679,7 @@ void main() { ...@@ -1674,7 +1679,7 @@ void main() {
expect(controller.selection.extentOffset - controller.selection.baseOffset, -5); expect(controller.selection.extentOffset - controller.selection.baseOffset, -5);
}, variant: KeySimulatorTransitModeVariant.all()); }, variant: KeySimulatorTransitModeVariant.all());
testWidgets('Down and up test', (WidgetTester tester) async { testWidgetsWithLeakTracking('Down and up test', (WidgetTester tester) async {
await setupWidget(tester, 'a big house'); await setupWidget(tester, 'a big house');
await tester.sendKeyDownEvent(LogicalKeyboardKey.shift); await tester.sendKeyDownEvent(LogicalKeyboardKey.shift);
...@@ -1692,7 +1697,7 @@ void main() { ...@@ -1692,7 +1697,7 @@ void main() {
expect(controller.selection.extentOffset - controller.selection.baseOffset, 0); expect(controller.selection.extentOffset - controller.selection.baseOffset, 0);
}, variant: KeySimulatorTransitModeVariant.all()); }, variant: KeySimulatorTransitModeVariant.all());
testWidgets('Down and up test 2', (WidgetTester tester) async { testWidgetsWithLeakTracking('Down and up test 2', (WidgetTester tester) async {
await setupWidget(tester, 'a big house\njumped over a mouse\nOne more line yay'); await setupWidget(tester, 'a big house\njumped over a mouse\nOne more line yay');
controller.selection = const TextSelection.collapsed(offset: 0); controller.selection = const TextSelection.collapsed(offset: 0);
...@@ -1744,8 +1749,9 @@ void main() { ...@@ -1744,8 +1749,9 @@ void main() {
}, variant: KeySimulatorTransitModeVariant.all()); }, variant: KeySimulatorTransitModeVariant.all());
}); });
testWidgets('Copy test', (WidgetTester tester) async { testWidgetsWithLeakTracking('Copy test', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode(); final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
String clipboardContent = ''; String clipboardContent = '';
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) async { tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) async {
...@@ -1802,8 +1808,9 @@ void main() { ...@@ -1802,8 +1808,9 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
}, variant: KeySimulatorTransitModeVariant.all()); }, variant: KeySimulatorTransitModeVariant.all());
testWidgets('Select all test', (WidgetTester tester) async { testWidgetsWithLeakTracking('Select all test', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode(); final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
const String testValue = 'a big house\njumped over a mouse'; const String testValue = 'a big house\njumped over a mouse';
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
...@@ -1836,8 +1843,9 @@ void main() { ...@@ -1836,8 +1843,9 @@ void main() {
expect(controller.selection.extentOffset, 31); expect(controller.selection.extentOffset, 31);
}, variant: KeySimulatorTransitModeVariant.all()); }, variant: KeySimulatorTransitModeVariant.all());
testWidgets('keyboard selection should call onSelectionChanged', (WidgetTester tester) async { testWidgetsWithLeakTracking('keyboard selection should call onSelectionChanged', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode(); final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
TextSelection? newSelection; TextSelection? newSelection;
const String testValue = 'a big house\njumped over a mouse'; const String testValue = 'a big house\njumped over a mouse';
await tester.pumpWidget( await tester.pumpWidget(
...@@ -1882,8 +1890,9 @@ void main() { ...@@ -1882,8 +1890,9 @@ void main() {
} }
}, variant: KeySimulatorTransitModeVariant.all()); }, variant: KeySimulatorTransitModeVariant.all());
testWidgets('Changing positions of selectable text', (WidgetTester tester) async { testWidgetsWithLeakTracking('Changing positions of selectable text', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode(); final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
final List<RawKeyEvent> events = <RawKeyEvent>[]; final List<RawKeyEvent> events = <RawKeyEvent>[];
final Key key1 = UniqueKey(); final Key key1 = UniqueKey();
...@@ -1972,8 +1981,9 @@ void main() { ...@@ -1972,8 +1981,9 @@ void main() {
expect(c1.selection.extentOffset - c1.selection.baseOffset, -10); expect(c1.selection.extentOffset - c1.selection.baseOffset, -10);
}, variant: KeySimulatorTransitModeVariant.all()); }, variant: KeySimulatorTransitModeVariant.all());
testWidgets('Changing focus test', (WidgetTester tester) async { testWidgetsWithLeakTracking('Changing focus test', (WidgetTester tester) async {
final FocusNode focusNode = FocusNode(); final FocusNode focusNode = FocusNode();
addTearDown(focusNode.dispose);
final List<RawKeyEvent> events = <RawKeyEvent>[]; final List<RawKeyEvent> events = <RawKeyEvent>[];
final Key key1 = UniqueKey(); final Key key1 = UniqueKey();
...@@ -2041,7 +2051,7 @@ void main() { ...@@ -2041,7 +2051,7 @@ void main() {
expect(c2.selection.extentOffset - c2.selection.baseOffset, -5); expect(c2.selection.extentOffset - c2.selection.baseOffset, -5);
}, variant: KeySimulatorTransitModeVariant.all()); }, variant: KeySimulatorTransitModeVariant.all());
testWidgets('Caret works when maxLines is null', (WidgetTester tester) async { testWidgetsWithLeakTracking('Caret works when maxLines is null', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
overlay( overlay(
child: const SelectableText( child: const SelectableText(
...@@ -2064,7 +2074,7 @@ void main() { ...@@ -2064,7 +2074,7 @@ void main() {
expect(controller.selection.baseOffset, 0); expect(controller.selection.baseOffset, 0);
}); });
testWidgets('SelectableText baseline alignment no-strut', (WidgetTester tester) async { testWidgetsWithLeakTracking('SelectableText baseline alignment no-strut', (WidgetTester tester) async {
final Key keyA = UniqueKey(); final Key keyA = UniqueKey();
final Key keyB = UniqueKey(); final Key keyB = UniqueKey();
...@@ -2113,7 +2123,7 @@ void main() { ...@@ -2113,7 +2123,7 @@ void main() {
expect(tester.getBottomLeft(find.byKey(keyB)).dy, rowBottomY); expect(tester.getBottomLeft(find.byKey(keyB)).dy, rowBottomY);
}); });
testWidgets('SelectableText baseline alignment', (WidgetTester tester) async { testWidgetsWithLeakTracking('SelectableText baseline alignment', (WidgetTester tester) async {
final Key keyA = UniqueKey(); final Key keyA = UniqueKey();
final Key keyB = UniqueKey(); final Key keyB = UniqueKey();
...@@ -2160,7 +2170,7 @@ void main() { ...@@ -2160,7 +2170,7 @@ void main() {
expect(tester.getBottomLeft(find.byKey(keyB)).dy, rowBottomY); expect(tester.getBottomLeft(find.byKey(keyB)).dy, rowBottomY);
}); });
testWidgets('SelectableText semantics', (WidgetTester tester) async { testWidgetsWithLeakTracking('SelectableText semantics', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester); final SemanticsTester semantics = SemanticsTester(tester);
final Key key = UniqueKey(); final Key key = UniqueKey();
...@@ -2280,7 +2290,7 @@ void main() { ...@@ -2280,7 +2290,7 @@ void main() {
semantics.dispose(); semantics.dispose();
}); });
testWidgets('SelectableText semantics, with semanticsLabel', (WidgetTester tester) async { testWidgetsWithLeakTracking('SelectableText semantics, with semanticsLabel', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester); final SemanticsTester semantics = SemanticsTester(tester);
final Key key = UniqueKey(); final Key key = UniqueKey();
...@@ -2307,7 +2317,7 @@ void main() { ...@@ -2307,7 +2317,7 @@ void main() {
semantics.dispose(); semantics.dispose();
}); });
testWidgets('SelectableText semantics, enableInteractiveSelection = false', (WidgetTester tester) async { testWidgetsWithLeakTracking('SelectableText semantics, enableInteractiveSelection = false', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester); final SemanticsTester semantics = SemanticsTester(tester);
final Key key = UniqueKey(); final Key key = UniqueKey();
...@@ -2353,7 +2363,7 @@ void main() { ...@@ -2353,7 +2363,7 @@ void main() {
semantics.dispose(); semantics.dispose();
}); });
testWidgets('SelectableText semantics for selections', (WidgetTester tester) async { testWidgetsWithLeakTracking('SelectableText semantics for selections', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester); final SemanticsTester semantics = SemanticsTester(tester);
final Key key = UniqueKey(); final Key key = UniqueKey();
...@@ -2449,13 +2459,15 @@ void main() { ...@@ -2449,13 +2459,15 @@ void main() {
semantics.dispose(); semantics.dispose();
}); });
testWidgets('semantic nodes of offscreen recognizers are marked hidden', (WidgetTester tester) async { testWidgetsWithLeakTracking('semantic nodes of offscreen recognizers are marked hidden', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/100395. // Regression test for https://github.com/flutter/flutter/issues/100395.
final SemanticsTester semantics = SemanticsTester(tester); final SemanticsTester semantics = SemanticsTester(tester);
const TextStyle textStyle = TextStyle(fontSize: 200); const TextStyle textStyle = TextStyle(fontSize: 200);
const String onScreenText = 'onscreen\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n'; const String onScreenText = 'onscreen\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n';
const String offScreenText = 'off screen'; const String offScreenText = 'off screen';
final ScrollController controller = ScrollController(); final ScrollController controller = ScrollController();
addTearDown(controller.dispose);
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
home: SingleChildScrollView( home: SingleChildScrollView(
...@@ -2542,7 +2554,7 @@ void main() { ...@@ -2542,7 +2554,7 @@ void main() {
semantics.dispose(); semantics.dispose();
}); });
testWidgets('SelectableText change selection with semantics', (WidgetTester tester) async { testWidgetsWithLeakTracking('SelectableText change selection with semantics', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester); final SemanticsTester semantics = SemanticsTester(tester);
final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner!; final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner!;
final Key key = UniqueKey(); final Key key = UniqueKey();
...@@ -2641,7 +2653,7 @@ void main() { ...@@ -2641,7 +2653,7 @@ void main() {
semantics.dispose(); semantics.dispose();
}); });
testWidgets('Can activate SelectableText with explicit controller via semantics', (WidgetTester tester) async { testWidgetsWithLeakTracking('Can activate SelectableText with explicit controller via semantics', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/17801 // Regression test for https://github.com/flutter/flutter/issues/17801
const String testValue = 'Hello'; const String testValue = 'Hello';
...@@ -2715,7 +2727,7 @@ void main() { ...@@ -2715,7 +2727,7 @@ void main() {
semantics.dispose(); semantics.dispose();
}); });
testWidgets('onTap is called upon tap', (WidgetTester tester) async { testWidgetsWithLeakTracking('onTap is called upon tap', (WidgetTester tester) async {
int tapCount = 0; int tapCount = 0;
await tester.pumpWidget( await tester.pumpWidget(
overlay( overlay(
...@@ -2739,7 +2751,7 @@ void main() { ...@@ -2739,7 +2751,7 @@ void main() {
expect(tapCount, 3); expect(tapCount, 3);
}); });
testWidgets('SelectableText style is merged with default text style', (WidgetTester tester) async { testWidgetsWithLeakTracking('SelectableText style is merged with default text style', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/23994 // Regression test for https://github.com/flutter/flutter/issues/23994
final TextStyle defaultStyle = TextStyle( final TextStyle defaultStyle = TextStyle(
color: Colors.blue[500], color: Colors.blue[500],
...@@ -2786,7 +2798,7 @@ void main() { ...@@ -2786,7 +2798,7 @@ void main() {
expect(editableText.style.color, isNull); expect(editableText.style.color, isNull);
}); });
testWidgets('style enforces required fields', (WidgetTester tester) async { testWidgetsWithLeakTracking('style enforces required fields', (WidgetTester tester) async {
Widget buildFrame(TextStyle style) { Widget buildFrame(TextStyle style) {
return MaterialApp( return MaterialApp(
home: Material( home: Material(
...@@ -2818,7 +2830,7 @@ void main() { ...@@ -2818,7 +2830,7 @@ void main() {
expect(tester.takeException(), isNotNull); expect(tester.takeException(), isNotNull);
}); });
testWidgets( testWidgetsWithLeakTracking(
'tap moves cursor to the edge of the word it tapped', 'tap moves cursor to the edge of the word it tapped',
(WidgetTester tester) async { (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
...@@ -2850,7 +2862,7 @@ void main() { ...@@ -2850,7 +2862,7 @@ void main() {
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }),
); );
testWidgets( testWidgetsWithLeakTracking(
'tap moves cursor to the position tapped (Android)', 'tap moves cursor to the position tapped (Android)',
(WidgetTester tester) async { (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
...@@ -2882,7 +2894,7 @@ void main() { ...@@ -2882,7 +2894,7 @@ void main() {
}, },
); );
testWidgets( testWidgetsWithLeakTracking(
'two slow taps do not trigger a word selection', 'two slow taps do not trigger a word selection',
(WidgetTester tester) async { (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
...@@ -2917,7 +2929,7 @@ void main() { ...@@ -2917,7 +2929,7 @@ void main() {
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }),
); );
testWidgets( testWidgetsWithLeakTracking(
'double tap selects word and first tap of double tap moves cursor', 'double tap selects word and first tap of double tap moves cursor',
(WidgetTester tester) async { (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
...@@ -2964,7 +2976,7 @@ void main() { ...@@ -2964,7 +2976,7 @@ void main() {
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }),
); );
testWidgets( testWidgetsWithLeakTracking(
'double tap selects word and first tap of double tap moves cursor and shows toolbar (Android)', 'double tap selects word and first tap of double tap moves cursor and shows toolbar (Android)',
(WidgetTester tester) async { (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
...@@ -3009,7 +3021,7 @@ void main() { ...@@ -3009,7 +3021,7 @@ void main() {
}, },
); );
testWidgets( testWidgetsWithLeakTracking(
'double tap on top of cursor also selects word (Android)', 'double tap on top of cursor also selects word (Android)',
(WidgetTester tester) async { (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
...@@ -3058,7 +3070,7 @@ void main() { ...@@ -3058,7 +3070,7 @@ void main() {
}, },
); );
testWidgets( testWidgetsWithLeakTracking(
'double tap hold selects word', 'double tap hold selects word',
(WidgetTester tester) async { (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
...@@ -3106,7 +3118,7 @@ void main() { ...@@ -3106,7 +3118,7 @@ void main() {
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }),
); );
testWidgets( testWidgetsWithLeakTracking(
'double tap selects word with semantics label', 'double tap selects word with semantics label',
(WidgetTester tester) async { (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
...@@ -3139,7 +3151,7 @@ void main() { ...@@ -3139,7 +3151,7 @@ void main() {
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }),
); );
testWidgets( testWidgetsWithLeakTracking(
'tap after a double tap select is not affected (iOS)', 'tap after a double tap select is not affected (iOS)',
(WidgetTester tester) async { (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
...@@ -3186,7 +3198,7 @@ void main() { ...@@ -3186,7 +3198,7 @@ void main() {
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }),
); );
testWidgets( testWidgetsWithLeakTracking(
'long press selects word and shows toolbar (iOS)', 'long press selects word and shows toolbar (iOS)',
(WidgetTester tester) async { (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
...@@ -3223,7 +3235,7 @@ void main() { ...@@ -3223,7 +3235,7 @@ void main() {
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }),
); );
testWidgets( testWidgetsWithLeakTracking(
'long press selects word and shows toolbar (Android)', 'long press selects word and shows toolbar (Android)',
(WidgetTester tester) async { (WidgetTester tester) async {
...@@ -3255,7 +3267,7 @@ void main() { ...@@ -3255,7 +3267,7 @@ void main() {
}, },
); );
testWidgets( testWidgetsWithLeakTracking(
'long press selects word and shows custom toolbar (Android)', 'long press selects word and shows custom toolbar (Android)',
(WidgetTester tester) async { (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
...@@ -3293,7 +3305,7 @@ void main() { ...@@ -3293,7 +3305,7 @@ void main() {
variant: TargetPlatformVariant.all(), variant: TargetPlatformVariant.all(),
); );
testWidgets( testWidgetsWithLeakTracking(
'long press selects word and shows custom toolbar (iOS)', 'long press selects word and shows custom toolbar (iOS)',
(WidgetTester tester) async { (WidgetTester tester) async {
...@@ -3328,7 +3340,7 @@ void main() { ...@@ -3328,7 +3340,7 @@ void main() {
variant: TargetPlatformVariant.all(), variant: TargetPlatformVariant.all(),
); );
testWidgets( testWidgetsWithLeakTracking(
'textSelectionControls is passed to EditableText', 'textSelectionControls is passed to EditableText',
(WidgetTester tester) async { (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
...@@ -3348,7 +3360,7 @@ void main() { ...@@ -3348,7 +3360,7 @@ void main() {
}, },
); );
testWidgets( testWidgetsWithLeakTracking(
'long press tap cannot initiate a double tap', 'long press tap cannot initiate a double tap',
(WidgetTester tester) async { (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
...@@ -3390,7 +3402,7 @@ void main() { ...@@ -3390,7 +3402,7 @@ void main() {
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }),
); );
testWidgets( testWidgetsWithLeakTracking(
'long press drag extends the selection to the word under the drag and shows toolbar on lift on non-Apple platforms', 'long press drag extends the selection to the word under the drag and shows toolbar on lift on non-Apple platforms',
(WidgetTester tester) async { (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
...@@ -3466,7 +3478,7 @@ void main() { ...@@ -3466,7 +3478,7 @@ void main() {
variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), variant: TargetPlatformVariant.all(excluding: <TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }),
); );
testWidgets( testWidgetsWithLeakTracking(
'long press drag extends the selection to the word under the drag and shows toolbar on lift (iOS)', 'long press drag extends the selection to the word under the drag and shows toolbar on lift (iOS)',
(WidgetTester tester) async { (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
...@@ -3559,7 +3571,7 @@ void main() { ...@@ -3559,7 +3571,7 @@ void main() {
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }), variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }),
); );
testWidgets( testWidgetsWithLeakTracking(
'long press drag moves the cursor under the drag and shows toolbar on lift (macOS)', 'long press drag moves the cursor under the drag and shows toolbar on lift (macOS)',
(WidgetTester tester) async { (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
...@@ -3637,7 +3649,7 @@ void main() { ...@@ -3637,7 +3649,7 @@ void main() {
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.macOS }), variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.macOS }),
); );
testWidgets('long press drag can edge scroll', (WidgetTester tester) async { testWidgetsWithLeakTracking('long press drag can edge scroll', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
const MaterialApp( const MaterialApp(
home: Material( home: Material(
...@@ -3741,7 +3753,7 @@ void main() { ...@@ -3741,7 +3753,7 @@ void main() {
skip: true, // https://github.com/flutter/flutter/issues/64059 skip: true, // https://github.com/flutter/flutter/issues/64059
); );
testWidgets( testWidgetsWithLeakTracking(
'long tap still selects after a double tap select (iOS)', 'long tap still selects after a double tap select (iOS)',
(WidgetTester tester) async { (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
...@@ -3787,7 +3799,7 @@ void main() { ...@@ -3787,7 +3799,7 @@ void main() {
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }), variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }),
); );
testWidgets( testWidgetsWithLeakTracking(
'long tap still selects after a double tap select (macOS)', 'long tap still selects after a double tap select (macOS)',
(WidgetTester tester) async { (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
...@@ -3831,7 +3843,7 @@ void main() { ...@@ -3831,7 +3843,7 @@ void main() {
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.macOS }), variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.macOS }),
); );
//convert //convert
testWidgets( testWidgetsWithLeakTracking(
'double tap after a long tap is not affected', 'double tap after a long tap is not affected',
(WidgetTester tester) async { (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
...@@ -3880,7 +3892,7 @@ void main() { ...@@ -3880,7 +3892,7 @@ void main() {
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }),
); );
testWidgets( testWidgetsWithLeakTracking(
'double tap chains work', 'double tap chains work',
(WidgetTester tester) async { (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
...@@ -3955,7 +3967,7 @@ void main() { ...@@ -3955,7 +3967,7 @@ void main() {
variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }), variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }),
); );
testWidgets('force press does not select a word on (android)', (WidgetTester tester) async { testWidgetsWithLeakTracking('force press does not select a word on (android)', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
const MaterialApp( const MaterialApp(
home: Material( home: Material(
...@@ -3991,7 +4003,7 @@ void main() { ...@@ -3991,7 +4003,7 @@ void main() {
expect(find.byType(TextButton), findsNothing); expect(find.byType(TextButton), findsNothing);
}); });
testWidgets('force press selects word', (WidgetTester tester) async { testWidgetsWithLeakTracking('force press selects word', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
const MaterialApp( const MaterialApp(
home: Material( home: Material(
...@@ -4032,7 +4044,7 @@ void main() { ...@@ -4032,7 +4044,7 @@ void main() {
expect(find.byType(CupertinoButton), findsNWidgets(4)); expect(find.byType(CupertinoButton), findsNWidgets(4));
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS })); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS }));
testWidgets('tap on non-force-press-supported devices work', (WidgetTester tester) async { testWidgetsWithLeakTracking('tap on non-force-press-supported devices work', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
const MaterialApp( const MaterialApp(
home: Material( home: Material(
...@@ -4080,7 +4092,7 @@ void main() { ...@@ -4080,7 +4092,7 @@ void main() {
// https://github.com/flutter/flutter/issues/43445 // https://github.com/flutter/flutter/issues/43445
}, variant: TargetPlatformVariant.only(TargetPlatform.iOS)); }, variant: TargetPlatformVariant.only(TargetPlatform.iOS));
testWidgets('default SelectableText debugFillProperties', (WidgetTester tester) async { testWidgetsWithLeakTracking('default SelectableText debugFillProperties', (WidgetTester tester) async {
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
const SelectableText('something').debugFillProperties(builder); const SelectableText('something').debugFillProperties(builder);
...@@ -4092,7 +4104,7 @@ void main() { ...@@ -4092,7 +4104,7 @@ void main() {
expect(description, <String>['data: something']); expect(description, <String>['data: something']);
}); });
testWidgets('SelectableText implements debugFillProperties', (WidgetTester tester) async { testWidgetsWithLeakTracking('SelectableText implements debugFillProperties', (WidgetTester tester) async {
final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder();
// Not checking controller, inputFormatters, focusNode // Not checking controller, inputFormatters, focusNode
...@@ -4139,7 +4151,7 @@ void main() { ...@@ -4139,7 +4151,7 @@ void main() {
]); ]);
}); });
testWidgets( testWidgetsWithLeakTracking(
'strut basic single line', 'strut basic single line',
(WidgetTester tester) async { (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
...@@ -4162,7 +4174,7 @@ void main() { ...@@ -4162,7 +4174,7 @@ void main() {
}, },
); );
testWidgets( testWidgetsWithLeakTracking(
'strut TextStyle increases height', 'strut TextStyle increases height',
(WidgetTester tester) async { (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
...@@ -4209,7 +4221,7 @@ void main() { ...@@ -4209,7 +4221,7 @@ void main() {
}, },
); );
testWidgets( testWidgetsWithLeakTracking(
'strut basic multi line', 'strut basic multi line',
(WidgetTester tester) async { (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
...@@ -4233,7 +4245,7 @@ void main() { ...@@ -4233,7 +4245,7 @@ void main() {
}, },
); );
testWidgets( testWidgetsWithLeakTracking(
'strut no force small strut', 'strut no force small strut',
(WidgetTester tester) async { (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
...@@ -4265,7 +4277,7 @@ void main() { ...@@ -4265,7 +4277,7 @@ void main() {
}, },
); );
testWidgets( testWidgetsWithLeakTracking(
'strut no force large strut', 'strut no force large strut',
(WidgetTester tester) async { (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
...@@ -4294,7 +4306,7 @@ void main() { ...@@ -4294,7 +4306,7 @@ void main() {
}, },
); );
testWidgets( testWidgetsWithLeakTracking(
'strut height override', 'strut height override',
(WidgetTester tester) async { (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
...@@ -4323,7 +4335,7 @@ void main() { ...@@ -4323,7 +4335,7 @@ void main() {
}, },
); );
testWidgets( testWidgetsWithLeakTracking(
'strut forces field taller', 'strut forces field taller',
(WidgetTester tester) async { (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
...@@ -4354,7 +4366,7 @@ void main() { ...@@ -4354,7 +4366,7 @@ void main() {
}, },
); );
testWidgets('Caret center position', (WidgetTester tester) async { testWidgetsWithLeakTracking('Caret center position', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
overlay( overlay(
child: const SizedBox( child: const SizedBox(
...@@ -4390,7 +4402,7 @@ void main() { ...@@ -4390,7 +4402,7 @@ void main() {
expect(topLeft.dx, equals(385)); expect(topLeft.dx, equals(385));
}); });
testWidgets('Caret indexes into trailing whitespace center align', (WidgetTester tester) async { testWidgetsWithLeakTracking('Caret indexes into trailing whitespace center align', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
overlay( overlay(
child: const SizedBox( child: const SizedBox(
...@@ -4436,7 +4448,7 @@ void main() { ...@@ -4436,7 +4448,7 @@ void main() {
expect(topLeft.dx, equals(385)); expect(topLeft.dx, equals(385));
}); });
testWidgets('selection handles are rendered and not faded away', (WidgetTester tester) async { testWidgetsWithLeakTracking('selection handles are rendered and not faded away', (WidgetTester tester) async {
const String testText = 'lorem ipsum'; const String testText = 'lorem ipsum';
await tester.pumpWidget( await tester.pumpWidget(
const MaterialApp( const MaterialApp(
...@@ -4466,7 +4478,7 @@ void main() { ...@@ -4466,7 +4478,7 @@ void main() {
expect(right.opacity.value, equals(1.0)); expect(right.opacity.value, equals(1.0));
}); });
testWidgets('selection handles are rendered and not faded away', (WidgetTester tester) async { testWidgetsWithLeakTracking('selection handles are rendered and not faded away', (WidgetTester tester) async {
const String testText = 'lorem ipsum'; const String testText = 'lorem ipsum';
await tester.pumpWidget( await tester.pumpWidget(
...@@ -4494,7 +4506,7 @@ void main() { ...@@ -4494,7 +4506,7 @@ void main() {
expect(right.opacity.value, equals(1.0)); expect(right.opacity.value, equals(1.0));
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS })); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.macOS }));
testWidgets('Long press shows handles and toolbar', (WidgetTester tester) async { testWidgetsWithLeakTracking('Long press shows handles and toolbar', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
const MaterialApp( const MaterialApp(
home: Material( home: Material(
...@@ -4513,7 +4525,7 @@ void main() { ...@@ -4513,7 +4525,7 @@ void main() {
expect(editableText.selectionOverlay!.toolbarIsVisible, isTrue); expect(editableText.selectionOverlay!.toolbarIsVisible, isTrue);
}); });
testWidgets('Double tap shows handles and toolbar', (WidgetTester tester) async { testWidgetsWithLeakTracking('Double tap shows handles and toolbar', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
const MaterialApp( const MaterialApp(
home: Material( home: Material(
...@@ -4534,7 +4546,7 @@ void main() { ...@@ -4534,7 +4546,7 @@ void main() {
expect(editableText.selectionOverlay!.toolbarIsVisible, isTrue); expect(editableText.selectionOverlay!.toolbarIsVisible, isTrue);
}); });
testWidgets( testWidgetsWithLeakTracking(
'Mouse tap does not show handles nor toolbar', 'Mouse tap does not show handles nor toolbar',
(WidgetTester tester) async { (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
...@@ -4562,7 +4574,7 @@ void main() { ...@@ -4562,7 +4574,7 @@ void main() {
}, },
); );
testWidgets( testWidgetsWithLeakTracking(
'Mouse long press does not show handles nor toolbar', 'Mouse long press does not show handles nor toolbar',
(WidgetTester tester) async { (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
...@@ -4590,7 +4602,7 @@ void main() { ...@@ -4590,7 +4602,7 @@ void main() {
}, },
); );
testWidgets( testWidgetsWithLeakTracking(
'Mouse double tap does not show handles nor toolbar', 'Mouse double tap does not show handles nor toolbar',
(WidgetTester tester) async { (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
...@@ -4622,7 +4634,7 @@ void main() { ...@@ -4622,7 +4634,7 @@ void main() {
}, },
); );
testWidgets('text span with tap gesture recognizer works in selectable rich text', (WidgetTester tester) async { testWidgetsWithLeakTracking('text span with tap gesture recognizer works in selectable rich text', (WidgetTester tester) async {
int spyTaps = 0; int spyTaps = 0;
final TapGestureRecognizer spyRecognizer = TapGestureRecognizer() final TapGestureRecognizer spyRecognizer = TapGestureRecognizer()
..onTap = () { ..onTap = () {
...@@ -4672,7 +4684,7 @@ void main() { ...@@ -4672,7 +4684,7 @@ void main() {
expect(spyTaps, 1); expect(spyTaps, 1);
}); });
testWidgets('text span with long press gesture recognizer works in selectable rich text', (WidgetTester tester) async { testWidgetsWithLeakTracking('text span with long press gesture recognizer works in selectable rich text', (WidgetTester tester) async {
int spyLongPress = 0; int spyLongPress = 0;
final LongPressGestureRecognizer spyRecognizer = LongPressGestureRecognizer() final LongPressGestureRecognizer spyRecognizer = LongPressGestureRecognizer()
..onLongPress = () { ..onLongPress = () {
...@@ -4723,7 +4735,7 @@ void main() { ...@@ -4723,7 +4735,7 @@ void main() {
expect(spyLongPress, 1); expect(spyLongPress, 1);
}); });
testWidgets('SelectableText changes mouse cursor when hovered', (WidgetTester tester) async { testWidgetsWithLeakTracking('SelectableText changes mouse cursor when hovered', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
const MaterialApp( const MaterialApp(
home: Material( home: Material(
...@@ -4742,7 +4754,7 @@ void main() { ...@@ -4742,7 +4754,7 @@ void main() {
expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text); expect(RendererBinding.instance.mouseTracker.debugDeviceActiveCursor(1), SystemMouseCursors.text);
}); });
testWidgets('The handles show after pressing Select All', (WidgetTester tester) async { testWidgetsWithLeakTracking('The handles show after pressing Select All', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
const MaterialApp( const MaterialApp(
home: Material( home: Material(
...@@ -4779,7 +4791,7 @@ void main() { ...@@ -4779,7 +4791,7 @@ void main() {
}), }),
); );
testWidgets('The Select All calls on selection changed', (WidgetTester tester) async { testWidgetsWithLeakTracking('The Select All calls on selection changed', (WidgetTester tester) async {
TextSelection? newSelection; TextSelection? newSelection;
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
...@@ -4815,7 +4827,7 @@ void main() { ...@@ -4815,7 +4827,7 @@ void main() {
}), }),
); );
testWidgets('The Select All calls on selection changed with a mouse on windows and linux', (WidgetTester tester) async { testWidgetsWithLeakTracking('The Select All calls on selection changed with a mouse on windows and linux', (WidgetTester tester) async {
const String string = 'abc def ghi'; const String string = 'abc def ghi';
TextSelection? newSelection; TextSelection? newSelection;
await tester.pumpWidget( await tester.pumpWidget(
...@@ -4857,7 +4869,7 @@ void main() { ...@@ -4857,7 +4869,7 @@ void main() {
}), }),
); );
testWidgets('Does not show handles when updated from the web engine', (WidgetTester tester) async { testWidgetsWithLeakTracking('Does not show handles when updated from the web engine', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
const MaterialApp( const MaterialApp(
home: Material( home: Material(
...@@ -4897,7 +4909,7 @@ void main() { ...@@ -4897,7 +4909,7 @@ void main() {
} }
}); });
testWidgets('onSelectionChanged is called when selection changes', (WidgetTester tester) async { testWidgetsWithLeakTracking('onSelectionChanged is called when selection changes', (WidgetTester tester) async {
int onSelectionChangedCallCount = 0; int onSelectionChangedCallCount = 0;
await tester.pumpWidget( await tester.pumpWidget(
...@@ -4927,7 +4939,7 @@ void main() { ...@@ -4927,7 +4939,7 @@ void main() {
expect(onSelectionChangedCallCount, equals(3)); expect(onSelectionChangedCallCount, equals(3));
}); });
testWidgets('selecting a space selects the previous word on mobile', (WidgetTester tester) async { testWidgetsWithLeakTracking('selecting a space selects the previous word on mobile', (WidgetTester tester) async {
TextSelection? selection; TextSelection? selection;
await tester.pumpWidget( await tester.pumpWidget(
...@@ -5028,7 +5040,7 @@ void main() { ...@@ -5028,7 +5040,7 @@ void main() {
expect(selection!.extentOffset, 1); expect(selection!.extentOffset, 1);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.macOS, TargetPlatform.windows, TargetPlatform.linux, TargetPlatform.fuchsia })); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.macOS, TargetPlatform.windows, TargetPlatform.linux, TargetPlatform.fuchsia }));
testWidgets('double tapping a space selects the previous word on mobile', (WidgetTester tester) async { testWidgetsWithLeakTracking('double tapping a space selects the previous word on mobile', (WidgetTester tester) async {
TextSelection? selection; TextSelection? selection;
await tester.pumpWidget( await tester.pumpWidget(
...@@ -5093,7 +5105,7 @@ void main() { ...@@ -5093,7 +5105,7 @@ void main() {
expect(selection!.extentOffset, 14); expect(selection!.extentOffset, 14);
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.android })); }, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.android }));
testWidgets('text selection style 1', (WidgetTester tester) async { testWidgetsWithLeakTracking('text selection style 1', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
theme: ThemeData(useMaterial3: false), theme: ThemeData(useMaterial3: false),
...@@ -5146,7 +5158,7 @@ void main() { ...@@ -5146,7 +5158,7 @@ void main() {
); );
}); });
testWidgets('text selection style 2', (WidgetTester tester) async { testWidgetsWithLeakTracking('text selection style 2', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
theme: ThemeData(useMaterial3: false), theme: ThemeData(useMaterial3: false),
...@@ -5198,7 +5210,7 @@ void main() { ...@@ -5198,7 +5210,7 @@ void main() {
); );
}); });
testWidgets('keeps alive when has focus', (WidgetTester tester) async { testWidgetsWithLeakTracking('keeps alive when has focus', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
MaterialApp( MaterialApp(
theme: ThemeData(useMaterial3: false), theme: ThemeData(useMaterial3: false),
...@@ -5290,7 +5302,7 @@ void main() { ...@@ -5290,7 +5302,7 @@ void main() {
late ValueNotifier<MagnifierInfo> magnifierInfo; late ValueNotifier<MagnifierInfo> magnifierInfo;
final Widget fakeMagnifier = Container(key: UniqueKey()); final Widget fakeMagnifier = Container(key: UniqueKey());
testWidgets( testWidgetsWithLeakTracking(
'Can drag handles to show, unshow, and update magnifier', 'Can drag handles to show, unshow, and update magnifier',
(WidgetTester tester) async { (WidgetTester tester) async {
const String testValue = 'abc def ghi'; const String testValue = 'abc def ghi';
...@@ -5359,7 +5371,7 @@ void main() { ...@@ -5359,7 +5371,7 @@ void main() {
}); });
}); });
testWidgets('SelectableText text span style is merged with default text style', (WidgetTester tester) async { testWidgetsWithLeakTracking('SelectableText text span style is merged with default text style', (WidgetTester tester) async {
// This is a regression test for https://github.com/flutter/flutter/issues/71389 // This is a regression test for https://github.com/flutter/flutter/issues/71389
const TextStyle textStyle = TextStyle(color: Color(0xff00ff00), fontSize: 12.0); const TextStyle textStyle = TextStyle(color: Color(0xff00ff00), fontSize: 12.0);
...@@ -5380,7 +5392,7 @@ void main() { ...@@ -5380,7 +5392,7 @@ void main() {
expect(editableText.style.fontSize, textStyle.fontSize); expect(editableText.style.fontSize, textStyle.fontSize);
}); });
testWidgets('SelectableText text span style is merged with default text style', (WidgetTester tester) async { testWidgetsWithLeakTracking('SelectableText text span style is merged with default text style', (WidgetTester tester) async {
TextSelection? selection; TextSelection? selection;
int count = 0; int count = 0;
...@@ -5420,7 +5432,7 @@ void main() { ...@@ -5420,7 +5432,7 @@ void main() {
expect(count, 1); // The `onSelectionChanged` will not be triggered. expect(count, 1); // The `onSelectionChanged` will not be triggered.
}); });
testWidgets("Off-screen selected text doesn't throw exception", (WidgetTester tester) async { testWidgetsWithLeakTracking("Off-screen selected text doesn't throw exception", (WidgetTester tester) async {
// This is a regression test for https://github.com/flutter/flutter/issues/123527 // This is a regression test for https://github.com/flutter/flutter/issues/123527
TextSelection? selection; TextSelection? selection;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment