// 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. import 'package:package_config/package_config.dart'; /// The JavaScript bootstrap script to support in-browser hot restart. /// /// The [requireUrl] loads our cached RequireJS script file. The [mapperUrl] /// loads the special Dart stack trace mapper. The [entrypoint] is the /// actual main.dart file. /// /// This file is served when the browser requests "main.dart.js" in debug mode, /// and is responsible for bootstrapping the RequireJS modules and attaching /// the hot reload hooks. String generateBootstrapScript({ required String requireUrl, required String mapperUrl, }) { return ''' "use strict"; var styles = ` .flutter-loader { width: 100%; height: 8px; background-color: #13B9FD; position: absolute; top: 0px; left: 0px; } .indeterminate { position: relative; width: 100%; height: 100%; } .indeterminate:before { content: ''; position: absolute; height: 100%; background-color: #0175C2; animation: indeterminate_first 2.0s infinite ease-out; } .indeterminate:after { content: ''; position: absolute; height: 100%; background-color: #02569B; animation: indeterminate_second 2.0s infinite ease-in; } @keyframes indeterminate_first { 0% { left: -100%; width: 100%; } 100% { left: 100%; width: 10%; } } @keyframes indeterminate_second { 0% { left: -150%; width: 100%; } 100% { left: 100%; width: 10%; } } `; var styleSheet = document.createElement("style") styleSheet.type = "text/css"; styleSheet.innerText = styles; document.head.appendChild(styleSheet); var loader = document.createElement('div'); loader.className = "flutter-loader"; document.body.append(loader); var indeterminate = document.createElement('div'); indeterminate.className = "indeterminate"; loader.appendChild(indeterminate); document.addEventListener('dart-app-ready', function (e) { loader.parentNode.removeChild(loader); styleSheet.parentNode.removeChild(styleSheet); }); // Attach source mapping. var mapperEl = document.createElement("script"); mapperEl.defer = true; mapperEl.async = false; mapperEl.src = "$mapperUrl"; document.head.appendChild(mapperEl); // Attach require JS. var requireEl = document.createElement("script"); requireEl.defer = true; requireEl.async = false; requireEl.src = "$requireUrl"; // This attribute tells require JS what to load as main (defined below). requireEl.setAttribute("data-main", "main_module.bootstrap"); document.head.appendChild(requireEl); '''; } /// Generate a synthetic main module which captures the application's main /// method. /// /// If a [bootstrapModule] name is not provided, defaults to 'main_module.bootstrap'. /// /// RE: Object.keys usage in app.main: /// This attaches the main entrypoint and hot reload functionality to the window. /// The app module will have a single property which contains the actual application /// code. The property name is based off of the entrypoint that is generated, for example /// the file `foo/bar/baz.dart` will generate a property named approximately /// `foo__bar__baz`. Rather than attempt to guess, we assume the first property of /// this object is the module. String generateMainModule({ required String entrypoint, required bool nullAssertions, required bool nativeNullAssertions, String bootstrapModule = 'main_module.bootstrap', }) { return ''' /* ENTRYPOINT_EXTENTION_MARKER */ // Create the main module loaded below. define("$bootstrapModule", ["$entrypoint", "dart_sdk"], function(app, dart_sdk) { dart_sdk.dart.setStartAsyncSynchronously(true); dart_sdk._debugger.registerDevtoolsFormatter(); dart_sdk.dart.nonNullAsserts($nullAssertions); dart_sdk.dart.nativeNonNullAsserts($nativeNullAssertions); // See the generateMainModule doc comment. var child = {}; child.main = app[Object.keys(app)[0]].main; /* MAIN_EXTENSION_MARKER */ child.main(); window.\$dartLoader = {}; window.\$dartLoader.rootDirectories = []; if (window.\$requireLoader) { window.\$requireLoader.getModuleLibraries = dart_sdk.dart.getModuleLibraries; } if (window.\$dartStackTraceUtility && !window.\$dartStackTraceUtility.ready) { window.\$dartStackTraceUtility.ready = true; let dart = dart_sdk.dart; window.\$dartStackTraceUtility.setSourceMapProvider(function(url) { var baseUrl = window.location.protocol + '//' + window.location.host; url = url.replace(baseUrl + '/', ''); if (url == 'dart_sdk.js') { return dart.getSourceMap('dart_sdk'); } url = url.replace(".lib.js", ""); return dart.getSourceMap(url); }); } }); '''; } /// Generates the bootstrap logic required for a flutter test running in a browser. /// /// This hard-codes the device pixel ratio to 3.0 and a 2400 x 1800 window size. String generateTestEntrypoint({ required String relativeTestPath, required String absolutePath, required String? testConfigPath, required LanguageVersion languageVersion, }) { return ''' // @dart = ${languageVersion.major}.${languageVersion.minor} import 'org-dartlang-app:///$relativeTestPath' as test; import 'dart:ui' as ui; import 'dart:html'; import 'dart:js'; ${testConfigPath != null ? "import '${Uri.file(testConfigPath)}' as test_config;" : ""} import 'package:stream_channel/stream_channel.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:test_api/src/backend/stack_trace_formatter.dart'; // ignore: implementation_imports import 'package:test_api/src/remote_listener.dart'; // ignore: implementation_imports import 'package:test_api/src/backend/suite_channel_manager.dart'; // ignore: implementation_imports Future<void> main() async { ui.debugEmulateFlutterTesterEnvironment = true; await ui.webOnlyInitializePlatform(); webGoldenComparator = DefaultWebGoldenComparator(Uri.parse('${Uri.file(absolutePath)}')); (ui.window as dynamic).debugOverrideDevicePixelRatio(3.0); (ui.window as dynamic).webOnlyDebugPhysicalSizeOverride = const ui.Size(2400, 1800); internalBootstrapBrowserTest(() { return ${testConfigPath != null ? "() => test_config.testExecutable(test.main)" : "test.main"}; }); } void internalBootstrapBrowserTest(Function getMain()) { var channel = serializeSuite(getMain, hidePrints: false); postMessageChannel().pipe(channel); } StreamChannel serializeSuite(Function getMain(), {bool hidePrints = true}) => RemoteListener.start(getMain, hidePrints: hidePrints); 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; } '''; } /// Generate the unit test bootstrap file. String generateTestBootstrapFileContents( String mainUri, String requireUrl, String mapperUrl) { return ''' (function() { if (typeof document != 'undefined') { var el = document.createElement("script"); el.defer = true; el.async = false; el.src = '$mapperUrl'; document.head.appendChild(el); el = document.createElement("script"); el.defer = true; el.async = false; el.src = '$requireUrl'; el.setAttribute("data-main", '$mainUri'); document.head.appendChild(el); } else { importScripts('$mapperUrl', '$requireUrl'); require.config({ baseUrl: baseUrl, }); window = self; require(['$mainUri']); } })(); '''; }