mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
[flutter_tools] remove globals from flutter web platform (#70863)
This commit is contained in:
parent
085ff127ae
commit
4832e64cad
195
packages/flutter_tools/lib/src/test/flutter_web_goldens.dart
Normal file
195
packages/flutter_tools/lib/src/test/flutter_web_goldens.dart
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
// ignore_for_file: implementation_imports
|
||||||
|
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import '../base/common.dart';
|
||||||
|
import '../base/file_system.dart';
|
||||||
|
import '../base/io.dart';
|
||||||
|
import '../convert.dart';
|
||||||
|
import '../globals.dart' as globals;
|
||||||
|
import 'test_compiler.dart';
|
||||||
|
import 'test_config.dart';
|
||||||
|
|
||||||
|
/// Helper class to start golden file comparison in a separate process.
|
||||||
|
///
|
||||||
|
/// Golden file comparator is configured using flutter_test_config.dart and that
|
||||||
|
/// file can contain arbitrary Dart code that depends on dart:ui. Thus it has to
|
||||||
|
/// be executed in a `flutter_tester` environment. This helper class generates a
|
||||||
|
/// Dart file configured with flutter_test_config.dart to perform the comparison
|
||||||
|
/// of golden files.
|
||||||
|
class TestGoldenComparator {
|
||||||
|
/// Creates a [TestGoldenComparator] instance.
|
||||||
|
TestGoldenComparator(this.shellPath, this.compilerFactory)
|
||||||
|
: tempDir = globals.fs.systemTempDirectory.createTempSync('flutter_web_platform.');
|
||||||
|
|
||||||
|
final String shellPath;
|
||||||
|
final Directory tempDir;
|
||||||
|
final TestCompiler Function() compilerFactory;
|
||||||
|
|
||||||
|
TestCompiler _compiler;
|
||||||
|
TestGoldenComparatorProcess _previousComparator;
|
||||||
|
Uri _previousTestUri;
|
||||||
|
|
||||||
|
Future<void> close() async {
|
||||||
|
tempDir.deleteSync(recursive: true);
|
||||||
|
await _compiler?.dispose();
|
||||||
|
await _previousComparator?.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start golden comparator in a separate process. Start one file per test file
|
||||||
|
/// to reduce the overhead of starting `flutter_tester`.
|
||||||
|
Future<TestGoldenComparatorProcess> _processForTestFile(Uri testUri) async {
|
||||||
|
if (testUri == _previousTestUri) {
|
||||||
|
return _previousComparator;
|
||||||
|
}
|
||||||
|
|
||||||
|
final String bootstrap = TestGoldenComparatorProcess.generateBootstrap(testUri);
|
||||||
|
final Process process = await _startProcess(bootstrap);
|
||||||
|
unawaited(_previousComparator?.close());
|
||||||
|
_previousComparator = TestGoldenComparatorProcess(process);
|
||||||
|
_previousTestUri = testUri;
|
||||||
|
|
||||||
|
return _previousComparator;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Process> _startProcess(String testBootstrap) async {
|
||||||
|
// Prepare the Dart file that will talk to us and start the test.
|
||||||
|
final File listenerFile = (await tempDir.createTemp('listener')).childFile('listener.dart');
|
||||||
|
await listenerFile.writeAsString(testBootstrap);
|
||||||
|
|
||||||
|
// Lazily create the compiler
|
||||||
|
_compiler = _compiler ?? compilerFactory();
|
||||||
|
final String output = await _compiler.compile(listenerFile.uri);
|
||||||
|
final List<String> command = <String>[
|
||||||
|
shellPath,
|
||||||
|
'--disable-observatory',
|
||||||
|
'--non-interactive',
|
||||||
|
'--packages=${globals.fs.path.join('.dart_tool', 'package_config.json')}',
|
||||||
|
output,
|
||||||
|
];
|
||||||
|
|
||||||
|
final Map<String, String> environment = <String, String>{
|
||||||
|
// Chrome is the only supported browser currently.
|
||||||
|
'FLUTTER_TEST_BROWSER': 'chrome',
|
||||||
|
};
|
||||||
|
return globals.processManager.start(command, environment: environment);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> compareGoldens(Uri testUri, Uint8List bytes, Uri goldenKey, bool updateGoldens) async {
|
||||||
|
final File imageFile = await (await tempDir.createTemp('image')).childFile('image').writeAsBytes(bytes);
|
||||||
|
final TestGoldenComparatorProcess process = await _processForTestFile(testUri);
|
||||||
|
process.sendCommand(imageFile, goldenKey, updateGoldens);
|
||||||
|
|
||||||
|
final Map<String, dynamic> result = await process.getResponse();
|
||||||
|
|
||||||
|
if (result == null) {
|
||||||
|
return 'unknown error';
|
||||||
|
} else {
|
||||||
|
return (result['success'] as bool) ? null : ((result['message'] as String) ?? 'does not match');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents a `flutter_tester` process started for golden comparison. Also
|
||||||
|
/// handles communication with the child process.
|
||||||
|
class TestGoldenComparatorProcess {
|
||||||
|
/// Creates a [TestGoldenComparatorProcess] backed by [process].
|
||||||
|
TestGoldenComparatorProcess(this.process) {
|
||||||
|
// Pipe stdout and stderr to printTrace and printError.
|
||||||
|
// Also parse stdout as a stream of JSON objects.
|
||||||
|
streamIterator = StreamIterator<Map<String, dynamic>>(
|
||||||
|
process.stdout
|
||||||
|
.transform<String>(utf8.decoder)
|
||||||
|
.transform<String>(const LineSplitter())
|
||||||
|
.where((String line) {
|
||||||
|
globals.printTrace('<<< $line');
|
||||||
|
return line.isNotEmpty && line[0] == '{';
|
||||||
|
})
|
||||||
|
.map<dynamic>(jsonDecode)
|
||||||
|
.cast<Map<String, dynamic>>());
|
||||||
|
|
||||||
|
process.stderr
|
||||||
|
.transform<String>(utf8.decoder)
|
||||||
|
.transform<String>(const LineSplitter())
|
||||||
|
.forEach((String line) {
|
||||||
|
globals.printError('<<< $line');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
final Process process;
|
||||||
|
StreamIterator<Map<String, dynamic>> streamIterator;
|
||||||
|
|
||||||
|
Future<void> close() async {
|
||||||
|
await process.stdin.close();
|
||||||
|
process.kill();
|
||||||
|
}
|
||||||
|
|
||||||
|
void sendCommand(File imageFile, Uri goldenKey, bool updateGoldens) {
|
||||||
|
final Object command = jsonEncode(<String, dynamic>{
|
||||||
|
'imageFile': imageFile.path,
|
||||||
|
'key': goldenKey.toString(),
|
||||||
|
'update': updateGoldens,
|
||||||
|
});
|
||||||
|
globals.printTrace('Preparing to send command: $command');
|
||||||
|
process.stdin.writeln(command);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Map<String, dynamic>> getResponse() async {
|
||||||
|
final bool available = await streamIterator.moveNext();
|
||||||
|
assert(available);
|
||||||
|
return streamIterator.current;
|
||||||
|
}
|
||||||
|
|
||||||
|
static String generateBootstrap(Uri testUri) {
|
||||||
|
final File testConfigFile = findTestConfigFile(globals.fs.file(testUri));
|
||||||
|
// Generate comparator process for the file.
|
||||||
|
return '''
|
||||||
|
// @dart=2.9
|
||||||
|
import 'dart:convert'; // ignore: dart_convert_import
|
||||||
|
import 'dart:io'; // ignore: dart_io_import
|
||||||
|
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
${testConfigFile != null ? "import '${Uri.file(testConfigFile.path)}' as test_config;" : ""}
|
||||||
|
|
||||||
|
void main() async {
|
||||||
|
LocalFileComparator comparator = LocalFileComparator(Uri.parse('$testUri'));
|
||||||
|
goldenFileComparator = comparator;
|
||||||
|
|
||||||
|
${testConfigFile != null ? 'test_config.testExecutable(() async {' : ''}
|
||||||
|
final commands = stdin
|
||||||
|
.transform<String>(utf8.decoder)
|
||||||
|
.transform<String>(const LineSplitter())
|
||||||
|
.map<Object>(jsonDecode);
|
||||||
|
await for (final Object command in commands) {
|
||||||
|
if (command is Map<String, dynamic>) {
|
||||||
|
File imageFile = File(command['imageFile']);
|
||||||
|
Uri goldenKey = Uri.parse(command['key']);
|
||||||
|
bool update = command['update'];
|
||||||
|
|
||||||
|
final bytes = await File(imageFile.path).readAsBytes();
|
||||||
|
if (update) {
|
||||||
|
await goldenFileComparator.update(goldenKey, bytes);
|
||||||
|
print(jsonEncode({'success': true}));
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
bool success = await goldenFileComparator.compare(bytes, goldenKey);
|
||||||
|
print(jsonEncode({'success': success}));
|
||||||
|
} on Exception catch (ex) {
|
||||||
|
print(jsonEncode({'success': false, 'message': '\$ex'}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
print('object type is not right');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
${testConfigFile != null ? '});' : ''}
|
||||||
|
}
|
||||||
|
''';
|
||||||
|
}
|
||||||
|
}
|
@ -11,7 +11,6 @@ import 'package:async/async.dart';
|
|||||||
import 'package:http_multi_server/http_multi_server.dart';
|
import 'package:http_multi_server/http_multi_server.dart';
|
||||||
import 'package:meta/meta.dart';
|
import 'package:meta/meta.dart';
|
||||||
import 'package:package_config/package_config.dart';
|
import 'package:package_config/package_config.dart';
|
||||||
import 'package:path/path.dart' as p; // ignore: package_path_import
|
|
||||||
import 'package:pool/pool.dart';
|
import 'package:pool/pool.dart';
|
||||||
import 'package:shelf/shelf.dart' as shelf;
|
import 'package:shelf/shelf.dart' as shelf;
|
||||||
import 'package:shelf/shelf_io.dart' as shelf_io;
|
import 'package:shelf/shelf_io.dart' as shelf_io;
|
||||||
@ -26,16 +25,16 @@ import '../artifacts.dart';
|
|||||||
import '../base/common.dart';
|
import '../base/common.dart';
|
||||||
import '../base/file_system.dart';
|
import '../base/file_system.dart';
|
||||||
import '../base/io.dart';
|
import '../base/io.dart';
|
||||||
|
import '../base/logger.dart';
|
||||||
import '../build_info.dart';
|
import '../build_info.dart';
|
||||||
import '../cache.dart';
|
import '../cache.dart';
|
||||||
import '../convert.dart';
|
import '../convert.dart';
|
||||||
import '../dart/package_map.dart';
|
import '../dart/package_map.dart';
|
||||||
import '../globals.dart' as globals;
|
|
||||||
import '../project.dart';
|
import '../project.dart';
|
||||||
import '../web/chrome.dart';
|
import '../web/chrome.dart';
|
||||||
import '../web/memory_fs.dart';
|
import '../web/memory_fs.dart';
|
||||||
|
import 'flutter_web_goldens.dart';
|
||||||
import 'test_compiler.dart';
|
import 'test_compiler.dart';
|
||||||
import 'test_config.dart';
|
|
||||||
|
|
||||||
class FlutterWebPlatform extends PlatformPlugin {
|
class FlutterWebPlatform extends PlatformPlugin {
|
||||||
FlutterWebPlatform._(this._server, this._config, this._root, {
|
FlutterWebPlatform._(this._server, this._config, this._root, {
|
||||||
@ -44,11 +43,20 @@ class FlutterWebPlatform extends PlatformPlugin {
|
|||||||
this.updateGoldens,
|
this.updateGoldens,
|
||||||
@required this.buildInfo,
|
@required this.buildInfo,
|
||||||
@required this.webMemoryFS,
|
@required this.webMemoryFS,
|
||||||
}) {
|
@required FileSystem fileSystem,
|
||||||
|
@required PackageConfig flutterToolPackageConfig,
|
||||||
|
@required ChromiumLauncher chromiumLauncher,
|
||||||
|
@required Logger logger,
|
||||||
|
@required Artifacts artifacts,
|
||||||
|
}) : _fileSystem = fileSystem,
|
||||||
|
_flutterToolPackageConfig = flutterToolPackageConfig,
|
||||||
|
_chromiumLauncher = chromiumLauncher,
|
||||||
|
_logger = logger,
|
||||||
|
_artifacts = artifacts {
|
||||||
final shelf.Cascade cascade = shelf.Cascade()
|
final shelf.Cascade cascade = shelf.Cascade()
|
||||||
.add(_webSocketHandler.handler)
|
.add(_webSocketHandler.handler)
|
||||||
.add(createStaticHandler(
|
.add(createStaticHandler(
|
||||||
globals.fs.path.join(Cache.flutterRoot, 'packages', 'flutter_tools'),
|
fileSystem.path.join(Cache.flutterRoot, 'packages', 'flutter_tools'),
|
||||||
serveFilesOutsidePath: true,
|
serveFilesOutsidePath: true,
|
||||||
))
|
))
|
||||||
.add(_handleStaticArtifact)
|
.add(_handleStaticArtifact)
|
||||||
@ -56,12 +64,11 @@ class FlutterWebPlatform extends PlatformPlugin {
|
|||||||
.add(_wrapperHandler)
|
.add(_wrapperHandler)
|
||||||
.add(_handleTestRequest)
|
.add(_handleTestRequest)
|
||||||
.add(createStaticHandler(
|
.add(createStaticHandler(
|
||||||
p.join(p.current, 'test'),
|
fileSystem.path.join(fileSystem.currentDirectory.path, 'test'),
|
||||||
serveFilesOutsidePath: true,
|
serveFilesOutsidePath: true,
|
||||||
))
|
))
|
||||||
.add(_packageFilesHandler);
|
.add(_packageFilesHandler);
|
||||||
_server.mount(cascade.handler);
|
_server.mount(cascade.handler);
|
||||||
|
|
||||||
_testGoldenComparator = TestGoldenComparator(
|
_testGoldenComparator = TestGoldenComparator(
|
||||||
shellPath,
|
shellPath,
|
||||||
() => TestCompiler(buildInfo, flutterProject),
|
() => TestCompiler(buildInfo, flutterProject),
|
||||||
@ -70,6 +77,11 @@ class FlutterWebPlatform extends PlatformPlugin {
|
|||||||
|
|
||||||
final WebMemoryFS webMemoryFS;
|
final WebMemoryFS webMemoryFS;
|
||||||
final BuildInfo buildInfo;
|
final BuildInfo buildInfo;
|
||||||
|
final FileSystem _fileSystem;
|
||||||
|
final PackageConfig _flutterToolPackageConfig;
|
||||||
|
final ChromiumLauncher _chromiumLauncher;
|
||||||
|
final Logger _logger;
|
||||||
|
final Artifacts _artifacts;
|
||||||
|
|
||||||
static Future<FlutterWebPlatform> start(String root, {
|
static Future<FlutterWebPlatform> start(String root, {
|
||||||
FlutterProject flutterProject,
|
FlutterProject flutterProject,
|
||||||
@ -78,8 +90,22 @@ class FlutterWebPlatform extends PlatformPlugin {
|
|||||||
bool pauseAfterLoad = false,
|
bool pauseAfterLoad = false,
|
||||||
@required BuildInfo buildInfo,
|
@required BuildInfo buildInfo,
|
||||||
@required WebMemoryFS webMemoryFS,
|
@required WebMemoryFS webMemoryFS,
|
||||||
|
@required FileSystem fileSystem,
|
||||||
|
@required Logger logger,
|
||||||
|
@required ChromiumLauncher chromiumLauncher,
|
||||||
|
@required Artifacts artifacts,
|
||||||
}) async {
|
}) async {
|
||||||
final shelf_io.IOServer server = shelf_io.IOServer(await HttpMultiServer.loopback(0));
|
final shelf_io.IOServer server = shelf_io.IOServer(await HttpMultiServer.loopback(0));
|
||||||
|
final PackageConfig packageConfig = await loadPackageConfigWithLogging(
|
||||||
|
fileSystem.file(fileSystem.path.join(
|
||||||
|
Cache.flutterRoot,
|
||||||
|
'packages',
|
||||||
|
'flutter_tools',
|
||||||
|
'.dart_tool',
|
||||||
|
'package_config.json',
|
||||||
|
)),
|
||||||
|
logger: logger,
|
||||||
|
);
|
||||||
return FlutterWebPlatform._(
|
return FlutterWebPlatform._(
|
||||||
server,
|
server,
|
||||||
Configuration.current.change(pauseAfterLoad: pauseAfterLoad),
|
Configuration.current.change(pauseAfterLoad: pauseAfterLoad),
|
||||||
@ -89,81 +115,65 @@ class FlutterWebPlatform extends PlatformPlugin {
|
|||||||
updateGoldens: updateGoldens,
|
updateGoldens: updateGoldens,
|
||||||
buildInfo: buildInfo,
|
buildInfo: buildInfo,
|
||||||
webMemoryFS: webMemoryFS,
|
webMemoryFS: webMemoryFS,
|
||||||
|
flutterToolPackageConfig: packageConfig,
|
||||||
|
fileSystem: fileSystem,
|
||||||
|
chromiumLauncher: chromiumLauncher,
|
||||||
|
artifacts: artifacts,
|
||||||
|
logger: logger,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final Future<PackageConfig> _flutterToolsPackageMap = loadPackageConfigWithLogging(
|
|
||||||
globals.fs.file(globals.fs.path.join(
|
|
||||||
Cache.flutterRoot,
|
|
||||||
'packages',
|
|
||||||
'flutter_tools',
|
|
||||||
'.packages',
|
|
||||||
)),
|
|
||||||
logger: globals.logger,
|
|
||||||
);
|
|
||||||
|
|
||||||
/// Uri of the test package.
|
/// Uri of the test package.
|
||||||
Future<Uri> get testUri async => (await _flutterToolsPackageMap)['test']?.packageUriRoot;
|
Uri get testUri => _flutterToolPackageConfig['test'].packageUriRoot;
|
||||||
|
|
||||||
/// The test runner configuration.
|
|
||||||
final Configuration _config;
|
final Configuration _config;
|
||||||
|
|
||||||
@visibleForTesting
|
|
||||||
Configuration get config => _config;
|
|
||||||
|
|
||||||
/// The underlying server.
|
|
||||||
final shelf.Server _server;
|
final shelf.Server _server;
|
||||||
|
|
||||||
@visibleForTesting
|
|
||||||
shelf.Server get server => _server;
|
|
||||||
|
|
||||||
/// The URL for this server.
|
|
||||||
Uri get url => _server.url;
|
Uri get url => _server.url;
|
||||||
|
|
||||||
/// The ahem text file.
|
/// The ahem text file.
|
||||||
File get ahem => globals.fs.file(globals.fs.path.join(
|
File get _ahem => _fileSystem.file(_fileSystem.path.join(
|
||||||
Cache.flutterRoot,
|
Cache.flutterRoot,
|
||||||
'packages',
|
'packages',
|
||||||
'flutter_tools',
|
'flutter_tools',
|
||||||
'static',
|
'static',
|
||||||
'Ahem.ttf',
|
'Ahem.ttf',
|
||||||
));
|
));
|
||||||
|
|
||||||
/// The require js binary.
|
/// The require js binary.
|
||||||
File get requireJs => globals.fs.file(globals.fs.path.join(
|
File get _requireJs => _fileSystem.file(_fileSystem.path.join(
|
||||||
globals.artifacts.getArtifactPath(Artifact.engineDartSdkPath),
|
_artifacts.getArtifactPath(Artifact.engineDartSdkPath),
|
||||||
'lib',
|
'lib',
|
||||||
'dev_compiler',
|
'dev_compiler',
|
||||||
'kernel',
|
'kernel',
|
||||||
'amd',
|
'amd',
|
||||||
'require.js',
|
'require.js',
|
||||||
));
|
));
|
||||||
|
|
||||||
/// The ddc to dart stack trace mapper.
|
/// The ddc to dart stack trace mapper.
|
||||||
File get stackTraceMapper => globals.fs.file(globals.fs.path.join(
|
File get _stackTraceMapper => _fileSystem.file(_fileSystem.path.join(
|
||||||
globals.artifacts.getArtifactPath(Artifact.engineDartSdkPath),
|
_artifacts.getArtifactPath(Artifact.engineDartSdkPath),
|
||||||
'lib',
|
'lib',
|
||||||
'dev_compiler',
|
'dev_compiler',
|
||||||
'web',
|
'web',
|
||||||
'dart_stack_trace_mapper.js',
|
'dart_stack_trace_mapper.js',
|
||||||
));
|
));
|
||||||
|
|
||||||
/// The precompiled dart sdk.
|
/// The precompiled dart sdk.
|
||||||
File get dartSdk => globals.fs.file(globals.fs.path.join(
|
File get _dartSdk => _fileSystem.file(_fileSystem.path.join(
|
||||||
globals.artifacts.getArtifactPath(Artifact.flutterWebSdk),
|
_artifacts.getArtifactPath(Artifact.flutterWebSdk),
|
||||||
'kernel',
|
'kernel',
|
||||||
'amd',
|
'amd',
|
||||||
'dart_sdk.js',
|
'dart_sdk.js',
|
||||||
));
|
));
|
||||||
|
|
||||||
/// The precompiled test javascript.
|
/// The precompiled test javascript.
|
||||||
Future<File> get testDartJs async => globals.fs.file(globals.fs.path.join(
|
File get _testDartJs => _fileSystem.file(_fileSystem.path.join(
|
||||||
(await testUri).toFilePath(),
|
testUri.toFilePath(),
|
||||||
'dart.js',
|
'dart.js',
|
||||||
));
|
));
|
||||||
|
|
||||||
Future<File> get testHostDartJs async => globals.fs.file(globals.fs.path.join(
|
File get _testHostDartJs => _fileSystem.file(_fileSystem.path.join(
|
||||||
(await testUri).toFilePath(),
|
testUri.toFilePath(),
|
||||||
'src',
|
'src',
|
||||||
'runner',
|
'runner',
|
||||||
'browser',
|
'browser',
|
||||||
@ -174,15 +184,15 @@ class FlutterWebPlatform extends PlatformPlugin {
|
|||||||
Future<shelf.Response> _handleTestRequest(shelf.Request request) async {
|
Future<shelf.Response> _handleTestRequest(shelf.Request request) async {
|
||||||
if (request.url.path.endsWith('.dart.browser_test.dart.js')) {
|
if (request.url.path.endsWith('.dart.browser_test.dart.js')) {
|
||||||
final String leadingPath = request.url.path.split('.browser_test.dart.js')[0];
|
final String leadingPath = request.url.path.split('.browser_test.dart.js')[0];
|
||||||
final String generatedFile = globals.fs.path.split(leadingPath).join('_') + '.bootstrap.js';
|
final String generatedFile = _fileSystem.path.split(leadingPath).join('_') + '.bootstrap.js';
|
||||||
return shelf.Response.ok(bootstrapFileContents('/' + generatedFile, 'require.js', 'dart_stack_trace_mapper.js'), headers: <String, String>{
|
return shelf.Response.ok(bootstrapFileContents('/' + generatedFile, 'require.js', 'dart_stack_trace_mapper.js'), headers: <String, String>{
|
||||||
HttpHeaders.contentTypeHeader: 'text/javascript',
|
HttpHeaders.contentTypeHeader: 'text/javascript',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (request.url.path.endsWith('.dart.bootstrap.js')) {
|
if (request.url.path.endsWith('.dart.bootstrap.js')) {
|
||||||
final String leadingPath = request.url.path.split('.dart.bootstrap.js')[0];
|
final String leadingPath = request.url.path.split('.dart.bootstrap.js')[0];
|
||||||
final String generatedFile = globals.fs.path.split(leadingPath).join('_') + '.dart.test.dart.js';
|
final String generatedFile = _fileSystem.path.split(leadingPath).join('_') + '.dart.test.dart.js';
|
||||||
return shelf.Response.ok(generatedActualMain(globals.fs.path.basename(leadingPath) + '.dart.bootstrap', '/' + generatedFile), headers: <String, String>{
|
return shelf.Response.ok(generatedActualMain(_fileSystem.path.basename(leadingPath) + '.dart.bootstrap', '/' + generatedFile), headers: <String, String>{
|
||||||
HttpHeaders.contentTypeHeader: 'text/javascript',
|
HttpHeaders.contentTypeHeader: 'text/javascript',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -203,30 +213,30 @@ class FlutterWebPlatform extends PlatformPlugin {
|
|||||||
Future<shelf.Response> _handleStaticArtifact(shelf.Request request) async {
|
Future<shelf.Response> _handleStaticArtifact(shelf.Request request) async {
|
||||||
if (request.requestedUri.path.contains('require.js')) {
|
if (request.requestedUri.path.contains('require.js')) {
|
||||||
return shelf.Response.ok(
|
return shelf.Response.ok(
|
||||||
requireJs.openRead(),
|
_requireJs.openRead(),
|
||||||
headers: <String, String>{'Content-Type': 'text/javascript'},
|
headers: <String, String>{'Content-Type': 'text/javascript'},
|
||||||
);
|
);
|
||||||
} else if (request.requestedUri.path.contains('ahem.ttf')) {
|
} else if (request.requestedUri.path.contains('ahem.ttf')) {
|
||||||
return shelf.Response.ok(ahem.openRead());
|
return shelf.Response.ok(_ahem.openRead());
|
||||||
} else if (request.requestedUri.path.contains('dart_sdk.js')) {
|
} else if (request.requestedUri.path.contains('dart_sdk.js')) {
|
||||||
return shelf.Response.ok(
|
return shelf.Response.ok(
|
||||||
dartSdk.openRead(),
|
_dartSdk.openRead(),
|
||||||
headers: <String, String>{'Content-Type': 'text/javascript'},
|
headers: <String, String>{'Content-Type': 'text/javascript'},
|
||||||
);
|
);
|
||||||
} else if (request.requestedUri.path
|
} else if (request.requestedUri.path
|
||||||
.contains('dart_stack_trace_mapper.js')) {
|
.contains('dart_stack_trace_mapper.js')) {
|
||||||
return shelf.Response.ok(
|
return shelf.Response.ok(
|
||||||
stackTraceMapper.openRead(),
|
_stackTraceMapper.openRead(),
|
||||||
headers: <String, String>{'Content-Type': 'text/javascript'},
|
headers: <String, String>{'Content-Type': 'text/javascript'},
|
||||||
);
|
);
|
||||||
} else if (request.requestedUri.path.contains('static/dart.js')) {
|
} else if (request.requestedUri.path.contains('static/dart.js')) {
|
||||||
return shelf.Response.ok(
|
return shelf.Response.ok(
|
||||||
(await testDartJs).openRead(),
|
_testDartJs.openRead(),
|
||||||
headers: <String, String>{'Content-Type': 'text/javascript'},
|
headers: <String, String>{'Content-Type': 'text/javascript'},
|
||||||
);
|
);
|
||||||
} else if (request.requestedUri.path.contains('host.dart.js')) {
|
} else if (request.requestedUri.path.contains('host.dart.js')) {
|
||||||
return shelf.Response.ok(
|
return shelf.Response.ok(
|
||||||
(await testHostDartJs).openRead(),
|
_testHostDartJs.openRead(),
|
||||||
headers: <String, String>{'Content-Type': 'text/javascript'},
|
headers: <String, String>{'Content-Type': 'text/javascript'},
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@ -241,8 +251,8 @@ class FlutterWebPlatform extends PlatformPlugin {
|
|||||||
pathSegments: request.requestedUri.pathSegments.skip(1),
|
pathSegments: request.requestedUri.pathSegments.skip(1),
|
||||||
));
|
));
|
||||||
if (fileUri != null) {
|
if (fileUri != null) {
|
||||||
final String dirname = p.dirname(fileUri.toFilePath());
|
final String dirname = _fileSystem.path.dirname(fileUri.toFilePath());
|
||||||
final String basename = p.basename(fileUri.toFilePath());
|
final String basename = _fileSystem.path.basename(fileUri.toFilePath());
|
||||||
final shelf.Handler handler = createStaticHandler(dirname);
|
final shelf.Handler handler = createStaticHandler(dirname);
|
||||||
final shelf.Request modifiedRequest = shelf.Request(
|
final shelf.Request modifiedRequest = shelf.Request(
|
||||||
request.method,
|
request.method,
|
||||||
@ -293,10 +303,10 @@ class FlutterWebPlatform extends PlatformPlugin {
|
|||||||
});
|
});
|
||||||
bytes = base64.decode(response.result['data'] as String);
|
bytes = base64.decode(response.result['data'] as String);
|
||||||
} on WipError catch (ex) {
|
} on WipError catch (ex) {
|
||||||
globals.printError('Caught WIPError: $ex');
|
_logger.printError('Caught WIPError: $ex');
|
||||||
return shelf.Response.ok('WIP error: $ex');
|
return shelf.Response.ok('WIP error: $ex');
|
||||||
} on FormatException catch (ex) {
|
} on FormatException catch (ex) {
|
||||||
globals.printError('Caught FormatException: $ex');
|
_logger.printError('Caught FormatException: $ex');
|
||||||
return shelf.Response.ok('Caught exception: $ex');
|
return shelf.Response.ok('Caught exception: $ex');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -321,10 +331,10 @@ class FlutterWebPlatform extends PlatformPlugin {
|
|||||||
|
|
||||||
// A handler that serves wrapper files used to bootstrap tests.
|
// A handler that serves wrapper files used to bootstrap tests.
|
||||||
shelf.Response _wrapperHandler(shelf.Request request) {
|
shelf.Response _wrapperHandler(shelf.Request request) {
|
||||||
final String path = globals.fs.path.fromUri(request.url);
|
final String path = _fileSystem.path.fromUri(request.url);
|
||||||
if (path.endsWith('.html')) {
|
if (path.endsWith('.html')) {
|
||||||
final String test = globals.fs.path.withoutExtension(path) + '.dart';
|
final String test = _fileSystem.path.withoutExtension(path) + '.dart';
|
||||||
final String scriptBase = htmlEscape.convert(globals.fs.path.basename(test));
|
final String scriptBase = htmlEscape.convert(_fileSystem.path.basename(test));
|
||||||
final String link = '<link rel="x-dart-test" href="$scriptBase">';
|
final String link = '<link rel="x-dart-test" href="$scriptBase">';
|
||||||
return shelf.Response.ok('''
|
return shelf.Response.ok('''
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
@ -369,10 +379,9 @@ class FlutterWebPlatform extends PlatformPlugin {
|
|||||||
throw StateError('Load called on a closed FlutterWebPlatform');
|
throw StateError('Load called on a closed FlutterWebPlatform');
|
||||||
}
|
}
|
||||||
|
|
||||||
final Uri suiteUrl = url.resolveUri(globals.fs.path.toUri(globals.fs.path.withoutExtension(
|
final Uri suiteUrl = url.resolveUri(_fileSystem.path.toUri(_fileSystem.path.withoutExtension(
|
||||||
globals.fs.path.relative(path, from: globals.fs.path.join(_root, 'test'))) +
|
_fileSystem.path.relative(path, from: _fileSystem.path.join(_root, 'test'))) + '.html'));
|
||||||
'.html'));
|
final String relativePath = _fileSystem.path.relative(_fileSystem.path.normalize(path), from: _fileSystem.currentDirectory.path);
|
||||||
final String relativePath = globals.fs.path.relative(globals.fs.path.normalize(path), from: globals.fs.currentDirectory.path);
|
|
||||||
final RunnerSuite suite = await _browserManager.load(relativePath, suiteUrl, suiteConfig, message, onDone: () async {
|
final RunnerSuite suite = await _browserManager.load(relativePath, suiteUrl, suiteConfig, message, onDone: () async {
|
||||||
await _browserManager.close();
|
await _browserManager.close();
|
||||||
_browserManager = null;
|
_browserManager = null;
|
||||||
@ -406,9 +415,10 @@ class FlutterWebPlatform extends PlatformPlugin {
|
|||||||
'debug': _config.pauseAfterLoad.toString(),
|
'debug': _config.pauseAfterLoad.toString(),
|
||||||
});
|
});
|
||||||
|
|
||||||
globals.printTrace('Serving tests at $hostUrl');
|
_logger.printTrace('Serving tests at $hostUrl');
|
||||||
|
|
||||||
return BrowserManager.start(
|
return BrowserManager.start(
|
||||||
|
_chromiumLauncher,
|
||||||
browser,
|
browser,
|
||||||
hostUrl,
|
hostUrl,
|
||||||
completer.future,
|
completer.future,
|
||||||
@ -459,7 +469,7 @@ class OneOffHandler {
|
|||||||
|
|
||||||
/// Dispatches [request] to the appropriate handler.
|
/// Dispatches [request] to the appropriate handler.
|
||||||
FutureOr<shelf.Response> _onRequest(shelf.Request request) {
|
FutureOr<shelf.Response> _onRequest(shelf.Request request) {
|
||||||
final List<String> components = p.url.split(request.url.path);
|
final List<String> components = request.url.path.split('/');
|
||||||
if (components.isEmpty) {
|
if (components.isEmpty) {
|
||||||
return shelf.Response.notFound(null);
|
return shelf.Response.notFound(null);
|
||||||
}
|
}
|
||||||
@ -513,10 +523,6 @@ class BrowserManager {
|
|||||||
|
|
||||||
/// The browser instance that this is connected to via [_channel].
|
/// The browser instance that this is connected to via [_channel].
|
||||||
final Chromium _browser;
|
final Chromium _browser;
|
||||||
|
|
||||||
// TODO(nweiz): Consider removing the duplication between this and
|
|
||||||
// [_browser.name].
|
|
||||||
/// The [Runtime] for [_browser].
|
|
||||||
final Runtime _runtime;
|
final Runtime _runtime;
|
||||||
|
|
||||||
/// The channel used to communicate with the browser.
|
/// The channel used to communicate with the browser.
|
||||||
@ -574,23 +580,14 @@ class BrowserManager {
|
|||||||
/// Returns the browser manager, or throws an [ApplicationException] if a
|
/// Returns the browser manager, or throws an [ApplicationException] if a
|
||||||
/// connection fails to be established.
|
/// connection fails to be established.
|
||||||
static Future<BrowserManager> start(
|
static Future<BrowserManager> start(
|
||||||
|
ChromiumLauncher chromiumLauncher,
|
||||||
Runtime runtime,
|
Runtime runtime,
|
||||||
Uri url,
|
Uri url,
|
||||||
Future<WebSocketChannel> future, {
|
Future<WebSocketChannel> future, {
|
||||||
bool debug = false,
|
bool debug = false,
|
||||||
bool headless = true,
|
bool headless = true,
|
||||||
}) async {
|
}) async {
|
||||||
final ChromiumLauncher chromiumLauncher = ChromiumLauncher(
|
final Chromium chrome = await chromiumLauncher.launch(url.toString(), headless: headless);
|
||||||
browserFinder: findChromeExecutable,
|
|
||||||
fileSystem: globals.fs,
|
|
||||||
operatingSystemUtils: globals.os,
|
|
||||||
platform: globals.platform,
|
|
||||||
processManager: globals.processManager,
|
|
||||||
logger: globals.logger,
|
|
||||||
);
|
|
||||||
final Chromium chrome =
|
|
||||||
await chromiumLauncher.launch(url.toString(), headless: headless);
|
|
||||||
|
|
||||||
final Completer<BrowserManager> completer = Completer<BrowserManager>();
|
final Completer<BrowserManager> completer = Completer<BrowserManager>();
|
||||||
|
|
||||||
unawaited(chrome.onExit.then((int browserExitCode) {
|
unawaited(chrome.onExit.then((int browserExitCode) {
|
||||||
@ -773,186 +770,6 @@ class _BrowserEnvironment implements Environment {
|
|||||||
CancelableOperation<dynamic> displayPause() => _manager._displayPause();
|
CancelableOperation<dynamic> displayPause() => _manager._displayPause();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Helper class to start golden file comparison in a separate process.
|
|
||||||
///
|
|
||||||
/// Golden file comparator is configured using flutter_test_config.dart and that
|
|
||||||
/// file can contain arbitrary Dart code that depends on dart:ui. Thus it has to
|
|
||||||
/// be executed in a `flutter_tester` environment. This helper class generates a
|
|
||||||
/// Dart file configured with flutter_test_config.dart to perform the comparison
|
|
||||||
/// of golden files.
|
|
||||||
class TestGoldenComparator {
|
|
||||||
/// Creates a [TestGoldenComparator] instance.
|
|
||||||
TestGoldenComparator(this.shellPath, this.compilerFactory)
|
|
||||||
: tempDir = globals.fs.systemTempDirectory.createTempSync('flutter_web_platform.');
|
|
||||||
|
|
||||||
final String shellPath;
|
|
||||||
final Directory tempDir;
|
|
||||||
final TestCompiler Function() compilerFactory;
|
|
||||||
|
|
||||||
TestCompiler _compiler;
|
|
||||||
TestGoldenComparatorProcess _previousComparator;
|
|
||||||
Uri _previousTestUri;
|
|
||||||
|
|
||||||
Future<void> close() async {
|
|
||||||
tempDir.deleteSync(recursive: true);
|
|
||||||
await _compiler?.dispose();
|
|
||||||
await _previousComparator?.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Start golden comparator in a separate process. Start one file per test file
|
|
||||||
/// to reduce the overhead of starting `flutter_tester`.
|
|
||||||
Future<TestGoldenComparatorProcess> _processForTestFile(Uri testUri) async {
|
|
||||||
if (testUri == _previousTestUri) {
|
|
||||||
return _previousComparator;
|
|
||||||
}
|
|
||||||
|
|
||||||
final String bootstrap = TestGoldenComparatorProcess.generateBootstrap(testUri);
|
|
||||||
final Process process = await _startProcess(bootstrap);
|
|
||||||
unawaited(_previousComparator?.close());
|
|
||||||
_previousComparator = TestGoldenComparatorProcess(process);
|
|
||||||
_previousTestUri = testUri;
|
|
||||||
|
|
||||||
return _previousComparator;
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Process> _startProcess(String testBootstrap) async {
|
|
||||||
// Prepare the Dart file that will talk to us and start the test.
|
|
||||||
final File listenerFile = (await tempDir.createTemp('listener')).childFile('listener.dart');
|
|
||||||
await listenerFile.writeAsString(testBootstrap);
|
|
||||||
|
|
||||||
// Lazily create the compiler
|
|
||||||
_compiler = _compiler ?? compilerFactory();
|
|
||||||
final String output = await _compiler.compile(listenerFile.uri);
|
|
||||||
final List<String> command = <String>[
|
|
||||||
shellPath,
|
|
||||||
'--disable-observatory',
|
|
||||||
'--non-interactive',
|
|
||||||
'--packages=${globals.fs.path.join('.dart_tool', 'package_config.json')}',
|
|
||||||
output,
|
|
||||||
];
|
|
||||||
|
|
||||||
final Map<String, String> environment = <String, String>{
|
|
||||||
// Chrome is the only supported browser currently.
|
|
||||||
'FLUTTER_TEST_BROWSER': 'chrome',
|
|
||||||
};
|
|
||||||
return globals.processManager.start(command, environment: environment);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<String> compareGoldens(Uri testUri, Uint8List bytes, Uri goldenKey, bool updateGoldens) async {
|
|
||||||
final File imageFile = await (await tempDir.createTemp('image')).childFile('image').writeAsBytes(bytes);
|
|
||||||
|
|
||||||
final TestGoldenComparatorProcess process = await _processForTestFile(testUri);
|
|
||||||
process.sendCommand(imageFile, goldenKey, updateGoldens);
|
|
||||||
|
|
||||||
final Map<String, dynamic> result = await process.getResponse();
|
|
||||||
|
|
||||||
if (result == null) {
|
|
||||||
return 'unknown error';
|
|
||||||
} else {
|
|
||||||
return (result['success'] as bool) ? null : ((result['message'] as String) ?? 'does not match');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents a `flutter_tester` process started for golden comparison. Also
|
|
||||||
/// handles communication with the child process.
|
|
||||||
class TestGoldenComparatorProcess {
|
|
||||||
/// Creates a [TestGoldenComparatorProcess] backed by [process].
|
|
||||||
TestGoldenComparatorProcess(this.process) {
|
|
||||||
// Pipe stdout and stderr to printTrace and printError.
|
|
||||||
// Also parse stdout as a stream of JSON objects.
|
|
||||||
streamIterator = StreamIterator<Map<String, dynamic>>(
|
|
||||||
process.stdout
|
|
||||||
.transform<String>(utf8.decoder)
|
|
||||||
.transform<String>(const LineSplitter())
|
|
||||||
.where((String line) {
|
|
||||||
globals.printTrace('<<< $line');
|
|
||||||
return line.isNotEmpty && line[0] == '{';
|
|
||||||
})
|
|
||||||
.map<dynamic>(jsonDecode)
|
|
||||||
.cast<Map<String, dynamic>>());
|
|
||||||
|
|
||||||
process.stderr
|
|
||||||
.transform<String>(utf8.decoder)
|
|
||||||
.transform<String>(const LineSplitter())
|
|
||||||
.forEach((String line) {
|
|
||||||
globals.printError('<<< $line');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
final Process process;
|
|
||||||
StreamIterator<Map<String, dynamic>> streamIterator;
|
|
||||||
|
|
||||||
Future<void> close() async {
|
|
||||||
await process.stdin.close();
|
|
||||||
process.kill();
|
|
||||||
}
|
|
||||||
|
|
||||||
void sendCommand(File imageFile, Uri goldenKey, bool updateGoldens) {
|
|
||||||
final Object command = jsonEncode(<String, dynamic>{
|
|
||||||
'imageFile': imageFile.path,
|
|
||||||
'key': goldenKey.toString(),
|
|
||||||
'update': updateGoldens,
|
|
||||||
});
|
|
||||||
globals.printTrace('Preparing to send command: $command');
|
|
||||||
process.stdin.writeln(command);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Map<String, dynamic>> getResponse() async {
|
|
||||||
final bool available = await streamIterator.moveNext();
|
|
||||||
assert(available);
|
|
||||||
return streamIterator.current;
|
|
||||||
}
|
|
||||||
|
|
||||||
static String generateBootstrap(Uri testUri) {
|
|
||||||
final File testConfigFile = findTestConfigFile(globals.fs.file(testUri));
|
|
||||||
// Generate comparator process for the file.
|
|
||||||
return '''
|
|
||||||
// @dart=2.9
|
|
||||||
import 'dart:convert'; // ignore: dart_convert_import
|
|
||||||
import 'dart:io'; // ignore: dart_io_import
|
|
||||||
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
|
||||||
|
|
||||||
${testConfigFile != null ? "import '${Uri.file(testConfigFile.path)}' as test_config;" : ""}
|
|
||||||
|
|
||||||
void main() async {
|
|
||||||
LocalFileComparator comparator = LocalFileComparator(Uri.parse('$testUri'));
|
|
||||||
goldenFileComparator = comparator;
|
|
||||||
|
|
||||||
${testConfigFile != null ? 'test_config.testExecutable(() async {' : ''}
|
|
||||||
final commands = stdin
|
|
||||||
.transform<String>(utf8.decoder)
|
|
||||||
.transform<String>(const LineSplitter())
|
|
||||||
.map<Object>(jsonDecode);
|
|
||||||
await for (final Object command in commands) {
|
|
||||||
if (command is Map<String, dynamic>) {
|
|
||||||
File imageFile = File(command['imageFile']);
|
|
||||||
Uri goldenKey = Uri.parse(command['key']);
|
|
||||||
bool update = command['update'];
|
|
||||||
|
|
||||||
final bytes = await File(imageFile.path).readAsBytes();
|
|
||||||
if (update) {
|
|
||||||
await goldenFileComparator.update(goldenKey, bytes);
|
|
||||||
print(jsonEncode({'success': true}));
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
bool success = await goldenFileComparator.compare(bytes, goldenKey);
|
|
||||||
print(jsonEncode({'success': success}));
|
|
||||||
} on Exception catch (ex) {
|
|
||||||
print(jsonEncode({'success': false, 'message': '\$ex'}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
print('object type is not right');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
${testConfigFile != null ? '});' : ''}
|
|
||||||
}
|
|
||||||
''';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String bootstrapFileContents(String mainUri, String requireUrl, String mapperUrl) {
|
String bootstrapFileContents(String mainUri, String requireUrl, String mapperUrl) {
|
||||||
return '''
|
return '''
|
||||||
(function() {
|
(function() {
|
||||||
|
@ -11,6 +11,7 @@ import '../base/io.dart';
|
|||||||
import '../build_info.dart';
|
import '../build_info.dart';
|
||||||
import '../globals.dart' as globals;
|
import '../globals.dart' as globals;
|
||||||
import '../project.dart';
|
import '../project.dart';
|
||||||
|
import '../web/chrome.dart';
|
||||||
import '../web/compile.dart';
|
import '../web/compile.dart';
|
||||||
import '../web/memory_fs.dart';
|
import '../web/memory_fs.dart';
|
||||||
import 'flutter_platform.dart' as loader;
|
import 'flutter_platform.dart' as loader;
|
||||||
@ -139,6 +140,8 @@ class _FlutterTestRunnerImpl implements FlutterTestRunner {
|
|||||||
testWrapper.registerPlatformPlugin(
|
testWrapper.registerPlatformPlugin(
|
||||||
<Runtime>[Runtime.chrome],
|
<Runtime>[Runtime.chrome],
|
||||||
() {
|
() {
|
||||||
|
// TODO(jonahwilliams): refactor this into a factory that handles
|
||||||
|
// providing dependencies.
|
||||||
return FlutterWebPlatform.start(
|
return FlutterWebPlatform.start(
|
||||||
flutterProject.directory.path,
|
flutterProject.directory.path,
|
||||||
updateGoldens: updateGoldens,
|
updateGoldens: updateGoldens,
|
||||||
@ -147,6 +150,17 @@ class _FlutterTestRunnerImpl implements FlutterTestRunner {
|
|||||||
pauseAfterLoad: startPaused,
|
pauseAfterLoad: startPaused,
|
||||||
buildInfo: buildInfo,
|
buildInfo: buildInfo,
|
||||||
webMemoryFS: result,
|
webMemoryFS: result,
|
||||||
|
logger: globals.logger,
|
||||||
|
fileSystem: globals.fs,
|
||||||
|
artifacts: globals.artifacts,
|
||||||
|
chromiumLauncher: ChromiumLauncher(
|
||||||
|
fileSystem: globals.fs,
|
||||||
|
platform: globals.platform,
|
||||||
|
processManager: globals.processManager,
|
||||||
|
operatingSystemUtils: globals.os,
|
||||||
|
browserFinder: findChromeExecutable,
|
||||||
|
logger: globals.logger,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -5,8 +5,8 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:flutter_tools/src/base/file_system.dart';
|
import 'package:flutter_tools/src/base/file_system.dart';
|
||||||
import 'package:flutter_tools/src/test/flutter_web_platform.dart';
|
|
||||||
import 'package:flutter_tools/src/globals.dart' as globals;
|
import 'package:flutter_tools/src/globals.dart' as globals;
|
||||||
|
import 'package:flutter_tools/src/test/flutter_web_goldens.dart';
|
||||||
|
|
||||||
import '../../src/common.dart';
|
import '../../src/common.dart';
|
||||||
import '../../src/mocks.dart';
|
import '../../src/mocks.dart';
|
||||||
|
@ -5,7 +5,7 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:flutter_tools/src/test/flutter_web_platform.dart';
|
import 'package:flutter_tools/src/test/flutter_web_goldens.dart';
|
||||||
import 'package:flutter_tools/src/test/test_compiler.dart';
|
import 'package:flutter_tools/src/test/test_compiler.dart';
|
||||||
import 'package:flutter_tools/src/globals.dart' as globals;
|
import 'package:flutter_tools/src/globals.dart' as globals;
|
||||||
import 'package:mockito/mockito.dart';
|
import 'package:mockito/mockito.dart';
|
||||||
|
Loading…
Reference in New Issue
Block a user