Skip to content

Commit

Permalink
[web] Fix JS crash when FF blocks service workers. (#106072)
Browse files Browse the repository at this point in the history
  • Loading branch information
ditman committed Jun 17, 2022
1 parent 2c15e3c commit b1b1ee9
Show file tree
Hide file tree
Showing 5 changed files with 160 additions and 3 deletions.
92 changes: 92 additions & 0 deletions dev/bots/service_worker_test.dart
Expand Up @@ -21,9 +21,11 @@ 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 _targetWithCachedResources = path.join('lib', 'service_worker_test_cached_resources.dart');
final String _targetWithBlockedServiceWorkers = path.join('lib', 'service_worker_test_blocked_service_workers.dart');
final String _targetPath = path.join(_testAppDirectory, _target);

enum ServiceWorkerTestType {
blockedServiceWorkers,
withoutFlutterJs,
withFlutterJs,
withFlutterJsShort,
Expand All @@ -37,6 +39,7 @@ Future<void> main() async {
await runWebServiceWorkerTestWithCachingResources(headless: false, testType: ServiceWorkerTestType.withoutFlutterJs);
await runWebServiceWorkerTestWithCachingResources(headless: false, testType: ServiceWorkerTestType.withFlutterJs);
await runWebServiceWorkerTestWithCachingResources(headless: false, testType: ServiceWorkerTestType.withFlutterJsShort);
await runWebServiceWorkerTestWithBlockedServiceWorkers(headless: false);
}

Future<void> _setAppVersion(int version) async {
Expand All @@ -52,6 +55,9 @@ Future<void> _setAppVersion(int version) async {
String _testTypeToIndexFile(ServiceWorkerTestType type) {
late String indexFile;
switch (type) {
case ServiceWorkerTestType.blockedServiceWorkers:
indexFile = 'index_with_blocked_service_workers.html';
break;
case ServiceWorkerTestType.withFlutterJs:
indexFile = 'index_with_flutterjs.html';
break;
Expand Down Expand Up @@ -562,3 +568,89 @@ Future<void> runWebServiceWorkerTestWithCachingResources({

print('END runWebServiceWorkerTestWithCachingResources(headless: $headless, testType: $testType)\n');
}

Future<void> runWebServiceWorkerTestWithBlockedServiceWorkers({
required bool headless
}) async {
final Map<String, int> requestedPathCounts = <String, int>{};
void expectRequestCounts(Map<String, int> expectedCounts) =>
_expectRequestCounts(expectedCounts, requestedPathCounts);

AppServer? server;
Future<void> waitForAppToLoad(Map<String, int> waitForCounts) async =>
_waitForAppToLoad(waitForCounts, requestedPathCounts, server);

Future<void> startAppServer({
required String cacheControl,
}) async {
final int serverPort = await findAvailablePort();
final int browserDebugPort = await findAvailablePort();
server = await AppServer.start(
headless: headless,
cacheControl: cacheControl,
// TODO(yjbanov): use a better port disambiguation strategy than trying
// to guess what ports other tests use.
appUrl: 'http://localhost:$serverPort/index.html',
serverPort: serverPort,
browserDebugPort: browserDebugPort,
appDirectory: _appBuildDirectory,
additionalRequestHandlers: <Handler>[
(Request request) {
final String requestedPath = request.url.path;
requestedPathCounts.putIfAbsent(requestedPath, () => 0);
requestedPathCounts[requestedPath] = requestedPathCounts[requestedPath]! + 1;
if (requestedPath == 'CLOSE') {
return Response.ok('OK');
}
return Response.notFound('');
},
],
);
}

// 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,
);

print('BEGIN runWebServiceWorkerTestWithBlockedServiceWorkers(headless: $headless)\n');
try {
await _rebuildApp(version: 1, testType: ServiceWorkerTestType.blockedServiceWorkers, target: _targetWithBlockedServiceWorkers);

print('Ensure app starts (when service workers are blocked)');
await startAppServer(cacheControl: 'max-age=3600');
await waitForAppToLoad(<String, int>{
'CLOSE': 1,
});
expectRequestCounts(<String, int>{
'index.html': 1,
'flutter.js': 1,
'main.dart.js': 1,
'assets/FontManifest.json': 1,
'assets/fonts/MaterialIcons-Regular.otf': 1,
'CLOSE': 1,
// In headless mode Chrome does not load 'manifest.json' and 'favicon.ico'.
if (!headless)
...<String, int>{
'manifest.json': 1,
'favicon.ico': 1,
},
});
} finally {
await runCommand(
'mv',
<String>[
'index_og.html',
'index.html',
],
workingDirectory: _testAppWebDirectory,
);
await server?.stop();
}
print('END runWebServiceWorkerTestWithBlockedServiceWorkers(headless: $headless)\n');
}
1 change: 1 addition & 0 deletions dev/bots/test.dart
Expand Up @@ -1087,6 +1087,7 @@ Future<void> _runWebLongRunningTests() async {
() => runWebServiceWorkerTestWithCachingResources(headless: true, testType: ServiceWorkerTestType.withoutFlutterJs),
() => runWebServiceWorkerTestWithCachingResources(headless: true, testType: ServiceWorkerTestType.withFlutterJs),
() => runWebServiceWorkerTestWithCachingResources(headless: true, testType: ServiceWorkerTestType.withFlutterJsShort),
() => runWebServiceWorkerTestWithBlockedServiceWorkers(headless: true),
() => _runWebStackTraceTest('profile', 'lib/stack_trace.dart'),
() => _runWebStackTraceTest('release', 'lib/stack_trace.dart'),
() => _runWebStackTraceTest('profile', 'lib/framework_stack_trace.dart'),
Expand Down
@@ -0,0 +1,10 @@
// 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 'dart:html' as html;
Future<void> main() async {
const String response = 'CLOSE?version=1';
await html.HttpRequest.getString(response);
html.document.body?.appendHtml(response);
}
@@ -0,0 +1,48 @@
<!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>
// This is to break the serviceWorker registration, and make it throw a DOMException!
// Test for issue https://github.com/flutter/flutter/issues/103972
window.navigator.serviceWorker.register = (url) => {
console.error('Failed to register/update a ServiceWorker for scope ', url,
': Storage access is restricted in this context due to user settings or private browsing mode.');
return Promise.reject(new DOMException('The operation is insecure.'));
}
</script>

<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>
Expand Up @@ -76,6 +76,7 @@ _flutter.loader = null;
_loadEntrypoint(entrypointUrl) {
if (!this._scriptLoaded) {
console.debug("Injecting <script> tag.");
this._scriptLoaded = new Promise((resolve, reject) => {
let scriptTag = document.createElement("script");
scriptTag.src = entrypointUrl;
Expand All @@ -96,7 +97,7 @@ _flutter.loader = null;
_waitForServiceWorkerActivation(serviceWorker, entrypointUrl) {
if (!serviceWorker || serviceWorker.state == "activated") {
if (!serviceWorker) {
console.warn("Cannot activate a null service worker. Falling back to plain <script> tag.");
console.warn("Cannot activate a null service worker.");
} else {
console.debug("Service worker already active.");
}
Expand All @@ -114,7 +115,7 @@ _flutter.loader = null;
_loadWithServiceWorker(entrypointUrl, serviceWorkerOptions) {
if (!("serviceWorker" in navigator) || serviceWorkerOptions == null) {
console.warn("Service worker not supported (or configured). Falling back to plain <script> tag.", serviceWorkerOptions);
console.warn("Service worker not supported (or configured).", serviceWorkerOptions);
return this._loadEntrypoint(entrypointUrl);
}
Expand Down Expand Up @@ -145,6 +146,11 @@ _flutter.loader = null;
console.debug("Loading app from service worker.");
return this._loadEntrypoint(entrypointUrl);
}
})
.catch((error) => {
// Some exception happened while registering/activating the service worker.
console.warn("Failed to register or activate service worker:", error);
return this._loadEntrypoint(entrypointUrl);
});
// Timeout race promise
Expand All @@ -153,7 +159,7 @@ _flutter.loader = null;
timeout = new Promise((resolve, _) => {
setTimeout(() => {
if (!this._scriptLoaded) {
console.warn("Failed to load app from service worker. Falling back to plain <script> tag.");
console.warn("Loading from service worker timed out after", timeoutMillis, "milliseconds.");
resolve(this._loadEntrypoint(entrypointUrl));
}
}, timeoutMillis);
Expand Down

0 comments on commit b1b1ee9

Please sign in to comment.