blob: 4f340185b6395cdbe8ab1e7fccec98e4b6b5f101 [file]
// 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';
void main() {
final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized();
testWidgets('asserts when built on an unsupported device', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'one two three',
);
addTearDown(controller.dispose);
await tester.pumpWidget(
// By default, MediaQueryData.supportsShowingSystemContextMenu is false.
MaterialApp(
home: Scaffold(
body: Center(
child: TextField(
controller: controller,
contextMenuBuilder: (BuildContext context, EditableTextState editableTextState) {
return SystemContextMenu.editableText(
editableTextState: editableTextState,
);
},
),
),
),
),
);
await tester.tap(find.byType(TextField));
final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText));
expect(state.showToolbar(), true);
await tester.pump();
expect(tester.takeException(), isAssertionError);
},
skip: kIsWeb, // [intended]
variant: TargetPlatformVariant.all(),
);
testWidgets('asserts when built on web', (WidgetTester tester) async {
// Disable the browser context menu so that contextMenuBuilder will be used.
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(
SystemChannels.contextMenu,
(MethodCall call) {
// Just complete successfully, so that BrowserContextMenu thinks that
// the engine successfully received its call.
return Future<void>.value();
},
);
await BrowserContextMenu.disableContextMenu();
addTearDown(() async {
await BrowserContextMenu.enableContextMenu();
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(SystemChannels.contextMenu, null);
});
final TextEditingController controller = TextEditingController(
text: 'one two three',
);
addTearDown(controller.dispose);
await tester.pumpWidget(
// By default, MediaQueryData.supportsShowingSystemContextMenu is false.
MaterialApp(
home: Scaffold(
body: Center(
child: TextField(
controller: controller,
contextMenuBuilder: (BuildContext context, EditableTextState editableTextState) {
return SystemContextMenu.editableText(
editableTextState: editableTextState,
);
},
),
),
),
),
);
await tester.tap(find.byType(TextField));
final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText));
expect(state.showToolbar(), true);
await tester.pump();
expect(tester.takeException(), isAssertionError);
},
skip: !kIsWeb, // [intended]
variant: TargetPlatformVariant.only(TargetPlatform.iOS),
);
testWidgets('can be shown and hidden like a normal context menu', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'one two three',
);
addTearDown(controller.dispose);
await tester.pumpWidget(
Builder(
builder: (BuildContext context) {
final MediaQueryData mediaQueryData = MediaQuery.of(context);
return MediaQuery(
data: mediaQueryData.copyWith(
supportsShowingSystemContextMenu: true,
),
child: MaterialApp(
home: Scaffold(
body: Center(
child: TextField(
controller: controller,
contextMenuBuilder: (BuildContext context, EditableTextState editableTextState) {
return SystemContextMenu.editableText(
editableTextState: editableTextState,
);
},
),
),
),
),
);
},
),
);
expect(find.byType(SystemContextMenu), findsNothing);
await tester.tap(find.byType(TextField));
final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText));
expect(state.showToolbar(), true);
await tester.pump();
expect(find.byType(SystemContextMenu), findsOneWidget);
state.hideToolbar();
await tester.pump();
expect(find.byType(SystemContextMenu), findsNothing);
},
skip: kIsWeb, // [intended]
variant: TargetPlatformVariant.only(TargetPlatform.iOS),
);
testWidgets('can be updated.', (WidgetTester tester) async {
final List<Map<String, double>> targetRects = <Map<String, double>>[];
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(SystemChannels.platform, (MethodCall methodCall) async {
if (methodCall.method == 'ContextMenu.showSystemContextMenu') {
final Map<String, dynamic> arguments = methodCall.arguments as Map<String, dynamic>;
final Map<String, dynamic> untypedTargetRect = arguments['targetRect'] as Map<String, dynamic>;
final Map<String, double> lastTargetRect = untypedTargetRect.map((String key, dynamic value) {
return MapEntry<String, double>(key, value as double);
});
targetRects.add(lastTargetRect);
}
return;
});
addTearDown(() {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(SystemChannels.platform, null);
});
final TextEditingController controller = TextEditingController(
text: 'one two three',
);
addTearDown(controller.dispose);
await tester.pumpWidget(
Builder(
builder: (BuildContext context) {
final MediaQueryData mediaQueryData = MediaQuery.of(context);
return MediaQuery(
data: mediaQueryData.copyWith(
supportsShowingSystemContextMenu: true,
),
child: MaterialApp(
home: Scaffold(
body: Center(
child: TextField(
controller: controller,
contextMenuBuilder: (BuildContext context, EditableTextState editableTextState) {
return SystemContextMenu.editableText(
editableTextState: editableTextState,
);
},
),
),
),
),
);
},
),
);
expect(targetRects, isEmpty);
await tester.tap(find.byType(TextField));
final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText));
expect(state.showToolbar(), true);
await tester.pump();
expect(targetRects, hasLength(1));
expect(targetRects.last, containsPair('width', 0.0));
controller.selection = const TextSelection(
baseOffset: 4,
extentOffset: 7,
);
await tester.pumpAndSettle();
expect(targetRects, hasLength(2));
expect(targetRects.last['width'], greaterThan(0.0));
},
skip: kIsWeb, // [intended]
variant: TargetPlatformVariant.only(TargetPlatform.iOS),
);
testWidgets('can be rebuilt', (WidgetTester tester) async {
final TextEditingController controller = TextEditingController(
text: 'one two three',
);
addTearDown(controller.dispose);
late StateSetter setState;
await tester.pumpWidget(
Builder(
builder: (BuildContext context) {
final MediaQueryData mediaQueryData = MediaQuery.of(context);
return MediaQuery(
data: mediaQueryData.copyWith(
supportsShowingSystemContextMenu: true,
),
child: MaterialApp(
home: Scaffold(
body: Center(
child: StatefulBuilder(
builder: (BuildContext context, StateSetter localSetState) {
setState = localSetState;
return TextField(
controller: controller,
contextMenuBuilder: (BuildContext context, EditableTextState editableTextState) {
return SystemContextMenu.editableText(
editableTextState: editableTextState,
);
},
);
},
),
),
),
),
);
},
),
);
await tester.tap(find.byType(TextField));
final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText));
expect(state.showToolbar(), true);
await tester.pump();
setState(() {});
await tester.pumpAndSettle();
expect(tester.takeException(), isNull);
},
skip: kIsWeb, // [intended]
variant: TargetPlatformVariant.only(TargetPlatform.iOS),
);
testWidgets('can handle multiple instances', (WidgetTester tester) async {
final TextEditingController controller1 = TextEditingController(
text: 'one two three',
);
addTearDown(controller1.dispose);
final TextEditingController controller2 = TextEditingController(
text: 'four five six',
);
addTearDown(controller2.dispose);
final GlobalKey field1Key = GlobalKey();
final GlobalKey field2Key = GlobalKey();
final GlobalKey menu1Key = GlobalKey();
final GlobalKey menu2Key = GlobalKey();
await tester.pumpWidget(
Builder(
builder: (BuildContext context) {
final MediaQueryData mediaQueryData = MediaQuery.of(context);
return MediaQuery(
data: mediaQueryData.copyWith(
supportsShowingSystemContextMenu: true,
),
child: MaterialApp(
home: Scaffold(
body: Center(
child: Column(
children: <Widget>[
TextField(
key: field1Key,
controller: controller1,
contextMenuBuilder: (BuildContext context, EditableTextState editableTextState) {
return SystemContextMenu.editableText(
key: menu1Key,
editableTextState: editableTextState,
);
},
),
TextField(
key: field2Key,
controller: controller2,
contextMenuBuilder: (BuildContext context, EditableTextState editableTextState) {
return SystemContextMenu.editableText(
key: menu2Key,
editableTextState: editableTextState,
);
},
),
],
),
),
),
),
);
},
),
);
expect(find.byType(SystemContextMenu), findsNothing);
await tester.tap(find.byKey(field1Key));
final EditableTextState state1 = tester.state<EditableTextState>(
find.descendant(
of: find.byKey(field1Key),
matching: find.byType(EditableText),
),
);
expect(state1.showToolbar(), true);
await tester.pump();
expect(find.byKey(menu1Key), findsOneWidget);
expect(find.byKey(menu2Key), findsNothing);
// In a real app, this message is sent by iOS when the user taps anywhere
// outside of the system context menu.
final ByteData? messageBytes = const JSONMessageCodec().encodeMessage(<String, dynamic>{
'method': 'ContextMenu.onDismissSystemContextMenu',
});
await binding.defaultBinaryMessenger.handlePlatformMessage(
'flutter/platform',
messageBytes,
(ByteData? data) {},
);
await tester.pump();
expect(find.byType(SystemContextMenu), findsNothing);
await tester.tap(find.byKey(field2Key));
final EditableTextState state2 = tester.state<EditableTextState>(
find.descendant(
of: find.byKey(field2Key),
matching: find.byType(EditableText),
),
);
expect(state2.showToolbar(), true);
await tester.pump();
expect(find.byKey(menu1Key), findsNothing);
expect(find.byKey(menu2Key), findsOneWidget);
},
skip: kIsWeb, // [intended]
variant: TargetPlatformVariant.only(TargetPlatform.iOS),
);
testWidgets('asserts when built with no text input connection',
experimentalLeakTesting: LeakTesting.settings.withIgnoredAll(), // leaking by design because of exception
(WidgetTester tester) async {
SystemContextMenu? systemContextMenu;
late StateSetter setState;
await tester.pumpWidget(
Builder(
builder: (BuildContext context) {
final MediaQueryData mediaQueryData = MediaQuery.of(context);
return MediaQuery(
data: mediaQueryData.copyWith(
supportsShowingSystemContextMenu: true,
),
child: MaterialApp(
home: Scaffold(
body: StatefulBuilder(
builder: (BuildContext context, StateSetter localSetState) {
setState = localSetState;
return Column(
children: <Widget>[
const TextField(),
if (systemContextMenu != null)
systemContextMenu!,
],
);
},
),
),
),
);
},
),
);
// No SystemContextMenu yet, so no assertion error.
expect(tester.takeException(), isNull);
// Add the SystemContextMenu and receive an assertion since there is no
// active text input connection.
setState(() {
final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText));
systemContextMenu = SystemContextMenu.editableText(
editableTextState: state,
);
});
final FlutterExceptionHandler? oldHandler = FlutterError.onError;
dynamic exception;
FlutterError.onError = (FlutterErrorDetails details) {
exception ??= details.exception;
};
addTearDown(() {
FlutterError.onError = oldHandler;
});
await tester.pump();
expect(exception, isAssertionError);
expect(exception.toString(), contains('only be shown for an active text input connection'));
},
skip: kIsWeb, // [intended]
variant: TargetPlatformVariant.only(TargetPlatform.iOS),
);
testWidgets('does not assert when built with an active text input connection', (WidgetTester tester) async {
SystemContextMenu? systemContextMenu;
late StateSetter setState;
await tester.pumpWidget(
Builder(
builder: (BuildContext context) {
final MediaQueryData mediaQueryData = MediaQuery.of(context);
return MediaQuery(
data: mediaQueryData.copyWith(
supportsShowingSystemContextMenu: true,
),
child: MaterialApp(
home: Scaffold(
body: StatefulBuilder(
builder: (BuildContext context, StateSetter localSetState) {
setState = localSetState;
return Column(
children: <Widget>[
const TextField(),
if (systemContextMenu != null)
systemContextMenu!,
],
);
},
),
),
),
);
},
),
);
// No SystemContextMenu yet, so no assertion error.
expect(tester.takeException(), isNull);
// Tap the field to open a text input connection.
await tester.tap(find.byType(TextField));
await tester.pump();
// Add the SystemContextMenu and expect no error.
setState(() {
final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText));
systemContextMenu = SystemContextMenu.editableText(
editableTextState: state,
);
});
final FlutterExceptionHandler? oldHandler = FlutterError.onError;
dynamic exception;
FlutterError.onError = (FlutterErrorDetails details) {
exception ??= details.exception;
};
addTearDown(() {
FlutterError.onError = oldHandler;
});
await tester.pump();
expect(exception, isNull);
},
skip: kIsWeb, // [intended]
variant: TargetPlatformVariant.only(TargetPlatform.iOS),
);
}