Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit 8a321d9

Browse files
author
nturgut
committed
adding screenshot capability to text_editing e2e test
1 parent df1148d commit 8a321d9

File tree

16 files changed

+487
-383
lines changed

16 files changed

+487
-383
lines changed
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// Copyright 2013 The Flutter 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:io' as io;
6+
7+
import 'package:flutter_driver/flutter_driver.dart';
8+
import 'package:golden_comparator/goldens.dart';
9+
import 'package:integration_test/integration_test_driver_extended.dart' as test;
10+
11+
import 'package:golden_comparator/image_compare.dart';
12+
13+
import 'package:image/image.dart';
14+
15+
/// Tolerable pixel difference ratio between the goldens and the screenshots.
16+
///
17+
/// We are allowing a higher difference rate compared to the unit tests (where
18+
/// this rate is set to 0.28), since during the end to end tests there are
19+
/// more components on the screen which are not related to the functinality
20+
/// under test ex: a blinking cursor.
21+
const double kMaxDiffRateFailure = 0.5 / 100; // 0.5%
22+
23+
/// Used for calling `integration_test` package.
24+
///
25+
/// Compared to other similar classes which only included the following call:
26+
/// ```
27+
/// Future<void> main() async => test.integrationDriver();
28+
/// ```
29+
///
30+
/// this method is able to take screenshot.
31+
///
32+
/// It provides an `onScreenshot` callback to the `integrationDriver` method.
33+
/// It also includes options for updating the golden files.
34+
Future<void> main() async {
35+
final WebFlutterDriver driver =
36+
await FlutterDriver.connect() as WebFlutterDriver;
37+
38+
// Learn the browser in use from the webDriver.
39+
final String browser = driver.webDriver.capabilities['browserName'] as String;
40+
41+
bool updateGoldens = false;
42+
try {
43+
// We are using an environment variable since instead of an argument, since
44+
// this code is not invoked from the shell but from the `flutter drive`
45+
// tool itself. Therefore we do not have control on the command line
46+
// arguments.
47+
// Please read the README, further info on how to update the goldens.
48+
updateGoldens =
49+
io.Platform.environment['UPDATE_GOLDENS'].toLowerCase() == 'true';
50+
} catch (ex) {
51+
if (ex
52+
.toString()
53+
.contains('is not a subtype of type \'bool\' in type cast')) {
54+
print('INFO: goldens will not be updated, please set `UPDATE_GOLDENS` '
55+
'environment variable to true');
56+
}
57+
}
58+
59+
test.integrationDriver(
60+
driver: driver,
61+
onScreenshot: (String screenshotName, List<int> screenshotBytes) async {
62+
final Image screenshot = decodePng(screenshotBytes);
63+
final String result = compareImage(
64+
screenshot,
65+
updateGoldens,
66+
'$screenshotName-$browser.png',
67+
PixelComparison.fuzzy,
68+
kMaxDiffRateFailure,
69+
forIntegrationTests: true,
70+
write: updateGoldens,
71+
);
72+
if (result != 'OK') {
73+
print('ERROR: $result');
74+
}
75+
return result == 'OK';
76+
},
77+
);
78+
}

