Unverified Commit bee95b69 authored by David Iglesias's avatar David Iglesias Committed by GitHub

[flutter.js] Wait for reg.update, then activate sw (if not active yet). (#101464)

parent fa48aed7
......@@ -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<void> 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<void> _setAppVersion(int version) async {
......@@ -36,13 +45,37 @@ Future<void> _setAppVersion(int version) async {
);
}
Future<void> _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<void> _rebuildApp({ required int version, required ServiceWorkerTestType testType }) async {
await _setAppVersion(version);
await runCommand(
_flutter,
<String>[ 'clean' ],
workingDirectory: _testAppDirectory,
);
await runCommand(
'cp',
<String>[
_testTypeToIndexFile(testType),
'index.html',
],
workingDirectory: _testAppWebDirectory,
);
await runCommand(
_flutter,
<String>['build', 'web', '--profile', '-t', _target],
......@@ -69,9 +102,8 @@ void expect(Object? actual, Object? expected) {
Future<void> runWebServiceWorkerTest({
required bool headless,
required ServiceWorkerTestType testType,
}) async {
await _rebuildApp(version: 1);
final Map<String, int> requestedPathCounts = <String, int>{};
void expectRequestCounts(Map<String, int> expectedCounts) {
expect(requestedPathCounts, expectedCounts);
......@@ -124,10 +156,64 @@ Future<void> runWebServiceWorkerTest({
);
}
// Preserve old index.html as index_og.html so we can restore it later for other tests
await runCommand(
'mv',
<String>[
'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(<String, int> {
if (shouldExpectFlutterJs)
'flutter.js': 1,
'CLOSE': 1,
});
expect(reportedVersion, '1');
reportedVersion = null;
await server!.chrome.reloadPage(ignoreCache: true);
await waitForAppToLoad(<String, int> {
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(<String, int>{
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(<String, int>{
......@@ -140,6 +226,8 @@ Future<void> 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<void> 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<void> runWebServiceWorkerTest({
});
expectRequestCounts(<String, int>{
'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<void> 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(<String, int>{
'CLOSE': 1,
......@@ -209,6 +299,8 @@ Future<void> runWebServiceWorkerTest({
expectRequestCounts(<String, int>{
'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<void> runWebServiceWorkerTest({
await server!.chrome.reloadPage();
await waitForAppToLoad(<String, int>{
'CLOSE': 1,
if (shouldExpectFlutterJs)
'flutter.js': 1,
'flutter_service_worker.js': 1,
});
expectRequestCounts(<String, int>{
if (shouldExpectFlutterJs)
'flutter.js': 1,
'flutter_service_worker.js': 1,
'CLOSE': 1,
if (!headless)
......@@ -244,7 +340,7 @@ Future<void> 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<void> runWebServiceWorkerTest({
});
expectRequestCounts(<String, int>{
'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<void> runWebServiceWorkerTest({
expect(reportedVersion, '4');
reportedVersion = null;
} finally {
await runCommand(
'mv',
<String>[
'index_og.html',
'index.html',
],
workingDirectory: _testAppWebDirectory,
);
await _setAppVersion(1);
await server?.stop();
}
print('END runWebServiceWorkerTest(headless: $headless, testType: $testType)\n');
}
......@@ -1069,7 +1069,9 @@ Future<void> _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'),
......
<!DOCTYPE HTML>
<!-- 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. -->
<html>
<head>
<meta charset="UTF-8">
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
<title>Web Test</title>
<!-- iOS meta tags & icons -->
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="Web Test">
<link rel="manifest" href="manifest.json">
<script>
// The value below is injected by flutter build, do not touch.
var serviceWorkerVersion = null;
</script>
<!-- This script adds the flutter initialization JS code -->
<script src="flutter.js" defer></script>
</head>
<body>
<script>
window.addEventListener('load', function(ev) {
// Download main.dart.js
_flutter.loader.loadEntrypoint({
serviceWorker: {
serviceWorkerVersion: serviceWorkerVersion,
}
}).then(function(engineInitializer) {
return engineInitializer.initializeEngine();
}).then(function(appRunner) {
return appRunner.runApp();
});
});
</script>
</body>
</html>
<!DOCTYPE HTML>
<!-- 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. -->
<html>
<head>
<meta charset="UTF-8">
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
<title>Web Test</title>
<!-- iOS meta tags & icons -->
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="Web Test">
<link rel="manifest" href="manifest.json">
<script>
// The value below is injected by flutter build, do not touch.
var serviceWorkerVersion = null;
</script>
<!-- This script adds the flutter initialization JS code -->
<script src="flutter.js" defer></script>
</head>
<body>
<script>
window.addEventListener('load', function(ev) {
// Download main.dart.js
_flutter.loader.loadEntrypoint({
serviceWorker: {
serviceWorkerVersion: serviceWorkerVersion,
}
}).then(function(engineInitializer) {
return engineInitializer.autoStart();
});
});
</script>
</body>
</html>
<!DOCTYPE HTML>
<!-- 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. -->
<html>
<head>
<meta charset="UTF-8">
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
<title>Web Test</title>
<!-- iOS meta tags & icons -->
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="Web Test">
<link rel="manifest" href="manifest.json">
</head>
<body>
<!-- This script installs service_worker.js to provide PWA functionality to
application. For more information, see:
https://developers.google.com/web/fundamentals/primers/service-workers -->
<script>
var serviceWorkerVersion = null;
var scriptLoaded = false;
function loadMainDartJs() {
if (scriptLoaded) {
return;
}
scriptLoaded = true;
var scriptTag = document.createElement('script');
scriptTag.src = 'main.dart.js';
scriptTag.type = 'application/javascript';
document.body.append(scriptTag);
}
if ('serviceWorker' in navigator) {
// Service workers are supported. Use them.
window.addEventListener('load', function () {
// Wait for registration to finish before dropping the <script> tag.
// Otherwise, the browser will load the script multiple times,
// potentially different versions.
var serviceWorkerUrl = 'flutter_service_worker.js?v=' + serviceWorkerVersion;
navigator.serviceWorker.register(serviceWorkerUrl)
.then((reg) => {
function waitForActivation(serviceWorker) {
serviceWorker.addEventListener('statechange', () => {
if (serviceWorker.state == 'activated') {
console.log('Installed new service worker.');
loadMainDartJs();
}
});
}
if (!reg.active && (reg.installing || reg.waiting)) {
// No active web worker and we have installed or are installing
// one for the first time. Simply wait for it to activate.
waitForActivation(reg.installing ?? reg.waiting);
} else if (!reg.active.scriptURL.endsWith(serviceWorkerVersion)) {
// When the app updates the serviceWorkerVersion changes, so we
// need to ask the service worker to update.
console.log('New service worker available.');
reg.update();
waitForActivation(reg.installing);
} else {
// Existing service worker is still good.
console.log('Loading app from service worker.');
loadMainDartJs();
}
});
// If service worker doesn't succeed in a reasonable amount of time,
// fallback to plaint <script> tag.
setTimeout(() => {
if (!scriptLoaded) {
console.warn(
'Failed to load app from service worker. Falling back to plain <script> tag.',
);
loadMainDartJs();
}
}, 4000);
});
} else {
// Service workers not supported. Just drop the <script> tag.
loadMainDartJs();
}
</script>
</body>
</html>
......@@ -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 <script> tag.");
} else {
console.debug("Service worker already active.");
}
return this._loadEntrypoint(entrypointUrl);
}
return new Promise((resolve, _) => {
serviceWorker.addEventListener("statechange", () => {
if (serviceWorker.state == "activated") {
console.log("Installed new service worker.");
console.debug("Installed new service worker.");
resolve(this._loadEntrypoint(entrypointUrl));
}
});
......@@ -103,22 +110,26 @@ _flutter.loader = null;
timeoutMillis = 4000,
} = serviceWorkerOptions;
var serviceWorkerUrl = "flutter_service_worker.js?v=" + serviceWorkerVersion;
let serviceWorkerUrl = "flutter_service_worker.js?v=" + serviceWorkerVersion;
let loader = navigator.serviceWorker.register(serviceWorkerUrl)
.then((reg) => {
if (!reg.active && (reg.installing || reg.waiting)) {
// No active web worker and we have installed or are installing
// one for the first time. Simply wait for it to activate.
return this._waitForServiceWorkerActivation(reg.installing || reg.waiting, entrypointUrl);
let sw = reg.installing || reg.waiting;
return this._waitForServiceWorkerActivation(sw, entrypointUrl);
} else if (!reg.active.scriptURL.endsWith(serviceWorkerVersion)) {
// When the app updates the serviceWorkerVersion changes, so we
// need to ask the service worker to update.
console.log("New service worker available.");
reg.update();
return this._waitForServiceWorkerActivation(reg.installing, entrypointUrl);
console.debug("New service worker available.");
return reg.update().then((reg) => {
console.debug("Service worker updated.");
let sw = reg.installing || reg.waiting || reg.active;
return this._waitForServiceWorkerActivation(sw, entrypointUrl);
});
} else {
// Existing service worker is still good.
console.log("Loading app from service worker.");
console.debug("Loading app from service worker.");
return this._loadEntrypoint(entrypointUrl);
}
});
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment