未验证 提交 4d44b7e7 编写于 作者: N nturgut 提交者: GitHub

Upgrades to felt (running on multiple modes, multiple backends, single test target option) (#22260)

* testing running the tests on all build modes

* don't run debug mode on other browsers

* fix platform message test failures

* some cleanup. change dispose platform channel message

* adding flags to control the integration tests better with felt

* running tests by target name, selecting web rendering backend

* fix conditions

* carrying some conditions to helper methods. Adding comments

* create a blocked list for failing canvaskit test

* parse parameters before all integration tests

* Give better warning to developers for tests that are blocked for CI

* address some reviwer comments (more remains)

* remove named parameters

* also run with auto mode

* add verbose option

* reduce the number of tests running. skip url_test for now
上级 9a2fdf08
......@@ -3,7 +3,6 @@
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() {
runApp(MyApp());
......
......@@ -2,7 +2,7 @@ name: regular_integration_tests
publish_to: none
environment:
sdk: ">=2.2.2 <3.0.0"
sdk: ">=2.11.0-0 <3.0.0"
dependencies:
flutter:
......
......@@ -28,7 +28,7 @@ void main() async {
await tester.tap(find.byKey(const Key('input')));
// Focus in input, otherwise clipboard will fail with
// 'document is not focused' platform exception.
html.document.querySelector('input').focus();
html.document.querySelector('input')?.focus();
await Clipboard.setData(const ClipboardData(text: 'sample text'));
}, skip: true); // https://github.com/flutter/flutter/issues/54296
......@@ -36,7 +36,7 @@ void main() async {
(WidgetTester tester) async {
int viewInstanceCount = 0;
final int currentViewId = platformViewsRegistry.getNextPlatformViewId();
platformViewsRegistry.getNextPlatformViewId();
// ignore: undefined_prefixed_name
ui.platformViewRegistry.registerViewFactory('MyView', (int viewId) {
++viewInstanceCount;
......@@ -46,14 +46,11 @@ void main() async {
app.main();
await tester.pumpAndSettle();
final Map<String, dynamic> createArgs = <String, dynamic>{
'id': '567',
'id': 567,
'viewType': 'MyView',
};
await SystemChannels.platform_views.invokeMethod<void>('create', createArgs);
final Map<String, dynamic> disposeArgs = <String, dynamic>{
'id': '567',
};
await SystemChannels.platform_views.invokeMethod<void>('dispose', disposeArgs);
await SystemChannels.platform_views.invokeMethod<void>('dispose', 567);
expect(viewInstanceCount, 1);
});
}
......@@ -13,7 +13,9 @@ import 'package:flutter/material.dart';
import 'package:integration_test/integration_test.dart';
void main() {
final IntegrationTestWidgetsFlutterBinding binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized() as IntegrationTestWidgetsFlutterBinding;
final IntegrationTestWidgetsFlutterBinding binding =
IntegrationTestWidgetsFlutterBinding.ensureInitialized()
as IntegrationTestWidgetsFlutterBinding;
testWidgets('Focused text field creates a native input element',
(WidgetTester tester) async {
......@@ -38,7 +40,7 @@ void main() {
// Change the value of the TextFormField.
final TextFormField textFormField = tester.widget(finder);
textFormField.controller.text = 'New Value';
textFormField.controller?.text = 'New Value';
// DOM element's value also changes.
expect(input.value, 'New Value');
......@@ -68,7 +70,7 @@ void main() {
// Change the value of the TextFormField.
final TextFormField textFormField = tester.widget(finder);
textFormField.controller.text = 'New Value';
textFormField.controller?.text = 'New Value';
// DOM element's value also changes.
expect(input.value, 'New Value');
});
......@@ -145,9 +147,9 @@ void main() {
expect(input2.value, 'Text2');
});
testWidgets('Jump between TextFormFields with tab key after CapsLock is'
'activated',
(WidgetTester tester) async {
testWidgets(
'Jump between TextFormFields with tab key after CapsLock is'
'activated', (WidgetTester tester) async {
app.main();
await tester.pumpAndSettle();
......@@ -163,7 +165,7 @@ void main() {
final List<Node> nodeList = document.getElementsByTagName('input');
expect(nodeList.length, equals(1));
final InputElement input =
document.getElementsByTagName('input')[0] as InputElement;
document.getElementsByTagName('input')[0] as InputElement;
// Press and release CapsLock.
dispatchKeyboardEvent(input, 'keydown', <String, dynamic>{
......@@ -207,7 +209,7 @@ void main() {
// A native input element for the next TextField should be attached to the
// DOM.
final InputElement input2 =
document.getElementsByTagName('input')[0] as InputElement;
document.getElementsByTagName('input')[0] as InputElement;
expect(input2.value, 'Text2');
});
......@@ -243,8 +245,8 @@ void main() {
expect(input.hasAttribute('readonly'), isTrue);
// Make sure the entire text is selected.
TextRange range =
TextRange(start: input.selectionStart, end: input.selectionEnd);
TextRange range = TextRange(
start: input.selectionStart ?? 0, end: input.selectionEnd ?? 0);
expect(range.textInside(text), text);
// Double tap to select the first word.
......@@ -257,7 +259,8 @@ void main() {
await gesture.up();
await gesture.down(firstWordOffset);
await gesture.up();
range = TextRange(start: input.selectionStart, end: input.selectionEnd);
range = TextRange(
start: input.selectionStart ?? 0, end: input.selectionEnd ?? 0);
expect(range.textInside(text), 'Lorem');
// Double tap to select the last word.
......@@ -270,7 +273,8 @@ void main() {
await gesture.up();
await gesture.down(lastWordOffset);
await gesture.up();
range = TextRange(start: input.selectionStart, end: input.selectionEnd);
range = TextRange(
start: input.selectionStart ?? 0, end: input.selectionEnd ?? 0);
expect(range.textInside(text), 'amet');
});
}
......
......@@ -5,6 +5,7 @@
// @dart = 2.6
import 'dart:io' as io;
import 'package:args/args.dart';
import 'package:path/path.dart' as pathlib;
import 'chrome_installer.dart';
......@@ -109,6 +110,9 @@ class IntegrationTestsManager {
return testResults;
}
int _numberOfPassedTests = 0;
int _numberOfFailedTests = 0;
Future<bool> _runTestsInDirectory(io.Directory directory) async {
final io.Directory testDirectory =
io.Directory(pathlib.join(directory.path, 'test_driver'));
......@@ -121,47 +125,83 @@ class IntegrationTestsManager {
final List<String> blockedTests =
blockedTestsListsMap[getBlockedTestsListMapKey(_browser)] ?? <String>[];
// The following loops over the contents of the directory and saves an
// expected driver file name for each e2e test assuming any dart file
// not ending with `_test.dart` is an e2e test.
// Other files are not considered since developers can add files such as
// README.
for (io.File f in entities) {
final String basename = pathlib.basename(f.path);
if (!basename.contains('_test.dart') && basename.endsWith('.dart')) {
// Do not add the basename if it is in the `blockedTests`.
if (!blockedTests.contains(basename)) {
e2eTestsToRun.add(basename);
} else {
print('INFO: Test $basename is skipped since it is blocked for '
'${getBlockedTestsListMapKey(_browser)}');
// If no target is specified run all the tests.
if (_runAllTestTargets) {
// The following loops over the contents of the directory and saves an
// expected driver file name for each e2e test assuming any dart file
// not ending with `_test.dart` is an e2e test.
// Other files are not considered since developers can add files such as
// README.
for (io.File f in entities) {
final String basename = pathlib.basename(f.path);
if (!basename.contains('_test.dart') && basename.endsWith('.dart')) {
// Do not add the basename if it is in the `blockedTests`.
if (!blockedTests.contains(basename)) {
e2eTestsToRun.add(basename);
} else {
print('INFO: Test $basename is skipped since it is blocked for '
'${getBlockedTestsListMapKey(_browser)}');
}
}
}
if (isVerboseLoggingEnabled) {
print(
'INFO: In project ${directory} ${e2eTestsToRun.length} tests to run.');
}
} else {
// If a target is specified it will run regardless of if it's blocked or
// not. There will be an info note to warn the developer.
final String targetTest =
IntegrationTestsArgumentParser.instance.testTarget;
final io.File file =
entities.singleWhere((f) => pathlib.basename(f.path) == targetTest);
final String basename = pathlib.basename(file.path);
if (blockedTests.contains(basename) && isVerboseLoggingEnabled) {
print('INFO: Test $basename do not run on CI environments. Please '
'remove it from the blocked tests list if you want to enable this '
'test on CI.');
}
e2eTestsToRun.add(basename);
}
print(
'INFO: In project ${directory} ${e2eTestsToRun.length} tests to run.');
int numberOfPassedTests = 0;
int numberOfFailedTests = 0;
final Set<String> buildModes = _getBuildModes();
for (String fileName in e2eTestsToRun) {
final bool testResults =
await _runTestsInProfileMode(directory, fileName);
if (testResults) {
numberOfPassedTests++;
} else {
numberOfFailedTests++;
}
await _runTestsTarget(directory, fileName, buildModes);
}
final int numberOfTestsRun = numberOfPassedTests + numberOfFailedTests;
final int numberOfTestsRun = _numberOfPassedTests + _numberOfFailedTests;
print('INFO: ${numberOfTestsRun} tests run. ${numberOfPassedTests} passed '
'and ${numberOfFailedTests} failed.');
return numberOfFailedTests == 0;
}
Future<bool> _runTestsInProfileMode(
io.Directory directory, String testName) async {
Future<void> _runTestsTarget(
io.Directory directory, String target, Set<String> buildModes) async {
final Set<String> renderingBackends = _getRenderingBackends();
for (String renderingBackend in renderingBackends) {
for (String mode in buildModes) {
if (!blockedTestsListsMapForModes[mode].contains(target) &&
!blockedTestsListsMapForRenderBackends[renderingBackend]
.contains(target)) {
final bool result = await _runTestsInMode(directory, target,
mode: mode, webRenderer: renderingBackend);
if (result) {
_numberOfPassedTests++;
} else {
_numberOfFailedTests++;
}
}
}
}
}
Future<bool> _runTestsInMode(io.Directory directory, String testName,
{String mode = 'profile', String webRenderer = 'html'}) async {
String executable =
_useSystemFlutter ? 'flutter' : environment.flutterCommand.path;
Map<String, String> enviroment = Map<String, String>();
......@@ -172,22 +212,54 @@ class IntegrationTestsManager {
IntegrationArguments.fromBrowser(_browser);
final int exitCode = await runProcess(
executable,
arguments.getTestArguments(testName, 'profile'),
arguments.getTestArguments(testName, mode, webRenderer),
workingDirectory: directory.path,
environment: enviroment,
);
if (exitCode != 0) {
final String command =
arguments.getCommandToRun(testName, mode, webRenderer);
io.stderr
.writeln('ERROR: Failed to run test. Exited with exit code $exitCode'
'. To run $testName locally use the following command:'
'\n\n${arguments.getCommandToRun(testName, 'profile')}');
'\n\n$command');
return false;
} else {
return true;
}
}
Set<String> _getRenderingBackends() {
Set<String> renderingBackends;
if (_renderingBackendSelected) {
final String mode = IntegrationTestsArgumentParser.instance.webRenderer;
renderingBackends = <String>{mode};
} else {
// TODO(nurhan): Enable `auto` when recipe is sharded.
renderingBackends = {'html', 'canvaskit'};
}
return renderingBackends;
}
Set<String> _getBuildModes() {
Set<String> buildModes;
if (_buildModeSelected) {
final String mode = IntegrationTestsArgumentParser.instance.buildMode;
if (mode == 'debug' && _browser != 'chrome') {
throw ToolException('Debug mode is only supported for Chrome.');
} else {
buildModes = <String>{mode};
}
} else {
// TODO(nurhan): Enable `release` when recipe is sharded.
buildModes = _browser == 'chrome'
? {'debug', 'profile'}
: {'profile'};
}
return buildModes;
}
/// Validate the directory has a `pubspec.yaml` file and a `test_driver`
/// directory.
///
......@@ -290,9 +362,25 @@ class IntegrationTestsManager {
}
}
bool get _buildModeSelected =>
!IntegrationTestsArgumentParser.instance.buildMode.isEmpty;
bool get _renderingBackendSelected =>
!IntegrationTestsArgumentParser.instance.webRenderer.isEmpty;
bool get _runAllTestTargets =>
IntegrationTestsArgumentParser.instance.testTarget.isEmpty;
/// Validate the given `browser`, `platform` combination is suitable for
/// integration tests to run.
bool validateIfTestsShouldRun() {
if (_buildModeSelected) {
final String mode = IntegrationTestsArgumentParser.instance.buildMode;
if (mode == 'debug' && _browser != 'chrome') {
throw ToolException('Debug mode is only supported for Chrome.');
}
}
// Chrome tests should run at all Platforms (Linux, macOS, Windows).
// They can also run successfully on CI and local.
if (_browser == 'chrome') {
......@@ -326,14 +414,16 @@ abstract class IntegrationArguments {
}
}
List<String> getTestArguments(String testName, String mode);
List<String> getTestArguments(
String testName, String mode, String webRenderer);
String getCommandToRun(String testName, String mode);
String getCommandToRun(String testName, String mode, String webRenderer);
}
/// Arguments to give `flutter drive` to run the integration tests on Chrome.
class ChromeIntegrationArguments extends IntegrationArguments {
List<String> getTestArguments(String testName, String mode) {
List<String> getTestArguments(
String testName, String mode, String webRenderer) {
return <String>[
'drive',
'--target=test_driver/${testName}',
......@@ -344,13 +434,15 @@ class ChromeIntegrationArguments extends IntegrationArguments {
if (isLuci) '--chrome-binary=${preinstalledChromeExecutable()}',
'--headless',
'--local-engine=host_debug_unopt',
'--web-renderer=$webRenderer',
];
}
String getCommandToRun(String testName, String mode) {
String getCommandToRun(String testName, String mode, String webRenderer) {
String statementToRun = 'flutter drive '
'--target=test_driver/${testName} -d web-server --profile '
'--browser-name=chrome --local-engine=host_debug_unopt';
'--target=test_driver/${testName} -d web-server --$mode '
'--browser-name=chrome --local-engine=host_debug_unopt '
'--web-renderer=$webRenderer';
if (isLuci) {
statementToRun = '$statementToRun --chrome-binary='
'${preinstalledChromeExecutable()}';
......@@ -361,7 +453,8 @@ class ChromeIntegrationArguments extends IntegrationArguments {
/// Arguments to give `flutter drive` to run the integration tests on Firefox.
class FirefoxIntegrationArguments extends IntegrationArguments {
List<String> getTestArguments(String testName, String mode) {
List<String> getTestArguments(
String testName, String mode, String webRenderer) {
return <String>[
'drive',
'--target=test_driver/${testName}',
......@@ -371,18 +464,23 @@ class FirefoxIntegrationArguments extends IntegrationArguments {
'--browser-name=firefox',
'--headless',
'--local-engine=host_debug_unopt',
'--web-renderer=$webRenderer',
];
}
String getCommandToRun(String testName, String mode) =>
'flutter ${getTestArguments(testName, mode).join(' ')}';
String getCommandToRun(String testName, String mode, String webRenderer) {
final String arguments =
getTestArguments(testName, mode, webRenderer).join(' ');
return 'flutter $arguments';
}
}
/// Arguments to give `flutter drive` to run the integration tests on Safari.
class SafariIntegrationArguments extends IntegrationArguments {
SafariIntegrationArguments();
List<String> getTestArguments(String testName, String mode) {
List<String> getTestArguments(
String testName, String mode, String webRenderer) {
return <String>[
'drive',
'--target=test_driver/${testName}',
......@@ -391,11 +489,96 @@ class SafariIntegrationArguments extends IntegrationArguments {
'--$mode',
'--browser-name=safari',
'--local-engine=host_debug_unopt',
'--web-renderer=$webRenderer',
];
}
String getCommandToRun(String testName, String mode) =>
'flutter ${getTestArguments(testName, mode).join(' ')}';
String getCommandToRun(String testName, String mode, String webRenderer) {
final String arguments =
getTestArguments(testName, mode, webRenderer).join(' ');
return 'flutter $arguments';
}
}
/// Parses additional options that can be used when running integration tests.
class IntegrationTestsArgumentParser {
static final IntegrationTestsArgumentParser _singletonInstance =
IntegrationTestsArgumentParser._();
/// The [IntegrationTestsArgumentParser] singleton.
static IntegrationTestsArgumentParser get instance => _singletonInstance;
IntegrationTestsArgumentParser._();
/// If target name is provided integration tests can run that one test
/// instead of running all the tests.
String testTarget;
/// The build mode to run the integration tests.
///
/// If not specified, these tests will run using 'debug, profile, release'
/// modes on Chrome and will run using 'profile, release' on other browsers.
///
/// In order to skip a test for one of the modes, add the test to the
/// `blockedTestsListsMapForModes` list for the relevant compile mode.
String buildMode;
/// Whether to use html, canvaskit or auto for web renderer.
///
/// If not set all backends will be used one after another for integration
/// tests. If set only the provided option will be used.
String webRenderer;
void populateOptions(ArgParser argParser) {
argParser
..addOption(
'target',
defaultsTo: '',
help: 'By default integration tests are run for all the tests under'
'flutter/e2etests/web directory. If a test name is specified, that '
'only that test will run. The test name will be the name of the '
'integration test (e2e test) file. For example: '
'text_editing_integration.dart or '
'profile_diagnostics_integration.dart',
)
..addOption('build-mode',
defaultsTo: '',
help: 'Flutter supports three modes when building your app. This '
'option sets the build mode for the integration tests. '
'By default an integration test will sequentially run on '
'multiple modes. All three modes (debug, release, profile) are '
'used for Chrome. Only profile, release modes will be used for '
'other browsers. In other words, if a build mode is selected '
'tests will only be run using that mode. '
'See https://flutter.dev/docs/testing/build-modes for more '
'details on the build modes.')
..addOption('web-renderer',
defaultsTo: '',
help: 'By default all three options (`html`, `canvaskit`, `auto`) '
' for rendering backends are tested when running integration '
' tests. If this option is set only the backend provided by this '
' option will be used. `auto`, `canvaskit` and `html`'
' are the available options. ');
}
/// Populate results of the arguments passed.
void parseOptions(ArgResults argResults) {
testTarget = argResults['target'] as String;
buildMode = argResults['build-mode'] as String;
if (!buildMode.isEmpty &&
buildMode != 'debug' &&
buildMode != 'profile' &&
buildMode != 'release') {
throw ArgumentError('Unexpected build mode: $buildMode');
}
webRenderer = argResults['web-renderer'] as String;
if (!webRenderer.isEmpty &&
webRenderer != 'html' &&
webRenderer != 'canvaskit' &&
webRenderer != 'auto') {
throw ArgumentError('Unexpected rendering backend: $webRenderer');
}
}
}
/// Prepares a key for the [blackList] map.
......@@ -440,3 +623,32 @@ const Map<String, List<String>> blockedTestsListsMap = <String, List<String>>{
'target_platform_ios_integration.dart',
],
};
/// Tests blocked for one of the build modes.
///
/// If a test is not supposed to run for one of the modes also add that test
/// to the corresponding list.
// TODO(nurhan): Remove the failing test after fixing.
const Map<String, List<String>> blockedTestsListsMapForModes =
<String, List<String>>{
'debug': [
'treeshaking_integration.dart',
'text_editing_integration.dart',
'url_strategy_integration.dart',
],
'profile': [],
'release': [],
};
/// Tests blocked for one of the rendering backends.
///
/// If a test is not suppose to run for one of the backends also add that test
/// to the corresponding list.
// TODO(nurhan): Remove the failing test after fixing.
const Map<String, List<String>> blockedTestsListsMapForRenderBackends =
<String, List<String>>{
'auto': [],
'html': [],
// This test failed on canvaskit on all three build modes.
'canvaskit': ['image_loading_integration.dart'],
};
......@@ -90,6 +90,8 @@ class TestCommand extends Command<bool> with ArgUtils {
SupportedBrowsers.instance.argParsers
.forEach((t) => t.populateOptions(argParser));
GeneralTestsArgumentParser.instance.populateOptions(argParser);
IntegrationTestsArgumentParser.instance.populateOptions(argParser);
}
@override
......@@ -133,6 +135,7 @@ class TestCommand extends Command<bool> with ArgUtils {
Future<bool> run() async {
SupportedBrowsers.instance
..argParsers.forEach((t) => t.parseOptions(argResults));
GeneralTestsArgumentParser.instance.parseOptions(argResults);
// Check the flags to see what type of integration tests are requested.
testTypesRequested = findTestType();
......@@ -165,6 +168,8 @@ class TestCommand extends Command<bool> with ArgUtils {
}
Future<bool> runIntegrationTests() async {
// Parse additional arguments specific for integration testing.
IntegrationTestsArgumentParser.instance.parseOptions(argResults);
if(!_testPreparationReady) {
await _prepare();
}
......@@ -204,7 +209,9 @@ class TestCommand extends Command<bool> with ArgUtils {
// If screenshot tests are available, fetch the screenshot goldens.
if (isScreenshotTestsAvailable) {
print('screenshot tests available');
if (isVerboseLoggingEnabled) {
print('INFO: Screenshot tests available');
}
final GoldensRepoFetcher goldensRepoFetcher = GoldensRepoFetcher(
environment.webUiGoldensRepositoryDirectory,
path.join(environment.webUiDevDir.path, 'goldens_lock.yaml'));
......
......@@ -6,6 +6,7 @@
import 'dart:async';
import 'dart:io' as io;
import 'package:args/args.dart';
import 'package:args/command_runner.dart';
import 'package:meta/meta.dart';
import 'package:path/path.dart' as path;
......@@ -185,6 +186,37 @@ mixin ArgUtils<T> on Command<T> {
}
}
/// Parses additional options that can be used for all tests.
class GeneralTestsArgumentParser {
static final GeneralTestsArgumentParser _singletonInstance =
GeneralTestsArgumentParser._();
/// The [GeneralTestsArgumentParser] singleton.
static GeneralTestsArgumentParser get instance => _singletonInstance;
GeneralTestsArgumentParser._();
/// If target name is provided integration tests can run that one test
/// instead of running all the tests.
bool verbose = false;
void populateOptions(ArgParser argParser) {
argParser
..addFlag(
'verbose',
defaultsTo: false,
help: 'Flag to indicate extra logs should also be printed.',
);
}
/// Populate results of the arguments passed.
void parseOptions(ArgResults argResults) {
verbose = argResults['verbose'] as bool;
}
}
bool get isVerboseLoggingEnabled => GeneralTestsArgumentParser.instance.verbose;
/// There might be proccesses started during the tests.
///
/// Use this list to store those Processes, for cleaning up before shutdown.
......
name: web_test_utils
environment:
sdk: ">=2.2.0 <3.0.0"
sdk: ">=2.11.0-0 <3.0.0"
dependencies:
path: 1.8.0-nullsafety.3
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册