e2etests/web/regular_integration_tests/lib/text_editing_main.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ class _MyHomePageState extends State<MyHomePage> {
5656
enabled: true,
5757
controller: _emptyController,
5858
decoration: const InputDecoration(
59+
contentPadding: EdgeInsets.all(10.0),
5960
labelText: 'Empty Input Field:',
6061
),
6162
),
@@ -67,6 +68,7 @@ class _MyHomePageState extends State<MyHomePage> {
6768
enabled: true,
6869
controller: _controller,
6970
decoration: const InputDecoration(
71+
contentPadding: EdgeInsets.all(10.0),
7072
labelText: 'Text Input Field:',
7173
),
7274
),
@@ -78,6 +80,7 @@ class _MyHomePageState extends State<MyHomePage> {
7880
enabled: true,
7981
controller: _controller2,
8082
decoration: const InputDecoration(
83+
contentPadding: EdgeInsets.all(10.0),
8184
labelText: 'Text Input Field 2:',
8285
),
8386
onFieldSubmitted: (String str) {

e2etests/web/regular_integration_tests/pubspec.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ dev_dependencies:
1515
sdk: flutter
1616
integration_test: 0.9.0
1717
http: 0.12.0+2
18-
test: any
18+
golden_comparator:
19+
path: ../../../web_sdk/golden_comparator
1920

2021
flutter:
2122
assets:

e2etests/web/regular_integration_tests/test_driver/text_editing_integration.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import 'package:flutter/material.dart';
1313
import 'package:integration_test/integration_test.dart';
1414

1515
void main() {
16-
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
16+
final IntegrationTestWidgetsFlutterBinding binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized() as IntegrationTestWidgetsFlutterBinding;
1717

1818
testWidgets('Focused text field creates a native input element',
1919
(WidgetTester tester) async {
@@ -41,6 +41,8 @@ void main() {
4141
textFormField.controller.text = 'New Value';
4242
// DOM element's value also changes.
4343
expect(input.value, 'New Value');
44+
45+
await binding.takeScreenshot('focused_text_field');
4446
});
4547

4648
testWidgets('Input field with no initial value works',

e2etests/web/regular_integration_tests/test_driver/text_editing_integration_test.dart

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,9 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5-
import 'package:flutter_driver/flutter_driver.dart';
6-
import 'package:integration_test/integration_test_driver_extended.dart' as test;
5+
import 'package:regular_integration_tests/screenshot_support.dart'
6+
as with_screenshot;
77

88
Future<void> main() async {
9-
final FlutterDriver driver = await FlutterDriver.connect();
10-
test.integrationDriver(
11-
driver: driver,
12-
onScreenshot: (String screenshotName, List<int> screenshotBytes) async {
13-
return true;
14-
},
15-
);
9+
await with_screenshot.main();
1610
}

lib/web_ui/dev/test_platform.dart

Lines changed: 10 additions & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import 'package:shelf_static/shelf_static.dart';
2121
import 'package:shelf_web_socket/shelf_web_socket.dart';
2222
import 'package:shelf_packages_handler/shelf_packages_handler.dart';
2323
import 'package:stream_channel/stream_channel.dart';
24+
import 'package:golden_comparator/goldens.dart';
25+
import 'package:golden_comparator/image_compare.dart';
2426
import 'package:web_socket_channel/web_socket_channel.dart';
2527

2628
import 'package:test_api/src/backend/runtime.dart'; // ignore: implementation_imports
@@ -39,7 +41,6 @@ import 'package:test_core/src/runner/configuration.dart'; // ignore: implementat
3941
import 'browser.dart';
4042
import 'common.dart';
4143
import 'environment.dart' as env;
42-
import 'goldens.dart';
4344
import 'screenshot_manager.dart';
4445
import 'supported_browsers.dart';
4546

@@ -197,7 +198,6 @@ class BrowserPlatform extends PlatformPlugin {
197198
'golden_files',
198199
);
199200
} else {
200-
await fetchGoldens();
201201
goldensDirectory = p.join(
202202
env.environment.webUiGoldensRepositoryDirectory.path,
203203
'engine',
@@ -215,128 +215,14 @@ class BrowserPlatform extends PlatformPlugin {
215215
// Take screenshot.
216216
final Image screenshot = await _screenshotManager.capture(regionAsRectange);
217217

218-
// Bail out fast if golden doesn't exist, and user doesn't want to create it.
219-
final File file = File(p.join(
220-
goldensDirectory,
221-
filename,
222-
));
223-
if (!file.existsSync() && !write) {
224-
return '''
225-
Golden file $filename does not exist.
226-
227-
To automatically create this file call matchGoldenFile('$filename', write: true).
228-
''';
229-
}
230-
231-
if (write) {
232-
// Don't even bother with the comparison, just write and return
233-
print('Updating screenshot golden: $file');
234-
file.writeAsBytesSync(encodePng(screenshot), flush: true);
235-
if (doUpdateScreenshotGoldens) {
236-
// Do not fail tests when bulk-updating screenshot goldens.
237-
return 'OK';
238-
} else {
239-
return 'Golden file $filename was updated. You can remove "write: true" in the call to matchGoldenFile.';
240-
}
241-
}
242-
243-
// Compare screenshots.
244-
ImageDiff diff = ImageDiff(
245-
golden: decodeNamedImage(file.readAsBytesSync(), filename),
246-
other: screenshot,
247-
pixelComparison: pixelComparison,
248-
);
249-
250-
if (diff.rate > 0) {
251-
final String testResultsPath =
252-
env.environment.webUiTestResultsDirectory.path;
253-
final String basename = p.basenameWithoutExtension(file.path);
254-
255-
final File actualFile =
256-
File(p.join(testResultsPath, '$basename.actual.png'));
257-
actualFile.writeAsBytesSync(encodePng(screenshot), flush: true);
258-
259-
final File diffFile = File(p.join(testResultsPath, '$basename.diff.png'));
260-
diffFile.writeAsBytesSync(encodePng(diff.diff), flush: true);
261-
262-
final File expectedFile =
263-
File(p.join(testResultsPath, '$basename.expected.png'));
264-
file.copySync(expectedFile.path);
265-
266-
final File reportFile =
267-
File(p.join(testResultsPath, '$basename.report.html'));
268-
reportFile.writeAsStringSync('''
269-
Golden file $filename did not match the image generated by the test.
270-
271-
<table>
272-
<tr>
273-
<th>Expected</th>
274-
<th>Diff</th>
275-
<th>Actual</th>
276-
</tr>
277-
<tr>
278-
<td>
279-
<img src="$basename.expected.png">
280-
</td>
281-
<td>
282-
<img src="$basename.diff.png">
283-
</td>
284-
<td>
285-
<img src="$basename.actual.png">
286-
</td>
287-
</tr>
288-
</table>
289-
''');
290-
291-
final StringBuffer message = StringBuffer();
292-
message.writeln(
293-
'Golden file $filename did not match the image generated by the test.');
294-
message.writeln(getPrintableDiffFilesInfo(diff.rate, maxDiffRateFailure));
295-
message
296-
.writeln('You can view the test report in your browser by opening:');
297-
298-
// Cirrus cannot serve HTML pages generated by build jobs, so we
299-
// archive all the files so that they can be downloaded and inspected
300-
// locally.
301-
if (isCirrus) {
302-
final String taskId = Platform.environment['CIRRUS_TASK_ID'];
303-
final String baseArtifactsUrl =
304-
'https://api.cirrus-ci.com/v1/artifact/task/$taskId/web_engine_test/test_results';
305-
final String cirrusReportUrl = '$baseArtifactsUrl/$basename.report.zip';
306-
message.writeln(cirrusReportUrl);
307-
308-
await Process.run(
309-
'zip',
310-
<String>[
311-
'$basename.report.zip',
312-
'$basename.report.html',
313-
'$basename.expected.png',
314-
'$basename.diff.png',
315-
'$basename.actual.png',
316-
],
317-
workingDirectory: testResultsPath,
318-
);
319-
} else {
320-
final String localReportPath = '$testResultsPath/$basename.report.html';
321-
message.writeln(localReportPath);
322-
}
323-
324-
message.writeln(
325-
'To update the golden file call matchGoldenFile(\'$filename\', write: true).');
326-
message.writeln('Golden file: ${expectedFile.path}');
327-
message.writeln('Actual file: ${actualFile.path}');
328-
329-
if (diff.rate < maxDiffRateFailure) {
330-
// Issue a warning but do not fail the test.
331-
print('WARNING:');
332-
print(message);
333-
return 'OK';
334-
} else {
335-
// Fail test
336-
return '$message';
337-
}
338-
}
339-
return 'OK';
218+
return compareImage(
219+
screenshot,
220+
doUpdateScreenshotGoldens,
221+
filename,
222+
pixelComparison,
223+
maxDiffRateFailure,
224+
goldensDirectory: goldensDirectory,
225+
write: write);
340226
}
341227

342228
/// A handler that serves wrapper files used to bootstrap tests.

lib/web_ui/dev/test_runner.dart

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import 'package:test_core/src/runner/hack_register_platform.dart'
1515
import 'package:test_api/src/backend/runtime.dart'; // ignore: implementation_imports
1616
import 'package:test_core/src/executable.dart'
1717
as test; // ignore: implementation_imports
18+
import 'package:golden_comparator/goldens.dart';
1819

1920
import 'common.dart';
2021
import 'environment.dart';
@@ -189,6 +190,14 @@ class TestCommand extends Command<bool> with ArgUtils {
189190
}
190191
environment.webUiTestResultsDirectory.createSync(recursive: true);
191192

193+
// If screenshot tests are available, fetch the screenshot goldens.
194+
if (isScreenhotTestsAvailable) {
195+
final GoldensRepoFetcher goldensRepoFetcher = GoldensRepoFetcher(
196+
environment.webUiGoldensRepositoryDirectory,
197+
path.join(environment.webUiDevDir.path, 'goldens_lock.yaml'));
198+
await goldensRepoFetcher.fetch();
199+
}
200+
192201
// In order to run iOS Safari unit tests we need to make sure iOS Simulator
193202
// is booted.
194203
if (isSafariIOS) {
@@ -371,14 +380,16 @@ class TestCommand extends Command<bool> with ArgUtils {
371380
isFirefoxIntegrationTestAvailable ||
372381
isSafariIntegrationTestAvailable;
373382

383+
// Whether the tests will do screenshot testing.
374384
bool get isScreenhotTestsAvailable =>
375385
isIntegrationTestsAvailable || isUnitTestsScreenshotsAvailable;
376386

377387
// For unit tests screenshot tests and smoke tests only run on:
378388
// "Chrome/iOS" for LUCI/local.
379389
bool get isUnitTestsScreenshotsAvailable =>
380390
((isChrome && isLuci && io.Platform.isLinux) ||
381-
((isChrome || isSafariIOS) && !isLuci));
391+
(isChrome || isSafariIOS) && !isLuci) ||
392+
(isSafariIOS && isLuci);
382393

383394
/// Use system flutter instead of cloning the repository.
384395
///
@@ -624,7 +635,8 @@ class TestCommand extends Command<bool> with ArgUtils {
624635

625636
/// Runs a batch of tests.
626637
///
627-
/// Unless [expectFailure] is set to false, sets [io.exitCode] to a non-zero value if any tests fail.
638+
/// Unless [expectFailure] is set to false, sets [io.exitCode] to a non-zero
639+
/// value if any tests fail.
628640
Future<void> _runTestBatch(
629641
List<FilePath> testFiles, {
630642
@required int concurrency,
@@ -647,7 +659,8 @@ class TestCommand extends Command<bool> with ArgUtils {
647659
return BrowserPlatform.start(
648660
browser,
649661
root: io.Directory.current.path,
650-
// It doesn't make sense to update a screenshot for a test that is expected to fail.
662+
// It doesn't make sense to update a screenshot for a test that is
663+
// expected to fail.
651664
doUpdateScreenshotGoldens: !expectFailure && doUpdateScreenshotGoldens,
652665
);
653666
});

lib/web_ui/pubspec.yaml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ dev_dependencies:
1414
image: 2.1.13
1515
js: 0.6.1+1
1616
mockito: 4.1.1
17-
path: 1.7.0
17+
path: 1.8.0-nullsafety
1818
test: 1.14.3
1919
quiver: 2.1.3
2020
build_resolvers: 1.3.10
@@ -23,6 +23,10 @@ dev_dependencies:
2323
build_web_compilers: 2.11.0
2424
yaml: 2.2.1
2525
watcher: 0.9.7+15
26+
common_test_utils:
27+
path: ../../web_sdk/common_test_utils
28+
golden_comparator:
29+
path: ../../web_sdk/golden_comparator
2630
web_engine_tester:
2731
path: ../../web_sdk/web_engine_tester
2832
simulators:

0 commit comments

Comments
 (0)