add Android instrumentation test (#11063)
* add Android instrumentation test * add devicelab test * add to manifest.yaml * rename _smoke_test.dart to _smoketest.dart to prevent flutter test from picking it up * volatile fields; style fixes * use ConditionVariable; fix sh script
This commit is contained in:
parent
7f0c98ab0b
commit
18d9b20ffb
@ -0,0 +1,26 @@
|
||||
// Copyright 2017 The Chromium 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 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_devicelab/framework/adb.dart';
|
||||
import 'package:flutter_devicelab/framework/framework.dart';
|
||||
import 'package:flutter_devicelab/framework/utils.dart';
|
||||
|
||||
Future<Null> main() async {
|
||||
deviceOperatingSystem = DeviceOperatingSystem.android;
|
||||
|
||||
await task(() async {
|
||||
final Directory galleryDirectory =
|
||||
dir('${flutterDirectory.path}/examples/flutter_gallery');
|
||||
await inDirectory(galleryDirectory, () async {
|
||||
await flutter('packages', options: <String>['get']);
|
||||
await flutter('build', options: <String>['clean']); // to reset the Dart entry point
|
||||
await exec('tool/run_instrumentation_test.sh', <String>[]);
|
||||
});
|
||||
|
||||
return new TaskResult.success(null);
|
||||
});
|
||||
}
|
@ -133,6 +133,15 @@ tasks:
|
||||
required_agent_capabilities: ["linux/android"]
|
||||
flaky: true
|
||||
|
||||
flutter_gallery_instrumentation_test:
|
||||
description: >
|
||||
Same as flutter_gallery__transition_perf but uses Android instrumentation
|
||||
framework, and therefore does not require a host computer to run. This
|
||||
test can run on off-the-shelf infrastructures, such as Firebase Test Lab.
|
||||
stage: devicelab
|
||||
required_agent_capabilities: ["linux/android"]
|
||||
flaky: true
|
||||
|
||||
# iOS on-device tests
|
||||
|
||||
channels_integration_test_ios:
|
||||
|
@ -56,4 +56,6 @@ dependencies {
|
||||
androidTestCompile 'com.android.support:support-annotations:25.4.0'
|
||||
androidTestCompile 'com.android.support.test:runner:0.5'
|
||||
androidTestCompile 'com.android.support.test:rules:0.5'
|
||||
androidTestCompile 'org.hamcrest:hamcrest-library:1.3'
|
||||
androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.2'
|
||||
}
|
||||
|
@ -0,0 +1,38 @@
|
||||
// Copyright 2017 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package io.flutter.examples.gallery;
|
||||
|
||||
import android.support.test.filters.LargeTest;
|
||||
import android.support.test.rule.ActivityTestRule;
|
||||
import android.support.test.runner.AndroidJUnit4;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import static org.hamcrest.Matchers.is;
|
||||
import static org.junit.Assert.assertThat;
|
||||
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
@LargeTest
|
||||
public class FlutterGalleryInstrumentationTest {
|
||||
@Rule
|
||||
public ActivityTestRule<MainActivity> mActivityRule =
|
||||
new ActivityTestRule<>(MainActivity.class);
|
||||
|
||||
private MainActivity activity;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
activity = mActivityRule.getActivity();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void activityLoaded() throws Exception {
|
||||
FlutterGalleryInstrumentation instrumentation = activity.getInstrumentation();
|
||||
instrumentation.waitForTestToFinish();
|
||||
assertThat(instrumentation.isTestSuccessful(), is(true));
|
||||
}
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
// Copyright 2017 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package io.flutter.examples.gallery;
|
||||
|
||||
import android.os.ConditionVariable;
|
||||
import io.flutter.plugin.common.MethodCall;
|
||||
import io.flutter.plugin.common.MethodChannel;
|
||||
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
|
||||
import io.flutter.plugin.common.MethodChannel.Result;
|
||||
import io.flutter.view.FlutterView;
|
||||
|
||||
/** Instrumentation for testing using Android Espresso framework. */
|
||||
public class FlutterGalleryInstrumentation implements MethodCallHandler {
|
||||
|
||||
private final ConditionVariable testFinished = new ConditionVariable();
|
||||
private volatile boolean testSuccessful;
|
||||
|
||||
FlutterGalleryInstrumentation(FlutterView view) {
|
||||
new MethodChannel(view, "io.flutter.examples.gallery/TestLifecycleListener")
|
||||
.setMethodCallHandler(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMethodCall(MethodCall call, Result result) {
|
||||
testSuccessful = call.method.equals("success");
|
||||
testFinished.open();
|
||||
result.success(null);
|
||||
}
|
||||
|
||||
public boolean isTestSuccessful() {
|
||||
return testSuccessful;
|
||||
}
|
||||
|
||||
public void waitForTestToFinish() throws Exception {
|
||||
testFinished.block();
|
||||
}
|
||||
}
|
@ -1,3 +1,7 @@
|
||||
// Copyright 2017 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package io.flutter.examples.gallery;
|
||||
|
||||
import android.os.Bundle;
|
||||
@ -7,9 +11,17 @@ import io.flutter.plugins.GeneratedPluginRegistrant;
|
||||
|
||||
public class MainActivity extends FlutterActivity {
|
||||
|
||||
private FlutterGalleryInstrumentation instrumentation;
|
||||
|
||||
/** Instrumentation for testing. */
|
||||
public FlutterGalleryInstrumentation getInstrumentation() {
|
||||
return instrumentation;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
GeneratedPluginRegistrant.registerWith(this);
|
||||
instrumentation = new FlutterGalleryInstrumentation(this.getFlutterView());
|
||||
}
|
||||
}
|
||||
|
150
examples/flutter_gallery/test/live_smoketest.dart
Normal file
150
examples/flutter_gallery/test/live_smoketest.dart
Normal file
@ -0,0 +1,150 @@
|
||||
// Copyright 2017 The Chromium 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 'dart:async';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:flutter_gallery/gallery/app.dart';
|
||||
|
||||
/// Reports success or failure to the native code.
|
||||
const MethodChannel _kTestChannel = const MethodChannel('io.flutter.examples.gallery/TestLifecycleListener');
|
||||
|
||||
Future<Null> main() async {
|
||||
try {
|
||||
runApp(const GalleryApp());
|
||||
|
||||
const Duration kWaitBetweenActions = const Duration(milliseconds: 250);
|
||||
final _LiveWidgetController controller = new _LiveWidgetController();
|
||||
|
||||
for (Demo demo in demos) {
|
||||
print('Testing "${demo.title}" demo');
|
||||
final Finder menuItem = find.text(demo.title);
|
||||
await controller.scrollIntoView(menuItem, alignment: 0.5);
|
||||
await new Future<Null>.delayed(kWaitBetweenActions);
|
||||
|
||||
for (int i = 0; i < 2; i += 1) {
|
||||
await controller.tap(menuItem); // Launch the demo
|
||||
await new Future<Null>.delayed(kWaitBetweenActions);
|
||||
controller.frameSync = demo.synchronized;
|
||||
await controller.tap(find.byTooltip('Back'));
|
||||
controller.frameSync = true;
|
||||
await new Future<Null>.delayed(kWaitBetweenActions);
|
||||
}
|
||||
print('Success');
|
||||
}
|
||||
|
||||
_kTestChannel.invokeMethod('success');
|
||||
} catch (error) {
|
||||
_kTestChannel.invokeMethod('failure');
|
||||
}
|
||||
}
|
||||
|
||||
class Demo {
|
||||
const Demo(this.title, {this.synchronized = true});
|
||||
|
||||
/// The title of the demo.
|
||||
final String title;
|
||||
|
||||
/// True if frameSync should be enabled for this test.
|
||||
final bool synchronized;
|
||||
}
|
||||
|
||||
// Warning: this list must be kept in sync with the value of
|
||||
// kAllGalleryItems.map((GalleryItem item) => item.title).toList();
|
||||
const List<Demo> demos = const <Demo>[
|
||||
// Demos
|
||||
const Demo('Shrine'),
|
||||
const Demo('Contact profile'),
|
||||
const Demo('Animation'),
|
||||
|
||||
// Material Components
|
||||
const Demo('Bottom navigation'),
|
||||
const Demo('Buttons'),
|
||||
const Demo('Cards'),
|
||||
const Demo('Chips'),
|
||||
const Demo('Date and time pickers'),
|
||||
const Demo('Dialog'),
|
||||
const Demo('Drawer'),
|
||||
const Demo('Expand/collapse list control'),
|
||||
const Demo('Expansion panels'),
|
||||
const Demo('Floating action button'),
|
||||
const Demo('Grid'),
|
||||
const Demo('Icons'),
|
||||
const Demo('Leave-behind list items'),
|
||||
const Demo('List'),
|
||||
const Demo('Menus'),
|
||||
const Demo('Modal bottom sheet'),
|
||||
const Demo('Page selector'),
|
||||
const Demo('Persistent bottom sheet'),
|
||||
const Demo('Progress indicators', synchronized: false),
|
||||
const Demo('Pull to refresh'),
|
||||
const Demo('Scrollable tabs'),
|
||||
const Demo('Selection controls'),
|
||||
const Demo('Sliders'),
|
||||
const Demo('Snackbar'),
|
||||
const Demo('Tabs'),
|
||||
const Demo('Text fields'),
|
||||
const Demo('Tooltips'),
|
||||
|
||||
// Cupertino Components
|
||||
const Demo('Activity Indicator', synchronized: false),
|
||||
const Demo('Buttons'),
|
||||
const Demo('Dialogs'),
|
||||
const Demo('Sliders'),
|
||||
const Demo('Switches'),
|
||||
|
||||
// Style
|
||||
const Demo('Colors'),
|
||||
const Demo('Typography'),
|
||||
];
|
||||
|
||||
|
||||
class _LiveWidgetController {
|
||||
|
||||
final WidgetController _controller = new WidgetController(WidgetsBinding.instance);
|
||||
|
||||
/// With [frameSync] enabled, Flutter Driver will wait to perform an action
|
||||
/// until there are no pending frames in the app under test.
|
||||
bool frameSync = true;
|
||||
|
||||
/// Waits until at the end of a frame the provided [condition] is [true].
|
||||
Future<Null> _waitUntilFrame(bool condition(), [Completer<Null> completer]) {
|
||||
completer ??= new Completer<Null>();
|
||||
if (!condition()) {
|
||||
SchedulerBinding.instance.addPostFrameCallback((Duration timestamp) {
|
||||
_waitUntilFrame(condition, completer);
|
||||
});
|
||||
} else {
|
||||
completer.complete();
|
||||
}
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
/// Runs `finder` repeatedly until it finds one or more [Element]s.
|
||||
Future<Finder> _waitForElement(Finder finder) async {
|
||||
if (frameSync)
|
||||
await _waitUntilFrame(() => SchedulerBinding.instance.transientCallbackCount == 0);
|
||||
|
||||
await _waitUntilFrame(() => finder.precache());
|
||||
|
||||
if (frameSync)
|
||||
await _waitUntilFrame(() => SchedulerBinding.instance.transientCallbackCount == 0);
|
||||
|
||||
return finder;
|
||||
}
|
||||
|
||||
Future<Null> tap(Finder finder) async {
|
||||
await _controller.tap(await _waitForElement(finder));
|
||||
}
|
||||
|
||||
Future<Null> scrollIntoView(Finder finder, {double alignment}) async {
|
||||
final Finder target = await _waitForElement(finder);
|
||||
await Scrollable.ensureVisible(target.evaluate().single, duration: const Duration(milliseconds: 100), alignment: alignment);
|
||||
}
|
||||
}
|
15
examples/flutter_gallery/tool/run_instrumentation_test.sh
Executable file
15
examples/flutter_gallery/tool/run_instrumentation_test.sh
Executable file
@ -0,0 +1,15 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
if [ ! -f "./pubspec.yaml" ]; then
|
||||
echo "ERROR: current directory must be the root of flutter_gallery package"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd android
|
||||
|
||||
# Currently there's no non-hacky way to pass a device ID to gradlew, but it's
|
||||
# OK as in the devicelab we have one device per host.
|
||||
#
|
||||
# See also: https://goo.gl/oe5aUW
|
||||
./gradlew connectedAndroidTest -Ptarget=test/live_smoketest.dart
|
Loading…
x
Reference in New Issue
Block a user