| // 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. |
| |
| // TODO(gspencergoog): Remove this tag once this test's state leaks/test |
| // dependencies have been fixed. |
| // https://github.com/flutter/flutter/issues/85160 |
| // Fails with "flutter test --test-randomize-ordering-seed=20210721" |
| @Tags(<String>['no-shuffle']) |
| |
| import 'package:flutter/foundation.dart'; |
| import 'package:flutter/material.dart'; |
| import 'package:flutter/rendering.dart'; |
| import 'package:flutter/scheduler.dart'; |
| import 'package:flutter/services.dart'; |
| import 'package:flutter_driver/flutter_driver.dart'; |
| import 'package:flutter_driver/src/extension/extension.dart'; |
| import 'package:flutter_test/flutter_test.dart'; |
| |
| import 'stubs/stub_command.dart'; |
| import 'stubs/stub_command_extension.dart'; |
| import 'stubs/stub_finder.dart'; |
| import 'stubs/stub_finder_extension.dart'; |
| |
| Future<void> silenceDriverLogger(AsyncCallback callback) async { |
| final DriverLogCallback oldLogger = driverLog; |
| driverLog = (String source, String message) { }; |
| try { |
| await callback(); |
| } finally { |
| driverLog = oldLogger; |
| } |
| } |
| |
| void main() { |
| group('waitUntilNoTransientCallbacks', () { |
| late FlutterDriverExtension driverExtension; |
| Map<String, dynamic>? result; |
| int messageId = 0; |
| final List<String?> log = <String?>[]; |
| |
| setUp(() { |
| result = null; |
| driverExtension = FlutterDriverExtension((String? message) async { log.add(message); return (messageId += 1).toString(); }, false, true); |
| }); |
| |
| testWidgets('returns immediately when transient callback queue is empty', (WidgetTester tester) async { |
| driverExtension.call(const WaitForCondition(NoTransientCallbacks()).serialize()) |
| .then<void>(expectAsync1((Map<String, dynamic> r) { |
| result = r; |
| })); |
| |
| await tester.idle(); |
| expect( |
| result, |
| <String, dynamic>{ |
| 'isError': false, |
| 'response': <String, dynamic>{}, |
| }, |
| ); |
| }); |
| |
| testWidgets('waits until no transient callbacks', (WidgetTester tester) async { |
| SchedulerBinding.instance.scheduleFrameCallback((_) { |
| // Intentionally blank. We only care about existence of a callback. |
| }); |
| |
| driverExtension.call(const WaitForCondition(NoTransientCallbacks()).serialize()) |
| .then<void>(expectAsync1((Map<String, dynamic> r) { |
| result = r; |
| })); |
| |
| // Nothing should happen until the next frame. |
| await tester.idle(); |
| expect(result, isNull); |
| |
| // NOW we should receive the result. |
| await tester.pump(); |
| expect( |
| result, |
| <String, dynamic>{ |
| 'isError': false, |
| 'response': <String, dynamic>{}, |
| }, |
| ); |
| }); |
| |
| testWidgets('handler', (WidgetTester tester) async { |
| expect(log, isEmpty); |
| final Map<String, dynamic> response = await driverExtension.call(const RequestData('hello').serialize()); |
| final RequestDataResult result = RequestDataResult.fromJson(response['response'] as Map<String, dynamic>); |
| expect(log, <String>['hello']); |
| expect(result.message, '1'); |
| }); |
| }); |
| |
| group('waitForCondition', () { |
| late FlutterDriverExtension driverExtension; |
| Map<String, dynamic>? result; |
| int messageId = 0; |
| final List<String?> log = <String?>[]; |
| |
| setUp(() { |
| result = null; |
| driverExtension = FlutterDriverExtension((String? message) async { log.add(message); return (messageId += 1).toString(); }, false, true); |
| }); |
| |
| testWidgets('waiting for NoTransientCallbacks returns immediately when transient callback queue is empty', (WidgetTester tester) async { |
| driverExtension.call(const WaitForCondition(NoTransientCallbacks()).serialize()) |
| .then<void>(expectAsync1((Map<String, dynamic> r) { |
| result = r; |
| })); |
| |
| await tester.idle(); |
| expect( |
| result, |
| <String, dynamic>{ |
| 'isError': false, |
| 'response': <String, dynamic>{}, |
| }, |
| ); |
| }); |
| |
| testWidgets('waiting for NoTransientCallbacks returns until no transient callbacks', (WidgetTester tester) async { |
| SchedulerBinding.instance.scheduleFrameCallback((_) { |
| // Intentionally blank. We only care about existence of a callback. |
| }); |
| |
| driverExtension.call(const WaitForCondition(NoTransientCallbacks()).serialize()) |
| .then<void>(expectAsync1((Map<String, dynamic> r) { |
| result = r; |
| })); |
| |
| // Nothing should happen until the next frame. |
| await tester.idle(); |
| expect(result, isNull); |
| |
| // NOW we should receive the result. |
| await tester.pump(); |
| expect( |
| result, |
| <String, dynamic>{ |
| 'isError': false, |
| 'response': <String, dynamic>{}, |
| }, |
| ); |
| }); |
| |
| testWidgets('waiting for NoPendingFrame returns immediately when frame is synced', ( |
| WidgetTester tester) async { |
| driverExtension.call(const WaitForCondition(NoPendingFrame()).serialize()) |
| .then<void>(expectAsync1((Map<String, dynamic> r) { |
| result = r; |
| })); |
| |
| await tester.idle(); |
| expect( |
| result, |
| <String, dynamic>{ |
| 'isError': false, |
| 'response': <String, dynamic>{}, |
| }, |
| ); |
| }); |
| |
| testWidgets('waiting for NoPendingFrame returns until no pending scheduled frame', (WidgetTester tester) async { |
| SchedulerBinding.instance.scheduleFrame(); |
| |
| driverExtension.call(const WaitForCondition(NoPendingFrame()).serialize()) |
| .then<void>(expectAsync1((Map<String, dynamic> r) { |
| result = r; |
| })); |
| |
| // Nothing should happen until the next frame. |
| await tester.idle(); |
| expect(result, isNull); |
| |
| // NOW we should receive the result. |
| await tester.pump(); |
| expect( |
| result, |
| <String, dynamic>{ |
| 'isError': false, |
| 'response': <String, dynamic>{}, |
| }, |
| ); |
| }); |
| |
| testWidgets( |
| 'waiting for combined conditions returns immediately', (WidgetTester tester) async { |
| const SerializableWaitCondition combinedCondition = |
| CombinedCondition(<SerializableWaitCondition>[NoTransientCallbacks(), NoPendingFrame()]); |
| driverExtension.call(const WaitForCondition(combinedCondition).serialize()) |
| .then<void>(expectAsync1((Map<String, dynamic> r) { |
| result = r; |
| })); |
| |
| await tester.idle(); |
| expect( |
| result, |
| <String, dynamic>{ |
| 'isError': false, |
| 'response': <String, dynamic>{}, |
| }, |
| ); |
| }); |
| |
| testWidgets( |
| 'waiting for combined conditions returns until no transient callbacks', (WidgetTester tester) async { |
| SchedulerBinding.instance.scheduleFrame(); |
| SchedulerBinding.instance.scheduleFrameCallback((_) { |
| // Intentionally blank. We only care about existence of a callback. |
| }); |
| |
| const SerializableWaitCondition combinedCondition = |
| CombinedCondition(<SerializableWaitCondition>[NoTransientCallbacks(), NoPendingFrame()]); |
| driverExtension.call(const WaitForCondition(combinedCondition).serialize()) |
| .then<void>(expectAsync1((Map<String, dynamic> r) { |
| result = r; |
| })); |
| |
| // Nothing should happen until the next frame. |
| await tester.idle(); |
| expect(result, isNull); |
| |
| // NOW we should receive the result. |
| await tester.pump(); |
| expect( |
| result, |
| <String, dynamic>{ |
| 'isError': false, |
| 'response': <String, dynamic>{}, |
| }, |
| ); |
| }); |
| |
| testWidgets( |
| 'waiting for combined conditions returns until no pending scheduled frame', (WidgetTester tester) async { |
| SchedulerBinding.instance.scheduleFrame(); |
| SchedulerBinding.instance.scheduleFrameCallback((_) { |
| // Intentionally blank. We only care about existence of a callback. |
| }); |
| |
| const SerializableWaitCondition combinedCondition = |
| CombinedCondition(<SerializableWaitCondition>[NoPendingFrame(), NoTransientCallbacks()]); |
| driverExtension.call(const WaitForCondition(combinedCondition).serialize()) |
| .then<void>(expectAsync1((Map<String, dynamic> r) { |
| result = r; |
| })); |
| |
| // Nothing should happen until the next frame. |
| await tester.idle(); |
| expect(result, isNull); |
| |
| // NOW we should receive the result. |
| await tester.pump(); |
| expect( |
| result, |
| <String, dynamic>{ |
| 'isError': false, |
| 'response': <String, dynamic>{}, |
| }, |
| ); |
| }); |
| |
| testWidgets( |
| 'waiting for NoPendingPlatformMessages returns immediately when there are no platform messages', (WidgetTester tester) async { |
| driverExtension |
| .call(const WaitForCondition(NoPendingPlatformMessages()).serialize()) |
| .then<void>(expectAsync1((Map<String, dynamic> r) { |
| result = r; |
| })); |
| |
| await tester.idle(); |
| expect( |
| result, |
| <String, dynamic>{ |
| 'isError': false, |
| 'response': <String, dynamic>{}, |
| }, |
| ); |
| }); |
| |
| testWidgets( |
| 'waiting for NoPendingPlatformMessages returns until a single method channel call returns', (WidgetTester tester) async { |
| const MethodChannel channel = MethodChannel('helloChannel', JSONMethodCodec()); |
| const MessageCodec<dynamic> jsonMessage = JSONMessageCodec(); |
| tester.binding.defaultBinaryMessenger.setMockMessageHandler( |
| 'helloChannel', (ByteData? message) { |
| return Future<ByteData>.delayed( |
| const Duration(milliseconds: 10), |
| () => jsonMessage.encodeMessage(<dynamic>['hello world'])!); |
| }); |
| channel.invokeMethod<String>('sayHello', 'hello'); |
| |
| driverExtension |
| .call(const WaitForCondition(NoPendingPlatformMessages()).serialize()) |
| .then<void>(expectAsync1((Map<String, dynamic> r) { |
| result = r; |
| })); |
| |
| // The channel message are delayed for 10 milliseconds, so nothing happens yet. |
| await tester.pump(const Duration(milliseconds: 5)); |
| expect(result, isNull); |
| |
| // Now we receive the result. |
| await tester.pump(const Duration(milliseconds: 5)); |
| expect( |
| result, |
| <String, dynamic>{ |
| 'isError': false, |
| 'response': <String, dynamic>{}, |
| }, |
| ); |
| }); |
| |
| testWidgets( |
| 'waiting for NoPendingPlatformMessages returns until both method channel calls return', (WidgetTester tester) async { |
| const MessageCodec<dynamic> jsonMessage = JSONMessageCodec(); |
| // Configures channel 1 |
| const MethodChannel channel1 = MethodChannel('helloChannel1', JSONMethodCodec()); |
| tester.binding.defaultBinaryMessenger.setMockMessageHandler( |
| 'helloChannel1', (ByteData? message) { |
| return Future<ByteData>.delayed( |
| const Duration(milliseconds: 10), |
| () => jsonMessage.encodeMessage(<dynamic>['hello world'])!); |
| }); |
| |
| // Configures channel 2 |
| const MethodChannel channel2 = MethodChannel('helloChannel2', JSONMethodCodec()); |
| tester.binding.defaultBinaryMessenger.setMockMessageHandler( |
| 'helloChannel2', (ByteData? message) { |
| return Future<ByteData>.delayed( |
| const Duration(milliseconds: 20), |
| () => jsonMessage.encodeMessage(<dynamic>['hello world'])!); |
| }); |
| |
| channel1.invokeMethod<String>('sayHello', 'hello'); |
| channel2.invokeMethod<String>('sayHello', 'hello'); |
| |
| driverExtension |
| .call(const WaitForCondition(NoPendingPlatformMessages()).serialize()) |
| .then<void>(expectAsync1((Map<String, dynamic> r) { |
| result = r; |
| })); |
| |
| // Neither of the channel responses is received, so nothing happens yet. |
| await tester.pump(const Duration(milliseconds: 5)); |
| expect(result, isNull); |
| |
| // Result of channel 1 is received, but channel 2 is still pending, so still waiting. |
| await tester.pump(const Duration(milliseconds: 10)); |
| expect(result, isNull); |
| |
| // Both of the results are received. Now we receive the result. |
| await tester.pump(const Duration(milliseconds: 30)); |
| expect( |
| result, |
| <String, dynamic>{ |
| 'isError': false, |
| 'response': <String, dynamic>{}, |
| }, |
| ); |
| }); |
| |
| testWidgets( |
| 'waiting for NoPendingPlatformMessages returns until new method channel call returns', (WidgetTester tester) async { |
| const MessageCodec<dynamic> jsonMessage = JSONMessageCodec(); |
| // Configures channel 1 |
| const MethodChannel channel1 = MethodChannel('helloChannel1', JSONMethodCodec()); |
| tester.binding.defaultBinaryMessenger.setMockMessageHandler( |
| 'helloChannel1', (ByteData? message) { |
| return Future<ByteData>.delayed( |
| const Duration(milliseconds: 10), |
| () => jsonMessage.encodeMessage(<dynamic>['hello world'])!); |
| }); |
| |
| // Configures channel 2 |
| const MethodChannel channel2 = MethodChannel('helloChannel2', JSONMethodCodec()); |
| tester.binding.defaultBinaryMessenger.setMockMessageHandler( |
| 'helloChannel2', (ByteData? message) { |
| return Future<ByteData>.delayed( |
| const Duration(milliseconds: 20), |
| () => jsonMessage.encodeMessage(<dynamic>['hello world'])!); |
| }); |
| |
| channel1.invokeMethod<String>('sayHello', 'hello'); |
| |
| // Calls the waiting API before the second channel message is sent. |
| driverExtension |
| .call(const WaitForCondition(NoPendingPlatformMessages()).serialize()) |
| .then<void>(expectAsync1((Map<String, dynamic> r) { |
| result = r; |
| })); |
| |
| // The first channel message is not received, so nothing happens yet. |
| await tester.pump(const Duration(milliseconds: 5)); |
| expect(result, isNull); |
| |
| channel2.invokeMethod<String>('sayHello', 'hello'); |
| |
| // Result of channel 1 is received, but channel 2 is still pending, so still waiting. |
| await tester.pump(const Duration(milliseconds: 15)); |
| expect(result, isNull); |
| |
| // Both of the results are received. Now we receive the result. |
| await tester.pump(const Duration(milliseconds: 10)); |
| expect( |
| result, |
| <String, dynamic>{ |
| 'isError': false, |
| 'response': <String, dynamic>{}, |
| }, |
| ); |
| }); |
| |
| testWidgets( |
| 'waiting for NoPendingPlatformMessages returns until both old and new method channel calls return', (WidgetTester tester) async { |
| const MessageCodec<dynamic> jsonMessage = JSONMessageCodec(); |
| // Configures channel 1 |
| const MethodChannel channel1 = MethodChannel('helloChannel1', JSONMethodCodec()); |
| tester.binding.defaultBinaryMessenger.setMockMessageHandler( |
| 'helloChannel1', (ByteData? message) { |
| return Future<ByteData>.delayed( |
| const Duration(milliseconds: 20), |
| () => jsonMessage.encodeMessage(<dynamic>['hello world'])!); |
| }); |
| |
| // Configures channel 2 |
| const MethodChannel channel2 = MethodChannel('helloChannel2', JSONMethodCodec()); |
| tester.binding.defaultBinaryMessenger.setMockMessageHandler( |
| 'helloChannel2', (ByteData? message) { |
| return Future<ByteData>.delayed( |
| const Duration(milliseconds: 10), |
| () => jsonMessage.encodeMessage(<dynamic>['hello world'])!); |
| }); |
| |
| channel1.invokeMethod<String>('sayHello', 'hello'); |
| |
| driverExtension |
| .call(const WaitForCondition(NoPendingPlatformMessages()).serialize()) |
| .then<void>(expectAsync1((Map<String, dynamic> r) { |
| result = r; |
| })); |
| |
| // The first channel message is not received, so nothing happens yet. |
| await tester.pump(const Duration(milliseconds: 5)); |
| expect(result, isNull); |
| |
| channel2.invokeMethod<String>('sayHello', 'hello'); |
| |
| // Result of channel 2 is received, but channel 1 is still pending, so still waiting. |
| await tester.pump(const Duration(milliseconds: 10)); |
| expect(result, isNull); |
| |
| // Now we receive the result. |
| await tester.pump(const Duration(milliseconds: 5)); |
| expect( |
| result, |
| <String, dynamic>{ |
| 'isError': false, |
| 'response': <String, dynamic>{}, |
| }, |
| ); |
| }); |
| }); |
| |
| group('getSemanticsId', () { |
| late FlutterDriverExtension driverExtension; |
| setUp(() { |
| driverExtension = FlutterDriverExtension((String? arg) async => '', true, true); |
| }); |
| |
| testWidgets('works when semantics are enabled', (WidgetTester tester) async { |
| final SemanticsHandle semantics = RendererBinding.instance.pipelineOwner.ensureSemantics(); |
| await tester.pumpWidget( |
| const Text('hello', textDirection: TextDirection.ltr)); |
| |
| final Map<String, String> arguments = GetSemanticsId(const ByText('hello')).serialize(); |
| final Map<String, dynamic> response = await driverExtension.call(arguments); |
| final GetSemanticsIdResult result = GetSemanticsIdResult.fromJson(response['response'] as Map<String, dynamic>); |
| |
| expect(result.id, 1); |
| semantics.dispose(); |
| }); |
| |
| testWidgets('throws state error if no data is found', (WidgetTester tester) async { |
| await tester.pumpWidget( |
| const Text('hello', textDirection: TextDirection.ltr)); |
| |
| final Map<String, String> arguments = GetSemanticsId(const ByText('hello')).serialize(); |
| final Map<String, dynamic> response = await driverExtension.call(arguments); |
| |
| expect(response['isError'], true); |
| expect(response['response'], contains('Bad state: No semantics data found')); |
| }, semanticsEnabled: false); |
| |
| testWidgets('throws state error multiple matches are found', (WidgetTester tester) async { |
| final SemanticsHandle semantics = RendererBinding.instance.pipelineOwner.ensureSemantics(); |
| await tester.pumpWidget( |
| Directionality( |
| textDirection: TextDirection.ltr, |
| child: ListView(children: const <Widget>[ |
| SizedBox(width: 100.0, height: 100.0, child: Text('hello')), |
| SizedBox(width: 100.0, height: 100.0, child: Text('hello')), |
| ]), |
| ), |
| ); |
| |
| final Map<String, String> arguments = GetSemanticsId(const ByText('hello')).serialize(); |
| final Map<String, dynamic> response = await driverExtension.call(arguments); |
| |
| expect(response['isError'], true); |
| expect(response['response'], contains('Bad state: Found more than one element with the same ID')); |
| semantics.dispose(); |
| }); |
| }); |
| |
| testWidgets('getOffset', (WidgetTester tester) async { |
| final FlutterDriverExtension driverExtension = FlutterDriverExtension((String? arg) async => '', true, true); |
| |
| Future<Offset> getOffset(OffsetType offset) async { |
| final Map<String, String> arguments = GetOffset(ByValueKey(1), offset).serialize(); |
| final Map<String, dynamic> response = await driverExtension.call(arguments); |
| final GetOffsetResult result = GetOffsetResult.fromJson(response['response'] as Map<String, dynamic>); |
| return Offset(result.dx, result.dy); |
| } |
| |
| await tester.pumpWidget( |
| Align( |
| alignment: Alignment.topLeft, |
| child: Transform.translate( |
| offset: const Offset(40, 30), |
| child: const SizedBox( |
| key: ValueKey<int>(1), |
| width: 100, |
| height: 120, |
| ), |
| ), |
| ), |
| ); |
| |
| expect(await getOffset(OffsetType.topLeft), const Offset(40, 30)); |
| expect(await getOffset(OffsetType.topRight), const Offset(40 + 100.0, 30)); |
| expect(await getOffset(OffsetType.bottomLeft), const Offset(40, 30 + 120.0)); |
| expect(await getOffset(OffsetType.bottomRight), const Offset(40 + 100.0, 30 + 120.0)); |
| expect(await getOffset(OffsetType.center), const Offset(40 + (100 / 2), 30 + (120 / 2))); |
| }); |
| |
| testWidgets('getText', (WidgetTester tester) async { |
| await silenceDriverLogger(() async { |
| final FlutterDriverExtension driverExtension = FlutterDriverExtension((String? arg) async => '', true, true); |
| |
| Future<String?> getTextInternal(SerializableFinder search) async { |
| final Map<String, String> arguments = GetText(search, timeout: const Duration(seconds: 1)).serialize(); |
| final Map<String, dynamic> result = await driverExtension.call(arguments); |
| if (result['isError'] as bool) { |
| return null; |
| } |
| return GetTextResult.fromJson(result['response'] as Map<String, dynamic>).text; |
| } |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Scaffold(body:Column( |
| key: const ValueKey<String>('column'), |
| children: <Widget>[ |
| const Text('Hello1', key: ValueKey<String>('text1')), |
| SizedBox( |
| height: 25.0, |
| child: RichText( |
| key: const ValueKey<String>('text2'), |
| text: const TextSpan(text: 'Hello2'), |
| ), |
| ), |
| SizedBox( |
| height: 25.0, |
| child: EditableText( |
| key: const ValueKey<String>('text3'), |
| controller: TextEditingController(text: 'Hello3'), |
| focusNode: FocusNode(), |
| style: const TextStyle(), |
| cursorColor: Colors.red, |
| backgroundCursorColor: Colors.black, |
| ), |
| ), |
| SizedBox( |
| height: 25.0, |
| child: TextField( |
| key: const ValueKey<String>('text4'), |
| controller: TextEditingController(text: 'Hello4'), |
| ), |
| ), |
| SizedBox( |
| height: 25.0, |
| child: TextFormField( |
| key: const ValueKey<String>('text5'), |
| controller: TextEditingController(text: 'Hello5'), |
| ), |
| ), |
| SizedBox( |
| height: 25.0, |
| child: RichText( |
| key: const ValueKey<String>('text6'), |
| text: const TextSpan(children: <TextSpan>[ |
| TextSpan(text: 'Hello'), |
| TextSpan(text: ', '), |
| TextSpan(text: 'World'), |
| TextSpan(text: '!'), |
| ]), |
| ), |
| ), |
| ], |
| )) |
| ) |
| ); |
| |
| expect(await getTextInternal(ByValueKey('text1')), 'Hello1'); |
| expect(await getTextInternal(ByValueKey('text2')), 'Hello2'); |
| expect(await getTextInternal(ByValueKey('text3')), 'Hello3'); |
| expect(await getTextInternal(ByValueKey('text4')), 'Hello4'); |
| expect(await getTextInternal(ByValueKey('text5')), 'Hello5'); |
| expect(await getTextInternal(ByValueKey('text6')), 'Hello, World!'); |
| |
| // Check if error thrown for other types |
| final Map<String, String> arguments = GetText(ByValueKey('column'), timeout: const Duration(seconds: 1)).serialize(); |
| final Map<String, dynamic> response = await driverExtension.call(arguments); |
| expect(response['isError'], true); |
| expect(response['response'], contains('is currently not supported by getText')); |
| }); |
| }); |
| |
| testWidgets('descendant finder', (WidgetTester tester) async { |
| await silenceDriverLogger(() async { |
| final FlutterDriverExtension driverExtension = FlutterDriverExtension((String? arg) async => '', true, true); |
| |
| Future<String?> getDescendantText({ String? of, bool matchRoot = false}) async { |
| final Map<String, String> arguments = GetText(Descendant( |
| of: ByValueKey(of), |
| matching: ByValueKey('text2'), |
| matchRoot: matchRoot, |
| ), timeout: const Duration(seconds: 1)).serialize(); |
| final Map<String, dynamic> result = await driverExtension.call(arguments); |
| if (result['isError'] as bool) { |
| return null; |
| } |
| return GetTextResult.fromJson(result['response'] as Map<String, dynamic>).text; |
| } |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Column( |
| key: const ValueKey<String>('column'), |
| children: const <Widget>[ |
| Text('Hello1', key: ValueKey<String>('text1')), |
| Text('Hello2', key: ValueKey<String>('text2')), |
| Text('Hello3', key: ValueKey<String>('text3')), |
| ], |
| ) |
| ) |
| ); |
| |
| expect(await getDescendantText(of: 'column'), 'Hello2'); |
| expect(await getDescendantText(of: 'column', matchRoot: true), 'Hello2'); |
| expect(await getDescendantText(of: 'text2', matchRoot: true), 'Hello2'); |
| |
| // Find nothing |
| Future<String?> result = getDescendantText(of: 'text1', matchRoot: true); |
| await tester.pump(const Duration(seconds: 2)); |
| expect(await result, null); |
| |
| result = getDescendantText(of: 'text2'); |
| await tester.pump(const Duration(seconds: 2)); |
| expect(await result, null); |
| }); |
| }); |
| |
| testWidgets('descendant finder firstMatchOnly', (WidgetTester tester) async { |
| await silenceDriverLogger(() async { |
| final FlutterDriverExtension driverExtension = FlutterDriverExtension((String? arg) async => '', true, true); |
| |
| Future<String?> getDescendantText() async { |
| final Map<String, String> arguments = GetText(Descendant( |
| of: ByValueKey('column'), |
| matching: const ByType('Text'), |
| firstMatchOnly: true, |
| ), timeout: const Duration(seconds: 1)).serialize(); |
| final Map<String, dynamic> result = await driverExtension.call(arguments); |
| if (result['isError'] as bool) { |
| return null; |
| } |
| return GetTextResult.fromJson(result['response'] as Map<String, dynamic>).text; |
| } |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Column( |
| key: const ValueKey<String>('column'), |
| children: const <Widget>[ |
| Text('Hello1', key: ValueKey<String>('text1')), |
| Text('Hello2', key: ValueKey<String>('text2')), |
| Text('Hello3', key: ValueKey<String>('text3')), |
| ], |
| ), |
| ), |
| ); |
| |
| expect(await getDescendantText(), 'Hello1'); |
| }); |
| }); |
| |
| testWidgets('ancestor finder', (WidgetTester tester) async { |
| await silenceDriverLogger(() async { |
| final FlutterDriverExtension driverExtension = FlutterDriverExtension((String? arg) async => '', true, true); |
| |
| Future<Offset?> getAncestorTopLeft({ String? of, String? matching, bool matchRoot = false}) async { |
| final Map<String, String> arguments = GetOffset(Ancestor( |
| of: ByValueKey(of), |
| matching: ByValueKey(matching), |
| matchRoot: matchRoot, |
| ), OffsetType.topLeft, timeout: const Duration(seconds: 1)).serialize(); |
| final Map<String, dynamic> response = await driverExtension.call(arguments); |
| if (response['isError'] as bool) { |
| return null; |
| } |
| final GetOffsetResult result = GetOffsetResult.fromJson(response['response'] as Map<String, dynamic>); |
| return Offset(result.dx, result.dy); |
| } |
| |
| await tester.pumpWidget( |
| MaterialApp( |
| home: Center( |
| child: SizedBox( |
| key: const ValueKey<String>('parent'), |
| height: 100, |
| width: 100, |
| child: Center( |
| child: Row( |
| children: const <Widget>[ |
| SizedBox( |
| key: ValueKey<String>('leftchild'), |
| width: 25, |
| height: 25, |
| ), |
| SizedBox( |
| key: ValueKey<String>('righttchild'), |
| width: 25, |
| height: 25, |
| ), |
| ], |
| ), |
| ), |
| ) |
| ), |
| ) |
| ); |
| |
| expect( |
| await getAncestorTopLeft(of: 'leftchild', matching: 'parent'), |
| const Offset((800 - 100) / 2, (600 - 100) / 2), |
| ); |
| expect( |
| await getAncestorTopLeft(of: 'leftchild', matching: 'parent', matchRoot: true), |
| const Offset((800 - 100) / 2, (600 - 100) / 2), |
| ); |
| expect( |
| await getAncestorTopLeft(of: 'parent', matching: 'parent', matchRoot: true), |
| const Offset((800 - 100) / 2, (600 - 100) / 2), |
| ); |
| |
| // Find nothing |
| Future<Offset?> result = getAncestorTopLeft(of: 'leftchild', matching: 'leftchild'); |
| await tester.pump(const Duration(seconds: 2)); |
| expect(await result, null); |
| |
| result = getAncestorTopLeft(of: 'leftchild', matching: 'righttchild'); |
| await tester.pump(const Duration(seconds: 2)); |
| expect(await result, null); |
| }); |
| }); |
| |
| testWidgets('ancestor finder firstMatchOnly', (WidgetTester tester) async { |
| await silenceDriverLogger(() async { |
| final FlutterDriverExtension driverExtension = FlutterDriverExtension((String? arg) async => '', true, true); |
| |
| Future<Offset?> getAncestorTopLeft() async { |
| final Map<String, String> arguments = GetOffset(Ancestor( |
| of: ByValueKey('leaf'), |
| matching: const ByType('SizedBox'), |
| firstMatchOnly: true, |
| ), OffsetType.topLeft, timeout: const Duration(seconds: 1)).serialize(); |
| final Map<String, dynamic> response = await driverExtension.call(arguments); |
| if (response['isError'] as bool) { |
| return null; |
| } |
| final GetOffsetResult result = GetOffsetResult.fromJson(response['response'] as Map<String, dynamic>); |
| return Offset(result.dx, result.dy); |
| } |
| |
| await tester.pumpWidget( |
| const MaterialApp( |
| home: Center( |
| child: SizedBox( |
| height: 200, |
| width: 200, |
| child: Center( |
| child: SizedBox( |
| height: 100, |
| width: 100, |
| child: Center( |
| child: SizedBox( |
| key: ValueKey<String>('leaf'), |
| height: 50, |
| width: 50, |
| ), |
| ), |
| ), |
| ), |
| ), |
| ), |
| ), |
| ); |
| |
| expect( |
| await getAncestorTopLeft(), |
| const Offset((800 - 100) / 2, (600 - 100) / 2), |
| ); |
| }); |
| }); |
| |
| testWidgets('GetDiagnosticsTree', (WidgetTester tester) async { |
| final FlutterDriverExtension driverExtension = FlutterDriverExtension((String? arg) async => '', true, true); |
| |
| Future<Map<String, dynamic>> getDiagnosticsTree(DiagnosticsType type, SerializableFinder finder, { int depth = 0, bool properties = true }) async { |
| final Map<String, String> arguments = GetDiagnosticsTree(finder, type, subtreeDepth: depth, includeProperties: properties).serialize(); |
| final Map<String, dynamic> response = await driverExtension.call(arguments); |
| final DiagnosticsTreeResult result = DiagnosticsTreeResult(response['response'] as Map<String, dynamic>); |
| return result.json; |
| } |
| |
| await tester.pumpWidget( |
| const Directionality( |
| textDirection: TextDirection.ltr, |
| child: Center( |
| child: Text('Hello World', key: ValueKey<String>('Text')) |
| ), |
| ), |
| ); |
| |
| // Widget |
| Map<String, dynamic> result = await getDiagnosticsTree(DiagnosticsType.widget, ByValueKey('Text')); |
| expect(result['children'], isNull); // depth: 0 |
| expect(result['widgetRuntimeType'], 'Text'); |
| |
| List<Map<String, dynamic>> properties = (result['properties']! as List<Object>).cast<Map<String, dynamic>>(); |
| Map<String, dynamic> stringProperty = properties.singleWhere((Map<String, dynamic> property) => property['name'] == 'data'); |
| expect(stringProperty['description'], '"Hello World"'); |
| expect(stringProperty['propertyType'], 'String'); |
| |
| result = await getDiagnosticsTree(DiagnosticsType.widget, ByValueKey('Text'), properties: false); |
| expect(result['widgetRuntimeType'], 'Text'); |
| expect(result['properties'], isNull); // properties: false |
| |
| result = await getDiagnosticsTree(DiagnosticsType.widget, ByValueKey('Text'), depth: 1); |
| List<Map<String, dynamic>> children = (result['children']! as List<Object>).cast<Map<String, dynamic>>(); |
| expect(children.single['children'], isNull); |
| |
| result = await getDiagnosticsTree(DiagnosticsType.widget, ByValueKey('Text'), depth: 100); |
| children = (result['children']! as List<Object>).cast<Map<String, dynamic>>(); |
| expect(children.single['children'], isEmpty); |
| |
| // RenderObject |
| result = await getDiagnosticsTree(DiagnosticsType.renderObject, ByValueKey('Text')); |
| expect(result['children'], isNull); // depth: 0 |
| expect(result['properties'], isNotNull); |
| expect(result['description'], startsWith('RenderParagraph')); |
| |
| result = await getDiagnosticsTree(DiagnosticsType.renderObject, ByValueKey('Text'), properties: false); |
| expect(result['properties'], isNull); // properties: false |
| expect(result['description'], startsWith('RenderParagraph')); |
| |
| result = await getDiagnosticsTree(DiagnosticsType.renderObject, ByValueKey('Text'), depth: 1); |
| children = (result['children']! as List<Object>).cast<Map<String, dynamic>>(); |
| final Map<String, dynamic> textSpan = children.single; |
| expect(textSpan['description'], 'TextSpan'); |
| properties = (textSpan['properties']! as List<Object>).cast<Map<String, dynamic>>(); |
| stringProperty = properties.singleWhere((Map<String, dynamic> property) => property['name'] == 'text'); |
| expect(stringProperty['description'], '"Hello World"'); |
| expect(stringProperty['propertyType'], 'String'); |
| expect(children.single['children'], isNull); |
| |
| result = await getDiagnosticsTree(DiagnosticsType.renderObject, ByValueKey('Text'), depth: 100); |
| children = (result['children']! as List<Object>).cast<Map<String, dynamic>>(); |
| expect(children.single['children'], isEmpty); |
| }); |
| |
| group('enableTextEntryEmulation', () { |
| late FlutterDriverExtension driverExtension; |
| |
| Future<Map<String, dynamic>> enterText() async { |
| final Map<String, String> arguments = const EnterText('foo').serialize(); |
| final Map<String, dynamic> result = await driverExtension.call(arguments); |
| return result; |
| } |
| |
| const Widget testWidget = MaterialApp( |
| home: Material( |
| child: Center( |
| child: TextField( |
| key: ValueKey<String>('foo'), |
| autofocus: true, |
| ), |
| ), |
| ), |
| ); |
| |
| testWidgets('enableTextEntryEmulation false', (WidgetTester tester) async { |
| driverExtension = FlutterDriverExtension((String? arg) async => '', true, false); |
| |
| await tester.pumpWidget(testWidget); |
| |
| final Map<String, dynamic> enterTextResult = await enterText(); |
| expect(enterTextResult['isError'], isTrue); |
| }); |
| |
| testWidgets('enableTextEntryEmulation true', (WidgetTester tester) async { |
| driverExtension = FlutterDriverExtension((String? arg) async => '', true, true); |
| |
| await tester.pumpWidget(testWidget); |
| |
| final Map<String, dynamic> enterTextResult = await enterText(); |
| expect(enterTextResult['isError'], isFalse); |
| }); |
| }); |
| |
| group('extension finders', () { |
| final Widget debugTree = Directionality( |
| textDirection: TextDirection.ltr, |
| child: Center( |
| child: Column( |
| key: const ValueKey<String>('Column'), |
| children: <Widget>[ |
| const Text('Foo', key: ValueKey<String>('Text1')), |
| const Text('Bar', key: ValueKey<String>('Text2')), |
| TextButton( |
| key: const ValueKey<String>('Button'), |
| onPressed: () {}, |
| child: const Text('Whatever'), |
| ), |
| ], |
| ), |
| ), |
| ); |
| |
| testWidgets('unknown extension finder', (WidgetTester tester) async { |
| final FlutterDriverExtension driverExtension = FlutterDriverExtension( |
| (String? arg) async => '', |
| true, |
| true, |
| finders: <FinderExtension>[], |
| ); |
| |
| Future<Map<String, dynamic>> getText(SerializableFinder finder) async { |
| final Map<String, String> arguments = GetText(finder, timeout: const Duration(seconds: 1)).serialize(); |
| return driverExtension.call(arguments); |
| } |
| |
| await tester.pumpWidget(debugTree); |
| |
| final Map<String, dynamic> result = await getText(StubFinder('Text1')); |
| expect(result['isError'], true); |
| expect(result['response'] is String, true); |
| expect(result['response'] as String?, contains('Unsupported search specification type Stub')); |
| }); |
| |
| testWidgets('simple extension finder', (WidgetTester tester) async { |
| final FlutterDriverExtension driverExtension = FlutterDriverExtension( |
| (String? arg) async => '', |
| true, |
| true, |
| finders: <FinderExtension>[ |
| StubFinderExtension(), |
| ], |
| ); |
| |
| Future<GetTextResult> getText(SerializableFinder finder) async { |
| final Map<String, String> arguments = GetText(finder, timeout: const Duration(seconds: 1)).serialize(); |
| final Map<String, dynamic> response = await driverExtension.call(arguments); |
| return GetTextResult.fromJson(response['response'] as Map<String, dynamic>); |
| } |
| |
| await tester.pumpWidget(debugTree); |
| |
| final GetTextResult result = await getText(StubFinder('Text1')); |
| expect(result.text, 'Foo'); |
| }); |
| |
| testWidgets('complex extension finder', (WidgetTester tester) async { |
| final FlutterDriverExtension driverExtension = FlutterDriverExtension( |
| (String? arg) async => '', |
| true, |
| true, |
| finders: <FinderExtension>[ |
| StubFinderExtension(), |
| ], |
| ); |
| |
| Future<GetTextResult> getText(SerializableFinder finder) async { |
| final Map<String, String> arguments = GetText(finder, timeout: const Duration(seconds: 1)).serialize(); |
| final Map<String, dynamic> response = await driverExtension.call(arguments); |
| return GetTextResult.fromJson(response['response'] as Map<String, dynamic>); |
| } |
| |
| await tester.pumpWidget(debugTree); |
| |
| final GetTextResult result = await getText(Descendant(of: StubFinder('Column'), matching: StubFinder('Text1'))); |
| expect(result.text, 'Foo'); |
| }); |
| |
| testWidgets('extension finder with command', (WidgetTester tester) async { |
| final FlutterDriverExtension driverExtension = FlutterDriverExtension( |
| (String? arg) async => '', |
| true, |
| true, |
| finders: <FinderExtension>[ |
| StubFinderExtension(), |
| ], |
| ); |
| |
| Future<Map<String, dynamic>> tap(SerializableFinder finder) async { |
| final Map<String, String> arguments = Tap(finder, timeout: const Duration(seconds: 1)).serialize(); |
| return driverExtension.call(arguments); |
| } |
| |
| await tester.pumpWidget(debugTree); |
| |
| final Map<String, dynamic> result = await tap(StubFinder('Button')); |
| expect(result['isError'], false); |
| }); |
| }); |
| |
| group('extension commands', () { |
| int invokes = 0; |
| void stubCallback() => invokes++; |
| |
| final Widget debugTree = Directionality( |
| textDirection: TextDirection.ltr, |
| child: Center( |
| child: Column( |
| children: <Widget>[ |
| TextButton( |
| key: const ValueKey<String>('Button'), |
| onPressed: stubCallback, |
| child: const Text('Whatever'), |
| ), |
| ], |
| ), |
| ), |
| ); |
| |
| setUp(() { |
| invokes = 0; |
| }); |
| |
| testWidgets('unknown extension command', (WidgetTester tester) async { |
| final FlutterDriverExtension driverExtension = FlutterDriverExtension( |
| (String? arg) async => '', |
| true, |
| true, |
| commands: <CommandExtension>[], |
| ); |
| |
| Future<Map<String, dynamic>> invokeCommand(SerializableFinder finder, int times) async { |
| final Map<String, String> arguments = StubNestedCommand(finder, times).serialize(); |
| return driverExtension.call(arguments); |
| } |
| |
| await tester.pumpWidget(debugTree); |
| |
| final Map<String, dynamic> result = await invokeCommand(ByValueKey('Button'), 10); |
| expect(result['isError'], true); |
| expect(result['response'] is String, true); |
| expect(result['response'] as String?, contains('Unsupported command kind StubNestedCommand')); |
| }); |
| |
| testWidgets('nested command', (WidgetTester tester) async { |
| final FlutterDriverExtension driverExtension = FlutterDriverExtension( |
| (String? arg) async => '', |
| true, |
| true, |
| commands: <CommandExtension>[ |
| StubNestedCommandExtension(), |
| ], |
| ); |
| |
| Future<StubCommandResult> invokeCommand(SerializableFinder finder, int times) async { |
| await driverExtension.call(const SetFrameSync(false).serialize()); // disable frame sync for test to avoid lock |
| final Map<String, String> arguments = StubNestedCommand(finder, times, timeout: const Duration(seconds: 1)).serialize(); |
| final Map<String, dynamic> response = await driverExtension.call(arguments); |
| final Map<String, dynamic> commandResponse = response['response'] as Map<String, dynamic>; |
| return StubCommandResult(commandResponse['resultParam'] as String); |
| } |
| |
| await tester.pumpWidget(debugTree); |
| |
| const int times = 10; |
| final StubCommandResult result = await invokeCommand(ByValueKey('Button'), times); |
| expect(result.resultParam, 'stub response'); |
| expect(invokes, times); |
| }); |
| |
| testWidgets('prober command', (WidgetTester tester) async { |
| final FlutterDriverExtension driverExtension = FlutterDriverExtension( |
| (String? arg) async => '', |
| true, |
| true, |
| commands: <CommandExtension>[ |
| StubProberCommandExtension(), |
| ], |
| ); |
| |
| Future<StubCommandResult> invokeCommand(SerializableFinder finder, int times) async { |
| await driverExtension.call(const SetFrameSync(false).serialize()); // disable frame sync for test to avoid lock |
| final Map<String, String> arguments = StubProberCommand(finder, times, timeout: const Duration(seconds: 1)).serialize(); |
| final Map<String, dynamic> response = await driverExtension.call(arguments); |
| final Map<String, dynamic> commandResponse = response['response'] as Map<String, dynamic>; |
| return StubCommandResult(commandResponse['resultParam'] as String); |
| } |
| |
| await tester.pumpWidget(debugTree); |
| |
| const int times = 10; |
| final StubCommandResult result = await invokeCommand(ByValueKey('Button'), times); |
| expect(result.resultParam, 'stub response'); |
| expect(invokes, times); |
| }); |
| }); |
| |
| group('waitForTappable', () { |
| late FlutterDriverExtension driverExtension; |
| |
| Future<Map<String, dynamic>> waitForTappable() async { |
| final SerializableFinder finder = ByValueKey('widgetOne'); |
| final Map<String, String> arguments = WaitForTappable(finder).serialize(); |
| final Map<String, dynamic> result = await driverExtension.call(arguments); |
| return result; |
| } |
| |
| final Widget testWidget = MaterialApp( |
| home: Material( |
| child: Column(children: const<Widget> [ |
| Text('Hello ', key: Key('widgetOne')), |
| SizedBox( |
| height: 0, |
| width: 0, |
| child: Text('World!', key: Key('widgetTwo')), |
| ), |
| ]), |
| ), |
| ); |
| |
| testWidgets('returns true when widget is tappable', ( |
| WidgetTester tester) async { |
| driverExtension = FlutterDriverExtension((String? arg) async => '', true, false); |
| |
| await tester.pumpWidget(testWidget); |
| |
| final Map<String, dynamic> waitForTappableResult = await waitForTappable(); |
| expect(waitForTappableResult['isError'], isFalse); |
| }); |
| }); |
| |
| group('waitUntilFrameSync', () { |
| late FlutterDriverExtension driverExtension; |
| Map<String, dynamic>? result; |
| |
| setUp(() { |
| driverExtension = FlutterDriverExtension((String? arg) async => '', true, true); |
| result = null; |
| }); |
| |
| testWidgets('returns immediately when frame is synced', ( |
| WidgetTester tester) async { |
| driverExtension.call(const WaitForCondition(NoPendingFrame()).serialize()) |
| .then<void>(expectAsync1((Map<String, dynamic> r) { |
| result = r; |
| })); |
| |
| await tester.idle(); |
| expect( |
| result, |
| <String, dynamic>{ |
| 'isError': false, |
| 'response': <String, dynamic>{}, |
| }, |
| ); |
| }); |
| |
| testWidgets( |
| 'waits until no transient callbacks', (WidgetTester tester) async { |
| SchedulerBinding.instance.scheduleFrameCallback((_) { |
| // Intentionally blank. We only care about existence of a callback. |
| }); |
| |
| driverExtension.call(const WaitForCondition(NoPendingFrame()).serialize()) |
| .then<void>(expectAsync1((Map<String, dynamic> r) { |
| result = r; |
| })); |
| |
| // Nothing should happen until the next frame. |
| await tester.idle(); |
| expect(result, isNull); |
| |
| // NOW we should receive the result. |
| await tester.pump(); |
| expect( |
| result, |
| <String, dynamic>{ |
| 'isError': false, |
| 'response': <String, dynamic>{}, |
| }, |
| ); |
| }); |
| |
| testWidgets( |
| 'waits until no pending scheduled frame', (WidgetTester tester) async { |
| SchedulerBinding.instance.scheduleFrame(); |
| |
| driverExtension.call(const WaitForCondition(NoPendingFrame()).serialize()) |
| .then<void>(expectAsync1((Map<String, dynamic> r) { |
| result = r; |
| })); |
| |
| // Nothing should happen until the next frame. |
| await tester.idle(); |
| expect(result, isNull); |
| |
| // NOW we should receive the result. |
| await tester.pump(); |
| expect( |
| result, |
| <String, dynamic>{ |
| 'isError': false, |
| 'response': <String, dynamic>{}, |
| }, |
| ); |
| }); |
| }); |
| } |