mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
<!-- start_original_pr_link --> Reverts: flutter/flutter#168437 <!-- end_original_pr_link --> <!-- start_initiating_author --> Initiated by: chingjun <!-- end_initiating_author --> <!-- start_revert_reason --> Reason for reverting: broke internal customers <!-- end_revert_reason --> <!-- start_original_pr_author --> Original PR Author: loic-sharma <!-- end_original_pr_author --> <!-- start_reviewers --> Reviewed By: {bkonyi, justinmc, matanlurey} <!-- end_reviewers --> <!-- start_revert_body --> This change reverts the following previous change: ## Motivation We'd like to let users opt-in to experimental features so that they can give early feedback while we iterate on the feature. For example: Example feature flags: 1. Android sensitive content: https://github.com/flutter/flutter/pull/158473. When enabled, Flutter will tell Android when the view contains sensitive content like a password. 2. Desktop multi-window. When enabled, Flutter will use child windows to allow things like a context menu to "escape" outside of the current window. ### Use case Users will be able to turn on features by: * **Option 1**: Run `flutter config --enable-my-feature`. This enables the feature for all projects on the machine * **Option 2**: Add `enable-my-feature: true` in their `pubspec.yaml`, under the `flutter` section. This would enable the for a single project on the machine. Turning on a feature affects _both_ development-time (`flutter run`) and deployment-time (`flutter build x`). For example, I can `flutter build windows` to create an `.exe` with multi-window features enabled. ## How this works This adds a new [`runtimeId`](https://github.com/flutter/flutter/pull/168437/files#diff-0ded384225f19a4c34d43c7c11f7cb084ff3db947cfa82d8d52fc94c112bb2a7R243-R247) property to the tool's `Feature` class. If a feature is on and has a `runtimeId`, its `runtimeId` will be [stamped into the Dart application as a Dart define](https://github.com/flutter/flutter/pull/168437/files#diff-bd662448bdc2e6f50e47cd3b20b22b41a828561bce65cb4d54ea4f5011cc604eR293-R327). The framework uses this Dart define to [determine which features are enabled](https://github.com/flutter/flutter/pull/168437/files#diff-c8dbd5cd3103bc5be53c4ac5be8bdb9bf73e10cd5d8e4ac34e737fd1f8602d45). ### Multi-window example https://github.com/flutter/flutter/pull/168697 shows how this new feature flag system can be used to add a multi-window feature flag: 1. It adds a new [multi-window feature](https://github.com/flutter/flutter/pull/168697/files#diff-0ded384225f19a4c34d43c7c11f7cb084ff3db947cfa82d8d52fc94c112bb2a7R189-R198) to the Flutter tool. This can be turned on using `flutter config --enable-multi-window` or by putting `enable-multi-window: true` in an app's .pubspec, under the `flutter` section. 2. It adds a new [`isMultiWindowEnabled`](https://github.com/flutter/flutter/pull/168697/files#diff-c8dbd5cd3103bc5be53c4ac5be8bdb9bf73e10cd5d8e4ac34e737fd1f8602d45R7-R11) property to the framework. 3. The Material library can use this new property to determine whether it should create a new window. [Example](https://github.com/flutter/flutter/pull/168697/files#diff-2cbc1634ed6b61d61dfa090e7bfbbb7c60b74c8abc3a28df6f79eee691fd1b73). ## Limitations ### Tool and framework only For now, these feature flags are available only to the Flutter tool and Flutter framework. The flags are not automatically available to the embedder or the engine. For example, embedders need to configure their surfaces differently if Impeller is enabled. This configuration must happen before the Dart isolate is launched. As a result, the framework's feature flags is not a viable solution for this scenario for now. For these kinds of scenarios, we should continue to use platform-specific configuration like the `AndroidManifest.xml` or `Info.plist` files. This is a fixable limitation, we just need to invest in this plumbing :) ### Tree shaking Feature flags are not designed to help tree shaking. For example, you cannot conditionally import Dart code depending on the enabled feature flags. Code that is feature flagged off will still be imported into user's apps. ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md <!-- end_revert_body --> Co-authored-by: auto-submit[bot] <flutter-engprod-team@google.com>
324 lines
9.4 KiB
Dart
324 lines
9.4 KiB
Dart
// 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 '../framework/devices.dart';
|
|
import '../framework/framework.dart';
|
|
import '../framework/talkback.dart';
|
|
import '../framework/task_result.dart';
|
|
import '../framework/utils.dart';
|
|
|
|
TaskFunction createChannelsIntegrationTest() {
|
|
return IntegrationTest(
|
|
'${flutterDirectory.path}/dev/integration_tests/channels',
|
|
'integration_test/main_test.dart',
|
|
).call;
|
|
}
|
|
|
|
TaskFunction createPlatformInteractionTest() {
|
|
return DriverTest(
|
|
'${flutterDirectory.path}/dev/integration_tests/platform_interaction',
|
|
'lib/main.dart',
|
|
).call;
|
|
}
|
|
|
|
TaskFunction createFlavorsTest({Map<String, String>? environment, List<String>? extraOptions}) {
|
|
return DriverTest(
|
|
'${flutterDirectory.path}/dev/integration_tests/flavors',
|
|
'lib/main.dart',
|
|
extraOptions: extraOptions ?? <String>['--flavor', 'paid'],
|
|
environment: environment,
|
|
).call;
|
|
}
|
|
|
|
TaskFunction createIntegrationTestFlavorsTest({Map<String, String>? environment}) {
|
|
return IntegrationTest(
|
|
'${flutterDirectory.path}/dev/integration_tests/flavors',
|
|
'integration_test/integration_test.dart',
|
|
extraOptions: <String>['--flavor', 'paid'],
|
|
environment: environment,
|
|
).call;
|
|
}
|
|
|
|
TaskFunction createExternalTexturesFrameRateIntegrationTest({
|
|
List<String> extraOptions = const <String>[],
|
|
}) {
|
|
return DriverTest(
|
|
'${flutterDirectory.path}/dev/integration_tests/external_textures',
|
|
'lib/frame_rate_main.dart',
|
|
extraOptions: extraOptions,
|
|
).call;
|
|
}
|
|
|
|
TaskFunction createPlatformChannelSampleTest({String? deviceIdOverride}) {
|
|
return DriverTest(
|
|
'${flutterDirectory.path}/examples/platform_channel',
|
|
'test_driver/button_tap.dart',
|
|
deviceIdOverride: deviceIdOverride,
|
|
).call;
|
|
}
|
|
|
|
TaskFunction createPlatformChannelSwiftSampleTest() {
|
|
return DriverTest(
|
|
'${flutterDirectory.path}/examples/platform_channel_swift',
|
|
'test_driver/button_tap.dart',
|
|
).call;
|
|
}
|
|
|
|
TaskFunction createEmbeddedAndroidViewsIntegrationTest() {
|
|
return DriverTest(
|
|
'${flutterDirectory.path}/dev/integration_tests/android_views',
|
|
'lib/main.dart',
|
|
).call;
|
|
}
|
|
|
|
TaskFunction createHybridAndroidViewsIntegrationTest() {
|
|
return DriverTest(
|
|
'${flutterDirectory.path}/dev/integration_tests/hybrid_android_views',
|
|
'lib/main.dart',
|
|
).call;
|
|
}
|
|
|
|
TaskFunction createAndroidSemanticsIntegrationTest() {
|
|
return IntegrationTest(
|
|
'${flutterDirectory.path}/dev/integration_tests/android_semantics_testing',
|
|
'integration_test/main_test.dart',
|
|
withTalkBack: true,
|
|
).call;
|
|
}
|
|
|
|
TaskFunction createIOSPlatformViewTests() {
|
|
return DriverTest(
|
|
'${flutterDirectory.path}/dev/integration_tests/ios_platform_view_tests',
|
|
'lib/main.dart',
|
|
extraOptions: <String>['--dart-define=ENABLE_DRIVER_EXTENSION=true'],
|
|
).call;
|
|
}
|
|
|
|
TaskFunction createEndToEndKeyboardTest() {
|
|
return DriverTest(
|
|
'${flutterDirectory.path}/dev/integration_tests/ui',
|
|
'lib/keyboard_resize.dart',
|
|
).call;
|
|
}
|
|
|
|
TaskFunction createEndToEndFrameNumberTest() {
|
|
return DriverTest(
|
|
'${flutterDirectory.path}/dev/integration_tests/ui',
|
|
'lib/frame_number.dart',
|
|
).call;
|
|
}
|
|
|
|
TaskFunction createEndToEndDriverTest({Map<String, String>? environment}) {
|
|
return DriverTest(
|
|
'${flutterDirectory.path}/dev/integration_tests/ui',
|
|
'lib/driver.dart',
|
|
environment: environment,
|
|
).call;
|
|
}
|
|
|
|
TaskFunction createEndToEndScreenshotTest() {
|
|
return DriverTest(
|
|
'${flutterDirectory.path}/dev/integration_tests/ui',
|
|
'lib/screenshot.dart',
|
|
).call;
|
|
}
|
|
|
|
TaskFunction createEndToEndKeyboardTextfieldTest() {
|
|
return DriverTest(
|
|
'${flutterDirectory.path}/dev/integration_tests/ui',
|
|
'lib/keyboard_textfield.dart',
|
|
).call;
|
|
}
|
|
|
|
TaskFunction createSolidColorTest({required bool enableImpeller}) {
|
|
return DriverTest(
|
|
'${flutterDirectory.path}/dev/integration_tests/ui',
|
|
'lib/solid_color.dart',
|
|
extraOptions: <String>[if (enableImpeller) '--enable-impeller'],
|
|
).call;
|
|
}
|
|
|
|
// Can run on emulator or physical android device.
|
|
// Device must have developer settings enabled.
|
|
// Device must be android api 30 or higher.
|
|
TaskFunction createDisplayCutoutTest() {
|
|
return IntegrationTest(
|
|
'${flutterDirectory.path}/dev/integration_tests/display_cutout_rotation/',
|
|
'integration_test/display_cutout_test.dart',
|
|
setup: (Device device) async {
|
|
if (device is! AndroidDevice) {
|
|
// Only android devices support this cutoutTest.
|
|
throw TaskResult.failure('This test should only target android');
|
|
}
|
|
// Test requires developer settings added in 28 and behavior added in 30.
|
|
final String sdkResult = await device.shellEval('getprop', <String>['ro.build.version.sdk']);
|
|
if (sdkResult.startsWith('2') || sdkResult.startsWith('1') || sdkResult.length == 1) {
|
|
throw TaskResult.failure('This test should only target android 30+.');
|
|
}
|
|
print('Adding Synthetic notch...');
|
|
// This command will cause any running android activity to be recreated.
|
|
await device.shellExec('cmd', <String>[
|
|
'overlay',
|
|
'enable',
|
|
'com.android.internal.display.cutout.emulation.tall',
|
|
]);
|
|
},
|
|
tearDown: (Device device) async {
|
|
if (device is AndroidDevice) {
|
|
print('Removing Synthetic notch...');
|
|
await device.shellExec('cmd', <String>[
|
|
'overlay',
|
|
'disable',
|
|
'com.android.internal.display.cutout.emulation.tall',
|
|
]);
|
|
}
|
|
},
|
|
).call;
|
|
}
|
|
|
|
TaskFunction dartDefinesTask() {
|
|
return DriverTest(
|
|
'${flutterDirectory.path}/dev/integration_tests/ui',
|
|
'lib/defines.dart',
|
|
extraOptions: <String>[
|
|
'--dart-define=test.valueA=Example,A',
|
|
'--dart-define=test.valueB=Value',
|
|
],
|
|
).call;
|
|
}
|
|
|
|
TaskFunction createEndToEndIntegrationTest() {
|
|
return IntegrationTest(
|
|
'${flutterDirectory.path}/dev/integration_tests/ui',
|
|
'integration_test/integration_test.dart',
|
|
).call;
|
|
}
|
|
|
|
TaskFunction createSpellCheckIntegrationTest() {
|
|
return IntegrationTest(
|
|
'${flutterDirectory.path}/dev/integration_tests/spell_check',
|
|
'integration_test/integration_test.dart',
|
|
).call;
|
|
}
|
|
|
|
TaskFunction createWindowsStartupDriverTest({String? deviceIdOverride}) {
|
|
return DriverTest(
|
|
'${flutterDirectory.path}/dev/integration_tests/windows_startup_test',
|
|
'lib/main.dart',
|
|
deviceIdOverride: deviceIdOverride,
|
|
).call;
|
|
}
|
|
|
|
TaskFunction createWideGamutTest() {
|
|
return IntegrationTest(
|
|
'${flutterDirectory.path}/dev/integration_tests/wide_gamut_test',
|
|
'integration_test/app_test.dart',
|
|
createPlatforms: <String>['ios'],
|
|
).call;
|
|
}
|
|
|
|
class DriverTest {
|
|
DriverTest(
|
|
this.testDirectory,
|
|
this.testTarget, {
|
|
this.extraOptions = const <String>[],
|
|
this.deviceIdOverride,
|
|
this.environment,
|
|
});
|
|
|
|
final String testDirectory;
|
|
final String testTarget;
|
|
final List<String> extraOptions;
|
|
final String? deviceIdOverride;
|
|
final Map<String, String>? environment;
|
|
|
|
Future<TaskResult> call() {
|
|
return inDirectory<TaskResult>(testDirectory, () async {
|
|
String deviceId;
|
|
if (deviceIdOverride != null) {
|
|
deviceId = deviceIdOverride!;
|
|
} else {
|
|
final Device device = await devices.workingDevice;
|
|
await device.unlock();
|
|
deviceId = device.deviceId;
|
|
}
|
|
await flutter('packages', options: <String>['get']);
|
|
|
|
final List<String> options = <String>[
|
|
'--no-android-gradle-daemon',
|
|
'-v',
|
|
'-t',
|
|
testTarget,
|
|
'-d',
|
|
deviceId,
|
|
...extraOptions,
|
|
];
|
|
await flutter('drive', options: options, environment: environment);
|
|
return TaskResult.success(null);
|
|
});
|
|
}
|
|
}
|
|
|
|
class IntegrationTest {
|
|
IntegrationTest(
|
|
this.testDirectory,
|
|
this.testTarget, {
|
|
this.extraOptions = const <String>[],
|
|
this.createPlatforms = const <String>[],
|
|
this.withTalkBack = false,
|
|
this.environment,
|
|
this.setup,
|
|
this.tearDown,
|
|
});
|
|
|
|
final String testDirectory;
|
|
final String testTarget;
|
|
final List<String> extraOptions;
|
|
final List<String> createPlatforms;
|
|
final bool withTalkBack;
|
|
final Map<String, String>? environment;
|
|
|
|
/// Run before flutter drive with the result from devices.workingDevice.
|
|
final Future<void> Function(Device device)? setup;
|
|
|
|
/// Run after flutter drive with the result from devices.workingDevice.
|
|
final Future<void> Function(Device device)? tearDown;
|
|
|
|
Future<TaskResult> call() {
|
|
return inDirectory<TaskResult>(testDirectory, () async {
|
|
final Device device = await devices.workingDevice;
|
|
await device.unlock();
|
|
final String deviceId = device.deviceId;
|
|
await flutter('packages', options: <String>['get']);
|
|
await setup?.call(await devices.workingDevice);
|
|
|
|
if (createPlatforms.isNotEmpty) {
|
|
await flutter(
|
|
'create',
|
|
options: <String>['--platforms', createPlatforms.join(','), '--no-overwrite', '.'],
|
|
);
|
|
}
|
|
|
|
if (withTalkBack) {
|
|
if (device is! AndroidDevice) {
|
|
return TaskResult.failure(
|
|
'A test that enables TalkBack can only be run on Android devices',
|
|
);
|
|
}
|
|
await enableTalkBack();
|
|
}
|
|
|
|
final List<String> options = <String>['-v', '-d', deviceId, testTarget, ...extraOptions];
|
|
await flutter('test', options: options, environment: environment);
|
|
await tearDown?.call(await devices.workingDevice);
|
|
|
|
if (withTalkBack) {
|
|
await disableTalkBack();
|
|
}
|
|
|
|
return TaskResult.success(null);
|
|
});
|
|
}
|
|
}
|