Skip to content

Commit 0bde668

Browse files
nturgutEgor
authored andcommitted
[web] Adding capability to e2e to take screenshot for web tests. (flutter#2904)
* screenshot taking works * squash commits. addressing reviewer comments. making drivercommandmanager->callback manager * addressing reviewer comment. mostly name changes * major rename on all files webdriveraction->webdrivercommand * remove files. use implements * remove timeout. add onScreenshot callback * remove remaning timeouts. add an error message for screenshots. use an object instead of strings for status messages * created a new issue * remove example screenshot saving since it's failing on android. examples in integration_test package is common * changing the version and the change log
1 parent 9662837 commit 0bde668

11 files changed

+638
-29
lines changed

packages/integration_test/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
## 0.9.0
2+
3+
* Add screenshot capability to web tests.
4+
15
## 0.8.2
26

37
* Add support to get timeline.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// This is a Flutter widget test can take a screenshot.
2+
//
3+
// NOTE: Screenshots are only supported on Web for now.
4+
//
5+
// To perform an interaction with a widget in your test, use the WidgetTester
6+
// utility that Flutter provides. For example, you can send tap and scroll
7+
// gestures. You can also use WidgetTester to find child widgets in the widget
8+
// tree, read text, and verify that the values of widget properties are correct.
9+
10+
import 'package:integration_test/integration_test.dart';
11+
12+
import 'example_integration_io_extended.dart'
13+
if (dart.library.html) 'example_integration_web_extended.dart' as tests;
14+
15+
void main() {
16+
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
17+
tests.main();
18+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import 'dart:async';
2+
3+
import 'package:flutter_driver/flutter_driver.dart';
4+
import 'package:integration_test/integration_test_driver_extended.dart';
5+
6+
Future<void> main() async {
7+
final FlutterDriver driver = await FlutterDriver.connect();
8+
await integrationDriver(
9+
driver: driver,
10+
onScreenshot: (String screenshotName, List<int> screenshotBytes) async {
11+
return true;
12+
},
13+
);
14+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// This is a basic Flutter widget test.
2+
//
3+
// To perform an interaction with a widget in your test, use the WidgetTester
4+
// utility that Flutter provides. For example, you can send tap and scroll
5+
// gestures. You can also use WidgetTester to find child widgets in the widget
6+
// tree, read text, and verify that the values of widget properties are correct.
7+
8+
import 'dart:io' show Platform;
9+
import 'package:flutter/material.dart';
10+
import 'package:flutter_test/flutter_test.dart';
11+
import 'package:integration_test/integration_test.dart';
12+
13+
import 'package:integration_test_example/main.dart' as app;
14+
15+
void main() {
16+
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
17+
18+
testWidgets('verify text', (WidgetTester tester) async {
19+
// Build our app and trigger a frame.
20+
app.main();
21+
22+
// Trigger a frame.
23+
await tester.pumpAndSettle();
24+
25+
// TODO: https://github.com/flutter/flutter/issues/51890
26+
// Add screenshot capability for mobile platforms.
27+
28+
// Verify that platform version is retrieved.
29+
expect(
30+
find.byWidgetPredicate(
31+
(Widget widget) =>
32+
widget is Text &&
33+
widget.data.startsWith('Platform: ${Platform.operatingSystem}'),
34+
),
35+
findsOneWidget,
36+
);
37+
});
38+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// This is a basic Flutter widget test.
2+
//
3+
// To perform an interaction with a widget in your test, use the WidgetTester
4+
// utility that Flutter provides. For example, you can send tap and scroll
5+
// gestures. You can also use WidgetTester to find child widgets in the widget
6+
// tree, read text, and verify that the values of widget properties are correct.
7+
8+
import 'dart:html' as html;
9+
import 'package:flutter/material.dart';
10+
import 'package:flutter_test/flutter_test.dart';
11+
import 'package:integration_test/integration_test.dart';
12+
13+
import 'package:integration_test_example/main.dart' as app;
14+
15+
void main() {
16+
final IntegrationTestWidgetsFlutterBinding binding =
17+
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
18+
19+
testWidgets('verify text', (WidgetTester tester) async {
20+
// Build our app and trigger a frame.
21+
app.main();
22+
23+
// Trigger a frame.
24+
await tester.pumpAndSettle();
25+
26+
// Take a screenshot.
27+
await binding.takeScreenshot('platform_name');
28+
29+
// Verify that platform is retrieved.
30+
expect(
31+
find.byWidgetPredicate(
32+
(Widget widget) =>
33+
widget is Text &&
34+
widget.data
35+
.startsWith('Platform: ${html.window.navigator.platform}\n'),
36+
),
37+
findsOneWidget,
38+
);
39+
});
40+
41+
testWidgets('verify screenshot', (WidgetTester tester) async {
42+
// Build our app and trigger a frame.
43+
app.main();
44+
45+
// Trigger a frame.
46+
await tester.pumpAndSettle();
47+
48+
// Multiple methods can take screenshots. Screenshots are taken with the
49+
// same order the methods run.
50+
await binding.takeScreenshot('platform_name_2');
51+
});
52+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
// Copyright 2019 The Chromium Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'common.dart';
6+
7+
/// The dart:io implementation of [CallbackManager].
8+
///
9+
/// See also:
10+
///
11+
/// * [_callback_web.dart], which has the dart:html implementation
12+
CallbackManager get callbackManager => _singletonCallbackManager;
13+
14+
/// IOCallbackManager singleton.
15+
final IOCallbackManager _singletonCallbackManager = IOCallbackManager();
16+
17+
/// Manages communication between `integration_tests` and the `driver_tests`.
18+
///
19+
/// This is the dart:io implementation.
20+
class IOCallbackManager implements CallbackManager {
21+
@override
22+
Future<Map<String, dynamic>> callback(
23+
Map<String, String> params, IntegrationTestResults testRunner) async {
24+
final String command = params['command'];
25+
Map<String, String> response;
26+
switch (command) {
27+
case 'request_data':
28+
final bool allTestsPassed = await testRunner.allTestsPassed.future;
29+
response = <String, String>{
30+
'message': allTestsPassed
31+
? Response.allTestsPassed(data: testRunner.reportData).toJson()
32+
: Response.someTestsFailed(
33+
testRunner.failureMethodsDetails,
34+
data: testRunner.reportData,
35+
).toJson(),
36+
};
37+
break;
38+
case 'get_health':
39+
response = <String, String>{'status': 'ok'};
40+
break;
41+
default:
42+
throw UnimplementedError('$command is not implemented');
43+
}
44+
return <String, dynamic>{
45+
'isError': false,
46+
'response': response,
47+
};
48+
}
49+
50+
@override
51+
void cleanup() {
52+
// no-op.
53+
// Add any IO platform specific Completer/Future cleanups to here if any
54+
// comes up in the future. For example: `WebCallbackManager.cleanup`.
55+
}
56+
57+
@override
58+
Future<void> takeScreenshot(String screenshot) {
59+
throw UnimplementedError(
60+
'Screenshots are not implemented on this platform');
61+
}
62+
}
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
// Copyright 2019 The Chromium Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'dart:async';
6+
7+
import 'package:flutter_test/flutter_test.dart';
8+
9+
import 'common.dart';
10+
11+
/// The dart:html implementation of [CallbackManager].
12+
///
13+
/// See also:
14+
///
15+
/// * [_callback_io.dart], which has the dart:io implementation
16+
CallbackManager get callbackManager => _singletonWebDriverCommandManager;
17+
18+
/// WebDriverCommandManager singleton.
19+
final WebCallbackManager _singletonWebDriverCommandManager =
20+
WebCallbackManager();
21+
22+
/// Manages communication between `integration_tests` and the `driver_tests`.
23+
///
24+
/// Along with responding to callbacks from the driver side this calls enables
25+
/// usage of Web Driver commands by sending [WebDriverCommand]s to driver side.
26+
///
27+
/// Tests can execute an Web Driver commands such as `screenshot` using browsers'
28+
/// WebDriver APIs.
29+
///
30+
/// See: https://www.w3.org/TR/webdriver/
31+
class WebCallbackManager implements CallbackManager {
32+
/// App side tests will put the command requests from WebDriver to this pipe.
33+
Completer<WebDriverCommand> _webDriverCommandPipe =
34+
Completer<WebDriverCommand>();
35+
36+
/// Updated when WebDriver completes the request by the test method.
37+
///
38+
/// For example, a test method will ask for a screenshot by calling
39+
/// `takeScreenshot`. When this screenshot is taken [_driverCommandComplete]
40+
/// will complete.
41+
Completer<bool> _driverCommandComplete = Completer<bool>();
42+
43+
/// Takes screenshot using WebDriver screenshot command.
44+
///
45+
/// Only works on Web when tests are run via `flutter driver` command.
46+
///
47+
/// See: https://www.w3.org/TR/webdriver/#screen-capture.
48+
@override
49+
Future<void> takeScreenshot(String screenshotName) async {
50+
await _sendWebDriverCommand(WebDriverCommand.screenshot(screenshotName));
51+
}
52+
53+
Future<void> _sendWebDriverCommand(WebDriverCommand command) async {
54+
try {
55+
_webDriverCommandPipe.complete(Future.value(command));
56+
final bool awaitCommand = await _driverCommandComplete.future;
57+
if (!awaitCommand) {
58+
throw Exception(
59+
'Web Driver Command ${command.type} failed while waiting for '
60+
'driver side');
61+
}
62+
} catch (exception) {
63+
throw Exception('Web Driver Command failed: ${command.type} with '
64+
'exception $exception');
65+
} finally {
66+
// Reset the completer.
67+
_driverCommandComplete = Completer<bool>();
68+
}
69+
}
70+
71+
/// The callback function to response the driver side input.
72+
///
73+
/// Provides a handshake mechanism for executing [WebDriverCommand]s on the
74+
/// driver side.
75+
@override
76+
Future<Map<String, dynamic>> callback(
77+
Map<String, String> params, IntegrationTestResults testRunner) async {
78+
final String command = params['command'];
79+
Map<String, String> response;
80+
switch (command) {
81+
case 'request_data':
82+
return params['message'] == null
83+
? _requestData(testRunner)
84+
: _requestDataWithMessage(params['message'], testRunner);
85+
break;
86+
case 'get_health':
87+
response = <String, String>{'status': 'ok'};
88+
break;
89+
default:
90+
throw UnimplementedError('$command is not implemented');
91+
}
92+
return <String, dynamic>{
93+
'isError': false,
94+
'response': response,
95+
};
96+
}
97+
98+
Future<Map<String, dynamic>> _requestDataWithMessage(
99+
String extraMessage, IntegrationTestResults testRunner) async {
100+
Map<String, String> response;
101+
// Driver side tests' status is added as an extra message.
102+
final DriverTestMessage message =
103+
DriverTestMessage.fromString(extraMessage);
104+
// If driver side tests are pending send the first command in the
105+
// `commandPipe` to the tests.
106+
if (message.isPending) {
107+
final WebDriverCommand command = await _webDriverCommandPipe.future;
108+
switch (command.type) {
109+
case WebDriverCommandType.screenshot:
110+
final Map<String, dynamic> data = Map.from(command.values);
111+
data.addAll(
112+
WebDriverCommand.typeToMap(WebDriverCommandType.screenshot));
113+
response = <String, String>{
114+
'message': Response.webDriverCommand(data: data).toJson(),
115+
};
116+
break;
117+
case WebDriverCommandType.noop:
118+
final Map<String, dynamic> data = Map();
119+
data.addAll(WebDriverCommand.typeToMap(WebDriverCommandType.noop));
120+
response = <String, String>{
121+
'message': Response.webDriverCommand(data: data).toJson(),
122+
};
123+
break;
124+
default:
125+
throw UnimplementedError('${command.type} is not implemented');
126+
}
127+
} else {
128+
final Map<String, dynamic> data = Map();
129+
data.addAll(WebDriverCommand.typeToMap(WebDriverCommandType.ack));
130+
response = <String, String>{
131+
'message': Response.webDriverCommand(data: data).toJson(),
132+
};
133+
_driverCommandComplete.complete(Future.value(message.isSuccess));
134+
_webDriverCommandPipe = Completer<WebDriverCommand>();
135+
}
136+
return <String, dynamic>{
137+
'isError': false,
138+
'response': response,
139+
};
140+
}
141+
142+
Future<Map<String, dynamic>> _requestData(
143+
IntegrationTestResults testRunner) async {
144+
final bool allTestsPassed = await testRunner.allTestsPassed.future;
145+
final Map<String, String> response = <String, String>{
146+
'message': allTestsPassed
147+
? Response.allTestsPassed(data: testRunner.reportData).toJson()
148+
: Response.someTestsFailed(
149+
testRunner.failureMethodsDetails,
150+
data: testRunner.reportData,
151+
).toJson(),
152+
};
153+
return <String, dynamic>{
154+
'isError': false,
155+
'response': response,
156+
};
157+
}
158+
159+
@override
160+
void cleanup() {
161+
if (!_webDriverCommandPipe.isCompleted) {
162+
_webDriverCommandPipe
163+
.complete(Future<WebDriverCommand>.value(WebDriverCommand.noop()));
164+
}
165+
166+
if (!_driverCommandComplete.isCompleted) {
167+
_driverCommandComplete.complete(Future<bool>.value(false));
168+
}
169+
}
170+
}

0 commit comments

Comments
 (0)