diff --git a/packages/flutter_driver/lib/src/driver/web_driver.dart b/packages/flutter_driver/lib/src/driver/web_driver.dart index 4e7852bd025..7852bfe7d4d 100644 --- a/packages/flutter_driver/lib/src/driver/web_driver.dart +++ b/packages/flutter_driver/lib/src/driver/web_driver.dart @@ -105,25 +105,60 @@ class WebFlutterDriver extends FlutterDriver { ); } + static DriverError _createMalformedExtensionResponseError(Object? data) { + throw DriverError( + 'Received malformed response from the FlutterDriver extension.\n' + 'Expected a JSON map containing a "response" field and, optionally, an ' + '"isError" field, but got ${data.runtimeType}: $data' + ); + } + @override Future> sendCommand(Command command) async { - Map response; + final Map response; + final Object? data; final Map serialized = command.serialize(); _logCommunication('>>> $serialized'); try { - final dynamic data = await _connection.sendCommand("window.\$flutterDriver('${jsonEncode(serialized)}')", command.timeout); - response = data != null ? (json.decode(data as String) as Map?)! : {}; + data = await _connection.sendCommand("window.\$flutterDriver('${jsonEncode(serialized)}')", command.timeout); + + // The returned data is expected to be a string. If it's null or anything + // other than a string, something's wrong. + if (data is! String) { + throw _createMalformedExtensionResponseError(data); + } + + final Object? decoded = json.decode(data); + if (decoded is! Map) { + throw _createMalformedExtensionResponseError(data); + } else { + response = decoded; + } + _logCommunication('<<< $response'); + } on DriverError catch(_) { + rethrow; } catch (error, stackTrace) { throw DriverError( - "Failed to respond to $command due to remote error\n : \$flutterDriver('${jsonEncode(serialized)}')", + 'FlutterDriver command ${command.runtimeType} failed due to a remote error.\n' + 'Command sent: ${jsonEncode(serialized)}', error, stackTrace ); } - if (response['isError'] == true) - throw DriverError('Error in Flutter application: ${response['response']}'); - return response['response'] as Map; + + final Object? isError = response['isError']; + final Object? responseData = response['response']; + if (isError is! bool?) { + throw _createMalformedExtensionResponseError(data); + } else if (isError == true) { + throw DriverError('Error in Flutter application: $responseData'); + } + + if (responseData is! Map) { + throw _createMalformedExtensionResponseError(data); + } + return responseData; } @override diff --git a/packages/flutter_driver/test/src/real_tests/flutter_driver_test.dart b/packages/flutter_driver/test/src/real_tests/flutter_driver_test.dart index ff7661ebce9..8679f66bf82 100644 --- a/packages/flutter_driver/test/src/real_tests/flutter_driver_test.dart +++ b/packages/flutter_driver/test/src/real_tests/flutter_driver_test.dart @@ -745,8 +745,8 @@ void main() { const String waitForCommandLog = '>>> {command: waitFor, timeout: 1234, finderType: ByTooltipMessage, text: logCommunicationToFile test}'; const String responseLog = '<<< {isError: false, response: {status: ok}, type: Response}'; - expect(commandLog.contains(waitForCommandLog), true, reason: '$commandLog not contains $waitForCommandLog'); - expect(commandLog.contains(responseLog), true, reason: '$commandLog not contains $responseLog'); + expect(commandLog, contains(waitForCommandLog), reason: '$commandLog not contains $waitForCommandLog'); + expect(commandLog, contains(responseLog), reason: '$commandLog not contains $responseLog'); }); test('logCommunicationToFile = false', () async { diff --git a/packages/flutter_driver/test/src/web_tests/web_driver_test.dart b/packages/flutter_driver/test/src/web_tests/web_driver_test.dart new file mode 100644 index 00000000000..9370e0e1d4a --- /dev/null +++ b/packages/flutter_driver/test/src/web_tests/web_driver_test.dart @@ -0,0 +1,139 @@ +// 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_driver/src/common/error.dart'; +import 'package:flutter_driver/src/common/health.dart'; +import 'package:flutter_driver/src/driver/web_driver.dart'; +import 'package:webdriver/src/common/log.dart'; + +import '../../common.dart'; + +void main() { + group('WebDriver', () { + late FakeFlutterWebConnection fakeConnection; + late WebFlutterDriver driver; + + setUp(() { + fakeConnection = FakeFlutterWebConnection(); + driver = WebFlutterDriver.connectedTo(fakeConnection); + }); + + test('sendCommand succeeds', () async { + fakeConnection.fakeResponse = ''' +{ + "isError": false, + "response": { + "test": "hello" + } +} +'''; + + final Map response = await driver.sendCommand(const GetHealth()); + expect(response['test'], 'hello'); + }); + + test('sendCommand fails on communication error', () async { + fakeConnection.communicationError = Error(); + expect( + () => driver.sendCommand(const GetHealth()), + _throwsDriverErrorWithMessage( + 'FlutterDriver command GetHealth failed due to a remote error.\n' + 'Command sent: {"command":"get_health"}' + ), + ); + }); + + test('sendCommand fails on null', () async { + fakeConnection.fakeResponse = null; + expect( + () => driver.sendCommand(const GetHealth()), + _throwsDriverErrorWithDataString('Null', 'null'), + ); + }); + + test('sendCommand fails when response data is not a string', () async { + fakeConnection.fakeResponse = 1234; + expect( + () => driver.sendCommand(const GetHealth()), + _throwsDriverErrorWithDataString('int', '1234'), + ); + }); + + test('sendCommand fails when isError is true', () async { + fakeConnection.fakeResponse = ''' +{ + "isError": true, + "response": "test error message" +} +'''; + expect( + () => driver.sendCommand(const GetHealth()), + _throwsDriverErrorWithMessage( + 'Error in Flutter application: test error message' + ), + ); + }); + + test('sendCommand fails when isError is not bool', () async { + fakeConnection.fakeResponse = '{ "isError": 5 }'; + expect( + () => driver.sendCommand(const GetHealth()), + _throwsDriverErrorWithDataString('String', '{ "isError": 5 }'), + ); + }); + + test('sendCommand fails when "response" field is not a JSON map', () async { + fakeConnection.fakeResponse = '{ "response": 5 }'; + expect( + () => driver.sendCommand(const GetHealth()), + _throwsDriverErrorWithDataString('String', '{ "response": 5 }'), + ); + }); + }); +} + +Matcher _throwsDriverErrorWithMessage(String expectedMessage) { + return throwsA(allOf( + isA(), + predicate((DriverError error) { + final String actualMessage = error.message; + return actualMessage == expectedMessage; + }, 'contains message: $expectedMessage'), + )); +} + +Matcher _throwsDriverErrorWithDataString(String dataType, String dataString) { + return _throwsDriverErrorWithMessage( + 'Received malformed response from the FlutterDriver extension.\n' + 'Expected a JSON map containing a "response" field and, optionally, an ' + '"isError" field, but got $dataType: $dataString' + ); +} + +class FakeFlutterWebConnection implements FlutterWebConnection { + @override + bool supportsTimelineAction = false; + + @override + Future close() async {} + + @override + Stream get logs => throw UnimplementedError(); + + @override + Future> screenshot() { + throw UnimplementedError(); + } + + Object? fakeResponse; + Error? communicationError; + + @override + Future sendCommand(String script, Duration? duration) async { + if (communicationError != null) { + throw communicationError!; + } + return fakeResponse; + } +}