Unverified Commit 725c1415 authored by Kenzie Davisson's avatar Kenzie Davisson Committed by GitHub

Fix screenshot testing for flutter web integration_test (#117114)

* Fix screenshot testing for flutter web integration_test

* update packages

* fix method signature and todo

* Run tests on CI

* fix type

* remove silences

* Add docs

* fix comment

* fix whitespace

* review comments
parent bd482ebc
...@@ -1138,7 +1138,7 @@ Future<void> _runWebUnitTests(String webRenderer) async { ...@@ -1138,7 +1138,7 @@ Future<void> _runWebUnitTests(String webRenderer) async {
/// Coarse-grained integration tests running on the Web. /// Coarse-grained integration tests running on the Web.
Future<void> _runWebLongRunningTests() async { Future<void> _runWebLongRunningTests() async {
final List<ShardRunner> tests = <ShardRunner>[ final List<ShardRunner> tests = <ShardRunner>[
for (String buildMode in _kAllBuildModes) for (String buildMode in _kAllBuildModes) ...<ShardRunner>[
() => _runFlutterDriverWebTest( () => _runFlutterDriverWebTest(
testAppDirectory: path.join('packages', 'integration_test', 'example'), testAppDirectory: path.join('packages', 'integration_test', 'example'),
target: path.join('test_driver', 'failure.dart'), target: path.join('test_driver', 'failure.dart'),
...@@ -1148,6 +1148,21 @@ Future<void> _runWebLongRunningTests() async { ...@@ -1148,6 +1148,21 @@ Future<void> _runWebLongRunningTests() async {
// logs. To avoid confusion, silence browser output. // logs. To avoid confusion, silence browser output.
silenceBrowserOutput: true, silenceBrowserOutput: true,
), ),
() => _runFlutterDriverWebTest(
testAppDirectory: path.join('packages', 'integration_test', 'example'),
target: path.join('integration_test', 'example_test.dart'),
driver: path.join('test_driver', 'integration_test.dart'),
buildMode: buildMode,
renderer: 'canvaskit',
),
() => _runFlutterDriverWebTest(
testAppDirectory: path.join('packages', 'integration_test', 'example'),
target: path.join('integration_test', 'extended_test.dart'),
driver: path.join('test_driver', 'extended_integration_test.dart'),
buildMode: buildMode,
renderer: 'canvaskit',
),
],
// This test specifically tests how images are loaded in HTML mode, so we don't run it in CanvasKit mode. // This test specifically tests how images are loaded in HTML mode, so we don't run it in CanvasKit mode.
() => _runWebE2eTest('image_loading_integration', buildMode: 'debug', renderer: 'html'), () => _runWebE2eTest('image_loading_integration', buildMode: 'debug', renderer: 'html'),
...@@ -1281,6 +1296,7 @@ Future<void> _runFlutterDriverWebTest({ ...@@ -1281,6 +1296,7 @@ Future<void> _runFlutterDriverWebTest({
required String buildMode, required String buildMode,
required String renderer, required String renderer,
required String testAppDirectory, required String testAppDirectory,
String? driver,
bool expectFailure = false, bool expectFailure = false,
bool silenceBrowserOutput = false, bool silenceBrowserOutput = false,
}) async { }) async {
...@@ -1295,6 +1311,7 @@ Future<void> _runFlutterDriverWebTest({ ...@@ -1295,6 +1311,7 @@ Future<void> _runFlutterDriverWebTest({
<String>[ <String>[
...flutterTestArgs, ...flutterTestArgs,
'drive', 'drive',
if (driver != null) '--driver=$driver',
'--target=$target', '--target=$target',
'--browser-name=chrome', '--browser-name=chrome',
'--no-sound-null-safety', '--no-sound-null-safety',
......
...@@ -25,7 +25,7 @@ Future<void> runTestWithScreenshots({ ...@@ -25,7 +25,7 @@ Future<void> runTestWithScreenshots({
test.integrationDriver( test.integrationDriver(
driver: driver, driver: driver,
onScreenshot: (String screenshotName, List<int> screenshotBytes) async { onScreenshot: (String screenshotName, List<int> screenshotBytes, [Map<String, Object?>? args]) async {
// TODO(yjbanov): implement, see https://github.com/flutter/flutter/issues/86120 // TODO(yjbanov): implement, see https://github.com/flutter/flutter/issues/86120
return true; return true;
}, },
......
...@@ -17,7 +17,8 @@ import 'package:integration_test/integration_test.dart'; ...@@ -17,7 +17,8 @@ import 'package:integration_test/integration_test.dart';
import 'package:integration_test_example/main.dart' as app; import 'package:integration_test_example/main.dart' as app;
void main() { void main() {
final IntegrationTestWidgetsFlutterBinding binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized(); final IntegrationTestWidgetsFlutterBinding binding =
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
testWidgets('verify text', (WidgetTester tester) async { testWidgets('verify text', (WidgetTester tester) async {
// Build our app and trigger a frame. // Build our app and trigger a frame.
...@@ -27,7 +28,18 @@ void main() { ...@@ -27,7 +28,18 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
// Take a screenshot. // Take a screenshot.
await binding.takeScreenshot('platform_name'); await binding.takeScreenshot(
'platform_name',
// The optional parameter 'args' can be used to pass values to the
// [integrationDriver.onScreenshot] handler
// (see test_driver/extended_integration_test.dart). For example, you
// could look up environment variables in this test that were passed to
// the run command via `--dart-define=`, and then pass the values to the
// [integrationDriver.onScreenshot] handler through this 'args' map.
<String, Object?>{
'someArgumentKey': 'someArgumentValue',
},
);
// Verify that platform is retrieved. // Verify that platform is retrieved.
expect( expect(
...@@ -49,7 +61,20 @@ void main() { ...@@ -49,7 +61,20 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
// Multiple methods can take screenshots. Screenshots are taken with the // Multiple methods can take screenshots. Screenshots are taken with the
// same order the methods run. // same order the methods run. We pass an argument that can be looked up
await binding.takeScreenshot('platform_name_2'); // from the [onScreenshot] handler in
// [test_driver/extended_integration_test.dart].
await binding.takeScreenshot(
'platform_name_2',
// The optional parameter 'args' can be used to pass values to the
// [integrationDriver.onScreenshot] handler
// (see test_driver/extended_integration_test.dart). For example, you
// could look up environment variables in this test that were passed to
// the run command via `--dart-define=`, and then pass the values to the
// [integrationDriver.onScreenshot] handler through this 'args' map.
<String, Object?>{
'someArgumentKey': 'someArgumentValue',
},
);
}); });
} }
...@@ -9,8 +9,20 @@ Future<void> main() async { ...@@ -9,8 +9,20 @@ Future<void> main() async {
final FlutterDriver driver = await FlutterDriver.connect(); final FlutterDriver driver = await FlutterDriver.connect();
await integrationDriver( await integrationDriver(
driver: driver, driver: driver,
onScreenshot: (String screenshotName, List<int> screenshotBytes) async { onScreenshot: (
String screenshotName,
List<int> screenshotBytes, [
Map<String, Object?>? args,
]) async {
// Return false if the screenshot is invalid. // Return false if the screenshot is invalid.
// TODO(yjbanov): implement, see https://github.com/flutter/flutter/issues/86120
// Here is an example of using an argument that was passed in via the
// optional 'args' Map.
if (args != null) {
final String? someArgumentValue = args['someArgumentKey'] as String?;
return someArgumentValue != null;
}
return true; return true;
}, },
); );
......
...@@ -86,7 +86,8 @@ class IOCallbackManager implements CallbackManager { ...@@ -86,7 +86,8 @@ class IOCallbackManager implements CallbackManager {
} }
@override @override
Future<Map<String, dynamic>> takeScreenshot(String screenshot) async { Future<Map<String, dynamic>> takeScreenshot(String screenshot, [Map<String, Object?>? args]) async {
assert(args == null, '[args] handling has not been implemented for this platform');
if (Platform.isAndroid && !_isSurfaceRendered) { if (Platform.isAndroid && !_isSurfaceRendered) {
throw StateError('Call convertFlutterSurfaceToImage() before taking a screenshot'); throw StateError('Call convertFlutterSurfaceToImage() before taking a screenshot');
} }
......
...@@ -44,10 +44,13 @@ class WebCallbackManager implements CallbackManager { ...@@ -44,10 +44,13 @@ class WebCallbackManager implements CallbackManager {
/// ///
/// See: https://www.w3.org/TR/webdriver/#screen-capture. /// See: https://www.w3.org/TR/webdriver/#screen-capture.
@override @override
Future<Map<String, dynamic>> takeScreenshot(String screenshotName) async { Future<Map<String, dynamic>> takeScreenshot(String screenshotName, [Map<String, Object?>? args]) async {
await _sendWebDriverCommand(WebDriverCommand.screenshot(screenshotName)); await _sendWebDriverCommand(WebDriverCommand.screenshot(screenshotName, args));
// Flutter Web doesn't provide the bytes. return <String, dynamic>{
return const <String, dynamic>{'bytes': <int>[]}; 'screenshotName': screenshotName,
// Flutter Web doesn't provide the bytes.
'bytes': <int>[]
};
} }
@override @override
......
...@@ -7,17 +7,23 @@ import 'dart:convert'; ...@@ -7,17 +7,23 @@ import 'dart:convert';
/// A callback to use with [integrationDriver]. /// A callback to use with [integrationDriver].
/// ///
/// The callback receives the name of screenshot passed to `binding.takeScreenshot(<name>)` and /// The callback receives the name of screenshot passed to `binding.takeScreenshot(<name>)`,
/// a PNG byte buffer. /// a PNG byte buffer representing the screenshot, and an optional `Map` of arguments.
/// ///
/// The callback returns `true` if the test passes or `false` otherwise. /// 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 /// 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. /// that compares the image against a gold or baseline version.
/// ///
/// The optional `Map` of arguments can be passed from the
/// `binding.takeScreenshot(<name>, <args>)` callsite in the integration test,
/// and then the arguments can be used in the `onScreenshot` handler that is defined by
/// the Flutter driver. This `Map` should only contain values that can be serialized
/// to JSON.
///
/// Since the function is executed on the host driving the test, you can access any environment /// Since the function is executed on the host driving the test, you can access any environment
/// variable from it. /// variable from it.
typedef ScreenshotCallback = Future<bool> Function(String name, List<int> image); typedef ScreenshotCallback = Future<bool> Function(String name, List<int> image, [Map<String, Object?>? args]);
/// Classes shared between `integration_test.dart` and `flutter drive` based /// Classes shared between `integration_test.dart` and `flutter drive` based
/// adoptor (ex: `integration_test_driver.dart`). /// adoptor (ex: `integration_test_driver.dart`).
...@@ -247,9 +253,12 @@ class WebDriverCommand { ...@@ -247,9 +253,12 @@ class WebDriverCommand {
values = <String, dynamic>{}; values = <String, dynamic>{};
/// Constructor for [WebDriverCommandType.noop] screenshot. /// Constructor for [WebDriverCommandType.noop] screenshot.
WebDriverCommand.screenshot(String screenshotName) WebDriverCommand.screenshot(String screenshotName, [Map<String, Object?>? args])
: type = WebDriverCommandType.screenshot, : type = WebDriverCommandType.screenshot,
values = <String, dynamic>{'screenshot_name': screenshotName}; values = <String, dynamic>{
'screenshot_name': screenshotName,
if (args != null) 'args': args,
};
/// Type of the [WebDriverCommand]. /// Type of the [WebDriverCommand].
/// ///
...@@ -286,7 +295,7 @@ abstract class CallbackManager { ...@@ -286,7 +295,7 @@ abstract class CallbackManager {
/// Takes a screenshot of the application. /// Takes a screenshot of the application.
/// Returns the data that is sent back to the host. /// Returns the data that is sent back to the host.
Future<Map<String, dynamic>> takeScreenshot(String screenshot); Future<Map<String, dynamic>> takeScreenshot(String screenshot, [Map<String, Object?>? args]);
/// Android only. Converts the Flutter surface to an image view. /// Android only. Converts the Flutter surface to an image view.
Future<void> convertFlutterSurfaceToImage(); Future<void> convertFlutterSurfaceToImage();
......
...@@ -184,10 +184,10 @@ https://flutter.dev/docs/testing/integration-tests#testing-on-firebase-test-lab ...@@ -184,10 +184,10 @@ https://flutter.dev/docs/testing/integration-tests#testing-on-firebase-test-lab
/// ///
/// On Android, you need to call `convertFlutterSurfaceToImage()`, and /// On Android, you need to call `convertFlutterSurfaceToImage()`, and
/// pump a frame before taking a screenshot. /// pump a frame before taking a screenshot.
Future<List<int>> takeScreenshot(String screenshotName) async { Future<List<int>> takeScreenshot(String screenshotName, [Map<String, Object?>? args]) async {
reportData ??= <String, dynamic>{}; reportData ??= <String, dynamic>{};
reportData!['screenshots'] ??= <dynamic>[]; reportData!['screenshots'] ??= <dynamic>[];
final Map<String, dynamic> data = await callbackManager.takeScreenshot(screenshotName); final Map<String, dynamic> data = await callbackManager.takeScreenshot(screenshotName, args);
assert(data.containsKey('bytes')); assert(data.containsKey('bytes'));
(reportData!['screenshots']! as List<dynamic>).add(data); (reportData!['screenshots']! as List<dynamic>).add(data);
......
...@@ -50,6 +50,8 @@ Future<void> integrationDriver( ...@@ -50,6 +50,8 @@ Future<void> integrationDriver(
// error if it's used as a message for requestData. // error if it's used as a message for requestData.
String jsonResponse = await driver.requestData(DriverTestMessage.pending().toString()); String jsonResponse = await driver.requestData(DriverTestMessage.pending().toString());
final Map<String, bool> onScreenshotResults = <String, bool>{};
Response response = Response.fromJson(jsonResponse); Response response = Response.fromJson(jsonResponse);
// Until `integration_test` returns a [WebDriverCommandType.noop], keep // Until `integration_test` returns a [WebDriverCommandType.noop], keep
...@@ -63,8 +65,10 @@ Future<void> integrationDriver( ...@@ -63,8 +65,10 @@ Future<void> integrationDriver(
// Use `driver.screenshot()` method to get a screenshot of the web page. // Use `driver.screenshot()` method to get a screenshot of the web page.
final List<int> screenshotImage = await driver.screenshot(); final List<int> screenshotImage = await driver.screenshot();
final String screenshotName = response.data!['screenshot_name']! as String; final String screenshotName = response.data!['screenshot_name']! as String;
final Map<String, Object?>? args = (response.data!['args'] as Map<String, Object?>?)?.cast<String, Object?>();
final bool screenshotSuccess = await onScreenshot!(screenshotName, screenshotImage); final bool screenshotSuccess = await onScreenshot!(screenshotName, screenshotImage, args);
onScreenshotResults[screenshotName] = screenshotSuccess;
if (screenshotSuccess) { if (screenshotSuccess) {
jsonResponse = await driver.requestData(DriverTestMessage.complete().toString()); jsonResponse = await driver.requestData(DriverTestMessage.complete().toString());
} else { } else {
...@@ -104,7 +108,8 @@ Future<void> integrationDriver( ...@@ -104,7 +108,8 @@ Future<void> integrationDriver(
bool ok = false; bool ok = false;
try { try {
ok = await onScreenshot(screenshotName, screenshotBytes.cast<int>()); ok = onScreenshotResults[screenshotName] ??
await onScreenshot(screenshotName, screenshotBytes.cast<int>());
} catch (exception) { } catch (exception) {
throw StateError( throw StateError(
'Screenshot failure:\n' 'Screenshot failure:\n'
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment