Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Sign in
Toggle navigation
F
Front-End
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
abdullh.alsoleman
Front-End
Commits
0504fac7
Unverified
Commit
0504fac7
authored
Jul 20, 2021
by
Emmanuel Garcia
Committed by
GitHub
Jul 20, 2021
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Android e2e screenshot (#84472)
parent
2b7b4bdc
Changes
19
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
19 changed files
with
743 additions
and
72 deletions
+743
-72
_matchers_io.dart
packages/flutter_test/lib/src/_matchers_io.dart
+21
-3
matchers_test.dart
packages/flutter_test/test/matchers_test.dart
+14
-1
README.md
packages/integration_test/README.md
+71
-0
build.gradle
packages/integration_test/android/build.gradle
+2
-0
FlutterDeviceScreenshot.java
...ter/plugins/integration_test/FlutterDeviceScreenshot.java
+262
-0
IntegrationTestPlugin.java
...utter/plugins/integration_test/IntegrationTestPlugin.java
+71
-11
AndroidManifest.xml
...ion_test/example/android/app/src/main/AndroidManifest.xml
+0
-6
EmbedderV1Activity.java
...main/java/com/example/e2e_example/EmbedderV1Activity.java
+0
-18
project-webview_flutter.lockfile
...ion_test/example/android/project-webview_flutter.lockfile
+100
-0
_extended_test_io.dart
...tion_test/example/integration_test/_extended_test_io.dart
+16
-5
extended_test.dart
...egration_test/example/integration_test/extended_test.dart
+1
-2
extended_integration_test.dart
...n_test/example/test_driver/extended_integration_test.dart
+1
-0
_callback_io.dart
packages/integration_test/lib/_callback_io.dart
+53
-3
_callback_web.dart
packages/integration_test/lib/_callback_web.dart
+8
-1
common.dart
packages/integration_test/lib/common.dart
+20
-2
integration_test.dart
packages/integration_test/lib/integration_test.dart
+24
-8
integration_test_driver.dart
packages/integration_test/lib/integration_test_driver.dart
+16
-7
integration_test_driver_extended.dart
...ntegration_test/lib/integration_test_driver_extended.dart
+54
-5
channel.dart
packages/integration_test/lib/src/channel.dart
+9
-0
No files found.
packages/flutter_test/lib/src/_matchers_io.dart
View file @
0504fac7
...
...
@@ -48,6 +48,26 @@ class MatchesGoldenFile extends AsyncMatcher {
@override
Future
<
String
?>
matchAsync
(
dynamic
item
)
async
{
final
Uri
testNameUri
=
goldenFileComparator
.
getTestUri
(
key
,
version
);
Uint8List
?
buffer
;
if
(
item
is
Future
<
List
<
int
>>)
{
buffer
=
Uint8List
.
fromList
(
await
item
);
}
else
if
(
item
is
List
<
int
>)
{
buffer
=
Uint8List
.
fromList
(
item
);
}
if
(
buffer
!=
null
)
{
if
(
autoUpdateGoldenFiles
)
{
await
goldenFileComparator
.
update
(
testNameUri
,
buffer
);
return
null
;
}
try
{
final
bool
success
=
await
goldenFileComparator
.
compare
(
buffer
,
testNameUri
);
return
success
?
null
:
'does not match'
;
}
on
TestFailure
catch
(
ex
)
{
return
ex
.
message
;
}
}
Future
<
ui
.
Image
?>
imageFuture
;
if
(
item
is
Future
<
ui
.
Image
?>)
{
imageFuture
=
item
;
...
...
@@ -62,11 +82,9 @@ class MatchesGoldenFile extends AsyncMatcher {
}
imageFuture
=
captureImage
(
elements
.
single
);
}
else
{
throw
'must provide a Finder, Image,
or Future<Image
>'
;
throw
'must provide a Finder, Image,
Future<Image>, List<int>, or Future<List<int>
>'
;
}
final
Uri
testNameUri
=
goldenFileComparator
.
getTestUri
(
key
,
version
);
final
TestWidgetsFlutterBinding
binding
=
TestWidgetsFlutterBinding
.
ensureInitialized
()
as
TestWidgetsFlutterBinding
;
return
binding
.
runAsync
<
String
?>(()
async
{
final
ui
.
Image
?
image
=
await
imageFuture
;
...
...
packages/flutter_test/test/matchers_test.dart
View file @
0504fac7
...
...
@@ -352,7 +352,6 @@ void main() {
});
group
(
'matches'
,
()
{
testWidgets
(
'if comparator succeeds'
,
(
WidgetTester
tester
)
async
{
await
tester
.
pumpWidget
(
boilerplate
(
const
Text
(
'hello'
)));
final
Finder
finder
=
find
.
byType
(
Text
);
...
...
@@ -361,6 +360,20 @@ void main() {
expect
(
comparator
.
imageBytes
,
hasLength
(
greaterThan
(
0
)));
expect
(
comparator
.
golden
,
Uri
.
parse
(
'foo.png'
));
});
testWidgets
(
'list of integers'
,
(
WidgetTester
tester
)
async
{
await
expectLater
(<
int
>[
1
,
2
],
matchesGoldenFile
(
'foo.png'
));
expect
(
comparator
.
invocation
,
_ComparatorInvocation
.
compare
);
expect
(
comparator
.
imageBytes
,
equals
(<
int
>[
1
,
2
]));
expect
(
comparator
.
golden
,
Uri
.
parse
(
'foo.png'
));
});
testWidgets
(
'future list of integers'
,
(
WidgetTester
tester
)
async
{
await
expectLater
(
Future
<
List
<
int
>>.
value
(<
int
>[
1
,
2
]),
matchesGoldenFile
(
'foo.png'
));
expect
(
comparator
.
invocation
,
_ComparatorInvocation
.
compare
);
expect
(
comparator
.
imageBytes
,
equals
(<
int
>[
1
,
2
]));
expect
(
comparator
.
golden
,
Uri
.
parse
(
'foo.png'
));
});
});
group
(
'does not match'
,
()
{
...
...
packages/integration_test/README.md
View file @
0504fac7
...
...
@@ -95,6 +95,77 @@ flutter drive \
-d
web-server
```
### Screenshots
You can use
`integration_test`
to take screenshots of the UI rendered on the mobile device or
Web browser at a specific time during the test.
This feature is currently supported on Android, and Web.
#### Android
**integration_test/screenshot_test.dart**
```
dart
void
main
(
)
{
final
IntegrationTestWidgetsFlutterBinding
binding
=
IntegrationTestWidgetsFlutterBinding
.
ensureInitialized
();
testWidgets
(
'screenshot'
,
(
WidgetTester
tester
)
async
{
// Build the app.
app
.
main
();
// This is required prior to taking the screenshot.
await
binding
.
convertFlutterSurfaceToImage
();
// Trigger a frame.
await
tester
.
pumpAndSettle
();
await
binding
.
takeScreenshot
(
'screenshot-1'
);
});
}
```
You can use a driver script to pull in the screenshot from the device.
This way, you can store the images locally on your computer.
**test_driver/integration_test.dart**
```
dart
import
'dart:io'
;
import
'package:integration_test/integration_test_driver_extended.dart'
;
Future
<
void
>
main
()
async
{
await
integrationDriver
(
onScreenshot:
(
String
screenshotName
,
List
<
int
>
screenshotBytes
)
async
{
final
File
image
=
File
(
'
$screenshotName
.png'
);
image
.
writeAsBytesSync
(
screenshotBytes
);
// Return false if the screenshot is invalid.
return
true
;
},
);
}
```
#### Web
**integration_test/screenshot_test.dart**
```
dart
void
main
(
)
{
final
IntegrationTestWidgetsFlutterBinding
binding
=
IntegrationTestWidgetsFlutterBinding
.
ensureInitialized
();
testWidgets
(
'screenshot'
,
(
WidgetTester
tester
)
async
{
// Build the app.
app
.
main
();
// Trigger a frame.
await
tester
.
pumpAndSettle
();
await
binding
.
takeScreenshot
(
'screenshot-1'
);
});
}
```
## Android Device Testing
Create an instrumentation test file in your application's
...
...
packages/integration_test/android/build.gradle
View file @
0504fac7
...
...
@@ -39,6 +39,8 @@ android {
}
dependencies
{
// TODO(egarciad): These dependencies should not be added to release builds.
// https://github.com/flutter/flutter/issues/56591
api
'junit:junit:4.12'
// https://developer.android.com/jetpack/androidx/releases/test/#1.2.0
...
...
packages/integration_test/android/src/main/java/dev/flutter/plugins/integration_test/FlutterDeviceScreenshot.java
0 → 100644
View file @
0504fac7
// 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.
package
dev
.
flutter
.
plugins
.
integration_test
;
import
android.annotation.TargetApi
;
import
android.app.Activity
;
import
android.graphics.Bitmap
;
import
android.graphics.Canvas
;
import
android.graphics.Rect
;
import
android.os.Build
;
import
android.os.Handler
;
import
android.os.HandlerThread
;
import
android.os.Looper
;
import
android.view.Choreographer
;
import
android.view.PixelCopy
;
import
android.view.View
;
import
android.view.ViewGroup
;
import
androidx.annotation.NonNull
;
import
androidx.annotation.Nullable
;
import
io.flutter.embedding.android.FlutterActivity
;
import
io.flutter.embedding.android.FlutterSurfaceView
;
import
io.flutter.embedding.android.FlutterView
;
import
io.flutter.plugin.common.MethodChannel
;
import
io.flutter.plugin.common.MethodChannel.Result
;
import
java.io.ByteArrayOutputStream
;
import
java.io.IOException
;
import
java.lang.StringBuilder
;
/**
* FlutterDeviceScreenshot is a utility class that allows to capture a screenshot
* that includes both Android views and the Flutter UI.
*
* To take screenshots, the rendering surface must be changed to {@code FlutterImageView},
* since surfaces like {@code FlutterSurfaceView} and {@code FlutterTextureView} are opaque
* when the view hierarchy is rendered to a bitmap.
*
* It's also necessary to ask the framework to schedule a frame, and then add a listener
* that waits for that frame to be presented by the Android framework.
*/
@TargetApi
(
19
)
class
FlutterDeviceScreenshot
{
/**
* Finds the {@code FlutterView} added to the {@code activity} view hierarchy.
*
* <p> This assumes that there's only one {@code FlutterView} per activity, which
* is always the case.
*
* @param activity typically, {code FlutterActivity}.
* @return the Flutter view.
*/
@Nullable
private
static
FlutterView
getFlutterView
(
@NonNull
Activity
activity
)
{
return
(
FlutterView
)
activity
.
findViewById
(
FlutterActivity
.
FLUTTER_VIEW_ID
);
}
/**
* Whether the app is run with instrumentation.
*
* @return true if the app is running with instrumentation.
*/
static
boolean
hasInstrumentation
()
{
// TODO(egarciad): InstrumentationRegistry requires the uiautomator dependency.
// However, Flutter adds test dependencies to release builds.
// As a result, disable screenshots with instrumentation until the issue is fixed.
// https://github.com/flutter/flutter/issues/56591
return
false
;
}
/**
* Captures a screenshot using ui automation.
*
* @return byte array containing the screenshot.
*/
static
byte
[]
captureWithUiAutomation
()
throws
IOException
{
return
new
byte
[
0
];
}
// Whether the flutter surface is already converted to an image.
private
static
boolean
flutterSurfaceConvertedToImage
=
false
;
/**
* Converts the Flutter surface to an image view.
* This allows to render the view hierarchy to a bitmap since
* {@code FlutterSurfaceView} and {@code FlutterTextureView} cannot be rendered to a bitmap.
*
* @param activity typically {@code FlutterActivity}.
*/
static
void
convertFlutterSurfaceToImage
(
@NonNull
Activity
activity
)
{
final
FlutterView
flutterView
=
getFlutterView
(
activity
);
if
(
flutterView
!=
null
&&
!
flutterSurfaceConvertedToImage
)
{
flutterView
.
convertToImageView
();
flutterSurfaceConvertedToImage
=
true
;
}
}
/**
* Restores the original Flutter surface.
* The new surface will either be {@code FlutterSurfaceView} or {@code FlutterTextureView}.
*
* @param activity typically {@code FlutterActivity}.
* @param onDone callback called once the surface has been restored.
*/
static
void
revertFlutterImage
(
@NonNull
Activity
activity
)
{
final
FlutterView
flutterView
=
getFlutterView
(
activity
);
if
(
flutterView
!=
null
&&
flutterSurfaceConvertedToImage
)
{
flutterView
.
revertImageView
(()
->
{
flutterSurfaceConvertedToImage
=
false
;
});
}
}
// Handlers use to capture a view.
private
static
Handler
backgroundHandler
;
private
static
Handler
mainHandler
;
/**
* Captures a screenshot by drawing the view to a Canvas.
*
* <p> {@code convertFlutterSurfaceToImage} must be called prior to capturing the view,
* otherwise the result is an error.
*
* @param activity this is {@link FlutterActivity}.
* @param methodChannel the method channel to call into Dart.
* @param result the result for the method channel that will contain the byte array.
*/
static
void
captureView
(
@NonNull
Activity
activity
,
@NonNull
MethodChannel
methodChannel
,
@NonNull
Result
result
)
{
final
FlutterView
flutterView
=
getFlutterView
(
activity
);
if
(
flutterView
==
null
)
{
result
.
error
(
"Could not copy the pixels"
,
"FlutterView is null"
,
null
);
return
;
}
if
(!
flutterSurfaceConvertedToImage
)
{
result
.
error
(
"Could not copy the pixels"
,
"Flutter surface must be converted to image first"
,
null
);
return
;
}
// Ask the framework to schedule a new frame.
methodChannel
.
invokeMethod
(
"scheduleFrame"
,
null
);
if
(
backgroundHandler
==
null
)
{
final
HandlerThread
screenshotBackgroundThread
=
new
HandlerThread
(
"screenshot"
);
screenshotBackgroundThread
.
start
();
backgroundHandler
=
new
Handler
(
screenshotBackgroundThread
.
getLooper
());
}
if
(
mainHandler
==
null
)
{
mainHandler
=
new
Handler
(
Looper
.
getMainLooper
());
}
takeScreenshot
(
backgroundHandler
,
mainHandler
,
flutterView
,
result
);
}
/**
* Waits for the next Android frame.
*
* @param r a callback.
*/
private
static
void
waitForAndroidFrame
(
Runnable
r
)
{
Choreographer
.
getInstance
()
.
postFrameCallback
(
new
Choreographer
.
FrameCallback
()
{
@Override
public
void
doFrame
(
long
frameTimeNanos
)
{
r
.
run
();
}
});
}
/**
* Waits until a Flutter frame is rendered by the Android OS.
*
* @param backgroundHandler the handler associated to a background thread.
* @param mainHandler the handler associated to the platform thread.
* @param view the flutter view.
* @param result the result that contains the byte array.
*/
private
static
void
takeScreenshot
(
@NonNull
Handler
backgroundHandler
,
@NonNull
Handler
mainHandler
,
@NonNull
FlutterView
view
,
@NonNull
Result
result
)
{
final
boolean
acquired
=
view
.
acquireLatestImageViewFrame
();
// The next frame may already have already been comitted.
// The next frame is guaranteed to have the Flutter image.
waitForAndroidFrame
(
()
->
{
waitForAndroidFrame
(
()
->
{
if
(
acquired
)
{
FlutterDeviceScreenshot
.
convertViewToBitmap
(
view
,
result
,
backgroundHandler
);
}
else
{
takeScreenshot
(
backgroundHandler
,
mainHandler
,
view
,
result
);
}
});
});
}
/**
* Renders {@code FlutterView} to a Bitmap.
*
* If successful, The byte array is provided in the result.
*
* @param flutterView the Flutter view.
* @param result the result that contains the byte array.
* @param backgroundHandler a background handler to avoid blocking the platform thread.
*/
private
static
void
convertViewToBitmap
(
@NonNull
FlutterView
flutterView
,
@NonNull
Result
result
,
@NonNull
Handler
backgroundHandler
)
{
if
(
Build
.
VERSION
.
SDK_INT
<
Build
.
VERSION_CODES
.
O
)
{
final
Bitmap
bitmap
=
Bitmap
.
createBitmap
(
flutterView
.
getWidth
(),
flutterView
.
getHeight
(),
Bitmap
.
Config
.
RGB_565
);
final
Canvas
canvas
=
new
Canvas
(
bitmap
);
flutterView
.
draw
(
canvas
);
final
ByteArrayOutputStream
output
=
new
ByteArrayOutputStream
();
bitmap
.
compress
(
Bitmap
.
CompressFormat
.
PNG
,
/*quality=*/
100
,
output
);
result
.
success
(
output
.
toByteArray
());
return
;
}
final
Bitmap
bitmap
=
Bitmap
.
createBitmap
(
flutterView
.
getWidth
(),
flutterView
.
getHeight
(),
Bitmap
.
Config
.
ARGB_8888
);
final
int
[]
flutterViewLocation
=
new
int
[
2
];
flutterView
.
getLocationInWindow
(
flutterViewLocation
);
final
int
flutterViewLeft
=
flutterViewLocation
[
0
];
final
int
flutterViewTop
=
flutterViewLocation
[
1
];
final
Rect
flutterViewRect
=
new
Rect
(
flutterViewLeft
,
flutterViewTop
,
flutterViewLeft
+
flutterView
.
getWidth
(),
flutterViewTop
+
flutterView
.
getHeight
());
final
Activity
flutterActivity
=
(
Activity
)
flutterView
.
getContext
();
PixelCopy
.
request
(
flutterActivity
.
getWindow
(),
flutterViewRect
,
bitmap
,
(
int
copyResult
)
->
{
final
Handler
mainHandler
=
new
Handler
(
Looper
.
getMainLooper
());
if
(
copyResult
==
PixelCopy
.
SUCCESS
)
{
final
ByteArrayOutputStream
output
=
new
ByteArrayOutputStream
();
bitmap
.
compress
(
Bitmap
.
CompressFormat
.
PNG
,
/*quality=*/
100
,
output
);
mainHandler
.
post
(
()
->
{
result
.
success
(
output
.
toByteArray
());
});
}
else
{
mainHandler
.
post
(
()
->
{
result
.
error
(
"Could not copy the pixels"
,
"result was "
+
copyResult
,
null
);
});
}
},
backgroundHandler
);
}
}
packages/integration_test/android/src/main/java/dev/flutter/plugins/integration_test/IntegrationTestPlugin.java
View file @
0504fac7
...
...
@@ -4,26 +4,30 @@
package
dev
.
flutter
.
plugins
.
integration_test
;
import
android.app.Activity
;
import
android.content.Context
;
import
com.google.common.util.concurrent.SettableFuture
;
import
io.flutter.embedding.engine.plugins.FlutterPlugin
;
import
io.flutter.embedding.engine.plugins.activity.ActivityAware
;
import
io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
;
import
io.flutter.plugin.common.BinaryMessenger
;
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
java.io.IOException
;
import
java.util.Map
;
import
java.util.concurrent.Future
;
/** IntegrationTestPlugin */
public
class
IntegrationTestPlugin
implements
MethodCallHandler
,
FlutterPlugin
{
private
MethodChannel
methodChannel
;
public
class
IntegrationTestPlugin
implements
MethodCallHandler
,
FlutterPlugin
,
ActivityAware
{
private
static
final
String
CHANNEL
=
"plugins.flutter.io/integration_test"
;
private
static
final
SettableFuture
<
Map
<
String
,
String
>>
testResultsSettable
=
SettableFuture
.
create
();
public
static
final
Future
<
Map
<
String
,
String
>>
testResults
=
testResultsSettable
;
private
static
final
String
CHANNEL
=
"plugins.flutter.io/integration_test"
;
private
MethodChannel
methodChannel
;
private
Activity
flutterActivity
;
public
static
final
Future
<
Map
<
String
,
String
>>
testResults
=
testResultsSettable
;
/** Plugin registration. */
@SuppressWarnings
(
"deprecation"
)
...
...
@@ -48,14 +52,70 @@ public class IntegrationTestPlugin implements MethodCallHandler, FlutterPlugin {
methodChannel
=
null
;
}
@Override
public
void
onAttachedToActivity
(
ActivityPluginBinding
binding
)
{
flutterActivity
=
binding
.
getActivity
();
}
@Override
public
void
onReattachedToActivityForConfigChanges
(
ActivityPluginBinding
binding
)
{
flutterActivity
=
binding
.
getActivity
();
}
@Override
public
void
onDetachedFromActivity
()
{
flutterActivity
=
null
;
}
@Override
public
void
onDetachedFromActivityForConfigChanges
()
{
flutterActivity
=
null
;
}
@Override
public
void
onMethodCall
(
MethodCall
call
,
Result
result
)
{
if
(
call
.
method
.
equals
(
"allTestsFinished"
))
{
Map
<
String
,
String
>
results
=
call
.
argument
(
"results"
);
testResultsSettable
.
set
(
results
);
result
.
success
(
null
);
}
else
{
result
.
notImplemented
();
switch
(
call
.
method
)
{
case
"allTestsFinished"
:
final
Map
<
String
,
String
>
results
=
call
.
argument
(
"results"
);
testResultsSettable
.
set
(
results
);
result
.
success
(
null
);
return
;
case
"convertFlutterSurfaceToImage"
:
if
(
flutterActivity
==
null
)
{
result
.
error
(
"Could not convert to image"
,
"Activity not initialized"
,
null
);
return
;
}
FlutterDeviceScreenshot
.
convertFlutterSurfaceToImage
(
flutterActivity
);
result
.
success
(
null
);
return
;
case
"revertFlutterImage"
:
if
(
flutterActivity
==
null
)
{
result
.
error
(
"Could not revert Flutter image"
,
"Activity not initialized"
,
null
);
return
;
}
FlutterDeviceScreenshot
.
revertFlutterImage
(
flutterActivity
);
result
.
success
(
null
);
return
;
case
"captureScreenshot"
:
if
(
FlutterDeviceScreenshot
.
hasInstrumentation
())
{
byte
[]
image
;
try
{
image
=
FlutterDeviceScreenshot
.
captureWithUiAutomation
();
}
catch
(
IOException
exception
)
{
result
.
error
(
"Could not capture screenshot"
,
"UiAutomation failed"
,
exception
);
return
;
}
result
.
success
(
image
);
return
;
}
if
(
flutterActivity
==
null
)
{
result
.
error
(
"Could not capture screenshot"
,
"Activity not initialized"
,
null
);
return
;
}
FlutterDeviceScreenshot
.
captureView
(
flutterActivity
,
methodChannel
,
result
);
return
;
default
:
result
.
notImplemented
();
}
}
}
packages/integration_test/example/android/app/src/main/AndroidManifest.xml
View file @
0504fac7
...
...
@@ -14,12 +14,6 @@ found in the LICENSE file. -->
android:name=
"io.flutter.app.FlutterApplication"
android:label=
"integration_test_example"
android:icon=
"@mipmap/ic_launcher"
>
<activity
android:name=
".EmbedderV1Activity"
android:theme=
"@android:style/Theme.Black.NoTitleBar"
android:configChanges=
"orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection"
android:hardwareAccelerated=
"true"
android:windowSoftInputMode=
"adjustResize"
>
</activity>
<activity
android:name=
"io.flutter.embedding.android.FlutterActivity"
android:theme=
"@style/LaunchTheme"
...
...
packages/integration_test/example/android/app/src/main/java/com/example/e2e_example/EmbedderV1Activity.java
deleted
100644 → 0
View file @
2b7b4bdc
// 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.
package
com
.
example
.
integration_test_example
;
import
android.os.Bundle
;
import
dev.flutter.plugins.integration_test.IntegrationTestPlugin
;
import
io.flutter.app.FlutterActivity
;
public
class
EmbedderV1Activity
extends
FlutterActivity
{
@Override
protected
void
onCreate
(
Bundle
savedInstanceState
)
{
super
.
onCreate
(
savedInstanceState
);
IntegrationTestPlugin
.
registerWith
(
registrarFor
(
"dev.flutter.plugins.integration_test.IntegrationTestPlugin"
));
}
}
packages/integration_test/example/android/project-webview_flutter.lockfile
0 → 100644
View file @
0504fac7
This diff is collapsed.
Click to expand it.
packages/integration_test/example/integration_test/_extended_test_io.dart
View file @
0504fac7
...
...
@@ -10,6 +10,7 @@
// tree, read text, and verify that the values of widget properties are correct.
import
'dart:io'
show
Platform
;
import
'package:flutter/foundation.dart'
;
import
'package:flutter/material.dart'
;
import
'package:flutter_test/flutter_test.dart'
;
import
'package:integration_test/integration_test.dart'
;
...
...
@@ -17,17 +18,27 @@ import 'package:integration_test/integration_test.dart';
import
'package:integration_test_example/main.dart'
as
app
;
void
main
(
)
{
IntegrationTestWidgetsFlutterBinding
.
ensureInitialized
();
final
IntegrationTestWidgetsFlutterBinding
binding
=
IntegrationTestWidgetsFlutterBinding
.
ensureInitialized
()
as
IntegrationTestWidgetsFlutterBinding
;
testWidgets
(
'verify text'
,
(
WidgetTester
tester
)
async
{
// Build our app
and trigger a frame
.
// Build our app.
app
.
main
();
// Trigger a frame.
// On Android, this is required prior to taking the screenshot.
await
binding
.
convertFlutterSurfaceToImage
();
// Pump a frame before taking the screenshot.
await
tester
.
pumpAndSettle
();
final
List
<
int
>
firstPng
=
await
binding
.
takeScreenshot
(
'first'
);
expect
(
firstPng
.
isNotEmpty
,
isTrue
);
// Pump another frame before taking the screenshot.
await
tester
.
pumpAndSettle
();
final
List
<
int
>
secondPng
=
await
binding
.
takeScreenshot
(
'second'
);
expect
(
secondPng
.
isNotEmpty
,
isTrue
);
// TODO(nturgut): https://github.com/flutter/flutter/issues/51890
// Add screenshot capability for mobile platforms.
expect
(
listEquals
(
firstPng
,
secondPng
),
isTrue
);
// Verify that platform version is retrieved.
expect
(
...
...
packages/integration_test/example/integration_test/extended_test.dart
View file @
0504fac7
...
...
@@ -4,8 +4,7 @@
// This is a Flutter widget test can take a screenshot.
//
// NOTE: Screenshots are only supported on Web for now. For Web, this needs to
// be executed with the `test_driver/integration_test_extended_driver.dart`.
// For Web, this needs to be executed with the `test_driver/integration_test_extended_driver.dart`.
//
// To perform an interaction with a widget in your test, use the WidgetTester
// utility that Flutter provides. For example, you can send tap and scroll
...
...
packages/integration_test/example/test_driver/extended_integration_test.dart
View file @
0504fac7
...
...
@@ -10,6 +10,7 @@ Future<void> main() async {
await
integrationDriver
(
driver:
driver
,
onScreenshot:
(
String
screenshotName
,
List
<
int
>
screenshotBytes
)
async
{
// Return false if the screenshot is invalid.
return
true
;
},
);
...
...
packages/integration_test/lib/_callback_io.dart
View file @
0504fac7
...
...
@@ -2,7 +2,13 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import
'dart:ui'
;
import
'package:flutter/services.dart'
;
import
'package:flutter_test/flutter_test.dart'
;
import
'common.dart'
;
import
'src/channel.dart'
;
/// The dart:io implementation of [CallbackManager].
///
...
...
@@ -54,9 +60,53 @@ class IOCallbackManager implements CallbackManager {
// comes up in the future. For example: `WebCallbackManager.cleanup`.
}
// Whether the Flutter surface uses an Image.
bool
_usesFlutterImage
=
false
;
@override
Future
<
void
>
convertFlutterSurfaceToImage
()
async
{
assert
(!
_usesFlutterImage
,
'Surface already converted to an image'
);
await
integrationTestChannel
.
invokeMethod
<
void
>(
'convertFlutterSurfaceToImage'
,
null
,
);
_usesFlutterImage
=
true
;
addTearDown
(()
async
{
assert
(
_usesFlutterImage
,
'Surface is not an image'
);
await
integrationTestChannel
.
invokeMethod
<
void
>(
'revertFlutterImage'
,
null
,
);
_usesFlutterImage
=
false
;
});
}
@override
Future
<
void
>
takeScreenshot
(
String
screenshot
)
{
throw
UnimplementedError
(
'Screenshots are not implemented on this platform'
);
Future
<
Map
<
String
,
dynamic
>>
takeScreenshot
(
String
screenshot
)
async
{
if
(!
_usesFlutterImage
)
{
throw
StateError
(
'Call convertFlutterSurfaceToImage() before taking a screenshot'
);
}
integrationTestChannel
.
setMethodCallHandler
(
_onMethodChannelCall
);
final
List
<
int
>?
rawBytes
=
await
integrationTestChannel
.
invokeMethod
<
List
<
int
>>(
'captureScreenshot'
,
null
,
);
if
(
rawBytes
==
null
)
{
throw
StateError
(
'Expected a list of bytes, but instead captureScreenshot returned null'
);
}
return
<
String
,
dynamic
>{
'screenshotName'
:
screenshot
,
'bytes'
:
rawBytes
,
};
}
Future
<
dynamic
>
_onMethodChannelCall
(
MethodCall
call
)
async
{
switch
(
call
.
method
)
{
case
'scheduleFrame'
:
window
.
scheduleFrame
();
break
;
}
return
null
;
}
}
packages/integration_test/lib/_callback_web.dart
View file @
0504fac7
...
...
@@ -44,8 +44,15 @@ class WebCallbackManager implements CallbackManager {
///
/// See: https://www.w3.org/TR/webdriver/#screen-capture.
@override
Future
<
void
>
takeScreenshot
(
String
screenshotName
)
async
{
Future
<
Map
<
String
,
dynamic
>
>
takeScreenshot
(
String
screenshotName
)
async
{
await
_sendWebDriverCommand
(
WebDriverCommand
.
screenshot
(
screenshotName
));
// Flutter Web doesn't provide the bytes.
return
const
<
String
,
dynamic
>{
'bytes'
:
<
int
>[]};
}
@override
Future
<
void
>
convertFlutterSurfaceToImage
()
async
{
// Noop on Web.
}
Future
<
void
>
_sendWebDriverCommand
(
WebDriverCommand
command
)
async
{
...
...
packages/integration_test/lib/common.dart
View file @
0504fac7
...
...
@@ -5,6 +5,20 @@
import
'dart:async'
;
import
'dart:convert'
;
/// A callback to use with [integrationDriver].
///
/// The callback receives the name of screenshot passed to `binding.takeScreenshot(<name>)` and
/// a PNG byte buffer.
///
/// The callback returns `true` if the test passes or `false` otherwise.
///
/// You can use this callback to store the bytes locally in a file or upload them to a service
/// that compares the image against a gold or baseline version.
///
/// Since the function is executed on the host driving the test, you can access any environment
/// variable from it.
typedef
ScreenshotCallback
=
Future
<
bool
>
Function
(
String
name
,
List
<
int
>
image
);
/// Classes shared between `integration_test.dart` and `flutter drive` based
/// adoptor (ex: `integration_test_driver.dart`).
...
...
@@ -270,8 +284,12 @@ abstract class CallbackManager {
Future
<
Map
<
String
,
dynamic
>>
callback
(
Map
<
String
,
String
>
params
,
IntegrationTestResults
testRunner
);
/// Request to take a screenshot of the application.
Future
<
void
>
takeScreenshot
(
String
screenshot
);
/// Takes a screenshot of the application.
/// Returns the data that is sent back to the host.
Future
<
Map
<
String
,
dynamic
>>
takeScreenshot
(
String
screenshot
);
/// Android only. Converts the Flutter surface to an image view.
Future
<
void
>
convertFlutterSurfaceToImage
();
/// Cleanup and completers or locks used during the communication.
void
cleanup
();
...
...
packages/integration_test/lib/integration_test.dart
View file @
0504fac7
...
...
@@ -17,6 +17,7 @@ import 'package:vm_service/vm_service_io.dart' as vm_io;
import
'_callback_io.dart'
if
(
dart
.
library
.
html
)
'_callback_web.dart'
as
driver_actions
;
import
'_extension_io.dart'
if
(
dart
.
library
.
html
)
'_extension_web.dart'
;
import
'common.dart'
;
import
'src/channel.dart'
;
const
String
_success
=
'success'
;
...
...
@@ -51,7 +52,7 @@ class IntegrationTestWidgetsFlutterBinding extends LiveTestWidgetsFlutterBinding
}
try
{
await
_c
hannel
.
invokeMethod
<
void
>(
await
integrationTestC
hannel
.
invokeMethod
<
void
>(
'allTestsFinished'
,
<
String
,
dynamic
>{
'results'
:
results
.
map
<
String
,
dynamic
>((
String
name
,
Object
result
)
{
...
...
@@ -144,9 +145,6 @@ https://flutter.dev/docs/testing/integration-tests#testing-on-firebase-test-lab
return
WidgetsBinding
.
instance
!;
}
static
const
MethodChannel
_channel
=
MethodChannel
(
'plugins.flutter.io/integration_test'
);
/// Test results that will be populated after the tests have completed.
///
/// Keys are the test descriptions, and values are either [_success] or
...
...
@@ -167,11 +165,29 @@ https://flutter.dev/docs/testing/integration-tests#testing-on-firebase-test-lab
/// side.
final
CallbackManager
callbackManager
=
driver_actions
.
callbackManager
;
/// Taking a screenshot.
/// Takes a screenshot.
///
/// On Android, you need to call `convertFlutterSurfaceToImage()`, and
/// pump a frame before taking a screenshot.
Future
<
List
<
int
>>
takeScreenshot
(
String
screenshotName
)
async
{
reportData
??=
<
String
,
dynamic
>{};
reportData
![
'screenshots'
]
??=
<
dynamic
>[];
final
Map
<
String
,
dynamic
>
data
=
await
callbackManager
.
takeScreenshot
(
screenshotName
);
assert
(
data
.
containsKey
(
'bytes'
));
(
reportData
![
'screenshots'
]!
as
List
<
dynamic
>).
add
(
data
);
return
data
[
'bytes'
]!
as
List
<
int
>;
}
/// Android only. Converts the Flutter surface to an image view.
/// Be aware that if you are conducting a perf test, you may not want to call
/// this method since the this is an expensive operation that affects the
/// rendering of a Flutter app.
///
/// Called by test methods. Implementation differs for each platform.
Future
<
void
>
takeScreenshot
(
String
screenshotName
)
async
{
await
callbackManager
.
takeScreenshot
(
screenshotName
);
/// Once the screenshot is taken, call `revertFlutterImage()` to restore
/// the original Flutter surface.
Future
<
void
>
convertFlutterSurfaceToImage
()
async
{
await
callbackManager
.
convertFlutterSurfaceToImage
();
}
/// The callback function to response the driver side input.
...
...
packages/integration_test/lib/integration_test_driver.dart
View file @
0504fac7
...
...
@@ -45,13 +45,6 @@ Future<void> writeResponseData(
/// Adaptor to run an integration test using `flutter drive`.
///
/// `timeout` controls the longest time waited before the test ends.
/// It is not necessarily the execution time for the test app: the test may
/// finish sooner than the `timeout`.
///
/// `responseDataCallback` is the handler for processing [Response.data].
/// The default value is `writeResponseData`.
///
/// To an integration test `<test_name>.dart` using `flutter drive`, put a file named
/// `<test_name>_test.dart` in the app's `test_driver` directory:
///
...
...
@@ -63,6 +56,21 @@ Future<void> writeResponseData(
/// Future<void> main() async => integrationDriver();
///
/// ```
///
/// ## Parameters:
///
/// `timeout` controls the longest time waited before the test ends.
/// It is not necessarily the execution time for the test app: the test may
/// finish sooner than the `timeout`.
///
/// `responseDataCallback` is the handler for processing [Response.data].
/// The default value is `writeResponseData`.
///
/// `onScreenshot` can be used to process the screenshots taken during the test.
/// An example could be that this callback compares the byte array against a baseline image,
/// and it returns `true` if both images are equal.
///
/// As a result, returning `false` from `onScreenshot` will make the test fail.
Future
<
void
>
integrationDriver
({
Duration
timeout
=
const
Duration
(
minutes:
20
),
ResponseDataCallback
?
responseDataCallback
=
writeResponseData
,
...
...
@@ -70,6 +78,7 @@ Future<void> integrationDriver({
final
FlutterDriver
driver
=
await
FlutterDriver
.
connect
();
final
String
jsonResult
=
await
driver
.
requestData
(
null
,
timeout:
timeout
);
final
Response
response
=
Response
.
fromJson
(
jsonResult
);
await
driver
.
close
();
if
(
response
.
allTestsPassed
)
{
...
...
packages/integration_test/lib/integration_test_driver_extended.dart
View file @
0504fac7
...
...
@@ -9,11 +9,36 @@ import 'package:flutter_driver/flutter_driver.dart';
import
'common.dart'
;
/// A callback to use with [integrationDriver].
typedef
ScreenshotCallback
=
Future
<
bool
>
Function
(
String
name
,
List
<
int
>
image
);
/// Example Integration Test which can also run WebDriver command depending on
/// the requests coming from the test methods.
/// Adaptor to run an integration test using `flutter drive`.
///
/// To an integration test `<test_name>.dart` using `flutter drive`, put a file named
/// `<test_name>_test.dart` in the app's `test_driver` directory:
///
/// ```dart
/// import 'dart:async';
///
/// import 'package:integration_test/integration_test_driver_extended.dart';
///
/// Future<void> main() async {
/// final FlutterDriver driver = await FlutterDriver.connect();
/// await integrationDriver(
/// driver: driver,
/// onScreenshot: (String screenshotName, List<int> screenshotBytes) async {
/// return true;
/// },
/// );
/// }
/// ```
///
/// ## Parameters:
///
/// `driver` A custom driver. Defaults to `FlutterDriver.connect()`.
///
/// `onScreenshot` can be used to process the screenshots taken during the test.
/// An example could be that this callback compares the byte array against a baseline image,
/// and it returns `true` if both images are equal.
///
/// As a result, returning `false` from `onScreenshot` will make the test fail.
Future
<
void
>
integrationDriver
(
{
FlutterDriver
?
driver
,
ScreenshotCallback
?
onScreenshot
})
async
{
driver
??=
await
FlutterDriver
.
connect
();
...
...
@@ -66,6 +91,30 @@ Future<void> integrationDriver(
print
(
'result
$jsonResponse
'
);
}
if
(
response
.
data
!=
null
&&
response
.
data
![
'screenshots'
]
!=
null
&&
onScreenshot
!=
null
)
{
final
List
<
dynamic
>
screenshots
=
response
.
data
![
'screenshots'
]
as
List
<
dynamic
>;
final
List
<
String
>
failures
=
<
String
>[];
for
(
final
dynamic
screenshot
in
screenshots
)
{
final
Map
<
String
,
dynamic
>
data
=
screenshot
as
Map
<
String
,
dynamic
>;
final
List
<
dynamic
>
screenshotBytes
=
data
[
'bytes'
]
as
List
<
dynamic
>;
final
String
screenshotName
=
data
[
'screenshotName'
]
as
String
;
bool
ok
=
false
;
try
{
ok
=
await
onScreenshot
(
screenshotName
,
screenshotBytes
.
cast
<
int
>());
}
catch
(
exception
)
{
throw
StateError
(
'Screenshot failure:
\n
'
'onScreenshot("
$screenshotName
", <bytes>) threw an exception:
$exception
'
);
}
if
(!
ok
)
{
failures
.
add
(
screenshotName
);
}
}
if
(
failures
.
isNotEmpty
)
{
throw
StateError
(
'The following screenshot tests failed:
${failures.join(', ')}
'
);
}
}
await
driver
.
close
();
if
(
response
.
allTestsPassed
)
{
...
...
packages/integration_test/lib/src/channel.dart
0 → 100644
View file @
0504fac7
// 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/services.dart'
;
/// The method channel used to report the result of the tests to the platform.
/// On Android, this is relevant when running instrumented tests.
const
MethodChannel
integrationTestChannel
=
MethodChannel
(
'plugins.flutter.io/integration_test'
);
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment