Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions lib/web_ui/dev/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,15 @@ or:
```
felt build --watch
```

## Configuration files

`chrome_lock.yaml` contains the version of Chrome we use to test Flutter for
web. Chrome is not automatically updated whenever a new release is available.
Instead, we update this file manually once in a while.

`goldens_lock.yaml` refers to a revision in the https://github.com/flutter/goldens
repo. Screenshot tests are compared with the golden files at that revision.
When making engine changes that affect screenshots, first submit a PR to
flutter/goldens updating the screenshots. Then update this file pointing to
the new revision.
13 changes: 13 additions & 0 deletions lib/web_ui/dev/environment.dart
Original file line number Diff line number Diff line change
Expand Up @@ -103,4 +103,17 @@ class Environment {
webUiRootDir.path,
'.dart_tool',
));

/// Path to the "dev" directory containing engine developer tools and
/// configuration files.
io.Directory get webUiDevDir => io.Directory(pathlib.join(
webUiRootDir.path,
'dev',
));

/// Path to the clone of the flutter/goldens repository.
io.Directory get webUiGoldensRepositoryDirectory => io.Directory(pathlib.join(
webUiDartToolDir.path,
'goldens',
));
}
76 changes: 75 additions & 1 deletion lib/web_ui/dev/goldens.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,19 @@
// found in the LICENSE file.
import 'dart:io' as io;
import 'package:image/image.dart';
import 'package:path/path.dart' as path;
import 'package:yaml/yaml.dart';

import 'environment.dart';
import 'utils.dart';

void main(List<String> args) {
final io.File fileA = io.File(args[0]);
final io.File fileB = io.File(args[1]);
final Image imageA = decodeNamedImage(fileA.readAsBytesSync(), 'a.png');
final Image imageB = decodeNamedImage(fileB.readAsBytesSync(), 'b.png');
final ImageDiff diff = ImageDiff(golden: imageA, other: imageB);
print('Diff: ${(diff.rate * 100).toStringAsFixed(4)}');
print('Diff: ${(diff.rate * 100).toStringAsFixed(4)}%');
}

