Reland support flutter test on platform chrome (#33859)
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
targets:
|
||||
$default:
|
||||
builders:
|
||||
build_web_compilers|entrypoint:
|
||||
enabled: false
|
||||
sources:
|
||||
exclude:
|
||||
- "test/data/**"
|
||||
|
||||
@@ -12,17 +12,21 @@ import 'package:build_modules/src/platform.dart';
|
||||
import 'package:build_runner_core/build_runner_core.dart' as core;
|
||||
import 'package:build_runner_core/src/generate/build_impl.dart';
|
||||
import 'package:build_runner_core/src/generate/options.dart';
|
||||
import 'package:build_test/builder.dart';
|
||||
import 'package:build_test/src/debug_test_builder.dart';
|
||||
import 'package:build_web_compilers/build_web_compilers.dart';
|
||||
import 'package:build_web_compilers/builders.dart';
|
||||
import 'package:build_web_compilers/src/dev_compiler_bootstrap.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:test_core/backend.dart';
|
||||
import 'package:watcher/watcher.dart';
|
||||
|
||||
import '../artifacts.dart';
|
||||
import '../base/file_system.dart';
|
||||
import '../base/logger.dart';
|
||||
import '../base/platform.dart';
|
||||
import '../compile.dart';
|
||||
import '../dart/package_map.dart';
|
||||
import '../globals.dart';
|
||||
@@ -65,6 +69,20 @@ final DartPlatform flutterWebPlatform =
|
||||
|
||||
/// The build application to compile a flutter application to the web.
|
||||
final List<core.BuilderApplication> builders = <core.BuilderApplication>[
|
||||
core.apply(
|
||||
'flutter_tools|test_bootstrap',
|
||||
<BuilderFactory>[
|
||||
(BuilderOptions options) => const DebugTestBuilder(),
|
||||
(BuilderOptions options) => const FlutterWebTestBootstrapBuilder(),
|
||||
],
|
||||
core.toRoot(),
|
||||
hideOutput: true,
|
||||
defaultGenerateFor: const InputSet(
|
||||
include: <String>[
|
||||
'test/**',
|
||||
],
|
||||
),
|
||||
),
|
||||
core.apply(
|
||||
'flutter_tools|module_library',
|
||||
<Builder Function(BuilderOptions)>[moduleLibraryBuilder],
|
||||
@@ -109,7 +127,7 @@ final List<core.BuilderApplication> builders = <core.BuilderApplication>[
|
||||
'flutter_tools|entrypoint',
|
||||
<BuilderFactory>[
|
||||
(BuilderOptions options) => FlutterWebEntrypointBuilder(
|
||||
options.config['target'] ?? 'lib/main.dart'),
|
||||
options.config['targets'] ?? <String>['lib/main.dart']),
|
||||
],
|
||||
core.toRoot(),
|
||||
hideOutput: true,
|
||||
@@ -117,6 +135,7 @@ final List<core.BuilderApplication> builders = <core.BuilderApplication>[
|
||||
include: <String>[
|
||||
'lib/**',
|
||||
'web/**',
|
||||
'test/**_test.dart.browser_test.dart',
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -135,13 +154,14 @@ class BuildRunnerWebCompilationProxy extends WebCompilationProxy {
|
||||
@override
|
||||
Future<void> initialize({
|
||||
@required Directory projectDirectory,
|
||||
@required String target,
|
||||
@required List<String> targets,
|
||||
String testOutputDir,
|
||||
}) async {
|
||||
// Override the generated output directory so this does not conflict with
|
||||
// other build_runner output.
|
||||
core.overrideGeneratedOutputDirectory('flutter_web');
|
||||
_packageUriMapper = PackageUriMapper(
|
||||
path.absolute(target), PackageMap.globalPackagesPath, null, null);
|
||||
path.absolute('lib/main.dart'), PackageMap.globalPackagesPath, null, null);
|
||||
_packageGraph = core.PackageGraph.forPath(projectDirectory.path);
|
||||
final core.BuildEnvironment buildEnvironment = core.OverrideableEnvironment(
|
||||
core.IOEnvironment(_packageGraph), onLog: (LogRecord record) {
|
||||
@@ -163,8 +183,18 @@ class BuildRunnerWebCompilationProxy extends WebCompilationProxy {
|
||||
trackPerformance: false,
|
||||
deleteFilesByDefault: true,
|
||||
);
|
||||
final Set<core.BuildDirectory> buildDirs = <core.BuildDirectory>{
|
||||
if (testOutputDir != null)
|
||||
core.BuildDirectory(
|
||||
'test',
|
||||
outputLocation: core.OutputLocation(
|
||||
testOutputDir,
|
||||
useSymlinks: !platform.isWindows,
|
||||
),
|
||||
),
|
||||
};
|
||||
final Status status =
|
||||
logger.startProgress('Compiling $target for the Web...', timeout: null);
|
||||
logger.startProgress('Compiling ${targets.first} for the Web...', timeout: null);
|
||||
try {
|
||||
_builder = await BuildImpl.create(
|
||||
buildOptions,
|
||||
@@ -172,12 +202,12 @@ class BuildRunnerWebCompilationProxy extends WebCompilationProxy {
|
||||
builders,
|
||||
<String, Map<String, dynamic>>{
|
||||
'flutter_tools|entrypoint': <String, dynamic>{
|
||||
'target': target,
|
||||
'targets': targets,
|
||||
}
|
||||
},
|
||||
isReleaseBuild: false,
|
||||
);
|
||||
await _builder.run(const <AssetId, ChangeType>{});
|
||||
await _builder.run(const <AssetId, ChangeType>{}, buildDirs: buildDirs);
|
||||
} finally {
|
||||
status.stop();
|
||||
}
|
||||
@@ -205,9 +235,9 @@ class BuildRunnerWebCompilationProxy extends WebCompilationProxy {
|
||||
|
||||
/// A ddc-only entrypoint builder that respects the Flutter target flag.
|
||||
class FlutterWebEntrypointBuilder implements Builder {
|
||||
const FlutterWebEntrypointBuilder(this.target);
|
||||
const FlutterWebEntrypointBuilder(this.targets);
|
||||
|
||||
final String target;
|
||||
final List<String> targets;
|
||||
|
||||
@override
|
||||
Map<String, List<String>> get buildExtensions => const <String, List<String>>{
|
||||
@@ -222,10 +252,123 @@ class FlutterWebEntrypointBuilder implements Builder {
|
||||
|
||||
@override
|
||||
Future<void> build(BuildStep buildStep) async {
|
||||
if (!buildStep.inputId.path.contains(target)) {
|
||||
bool matches = false;
|
||||
for (String target in targets) {
|
||||
if (buildStep.inputId.path.contains(target)) {
|
||||
matches = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!matches) {
|
||||
return;
|
||||
}
|
||||
log.info('building for target ${buildStep.inputId.path}');
|
||||
await bootstrapDdc(buildStep, platform: flutterWebPlatform);
|
||||
}
|
||||
}
|
||||
|
||||
class FlutterWebTestBootstrapBuilder implements Builder {
|
||||
const FlutterWebTestBootstrapBuilder();
|
||||
|
||||
@override
|
||||
Map<String, List<String>> get buildExtensions => const <String, List<String>>{
|
||||
'_test.dart': <String>[
|
||||
'_test.dart.browser_test.dart',
|
||||
]
|
||||
};
|
||||
|
||||
@override
|
||||
Future<void> build(BuildStep buildStep) async {
|
||||
final AssetId id = buildStep.inputId;
|
||||
final String contents = await buildStep.readAsString(id);
|
||||
final String assetPath = id.pathSegments.first == 'lib'
|
||||
? path.url.join('packages', id.package, id.path)
|
||||
: id.path;
|
||||
final Metadata metadata = parseMetadata(
|
||||
assetPath, contents, Runtime.builtIn.map((Runtime runtime) => runtime.name).toSet());
|
||||
|
||||
if (metadata.testOn.evaluate(SuitePlatform(Runtime.chrome))) {
|
||||
await buildStep.writeAsString(id.addExtension('.browser_test.dart'), '''
|
||||
import 'dart:ui' as ui;
|
||||
import 'dart:html';
|
||||
import 'dart:js';
|
||||
|
||||
import 'package:stream_channel/stream_channel.dart';
|
||||
import 'package:test_api/src/backend/stack_trace_formatter.dart'; // ignore: implementation_imports
|
||||
import 'package:test_api/src/util/stack_trace_mapper.dart'; // ignore: implementation_imports
|
||||
import 'package:test_api/src/remote_listener.dart'; // ignore: implementation_imports
|
||||
import 'package:test_api/src/suite_channel_manager.dart'; // ignore: implementation_imports
|
||||
|
||||
import "${path.url.basename(id.path)}" as test;
|
||||
|
||||
Future<void> main() async {
|
||||
// Extra initialization for flutter_web.
|
||||
// The following parameters are hard-coded in Flutter's test embedder. Since
|
||||
// we don't have an embedder yet this is the lowest-most layer we can put
|
||||
// this stuff in.
|
||||
await ui.webOnlyInitializeEngine();
|
||||
internalBootstrapBrowserTest(() => test.main);
|
||||
}
|
||||
|
||||
void internalBootstrapBrowserTest(Function getMain()) {
|
||||
var channel =
|
||||
serializeSuite(getMain, hidePrints: false, beforeLoad: () async {
|
||||
var serialized =
|
||||
await suiteChannel("test.browser.mapper").stream.first as Map;
|
||||
if (serialized == null) return;
|
||||
});
|
||||
postMessageChannel().pipe(channel);
|
||||
}
|
||||
StreamChannel serializeSuite(Function getMain(),
|
||||
{bool hidePrints = true, Future beforeLoad()}) =>
|
||||
RemoteListener.start(getMain,
|
||||
hidePrints: hidePrints, beforeLoad: beforeLoad);
|
||||
|
||||
StreamChannel suiteChannel(String name) {
|
||||
var manager = SuiteChannelManager.current;
|
||||
if (manager == null) {
|
||||
throw StateError('suiteChannel() may only be called within a test worker.');
|
||||
}
|
||||
|
||||
return manager.connectOut(name);
|
||||
}
|
||||
|
||||
StreamChannel postMessageChannel() {
|
||||
var controller = StreamChannelController(sync: true);
|
||||
window.onMessage.firstWhere((message) {
|
||||
return message.origin == window.location.origin && message.data == "port";
|
||||
}).then((message) {
|
||||
var port = message.ports.first;
|
||||
var portSubscription = port.onMessage.listen((message) {
|
||||
controller.local.sink.add(message.data);
|
||||
});
|
||||
|
||||
controller.local.stream.listen((data) {
|
||||
port.postMessage({"data": data});
|
||||
}, onDone: () {
|
||||
port.postMessage({"event": "done"});
|
||||
portSubscription.cancel();
|
||||
});
|
||||
});
|
||||
|
||||
context['parent'].callMethod('postMessage', [
|
||||
JsObject.jsify({"href": window.location.href, "ready": true}),
|
||||
window.location.origin,
|
||||
]);
|
||||
return controller.foreign;
|
||||
}
|
||||
|
||||
void setStackTraceMapper(StackTraceMapper mapper) {
|
||||
var formatter = StackTraceFormatter.current;
|
||||
if (formatter == null) {
|
||||
throw StateError(
|
||||
'setStackTraceMapper() may only be called within a test worker.');
|
||||
}
|
||||
|
||||
formatter.configure(mapper: mapper);
|
||||
}
|
||||
''');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -99,6 +99,11 @@ class TestCommand extends FastFlutterCommand {
|
||||
negatable: true,
|
||||
help: 'Whether to build the assets bundle for testing.\n'
|
||||
'Consider using --no-test-assets if assets are not required.',
|
||||
)
|
||||
..addOption('platform',
|
||||
allowed: const <String>['tester', 'chrome'],
|
||||
defaultsTo: 'tester',
|
||||
help: 'The platform to run the unit tests on. Defaults to "tester".'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -166,6 +171,16 @@ class TestCommand extends FastFlutterCommand {
|
||||
'Test files must be in that directory and end with the pattern "_test.dart".'
|
||||
);
|
||||
}
|
||||
} else {
|
||||
final List<String> fileCopy = <String>[];
|
||||
for (String file in files) {
|
||||
if (file.endsWith(platform.pathSeparator)) {
|
||||
fileCopy.addAll(_findTests(fs.directory(file)));
|
||||
} else {
|
||||
fileCopy.add(file);
|
||||
}
|
||||
}
|
||||
files = fileCopy;
|
||||
}
|
||||
|
||||
CoverageCollector collector;
|
||||
@@ -222,6 +237,7 @@ class TestCommand extends FastFlutterCommand {
|
||||
concurrency: jobs,
|
||||
buildTestAssets: buildTestAssets,
|
||||
flutterProject: flutterProject,
|
||||
web: argResults['platform'] == 'chrome',
|
||||
);
|
||||
|
||||
if (collector != null) {
|
||||
|
||||
@@ -120,7 +120,7 @@ class ResidentWebRunner extends ResidentRunner {
|
||||
// Start the web compiler and build the assets.
|
||||
await webCompilationProxy.initialize(
|
||||
projectDirectory: currentProject.directory,
|
||||
target: target,
|
||||
targets: <String>[target],
|
||||
);
|
||||
_lastCompiled = DateTime.now();
|
||||
final AssetBundle assetBundle = AssetBundleFactory.instance.createBundle();
|
||||
|
||||
681
packages/flutter_tools/lib/src/test/flutter_web_platform.dart
Normal file
681
packages/flutter_tools/lib/src/test/flutter_web_platform.dart
Normal file
@@ -0,0 +1,681 @@
|
||||
// Copyright 2019 The Chromium 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 'package:async/async.dart';
|
||||
import 'package:http_multi_server/http_multi_server.dart';
|
||||
import 'package:path/path.dart' as p; // ignore: package_path_import
|
||||
import 'package:pool/pool.dart';
|
||||
import 'package:shelf/shelf.dart' as shelf;
|
||||
import 'package:shelf/shelf_io.dart' as shelf_io;
|
||||
import 'package:shelf_packages_handler/shelf_packages_handler.dart';
|
||||
import 'package:shelf_static/shelf_static.dart';
|
||||
import 'package:shelf_web_socket/shelf_web_socket.dart';
|
||||
import 'package:stream_channel/stream_channel.dart';
|
||||
import 'package:test_api/backend.dart';
|
||||
import 'package:test_api/src/backend/runtime.dart';
|
||||
import 'package:test_api/src/backend/suite_platform.dart';
|
||||
import 'package:test_api/src/util/stack_trace_mapper.dart';
|
||||
import 'package:test_core/src/runner/configuration.dart';
|
||||
import 'package:test_core/src/runner/environment.dart';
|
||||
import 'package:test_core/src/runner/platform.dart';
|
||||
import 'package:test_core/src/runner/plugin/platform_helpers.dart';
|
||||
import 'package:test_core/src/runner/runner_suite.dart';
|
||||
import 'package:test_core/src/runner/suite.dart';
|
||||
import 'package:web_socket_channel/web_socket_channel.dart';
|
||||
|
||||
import '../artifacts.dart';
|
||||
import '../base/common.dart';
|
||||
import '../base/file_system.dart';
|
||||
import '../cache.dart';
|
||||
import '../convert.dart';
|
||||
import '../dart/package_map.dart';
|
||||
import '../globals.dart';
|
||||
import '../web/chrome.dart';
|
||||
|
||||
class FlutterWebPlatform extends PlatformPlugin {
|
||||
FlutterWebPlatform._(this._server, this._config, this._root) {
|
||||
// Look up the location of the testing resources.
|
||||
final Map<String, Uri> packageMap = PackageMap(fs.path.join(
|
||||
Cache.flutterRoot,
|
||||
'packages',
|
||||
'flutter_tools',
|
||||
'.packages',
|
||||
)).map;
|
||||
testUri = packageMap['test'];
|
||||
final shelf.Cascade cascade = shelf.Cascade()
|
||||
.add(_webSocketHandler.handler)
|
||||
.add(packagesDirHandler())
|
||||
.add(_jsHandler.handler)
|
||||
.add(createStaticHandler(
|
||||
fs.path.join(Cache.flutterRoot, 'packages', 'flutter_tools'),
|
||||
serveFilesOutsidePath: true,
|
||||
))
|
||||
.add(createStaticHandler(_config.suiteDefaults.precompiledPath,
|
||||
serveFilesOutsidePath: true))
|
||||
.add(_handleStaticArtifact)
|
||||
.add(_wrapperHandler);
|
||||
_server.mount(cascade.handler);
|
||||
}
|
||||
|
||||
static Future<FlutterWebPlatform> start(String root) async {
|
||||
final shelf_io.IOServer server =
|
||||
shelf_io.IOServer(await HttpMultiServer.loopback(0));
|
||||
return FlutterWebPlatform._(
|
||||
server,
|
||||
Configuration.current,
|
||||
root,
|
||||
);
|
||||
}
|
||||
|
||||
Uri testUri;
|
||||
|
||||
/// The test runner configuration.
|
||||
final Configuration _config;
|
||||
|
||||
/// The underlying server.
|
||||
final shelf.Server _server;
|
||||
|
||||
/// The URL for this server.
|
||||
Uri get url => _server.url;
|
||||
|
||||
/// The ahem text file.
|
||||
File get ahem => fs.file(fs.path.join(
|
||||
Cache.flutterRoot,
|
||||
'packages',
|
||||
'flutter_tools',
|
||||
'static',
|
||||
'Ahem.ttf',
|
||||
));
|
||||
|
||||
/// The require js binary.
|
||||
File get requireJs => fs.file(fs.path.join(
|
||||
artifacts.getArtifactPath(Artifact.engineDartSdkPath),
|
||||
'lib',
|
||||
'dev_compiler',
|
||||
'amd',
|
||||
'require.js',
|
||||
));
|
||||
|
||||
/// The ddc to dart stack trace mapper.
|
||||
File get stackTraceMapper => fs.file(fs.path.join(
|
||||
artifacts.getArtifactPath(Artifact.engineDartSdkPath),
|
||||
'lib',
|
||||
'dev_compiler',
|
||||
'web',
|
||||
'dart_stack_trace_mapper.js',
|
||||
));
|
||||
|
||||
/// The precompiled dart sdk.
|
||||
File get dartSdk => fs.file(fs.path.join(
|
||||
artifacts.getArtifactPath(Artifact.flutterWebSdk),
|
||||
'kernel',
|
||||
'amd',
|
||||
'dart_sdk.js',
|
||||
));
|
||||
|
||||
/// The precompiled test javascript.
|
||||
File get testDartJs => fs.file(fs.path.join(
|
||||
testUri.toFilePath(),
|
||||
'dart.js',
|
||||
));
|
||||
|
||||
File get testHostDartJs => fs.file(fs.path.join(
|
||||
testUri.toFilePath(),
|
||||
'src',
|
||||
'runner',
|
||||
'browser',
|
||||
'static',
|
||||
'host.dart.js',
|
||||
));
|
||||
|
||||
Future<shelf.Response> _handleStaticArtifact(shelf.Request request) async {
|
||||
if (request.requestedUri.path.contains('require.js')) {
|
||||
return shelf.Response.ok(
|
||||
requireJs.openRead(),
|
||||
headers: <String, String>{'Content-Type': 'text/javascript'},
|
||||
);
|
||||
} else if (request.requestedUri.path.contains('Ahem.ttf')) {
|
||||
return shelf.Response.ok(ahem.openRead());
|
||||
} else if (request.requestedUri.path.contains('dart_sdk.js')) {
|
||||
return shelf.Response.ok(
|
||||
dartSdk.openRead(),
|
||||
headers: <String, String>{'Content-Type': 'text/javascript'},
|
||||
);
|
||||
} else if (request.requestedUri.path
|
||||
.contains('stack_trace_mapper.dart.js')) {
|
||||
return shelf.Response.ok(
|
||||
stackTraceMapper.openRead(),
|
||||
headers: <String, String>{'Content-Type': 'text/javascript'},
|
||||
);
|
||||
} else if (request.requestedUri.path.contains('static/dart.js')) {
|
||||
return shelf.Response.ok(
|
||||
testDartJs.openRead(),
|
||||
headers: <String, String>{'Content-Type': 'text/javascript'},
|
||||
);
|
||||
} else if (request.requestedUri.path.contains('host.dart.js')) {
|
||||
return shelf.Response.ok(
|
||||
testHostDartJs.openRead(),
|
||||
headers: <String, String>{'Content-Type': 'text/javascript'},
|
||||
);
|
||||
} else {
|
||||
return shelf.Response.notFound('Not Found');
|
||||
}
|
||||
}
|
||||
|
||||
final OneOffHandler _webSocketHandler = OneOffHandler();
|
||||
final PathHandler _jsHandler = PathHandler();
|
||||
final AsyncMemoizer<void> _closeMemo = AsyncMemoizer<void>();
|
||||
final String _root;
|
||||
|
||||
bool get _closed => _closeMemo.hasRun;
|
||||
|
||||
// A map from browser identifiers to futures that will complete to the
|
||||
// [BrowserManager]s for those browsers, or `null` if they failed to load.
|
||||
final Map<Runtime, Future<BrowserManager>> _browserManagers =
|
||||
<Runtime, Future<BrowserManager>>{};
|
||||
|
||||
// Mappers for Dartifying stack traces, indexed by test path.
|
||||
final Map<String, StackTraceMapper> _mappers = <String, StackTraceMapper>{};
|
||||
|
||||
// A handler that serves wrapper files used to bootstrap tests.
|
||||
shelf.Response _wrapperHandler(shelf.Request request) {
|
||||
final String path = fs.path.fromUri(request.url);
|
||||
if (path.endsWith('.html')) {
|
||||
final String test = fs.path.withoutExtension(path) + '.dart';
|
||||
final String scriptBase = htmlEscape.convert(fs.path.basename(test));
|
||||
final String link = '<link rel="x-dart-test" href="$scriptBase">';
|
||||
return shelf.Response.ok('''
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>${htmlEscape.convert(test)} Test</title>
|
||||
$link
|
||||
<script src="static/dart.js"></script>
|
||||
</head>
|
||||
</html>
|
||||
''', headers: <String, String>{'Content-Type': 'text/html'});
|
||||
}
|
||||
printTrace('Did not find anything for request: ${request.url}');
|
||||
return shelf.Response.notFound('Not found.');
|
||||
}
|
||||
|
||||
@override
|
||||
Future<RunnerSuite> load(String path, SuitePlatform platform,
|
||||
SuiteConfiguration suiteConfig, Object message) async {
|
||||
if (_closed) {
|
||||
return null;
|
||||
}
|
||||
final Runtime browser = platform.runtime;
|
||||
final BrowserManager browserManager = await _browserManagerFor(browser);
|
||||
if (_closed || browserManager == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final Uri suiteUrl = url.resolveUri(fs.path.toUri(fs.path.withoutExtension(
|
||||
fs.path.relative(path, from: fs.path.join(_root, 'test'))) +
|
||||
'.html'));
|
||||
final RunnerSuite suite = await browserManager
|
||||
.load(path, suiteUrl, suiteConfig, message, mapper: _mappers[path]);
|
||||
if (_closed) {
|
||||
return null;
|
||||
}
|
||||
return suite;
|
||||
}
|
||||
|
||||
@override
|
||||
StreamChannel<dynamic> loadChannel(String path, SuitePlatform platform) =>
|
||||
throw UnimplementedError();
|
||||
|
||||
/// Returns the [BrowserManager] for [runtime], which should be a browser.
|
||||
///
|
||||
/// If no browser manager is running yet, starts one.
|
||||
Future<BrowserManager> _browserManagerFor(Runtime browser) {
|
||||
final Future<BrowserManager> managerFuture = _browserManagers[browser];
|
||||
if (managerFuture != null) {
|
||||
return managerFuture;
|
||||
}
|
||||
final Completer<WebSocketChannel> completer =
|
||||
Completer<WebSocketChannel>.sync();
|
||||
final String path =
|
||||
_webSocketHandler.create(webSocketHandler(completer.complete));
|
||||
final Uri webSocketUrl = url.replace(scheme: 'ws').resolve(path);
|
||||
final Uri hostUrl = url
|
||||
.resolve('static/index.html')
|
||||
.replace(queryParameters: <String, String>{
|
||||
'managerUrl': webSocketUrl.toString(),
|
||||
'debug': _config.pauseAfterLoad.toString()
|
||||
});
|
||||
|
||||
printTrace('Serving tests at $hostUrl');
|
||||
|
||||
final Future<BrowserManager> future = BrowserManager.start(
|
||||
browser,
|
||||
hostUrl,
|
||||
completer.future,
|
||||
);
|
||||
|
||||
// Store null values for browsers that error out so we know not to load them
|
||||
// again.
|
||||
_browserManagers[browser] = future.catchError((dynamic _) => null);
|
||||
|
||||
return future;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> closeEphemeral() {
|
||||
final List<Future<BrowserManager>> managers =
|
||||
_browserManagers.values.toList();
|
||||
_browserManagers.clear();
|
||||
return Future.wait(managers.map((Future<BrowserManager> manager) async {
|
||||
final BrowserManager result = await manager;
|
||||
if (result == null) {
|
||||
return;
|
||||
}
|
||||
await result.close();
|
||||
}));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() => _closeMemo.runOnce(() async {
|
||||
final List<Future<dynamic>> futures = _browserManagers.values
|
||||
.map<Future<dynamic>>((Future<BrowserManager> future) async {
|
||||
final BrowserManager result = await future;
|
||||
if (result == null) {
|
||||
return;
|
||||
}
|
||||
await result.close();
|
||||
}).toList();
|
||||
futures.add(_server.close());
|
||||
await Future.wait<void>(futures);
|
||||
});
|
||||
}
|
||||
|
||||
class OneOffHandler {
|
||||
/// A map from URL paths to handlers.
|
||||
final Map<String, shelf.Handler> _handlers = <String, shelf.Handler>{};
|
||||
|
||||
/// The counter of handlers that have been activated.
|
||||
int _counter = 0;
|
||||
|
||||
/// The actual [shelf.Handler] that dispatches requests.
|
||||
shelf.Handler get handler => _onRequest;
|
||||
|
||||
/// Creates a new one-off handler that forwards to [handler].
|
||||
///
|
||||
/// Returns a string that's the URL path for hitting this handler, relative to
|
||||
/// the URL for the one-off handler itself.
|
||||
///
|
||||
/// [handler] will be unmounted as soon as it receives a request.
|
||||
String create(shelf.Handler handler) {
|
||||
final String path = _counter.toString();
|
||||
_handlers[path] = handler;
|
||||
_counter++;
|
||||
return path;
|
||||
}
|
||||
|
||||
/// Dispatches [request] to the appropriate handler.
|
||||
FutureOr<shelf.Response> _onRequest(shelf.Request request) {
|
||||
final List<String> components = p.url.split(request.url.path);
|
||||
if (components.isEmpty) {
|
||||
return shelf.Response.notFound(null);
|
||||
}
|
||||
final String path = components.removeAt(0);
|
||||
final FutureOr<shelf.Response> Function(shelf.Request) handler =
|
||||
_handlers.remove(path);
|
||||
if (handler == null) {
|
||||
return shelf.Response.notFound(null);
|
||||
}
|
||||
return handler(request.change(path: path));
|
||||
}
|
||||
}
|
||||
|
||||
class PathHandler {
|
||||
/// A trie of path components to handlers.
|
||||
final _Node _paths = _Node();
|
||||
|
||||
/// The shelf handler.
|
||||
shelf.Handler get handler => _onRequest;
|
||||
|
||||
/// Returns middleware that nests all requests beneath the URL prefix
|
||||
/// [beneath].
|
||||
static shelf.Middleware nestedIn(String beneath) {
|
||||
return (FutureOr<shelf.Response> Function(shelf.Request) handler) {
|
||||
final PathHandler pathHandler = PathHandler()..add(beneath, handler);
|
||||
return pathHandler.handler;
|
||||
};
|
||||
}
|
||||
|
||||
/// Routes requests at or under [path] to [handler].
|
||||
///
|
||||
/// If [path] is a parent or child directory of another path in this handler,
|
||||
/// the longest matching prefix wins.
|
||||
void add(String path, shelf.Handler handler) {
|
||||
_Node node = _paths;
|
||||
for (String component in p.url.split(path)) {
|
||||
node = node.children.putIfAbsent(component, () => _Node());
|
||||
}
|
||||
node.handler = handler;
|
||||
}
|
||||
|
||||
FutureOr<shelf.Response> _onRequest(shelf.Request request) {
|
||||
shelf.Handler handler;
|
||||
int handlerIndex;
|
||||
_Node node = _paths;
|
||||
final List<String> components = p.url.split(request.url.path);
|
||||
for (int i = 0; i < components.length; i++) {
|
||||
node = node.children[components[i]];
|
||||
if (node == null) {
|
||||
break;
|
||||
}
|
||||
if (node.handler == null) {
|
||||
continue;
|
||||
}
|
||||
handler = node.handler;
|
||||
handlerIndex = i;
|
||||
}
|
||||
|
||||
if (handler == null) {
|
||||
return shelf.Response.notFound('Not found.');
|
||||
}
|
||||
|
||||
return handler(
|
||||
request.change(path: p.url.joinAll(components.take(handlerIndex + 1))));
|
||||
}
|
||||
}
|
||||
|
||||
/// A trie node.
|
||||
class _Node {
|
||||
shelf.Handler handler;
|
||||
final Map<String, _Node> children = <String, _Node>{};
|
||||
}
|
||||
|
||||
class BrowserManager {
|
||||
/// Creates a new BrowserManager that communicates with [browser] over
|
||||
/// [webSocket].
|
||||
BrowserManager._(this._browser, this._runtime, WebSocketChannel webSocket) {
|
||||
// The duration should be short enough that the debugging console is open as
|
||||
// soon as the user is done setting breakpoints, but long enough that a test
|
||||
// doing a lot of synchronous work doesn't trigger a false positive.
|
||||
//
|
||||
// Start this canceled because we don't want it to start ticking until we
|
||||
// get some response from the iframe.
|
||||
_timer = RestartableTimer(const Duration(seconds: 3), () {
|
||||
for (RunnerSuiteController controller in _controllers) {
|
||||
controller.setDebugging(true);
|
||||
}
|
||||
})
|
||||
..cancel();
|
||||
|
||||
// Whenever we get a message, no matter which child channel it's for, we the
|
||||
// know browser is still running code which means the user isn't debugging.
|
||||
_channel = MultiChannel<dynamic>(
|
||||
webSocket.cast<String>().transform(jsonDocument).changeStream((Stream<Object> stream) {
|
||||
return stream.map((Object message) {
|
||||
if (!_closed) {
|
||||
_timer.reset();
|
||||
}
|
||||
for (RunnerSuiteController controller in _controllers) {
|
||||
controller.setDebugging(false);
|
||||
}
|
||||
|
||||
return message;
|
||||
});
|
||||
}));
|
||||
|
||||
_environment = _loadBrowserEnvironment();
|
||||
_channel.stream.listen(_onMessage, onDone: close);
|
||||
}
|
||||
|
||||
/// The browser instance that this is connected to via [_channel].
|
||||
final Chrome _browser;
|
||||
|
||||
// TODO(nweiz): Consider removing the duplication between this and
|
||||
// [_browser.name].
|
||||
/// The [Runtime] for [_browser].
|
||||
final Runtime _runtime;
|
||||
|
||||
/// The channel used to communicate with the browser.
|
||||
///
|
||||
/// This is connected to a page running `static/host.dart`.
|
||||
MultiChannel<dynamic> _channel;
|
||||
|
||||
/// A pool that ensures that limits the number of initial connections the
|
||||
/// manager will wait for at once.
|
||||
///
|
||||
/// This isn't the *total* number of connections; any number of iframes may be
|
||||
/// loaded in the same browser. However, the browser can only load so many at
|
||||
/// once, and we want a timeout in case they fail so we only wait for so many
|
||||
/// at once.
|
||||
final Pool _pool = Pool(8);
|
||||
|
||||
/// The ID of the next suite to be loaded.
|
||||
///
|
||||
/// This is used to ensure that the suites can be referred to consistently
|
||||
/// across the client and server.
|
||||
int _suiteID = 0;
|
||||
|
||||
/// Whether the channel to the browser has closed.
|
||||
bool _closed = false;
|
||||
|
||||
/// The completer for [_BrowserEnvironment.displayPause].
|
||||
///
|
||||
/// This will be `null` as long as the browser isn't displaying a pause
|
||||
/// screen.
|
||||
CancelableCompleter<dynamic> _pauseCompleter;
|
||||
|
||||
/// The controller for [_BrowserEnvironment.onRestart].
|
||||
final StreamController<dynamic> _onRestartController =
|
||||
StreamController<dynamic>.broadcast();
|
||||
|
||||
/// The environment to attach to each suite.
|
||||
Future<_BrowserEnvironment> _environment;
|
||||
|
||||
/// Controllers for every suite in this browser.
|
||||
///
|
||||
/// These are used to mark suites as debugging or not based on the browser's
|
||||
/// pings.
|
||||
final Set<RunnerSuiteController> _controllers = <RunnerSuiteController>{};
|
||||
|
||||
// A timer that's reset whenever we receive a message from the browser.
|
||||
//
|
||||
// Because the browser stops running code when the user is actively debugging,
|
||||
// this lets us detect whether they're debugging reasonably accurately.
|
||||
RestartableTimer _timer;
|
||||
|
||||
final AsyncMemoizer<dynamic> _closeMemoizer = AsyncMemoizer<dynamic>();
|
||||
|
||||
/// Starts the browser identified by [runtime] and has it connect to [url].
|
||||
///
|
||||
/// [url] should serve a page that establishes a WebSocket connection with
|
||||
/// this process. That connection, once established, should be emitted via
|
||||
/// [future]. If [debug] is true, starts the browser in debug mode, with its
|
||||
/// debugger interfaces on and detected.
|
||||
///
|
||||
/// The [settings] indicate how to invoke this browser's executable.
|
||||
///
|
||||
/// Returns the browser manager, or throws an [ApplicationException] if a
|
||||
/// connection fails to be established.
|
||||
static Future<BrowserManager> start(
|
||||
Runtime runtime, Uri url, Future<WebSocketChannel> future,
|
||||
{bool debug = false}) async {
|
||||
final Chrome chrome =
|
||||
await chromeLauncher.launch(url.toString(), headless: true);
|
||||
|
||||
final Completer<BrowserManager> completer = Completer<BrowserManager>();
|
||||
|
||||
unawaited(chrome.onExit.then((void _) {
|
||||
throwToolExit('${runtime.name} exited before connecting.');
|
||||
}).catchError((dynamic error, StackTrace stackTrace) {
|
||||
if (completer.isCompleted) {
|
||||
return;
|
||||
}
|
||||
completer.completeError(error, stackTrace);
|
||||
}));
|
||||
unawaited(future.then((WebSocketChannel webSocket) {
|
||||
if (completer.isCompleted) {
|
||||
return;
|
||||
}
|
||||
completer.complete(BrowserManager._(chrome, runtime, webSocket));
|
||||
}).catchError((dynamic error, StackTrace stackTrace) {
|
||||
chrome.close();
|
||||
if (completer.isCompleted) {
|
||||
return;
|
||||
}
|
||||
completer.completeError(error, stackTrace);
|
||||
}));
|
||||
|
||||
return completer.future.timeout(const Duration(seconds: 30), onTimeout: () {
|
||||
chrome.close();
|
||||
throwToolExit('Timed out waiting for ${runtime.name} to connect.');
|
||||
return;
|
||||
});
|
||||
}
|
||||
|
||||
/// Loads [_BrowserEnvironment].
|
||||
Future<_BrowserEnvironment> _loadBrowserEnvironment() async {
|
||||
return _BrowserEnvironment(
|
||||
this, null, _browser.remoteDebuggerUri, _onRestartController.stream);
|
||||
}
|
||||
|
||||
/// Tells the browser the load a test suite from the URL [url].
|
||||
///
|
||||
/// [url] should be an HTML page with a reference to the JS-compiled test
|
||||
/// suite. [path] is the path of the original test suite file, which is used
|
||||
/// for reporting. [suiteConfig] is the configuration for the test suite.
|
||||
///
|
||||
/// If [mapper] is passed, it's used to map stack traces for errors coming
|
||||
/// from this test suite.
|
||||
Future<RunnerSuite> load(
|
||||
String path, Uri url, SuiteConfiguration suiteConfig, Object message,
|
||||
{StackTraceMapper mapper}) async {
|
||||
url = url.replace(
|
||||
fragment: Uri.encodeFull(jsonEncode(<String, Object>{
|
||||
'metadata': suiteConfig.metadata.serialize(),
|
||||
'browser': _runtime.identifier
|
||||
})));
|
||||
|
||||
final int suiteID = _suiteID++;
|
||||
RunnerSuiteController controller;
|
||||
void closeIframe() {
|
||||
if (_closed) {
|
||||
return;
|
||||
}
|
||||
_controllers.remove(controller);
|
||||
_channel.sink
|
||||
.add(<String, Object>{'command': 'closeSuite', 'id': suiteID});
|
||||
}
|
||||
|
||||
// The virtual channel will be closed when the suite is closed, in which
|
||||
// case we should unload the iframe.
|
||||
final VirtualChannel<dynamic> virtualChannel = _channel.virtualChannel();
|
||||
final int suiteChannelID = virtualChannel.id;
|
||||
final StreamChannel<dynamic> suiteChannel = virtualChannel.transformStream(
|
||||
StreamTransformer<dynamic, dynamic>.fromHandlers(handleDone: (EventSink<dynamic> sink) {
|
||||
closeIframe();
|
||||
sink.close();
|
||||
}));
|
||||
|
||||
return await _pool.withResource<RunnerSuite>(() async {
|
||||
_channel.sink.add(<String, Object>{
|
||||
'command': 'loadSuite',
|
||||
'url': url.toString(),
|
||||
'id': suiteID,
|
||||
'channel': suiteChannelID
|
||||
});
|
||||
|
||||
try {
|
||||
controller = deserializeSuite(path, SuitePlatform(Runtime.chrome),
|
||||
suiteConfig, await _environment, suiteChannel, message);
|
||||
controller.channel('test.browser.mapper').sink.add(mapper?.serialize());
|
||||
|
||||
_controllers.add(controller);
|
||||
return await controller.suite;
|
||||
} catch (_) {
|
||||
closeIframe();
|
||||
rethrow;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// An implementation of [Environment.displayPause].
|
||||
CancelableOperation<dynamic> _displayPause() {
|
||||
if (_pauseCompleter != null) {
|
||||
return _pauseCompleter.operation;
|
||||
}
|
||||
_pauseCompleter = CancelableCompleter<dynamic>(onCancel: () {
|
||||
_channel.sink.add(<String, String>{'command': 'resume'});
|
||||
_pauseCompleter = null;
|
||||
});
|
||||
_pauseCompleter.operation.value.whenComplete(() {
|
||||
_pauseCompleter = null;
|
||||
});
|
||||
_channel.sink.add(<String, String>{'command': 'displayPause'});
|
||||
|
||||
return _pauseCompleter.operation;
|
||||
}
|
||||
|
||||
/// The callback for handling messages received from the host page.
|
||||
void _onMessage(dynamic message) {
|
||||
switch (message['command'] as String) {
|
||||
case 'ping':
|
||||
break;
|
||||
case 'restart':
|
||||
_onRestartController.add(null);
|
||||
break;
|
||||
case 'resume':
|
||||
if (_pauseCompleter != null) {
|
||||
_pauseCompleter.complete();
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// Unreachable.
|
||||
assert(false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// Closes the manager and releases any resources it owns, including closing
|
||||
/// the browser.
|
||||
Future<dynamic> close() {
|
||||
return _closeMemoizer.runOnce(() {
|
||||
_closed = true;
|
||||
_timer.cancel();
|
||||
if (_pauseCompleter != null) {
|
||||
_pauseCompleter.complete();
|
||||
}
|
||||
_pauseCompleter = null;
|
||||
_controllers.clear();
|
||||
return _browser.close();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// An implementation of [Environment] for the browser.
|
||||
///
|
||||
/// All methods forward directly to [BrowserManager].
|
||||
class _BrowserEnvironment implements Environment {
|
||||
_BrowserEnvironment(this._manager, this.observatoryUrl,
|
||||
this.remoteDebuggerUrl, this.onRestart);
|
||||
|
||||
final BrowserManager _manager;
|
||||
|
||||
@override
|
||||
final bool supportsDebugging = true;
|
||||
|
||||
@override
|
||||
final Uri observatoryUrl;
|
||||
|
||||
@override
|
||||
final Uri remoteDebuggerUrl;
|
||||
|
||||
@override
|
||||
final Stream<dynamic> onRestart;
|
||||
|
||||
@override
|
||||
CancelableOperation<dynamic> displayPause() => _manager._displayPause();
|
||||
}
|
||||
@@ -5,7 +5,9 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:test_api/backend.dart';
|
||||
import 'package:test_core/src/executable.dart' as test; // ignore: implementation_imports
|
||||
import 'package:test_core/src/runner/hack_register_platform.dart' as hack; // ignore: implementation_imports
|
||||
|
||||
import '../artifacts.dart';
|
||||
import '../base/common.dart';
|
||||
@@ -16,7 +18,9 @@ import '../base/terminal.dart';
|
||||
import '../dart/package_map.dart';
|
||||
import '../globals.dart';
|
||||
import '../project.dart';
|
||||
import '../web/compile.dart';
|
||||
import 'flutter_platform.dart' as loader;
|
||||
import 'flutter_web_platform.dart';
|
||||
import 'watcher.dart';
|
||||
|
||||
/// Runs tests using package:test and the Flutter engine.
|
||||
@@ -40,6 +44,7 @@ Future<int> runTests(
|
||||
FlutterProject flutterProject,
|
||||
String icudtlPath,
|
||||
Directory coverageDirectory,
|
||||
bool web = false,
|
||||
}) async {
|
||||
// Compute the command-line arguments for package:test.
|
||||
final List<String> testArgs = <String>[];
|
||||
@@ -62,6 +67,32 @@ Future<int> runTests(
|
||||
for (String plainName in plainNames) {
|
||||
testArgs..add('--plain-name')..add(plainName);
|
||||
}
|
||||
if (web) {
|
||||
final String tempBuildDir = fs.systemTempDirectory
|
||||
.createTempSync('_flutter_test')
|
||||
.absolute
|
||||
.uri
|
||||
.toFilePath();
|
||||
await webCompilationProxy.initialize(
|
||||
projectDirectory: flutterProject.directory,
|
||||
testOutputDir: tempBuildDir,
|
||||
targets: testFiles.map((String testFile) {
|
||||
return fs.path.relative(testFile, from: flutterProject.directory.path);
|
||||
}).toList(),
|
||||
);
|
||||
testArgs.add('--platform=chrome');
|
||||
testArgs.add('--precompiled=$tempBuildDir');
|
||||
testArgs.add('--');
|
||||
testArgs.addAll(testFiles);
|
||||
hack.registerPlatformPlugin(
|
||||
<Runtime>[Runtime.chrome],
|
||||
() {
|
||||
return FlutterWebPlatform.start(flutterProject.directory.path);
|
||||
}
|
||||
);
|
||||
await test.main(testArgs);
|
||||
return exitCode;
|
||||
}
|
||||
|
||||
testArgs.add('--');
|
||||
testArgs.addAll(testFiles);
|
||||
|
||||
@@ -73,7 +73,10 @@ class ChromeLauncher {
|
||||
static final Completer<Chrome> _currentCompleter = Completer<Chrome>();
|
||||
|
||||
/// Launch the chrome browser to a particular `host` page.
|
||||
Future<Chrome> launch(String url) async {
|
||||
///
|
||||
/// `headless` defaults to false, and controls whether we open a headless or
|
||||
/// a `headfull` browser.
|
||||
Future<Chrome> launch(String url, { bool headless = false }) async {
|
||||
final String chromeExecutable = findChromeExecutable();
|
||||
final Directory dataDir = fs.systemTempDirectory.createTempSync();
|
||||
final int port = await os.findFreePort();
|
||||
@@ -94,6 +97,8 @@ class ChromeLauncher {
|
||||
'--no-default-browser-check',
|
||||
'--disable-default-apps',
|
||||
'--disable-translate',
|
||||
if (headless)
|
||||
...<String>['--headless', '--disable-gpu'],
|
||||
url,
|
||||
];
|
||||
final Process process = await processManager.start(args);
|
||||
@@ -107,12 +112,14 @@ class ChromeLauncher {
|
||||
throwToolExit('Unable to connect to Chrome DevTools.');
|
||||
return null;
|
||||
});
|
||||
final Uri remoteDebuggerUri = await _getRemoteDebuggerUrl(Uri.parse('http://localhost:$port'));
|
||||
|
||||
return _connect(Chrome._(
|
||||
port,
|
||||
ChromeConnection('localhost', port),
|
||||
process: process,
|
||||
dataDir: dataDir,
|
||||
remoteDebuggerUri: remoteDebuggerUri,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -138,15 +145,36 @@ class ChromeLauncher {
|
||||
_connect(Chrome._(port, ChromeConnection('localhost', port)));
|
||||
|
||||
static Future<Chrome> get connectedInstance => _currentCompleter.future;
|
||||
|
||||
/// Returns the full URL of the Chrome remote debugger for the main page.
|
||||
///
|
||||
/// This takes the [base] remote debugger URL (which points to a browser-wide
|
||||
/// page) and uses its JSON API to find the resolved URL for debugging the host
|
||||
/// page.
|
||||
Future<Uri> _getRemoteDebuggerUrl(Uri base) async {
|
||||
try {
|
||||
final HttpClient client = HttpClient();
|
||||
final HttpClientRequest request = await client.getUrl(base.resolve('/json/list'));
|
||||
final HttpClientResponse response = await request.close();
|
||||
final List<dynamic> jsonObject = await json.fuse(utf8).decoder.bind(response).single;
|
||||
return base.resolve(jsonObject.first['devtoolsFrontendUrl']);
|
||||
} catch (_) {
|
||||
// If we fail to talk to the remote debugger protocol, give up and return
|
||||
// the raw URL rather than crashing.
|
||||
return base;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// A class for managing an instance of Chrome.
|
||||
class Chrome {
|
||||
const Chrome._(
|
||||
Chrome._(
|
||||
this.debugPort,
|
||||
this.chromeConnection, {
|
||||
Process process,
|
||||
Directory dataDir,
|
||||
this.remoteDebuggerUri,
|
||||
}) : _process = process,
|
||||
_dataDir = dataDir;
|
||||
|
||||
@@ -154,15 +182,18 @@ class Chrome {
|
||||
final Process _process;
|
||||
final Directory _dataDir;
|
||||
final ChromeConnection chromeConnection;
|
||||
final Uri remoteDebuggerUri;
|
||||
|
||||
static Completer<Chrome> _currentCompleter = Completer<Chrome>();
|
||||
|
||||
Future<void> get onExit => _currentCompleter.future;
|
||||
|
||||
Future<void> close() async {
|
||||
if (_currentCompleter.isCompleted) {
|
||||
_currentCompleter = Completer<Chrome>();
|
||||
}
|
||||
chromeConnection.close();
|
||||
_process?.kill(ProcessSignal.SIGKILL);
|
||||
_process?.kill();
|
||||
await _process?.exitCode;
|
||||
try {
|
||||
// Chrome starts another process as soon as it dies that modifies the
|
||||
|
||||
@@ -91,7 +91,8 @@ class WebCompilationProxy {
|
||||
/// `projectDirectory`.
|
||||
Future<void> initialize({
|
||||
@required Directory projectDirectory,
|
||||
@required String target,
|
||||
@required List<String> targets,
|
||||
String testOutputDir,
|
||||
}) async {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
23
packages/flutter_tools/static/index.html
Normal file
23
packages/flutter_tools/static/index.html
Normal file
@@ -0,0 +1,23 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>test Browser Host</title>
|
||||
</head>
|
||||
<body>
|
||||
<svg id="dart" version="1.1" x="0px" y="0px" width="400px" height="400px" viewBox="0 0 400 400">
|
||||
<path id="right-flank" fill="#0083C9" d="M249.379,226.486l-6.676,15.572L166.174,166h58.82c0,0,2.807-0.409,3.645,1.966L249.379,226.486z"/>
|
||||
<path id="right-ear" fill="#00D2B8" d="M201.84,141.906L166.174,166h58.82c0,0,2.168-0.25,2.645,0.566l-2.694-8.848l-15.024-14.68C207.555,140.329,203.578,140.744,201.84,141.906z"/>
|
||||
<path id="left-flank" fill="#00D2B8" d="M242.616,241.856l-15.022,6.799l-60.493-21.429c-1.035-0.395-1.101-3.696-1.101-3.696v-57.932L242.616,241.856z"/>
|
||||
<path id="left-paw" fill="#55DECA" d="M167.003,227.098l60.636,21.558l15.064-6.799L237.224,259h-43.856c0,0-14.077-13.929-18.141-17.993C171.162,236.943,169.162,233.989,167.003,227.098z"/>
|
||||
<path id="right-paw" fill="#00A4E4" d="M227.676,166.365c0.963,1.401,1.361,2.473,1.361,2.473l20.352,57.648l-6.711,15.37L259,236.463v-44.854c0,0-13.678-13.965-17.741-17.882C237.193,169.811,231.466,166.319,227.676,166.365z"/>
|
||||
<path id="left-ear" fill="#0083C9" d="M166.769,227.098c0,0-0.769-1.104-0.769-4.355v-57.144l-23.115,34.877c-1.626,1.774-1.567,6.538,1.595,9.755l13.636,13.892L166.769,227.098z"/>
|
||||
</svg>
|
||||
<div id="dark"></div>
|
||||
<svg id="play" version="1.1" x="0px" y="0px" width="80px" height="80px" viewBox="0 0 25 25">
|
||||
<defs><filter id="blur"><feGaussianBlur stdDeviation="0.3" id="feGaussianBlur5097" /></filter></defs>
|
||||
<path d="M 3.777014,1.3715789 A 1.1838119,1.1838119 0 0 0 2.693923,2.5488509 V 22.444746 a 1.1838119,1.1838119 0 0 0 1.765908,1.035999 l 17.235259,-9.95972 a 1.1838119,1.1838119 0 0 0 0,-2.071998 L 4.459831,1.5128519 A 1.1838119,1.1838119 0 0 0 3.777014,1.3715789 z" style="opacity:0.5;stroke:#000000;stroke-width:1;filter:url(#blur)" />
|
||||
<path style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.32722104" d="M 3.4770491,1.0714664 A 1.1838119,1.1838119 0 0 0 2.3939589,2.2487382 V 22.144633 a 1.1838119,1.1838119 0 0 0 1.7659079,1.035999 l 17.2352602,-9.95972 a 1.1838119,1.1838119 0 0 0 0,-2.071998 L 4.1598668,1.2127389 A 1.1838119,1.1838119 0 0 0 3.4770491,1.0714664 z" />
|
||||
</svg>
|
||||
<script src="host.dart.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user