diff --git a/dev/bots/service_worker_test.dart b/dev/bots/service_worker_test.dart index 0b30533ff7..10432c7a35 100644 --- a/dev/bots/service_worker_test.dart +++ b/dev/bots/service_worker_test.dart @@ -17,13 +17,22 @@ final String _bat = Platform.isWindows ? '.bat' : ''; final String _flutterRoot = path.dirname(path.dirname(path.dirname(path.fromUri(Platform.script)))); final String _flutter = path.join(_flutterRoot, 'bin', 'flutter$_bat'); final String _testAppDirectory = path.join(_flutterRoot, 'dev', 'integration_tests', 'web'); +final String _testAppWebDirectory = path.join(_testAppDirectory, 'web'); final String _appBuildDirectory = path.join(_testAppDirectory, 'build', 'web'); final String _target = path.join('lib', 'service_worker_test.dart'); final String _targetPath = path.join(_testAppDirectory, _target); +enum ServiceWorkerTestType { + withoutFlutterJs, + withFlutterJs, + withFlutterJsShort, +} + // Run a web service worker test as a standalone Dart program. Future main() async { - await runWebServiceWorkerTest(headless: false); + await runWebServiceWorkerTest(headless: false, testType: ServiceWorkerTestType.withFlutterJs); + await runWebServiceWorkerTest(headless: false, testType: ServiceWorkerTestType.withoutFlutterJs); + await runWebServiceWorkerTest(headless: false, testType: ServiceWorkerTestType.withFlutterJsShort); } Future _setAppVersion(int version) async { @@ -36,13 +45,37 @@ Future _setAppVersion(int version) async { ); } -Future _rebuildApp({ required int version }) async { +String _testTypeToIndexFile(ServiceWorkerTestType type) { + late String indexFile; + switch (type) { + case ServiceWorkerTestType.withFlutterJs: + indexFile = 'index_with_flutterjs.html'; + break; + case ServiceWorkerTestType.withoutFlutterJs: + indexFile = 'index_without_flutterjs.html'; + break; + case ServiceWorkerTestType.withFlutterJsShort: + indexFile = 'index_with_flutterjs_short.html'; + break; + } + return indexFile; +} + +Future _rebuildApp({ required int version, required ServiceWorkerTestType testType }) async { await _setAppVersion(version); await runCommand( _flutter, [ 'clean' ], workingDirectory: _testAppDirectory, ); + await runCommand( + 'cp', + [ + _testTypeToIndexFile(testType), + 'index.html', + ], + workingDirectory: _testAppWebDirectory, + ); await runCommand( _flutter, ['build', 'web', '--profile', '-t', _target], @@ -69,9 +102,8 @@ void expect(Object? actual, Object? expected) { Future runWebServiceWorkerTest({ required bool headless, + required ServiceWorkerTestType testType, }) async { - await _rebuildApp(version: 1); - final Map requestedPathCounts = {}; void expectRequestCounts(Map expectedCounts) { expect(requestedPathCounts, expectedCounts); @@ -124,10 +156,64 @@ Future runWebServiceWorkerTest({ ); } + // Preserve old index.html as index_og.html so we can restore it later for other tests + await runCommand( + 'mv', + [ + 'index.html', + 'index_og.html', + ], + workingDirectory: _testAppWebDirectory, + ); + + final bool shouldExpectFlutterJs = testType != ServiceWorkerTestType.withoutFlutterJs; + + print('BEGIN runWebServiceWorkerTest(headless: $headless, testType: $testType)\n'); + try { + ///// + // Attempt to load a different version of the service worker! + ///// + await _rebuildApp(version: 1, testType: testType); + + print('Call update() on the current web worker'); + await startAppServer(cacheControl: 'max-age=0'); + await waitForAppToLoad( { + if (shouldExpectFlutterJs) + 'flutter.js': 1, + 'CLOSE': 1, + }); + expect(reportedVersion, '1'); + reportedVersion = null; + + await server!.chrome.reloadPage(ignoreCache: true); + await waitForAppToLoad( { + if (shouldExpectFlutterJs) + 'flutter.js': 2, + 'CLOSE': 2, + }); + expect(reportedVersion, '1'); + reportedVersion = null; + + await _rebuildApp(version: 2, testType: testType); + + await server!.chrome.reloadPage(ignoreCache: true); + await waitForAppToLoad({ + if (shouldExpectFlutterJs) + 'flutter.js': 3, + 'CLOSE': 3, + }); + expect(reportedVersion, '2'); + + reportedVersion = null; + requestedPathCounts.clear(); + await server!.stop(); + ////////////////////////////////////////////////////// // Caching server ////////////////////////////////////////////////////// + await _rebuildApp(version: 1, testType: testType); + print('With cache: test first page load'); await startAppServer(cacheControl: 'max-age=3600'); await waitForAppToLoad({ @@ -140,6 +226,8 @@ Future runWebServiceWorkerTest({ // once by the initial page load, and once by the service worker. // Other resources are loaded once only by the service worker. 'index.html': 2, + if (shouldExpectFlutterJs) + 'flutter.js': 1, 'main.dart.js': 1, 'flutter_service_worker.js': 1, 'assets/FontManifest.json': 1, @@ -171,7 +259,7 @@ Future runWebServiceWorkerTest({ reportedVersion = null; print('With cache: test page reload after rebuild'); - await _rebuildApp(version: 2); + await _rebuildApp(version: 2, testType: testType); // Since we're caching, we need to ignore cache when reloading the page. await server!.chrome.reloadPage(ignoreCache: true); @@ -181,6 +269,8 @@ Future runWebServiceWorkerTest({ }); expectRequestCounts({ 'index.html': 2, + if (shouldExpectFlutterJs) + 'flutter.js': 1, 'flutter_service_worker.js': 2, 'main.dart.js': 1, 'assets/NOTICES': 1, @@ -200,7 +290,7 @@ Future runWebServiceWorkerTest({ // Non-caching server ////////////////////////////////////////////////////// print('No cache: test first page load'); - await _rebuildApp(version: 3); + await _rebuildApp(version: 3, testType: testType); await startAppServer(cacheControl: 'max-age=0'); await waitForAppToLoad({ 'CLOSE': 1, @@ -209,6 +299,8 @@ Future runWebServiceWorkerTest({ expectRequestCounts({ 'index.html': 2, + if (shouldExpectFlutterJs) + 'flutter.js': 1, // We still download some resources multiple times if the server is non-caching. 'main.dart.js': 2, 'assets/FontManifest.json': 2, @@ -231,10 +323,14 @@ Future runWebServiceWorkerTest({ await server!.chrome.reloadPage(); await waitForAppToLoad({ 'CLOSE': 1, + if (shouldExpectFlutterJs) + 'flutter.js': 1, 'flutter_service_worker.js': 1, }); expectRequestCounts({ + if (shouldExpectFlutterJs) + 'flutter.js': 1, 'flutter_service_worker.js': 1, 'CLOSE': 1, if (!headless) @@ -244,7 +340,7 @@ Future runWebServiceWorkerTest({ reportedVersion = null; print('No cache: test page reload after rebuild'); - await _rebuildApp(version: 4); + await _rebuildApp(version: 4, testType: testType); // TODO(yjbanov): when running Chrome with DevTools protocol, for some // reason a hard refresh is still required. This works without a hard @@ -258,6 +354,8 @@ Future runWebServiceWorkerTest({ }); expectRequestCounts({ 'index.html': 2, + if (shouldExpectFlutterJs) + 'flutter.js': 1, 'flutter_service_worker.js': 2, 'main.dart.js': 2, 'assets/NOTICES': 1, @@ -274,7 +372,17 @@ Future runWebServiceWorkerTest({ expect(reportedVersion, '4'); reportedVersion = null; } finally { + await runCommand( + 'mv', + [ + 'index_og.html', + 'index.html', + ], + workingDirectory: _testAppWebDirectory, + ); await _setAppVersion(1); await server?.stop(); } + + print('END runWebServiceWorkerTest(headless: $headless, testType: $testType)\n'); } diff --git a/dev/bots/test.dart b/dev/bots/test.dart index 063c4bc433..ec93efa0b8 100644 --- a/dev/bots/test.dart +++ b/dev/bots/test.dart @@ -1069,7 +1069,9 @@ Future _runWebLongRunningTests() async { () => _runGalleryE2eWebTest('profile', canvasKit: true), () => _runGalleryE2eWebTest('release'), () => _runGalleryE2eWebTest('release', canvasKit: true), - () => runWebServiceWorkerTest(headless: true), + () => runWebServiceWorkerTest(headless: true, testType: ServiceWorkerTestType.withoutFlutterJs), + () => runWebServiceWorkerTest(headless: true, testType: ServiceWorkerTestType.withFlutterJs), + () => runWebServiceWorkerTest(headless: true, testType: ServiceWorkerTestType.withFlutterJsShort), () => _runWebStackTraceTest('profile', 'lib/stack_trace.dart'), () => _runWebStackTraceTest('release', 'lib/stack_trace.dart'), () => _runWebStackTraceTest('profile', 'lib/framework_stack_trace.dart'), diff --git a/dev/integration_tests/web/web/index_with_flutterjs.html b/dev/integration_tests/web/web/index_with_flutterjs.html new file mode 100644 index 0000000000..8334b5b641 --- /dev/null +++ b/dev/integration_tests/web/web/index_with_flutterjs.html @@ -0,0 +1,39 @@ + + + + + + + + Web Test + + + + + + + + + + + + + diff --git a/dev/integration_tests/web/web/index_with_flutterjs_short.html b/dev/integration_tests/web/web/index_with_flutterjs_short.html new file mode 100644 index 0000000000..21a494facb --- /dev/null +++ b/dev/integration_tests/web/web/index_with_flutterjs_short.html @@ -0,0 +1,37 @@ + + + + + + + + Web Test + + + + + + + + + + + + + diff --git a/dev/integration_tests/web/web/index_without_flutterjs.html b/dev/integration_tests/web/web/index_without_flutterjs.html new file mode 100644 index 0000000000..05d4deecc1 --- /dev/null +++ b/dev/integration_tests/web/web/index_without_flutterjs.html @@ -0,0 +1,86 @@ + + + + + + + + Web Test + + + + + + + + + + + diff --git a/packages/flutter_tools/lib/src/web/flutter_js.dart b/packages/flutter_tools/lib/src/web/flutter_js.dart index dfff8270d3..af8950b1bc 100644 --- a/packages/flutter_tools/lib/src/web/flutter_js.dart +++ b/packages/flutter_tools/lib/src/web/flutter_js.dart @@ -31,7 +31,7 @@ _flutter.loader = null; // we support. In the meantime, we use the "revealing module" pattern. // Watchdog to prevent injecting the main entrypoint multiple times. - _scriptLoaded = false; + _scriptLoaded = null; // Resolver for the pending promise returned by loadEntrypoint. _didCreateEngineInitializerResolve = null; @@ -61,31 +61,38 @@ _flutter.loader = null; console.warn("Do not call didCreateEngineInitializer by hand. Start with loadEntrypoint instead."); } this._didCreateEngineInitializerResolve(engineInitializer); + // Remove this method after it's done, so Flutter Web can hot restart. + delete this.didCreateEngineInitializer; }).bind(this); _loadEntrypoint(entrypointUrl) { - if (this._scriptLoaded) { - return null; + if (!this._scriptLoaded) { + this._scriptLoaded = new Promise((resolve, reject) => { + let scriptTag = document.createElement("script"); + scriptTag.src = entrypointUrl; + scriptTag.type = "application/javascript"; + this._didCreateEngineInitializerResolve = resolve; // Cache the resolve, so it can be called from Flutter. + scriptTag.addEventListener("error", reject); + document.body.append(scriptTag); + }); } - this._scriptLoaded = true; - - return new Promise((resolve, reject) => { - let scriptTag = document.createElement("script"); - scriptTag.src = entrypointUrl; - scriptTag.type = "application/javascript"; - this._didCreateEngineInitializerResolve = resolve; // Cache the resolve, so it can be called from Flutter. - scriptTag.addEventListener("error", reject); - document.body.append(scriptTag); - }); + return this._scriptLoaded; } _waitForServiceWorkerActivation(serviceWorker, entrypointUrl) { - if (!serviceWorker) return; + if (!serviceWorker || serviceWorker.state == "activated") { + if (!serviceWorker) { + console.warn("Cannot activate a null service worker. Falling back to plain