/// This class encapsulates visually diffing an Image with any other.
Expand Down Expand Up @@ -140,3 +145,72 @@ class ImageDiff {
String getPrintableDiffFilesInfo(double diffRate, double maxRate) =>
'(${((diffRate) * 100).toStringAsFixed(4)}% of pixels were different. '
'Maximum allowed rate is: ${(maxRate * 100).toStringAsFixed(4)}%).';

/// Fetches golden files from github.com/flutter/goldens, cloning the repository if necessary.
///
/// The repository is cloned into web_ui/.dart_tool.
Future<void> fetchGoldens() async {
await _GoldensRepoFetcher().fetch();
}

class _GoldensRepoFetcher {
String _repository;
String _revision;

Future<void> fetch() async {
final io.File lockFile = io.File(
path.join(environment.webUiDevDir.path, 'goldens_lock.yaml')
);
final YamlMap lock = loadYaml(lockFile.readAsStringSync());
_repository = lock['repository'];
_revision = lock['revision'];

final String localRevision = await _getLocalRevision();
if (localRevision == _revision) {
return;
}

print('Fetching $_repository@$_revision');

if (!environment.webUiGoldensRepositoryDirectory.existsSync()) {
environment.webUiGoldensRepositoryDirectory.createSync(recursive: true);
await runProcess(
'git',
<String>['init'],
workingDirectory: environment.webUiGoldensRepositoryDirectory.path,
mustSucceed: true,
);
await runProcess(
'git',
<String>['remote', 'add', 'origin', _repository],
workingDirectory: environment.webUiGoldensRepositoryDirectory.path,
mustSucceed: true,
);
}

await runProcess(
'git',
<String>['fetch', 'origin', 'master'],
workingDirectory: environment.webUiGoldensRepositoryDirectory.path,
mustSucceed: true,
);
await runProcess(
'git',
<String>['checkout', _revision],
workingDirectory: environment.webUiGoldensRepositoryDirectory.path,
mustSucceed: true,
);
}

Future<String> _getLocalRevision() async {
final io.File head = io.File(path.join(
environment.webUiGoldensRepositoryDirectory.path, '.git', 'HEAD'
));

if (!head.existsSync()) {
return null;
}

return head.readAsStringSync().trim();
}
}
2 changes: 2 additions & 0 deletions lib/web_ui/dev/goldens_lock.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
repository: https://github.com/flutter/goldens.git
revision: dd993a32c23c5c542f083134467e7cda09cac975
22 changes: 20 additions & 2 deletions lib/web_ui/dev/test_platform.dart
Original file line number Diff line number Diff line change
Expand Up @@ -147,10 +147,28 @@ class BrowserPlatform extends PlatformPlugin {
}

Future<String> _diffScreenshot(String filename, bool write, [ Map<String, dynamic> region ]) async {
const String _kGoldensDirectory = 'test/golden_files';
String goldensDirectory;
if (filename.startsWith('__local__')) {
filename = filename.substring('__local__/'.length);
goldensDirectory = p.join(
env.environment.webUiRootDir.path,
'test',
'golden_files',
);
} else {
await fetchGoldens();
goldensDirectory = p.join(
env.environment.webUiGoldensRepositoryDirectory.path,
'engine',
'web',
);
}

// Bail out fast if golden doesn't exist, and user doesn't want to create it.
final File file = File(p.join(_kGoldensDirectory, filename));
final File file = File(p.join(
goldensDirectory,
filename,
));
if (!file.existsSync() && !write) {
return '''
Golden file $filename does not exist.
Expand Down
68 changes: 67 additions & 1 deletion lib/web_ui/dev/utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import 'dart:async';
import 'dart:io' as io;

import 'package:meta/meta.dart';
import 'package:path/path.dart' as path;

import 'environment.dart';
Expand All @@ -31,17 +32,54 @@ class FilePath {
String toString() => _absolutePath;
}

/// Runs [executable] merging its output into the current process' standard out and standard error.
Future<int> runProcess(
String executable,
List<String> arguments, {
String workingDirectory,
bool mustSucceed: false,
}) async {
final io.Process process = await io.Process.start(
executable,
arguments,
workingDirectory: workingDirectory,
);
return _forwardIOAndWait(process);
final int exitCode = await _forwardIOAndWait(process);
if (mustSucceed && exitCode != 0) {
throw ProcessException(
description: 'Sub-process failed.',
executable: executable,
arguments: arguments,
workingDirectory: workingDirectory,
exitCode: exitCode,
);
}
return exitCode;
}

/// Runs [executable] and returns its standard output as a string.
///
/// If the process fails, throws a [ProcessException].
Future<String> evalProcess(
String executable,
List<String> arguments, {
String workingDirectory,
}) async {
final io.ProcessResult result = await io.Process.run(
executable,
arguments,
workingDirectory: workingDirectory,
);
if (result.exitCode != 0) {
throw ProcessException(
description: result.stderr,
executable: executable,
arguments: arguments,
workingDirectory: workingDirectory,
exitCode: result.exitCode,
);
}
return result.stdout;
}

Future<int> _forwardIOAndWait(io.Process process) {
Expand All @@ -53,3 +91,31 @@ Future<int> _forwardIOAndWait(io.Process process) {
return exitCode;
});
}

@immutable
class ProcessException implements Exception {
ProcessException({
@required this.description,
@required this.executable,
@required this.arguments,
@required this.workingDirectory,
@required this.exitCode,
});

final String description;
final String executable;
final List<String> arguments;
final String workingDirectory;
final int exitCode;

@override
String toString() {
final StringBuffer message = StringBuffer();
message
..writeln(description)
..writeln('Command: $executable ${arguments.join(' ')}')
..writeln('Working directory: ${workingDirectory ?? io.Directory.current.path}')
..writeln('Exit code: $exitCode');
return '$message';
}
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Binary file removed lib/web_ui/test/golden_files/engine/svg_arc_1.png
Diff not rendered.
Binary file removed lib/web_ui/test/golden_files/engine/svg_arc_2.png
Diff not rendered.
Binary file removed lib/web_ui/test/golden_files/engine/svg_arc_3.png
Diff not rendered.
Binary file removed lib/web_ui/test/golden_files/engine/svg_arc_4.png
Diff not rendered.
Binary file removed lib/web_ui/test/golden_files/engine/svg_arc_5.png
Diff not rendered.
Binary file removed lib/web_ui/test/golden_files/engine/svg_arc_6.png
Diff not rendered.
Binary file removed lib/web_ui/test/golden_files/engine/svg_arc_7.png
Diff not rendered.
Binary file removed lib/web_ui/test/golden_files/engine/svg_arc_8.png
Diff not rendered.
Diff not rendered.
Binary file removed lib/web_ui/test/golden_files/engine/svg_notch.png
Diff not rendered.
Diff not rendered.
Binary file removed lib/web_ui/test/golden_files/engine/svg_rect.png
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Diff not rendered.
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ void main() async {
}

html.document.body.append(canvas.rootElement);
await matchGoldenFile('engine/canvas_rrect_round_square.png', region: region);
await matchGoldenFile('canvas_rrect_round_square.png', region: region);
}, timeout: const Timeout(Duration(seconds: 10)));

test('round rect with big radius scale down smaller radius', () async {
Expand All @@ -58,7 +58,7 @@ void main() async {
}

html.document.body.append(canvas.rootElement);
await matchGoldenFile('engine/canvas_rrect_overlapping_radius.png', region: region);
await matchGoldenFile('canvas_rrect_overlapping_radius.png', region: region);
}, timeout: const Timeout(Duration(seconds: 10)));

test('diff round rect with big radius scale down smaller radius', () async {
Expand All @@ -81,6 +81,6 @@ void main() async {
}

html.document.body.append(canvas.rootElement);
await matchGoldenFile('engine/canvas_drrect_overlapping_radius.png', region: region);
await matchGoldenFile('canvas_drrect_overlapping_radius.png', region: region);
}, timeout: const Timeout(Duration(seconds: 10)));
}
8 changes: 4 additions & 4 deletions lib/web_ui/test/golden_tests/engine/canvas_scuba_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ void main() async {

html.document.body.append(canvas.rootElement);

await matchGoldenFile('engine/misaligned_pixels_in_canvas_test.png', region: region);
await matchGoldenFile('misaligned_pixels_in_canvas_test.png', region: region);
}, timeout: const Timeout(Duration(seconds: 10)));

test('compensates for misalignment of the canvas', () async {
Expand All @@ -83,7 +83,7 @@ void main() async {

html.document.body.append(canvas.rootElement);

await matchGoldenFile('engine/misaligned_canvas_test.png', region: region);
await matchGoldenFile('misaligned_canvas_test.png', region: region);
}, timeout: const Timeout(Duration(seconds: 10)));

test('fill the whole canvas with color even when transformed', () async {
Expand All @@ -94,7 +94,7 @@ void main() async {

html.document.body.append(canvas.rootElement);

await matchGoldenFile('engine/bitmap_canvas_fills_color_when_transformed.png', region: region);
await matchGoldenFile('bitmap_canvas_fills_color_when_transformed.png', region: region);
}, timeout: const Timeout(Duration(seconds: 10)));

test('fill the whole canvas with paint even when transformed', () async {
Expand All @@ -107,6 +107,6 @@ void main() async {

html.document.body.append(canvas.rootElement);

await matchGoldenFile('engine/bitmap_canvas_fills_paint_when_transformed.png', region: region);
await matchGoldenFile('bitmap_canvas_fills_paint_when_transformed.png', region: region);
}, timeout: const Timeout(Duration(seconds: 10)));
}
Loading