diff --git a/examples/hello_world/test_driver/smoke_web_engine_test.dart b/examples/hello_world/test_driver/smoke_web_engine_test.dart index 8b2fa6f1d9..a86933e019 100644 --- a/examples/hello_world/test_driver/smoke_web_engine_test.dart +++ b/examples/hello_world/test_driver/smoke_web_engine_test.dart @@ -2,10 +2,13 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:async'; + import 'package:flutter_driver/flutter_driver.dart'; import 'package:test/test.dart' hide TypeMatcher, isInstanceOf; +import 'package:webdriver/async_io.dart'; -/// The following test is used as a simple smoke test for verfying Flutter +/// The following test is used as a simple smoke test for verifying Flutter /// Framework and Flutter Web Engine integration. void main() { group('Hello World App', () { @@ -28,5 +31,20 @@ void main() { test('title is correct', () async { expect(await driver.getText(titleFinder), 'Hello, world!'); }); + + test('enable accessibility', () async { + await driver.enableAccessibility(); + + await Future.delayed(const Duration(seconds: 2)); + + // Elements with tag "flt-semantics" would show up after enabling + // accessibility. + // + // The tag used here is based on + // https://github.com/flutter/engine/blob/master/lib/web_ui/lib/src/engine/semantics/semantics.dart#L534 + final WebElement element = await driver.webDriver.findElement(const By.tagName('flt-semantics')); + + expect(element, isNotNull); + }); }); } diff --git a/packages/flutter_driver/lib/src/driver/driver.dart b/packages/flutter_driver/lib/src/driver/driver.dart index ae612adaa2..4ddd8b895a 100644 --- a/packages/flutter_driver/lib/src/driver/driver.dart +++ b/packages/flutter_driver/lib/src/driver/driver.dart @@ -8,6 +8,7 @@ import 'dart:io'; import 'package:json_rpc_2/json_rpc_2.dart' as rpc; import 'package:meta/meta.dart'; import 'package:vm_service_client/vm_service_client.dart'; +import 'package:webdriver/async_io.dart' as async_io; import '../common/diagnostics_tree.dart'; import '../common/error.dart'; @@ -169,6 +170,14 @@ abstract class FlutterDriver { /// Getter of serviceClient. VMServiceClient get serviceClient => throw UnimplementedError(); + /// Getter of webDriver. + async_io.WebDriver get webDriver => throw UnimplementedError(); + + /// Enables accessibility feature. + Future enableAccessibility() async { + throw UnimplementedError(); + } + /// Sends [command] to the Flutter Driver extensions. /// This must be implemented by subclass. /// diff --git a/packages/flutter_driver/lib/src/driver/vmservice_driver.dart b/packages/flutter_driver/lib/src/driver/vmservice_driver.dart index 979819eb19..b85c79c91c 100644 --- a/packages/flutter_driver/lib/src/driver/vmservice_driver.dart +++ b/packages/flutter_driver/lib/src/driver/vmservice_driver.dart @@ -12,6 +12,7 @@ import 'package:json_rpc_2/json_rpc_2.dart' as rpc; import 'package:meta/meta.dart'; import 'package:path/path.dart' as p; import 'package:vm_service_client/vm_service_client.dart'; +import 'package:webdriver/async_io.dart' as async_io; import 'package:web_socket_channel/io.dart'; import '../../flutter_driver.dart'; @@ -303,6 +304,9 @@ class VMServiceFlutterDriver extends FlutterDriver { @override VMServiceClient get serviceClient => _serviceClient; + @override + async_io.WebDriver get webDriver => throw UnsupportedError('VMServiceFlutterDriver does not support webDriver'); + /// The main isolate hosting the Flutter application. /// /// If you used the [registerExtension] API to instrument your application, @@ -316,6 +320,11 @@ class VMServiceFlutterDriver extends FlutterDriver { /// Whether to log communication between host and app to `flutter_driver_commands.log`. final bool _logCommunicationToFile; + @override + Future enableAccessibility() async { + throw UnsupportedError('VMServiceFlutterDriver does not support enableAccessibility'); + } + @override Future> sendCommand(Command command) async { Map response; diff --git a/packages/flutter_driver/lib/src/driver/web_driver.dart b/packages/flutter_driver/lib/src/driver/web_driver.dart index 11a748f130..9f7d55cabe 100644 --- a/packages/flutter_driver/lib/src/driver/web_driver.dart +++ b/packages/flutter_driver/lib/src/driver/web_driver.dart @@ -30,6 +30,7 @@ class WebFlutterDriver extends FlutterDriver { final FlutterWebConnection _connection; DateTime _startTime; + bool _accessibilityEnabled = false; /// Start time for tracing. @visibleForTesting @@ -41,11 +42,15 @@ class WebFlutterDriver extends FlutterDriver { @override VMServiceClient get serviceClient => throw UnsupportedError('WebFlutterDriver does not support serviceClient'); + @override + async_io.WebDriver get webDriver => _connection._driver; + /// Creates a driver that uses a connection provided by the given /// [hostUrl] which would fallback to environment variable VM_SERVICE_URL. /// Driver also depends on environment variables DRIVER_SESSION_ID, - /// BROWSER_SUPPORTS_TIMELINE, DRIVER_SESSION_URI, DRIVER_SESSION_SPEC - /// and ANDROID_CHROME_ON_EMULATOR for configurations. + /// BROWSER_SUPPORTS_TIMELINE, DRIVER_SESSION_URI, DRIVER_SESSION_SPEC, + /// DRIVER_SESSION_CAPABILITIES and ANDROID_CHROME_ON_EMULATOR for + /// configurations. static Future connectWeb( {String hostUrl, Duration timeout}) async { hostUrl ??= Platform.environment['VM_SERVICE_URL']; @@ -55,12 +60,29 @@ class WebFlutterDriver extends FlutterDriver { 'session-uri': Platform.environment['DRIVER_SESSION_URI'], 'session-spec': Platform.environment['DRIVER_SESSION_SPEC'], 'android-chrome-on-emulator': Platform.environment['ANDROID_CHROME_ON_EMULATOR'] == 'true', + 'session-capabilities': Platform.environment['DRIVER_SESSION_CAPABILITIES'], }; final FlutterWebConnection connection = await FlutterWebConnection.connect (hostUrl, settings, timeout: timeout); return WebFlutterDriver.connectedTo(connection); } + @override + Future enableAccessibility() async { + if (!_accessibilityEnabled) { + // Clicks the button to enable accessibility via Javascript for Desktop Web. + // + // The tag used in the script is based on + // https://github.com/flutter/engine/blob/master/lib/web_ui/lib/src/engine/semantics/semantics_helper.dart#L193 + // + // TODO(angjieli): Support Mobile Web. (https://github.com/flutter/flutter/issues/65192) + await webDriver.execute( + 'document.querySelector(\'flt-semantics-placeholder\').click();', + []); + _accessibilityEnabled = true; + } + } + @override Future> sendCommand(Command command) async { Map response; @@ -177,12 +199,14 @@ class FlutterWebConnection { String url, Map settings, {Duration timeout}) async { - // Use sync WebDriver because async version will create a 15 seconds - // overhead when quitting. - final async_io.WebDriver driver = await async_io.fromExistingSession( - settings['session-id'].toString(), - uri: Uri.parse(settings['session-uri'].toString()), - spec: _convertToSpec(settings['session-spec'].toString().toLowerCase())); + final String sessionId = settings['session-id'].toString(); + final Uri sessionUri = Uri.parse(settings['session-uri'].toString()); + final async_io.WebDriver driver = async_io.WebDriver( + sessionUri, + sessionId, + json.decode(settings['session-capabilities'] as String) as Map, + async_io.AsyncIoRequestClient(sessionUri.resolve('session/$sessionId/')), + _convertToSpec(settings['session-spec'].toString().toLowerCase())); if (settings['android-chrome-on-emulator'] == true) { final Uri localUri = Uri.parse(url); // Converts to Android Emulator Uri. diff --git a/packages/flutter_driver/test/flutter_driver_test.dart b/packages/flutter_driver/test/flutter_driver_test.dart index 5a2844d393..3487fa3047 100644 --- a/packages/flutter_driver/test/flutter_driver_test.dart +++ b/packages/flutter_driver/test/flutter_driver_test.dart @@ -768,6 +768,16 @@ void main() { expect(driver.waitFor(find.byTooltip('foo')), throwsDriverError); }); }); + + group('VMServiceFlutterDriver Unsupported error', () { + test('enableAccessibility', () async { + expect(driver.enableAccessibility(), throwsA(isA())); + }); + + test('webDriver', () async { + expect(() => driver.webDriver, throwsA(isA())); + }); + }); }); group('VMServiceFlutterDriver with custom timeout', () { @@ -1117,7 +1127,7 @@ void main() { await driver.checkHealth(); }); - group('WebFlutterDriver Unimplemented error', () { + group('WebFlutterDriver Unimplemented/Unsupported error', () { test('forceGC', () async { expect(driver.forceGC(), throwsA(isA())); diff --git a/packages/flutter_tools/lib/src/commands/drive.dart b/packages/flutter_tools/lib/src/commands/drive.dart index 1f9772e5e8..f9c587db5c 100644 --- a/packages/flutter_tools/lib/src/commands/drive.dart +++ b/packages/flutter_tools/lib/src/commands/drive.dart @@ -18,6 +18,7 @@ import '../base/common.dart'; import '../base/file_system.dart'; import '../base/process.dart'; import '../build_info.dart'; +import '../convert.dart'; import '../dart/package_map.dart'; import '../device.dart'; import '../globals.dart' as globals; @@ -306,6 +307,7 @@ $ex 'DRIVER_SESSION_ID': driver.id, 'DRIVER_SESSION_URI': driver.uri.toString(), 'DRIVER_SESSION_SPEC': driver.spec.toString(), + 'DRIVER_SESSION_CAPABILITIES': json.encode(driver.capabilities), 'SUPPORT_TIMELINE_ACTION': (browser == Browser.chrome).toString(), 'FLUTTER_WEB_TEST': 'true', 'ANDROID_CHROME_ON_EMULATOR': (isAndroidChrome && useEmulator).toString(),