mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Introduce a self-cleaning service worker to replace the old one. Previous attempts (https://github.com/flutter/flutter/pull/170918, https://github.com/flutter/flutter/pull/173609) had (accidentally?) disabled the loading of the service worker in `flutter.js`. This PR preserves the logic for loading the service worker, and it prints a deprecation warning. Towards https://github.com/flutter/flutter/issues/156910 Closes https://github.com/flutter/flutter/issues/106225
209 lines
7.4 KiB
Dart
209 lines
7.4 KiB
Dart
// 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:core' hide print;
|
|
import 'dart:io';
|
|
|
|
import 'package:path/path.dart' as path;
|
|
import 'package:shelf/shelf.dart';
|
|
|
|
import 'browser.dart';
|
|
import 'run_command.dart';
|
|
import 'test/common.dart';
|
|
import 'utils.dart';
|
|
|
|
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 _appBuildDirectory = path.join(_testAppDirectory, 'build', 'web');
|
|
final String _target = path.join('lib', 'service_worker_test.dart');
|
|
final Set<String> _requestedPaths = <String>{};
|
|
|
|
Future<void> main() async {
|
|
await runServiceWorkerCleanupTest(headless: false);
|
|
|
|
if (hasError) {
|
|
reportErrorsAndExit('${bold}Cleanup test FAILED.$reset');
|
|
}
|
|
reportSuccessAndExit('${bold}Cleanup test PASSED successfully.$reset');
|
|
}
|
|
|
|
Future<void> runServiceWorkerCleanupTest({required bool headless}) async {
|
|
print('${bold}BEGIN: Service Worker Cleanup Verification Test$reset');
|
|
|
|
AppServer? server;
|
|
|
|
const String oldCachingWorkerContent = '''
|
|
'use strict';
|
|
const CACHE_NAME = 'flutter-app-cache';
|
|
self.addEventListener('install', (event) => {
|
|
self.skipWaiting();
|
|
event.waitUntil(
|
|
caches.open(CACHE_NAME).then((cache) => {
|
|
return cache.addAll(['/', 'index.html', 'main.dart.js']);
|
|
}).then(() => self.skipWaiting())
|
|
);
|
|
});
|
|
self.addEventListener('activate', (event) => {
|
|
event.waitUntil(self.clients.claim());
|
|
});
|
|
self.addEventListener('fetch', (event) => {
|
|
event.respondWith(
|
|
caches.open(CACHE_NAME).then((cache) => {
|
|
return cache.match(event.request).then((response) => {
|
|
return response || fetch(event.request).then((response) => {
|
|
if (response && Boolean(response.ok)) {
|
|
cache.put(event.request, response.clone());
|
|
}
|
|
return response;
|
|
});
|
|
})
|
|
})
|
|
);
|
|
});
|
|
''';
|
|
|
|
final File serviceWorkerBuildFile = File(
|
|
path.join(_appBuildDirectory, 'flutter_service_worker.js'),
|
|
);
|
|
|
|
try {
|
|
await runCommand(_flutter, <String>['clean'], workingDirectory: _testAppDirectory);
|
|
await runCommand(
|
|
_flutter,
|
|
<String>['build', 'web', '--no-web-resources-cdn', '--profile', '-t', _target],
|
|
workingDirectory: _testAppDirectory,
|
|
environment: <String, String>{'FLUTTER_WEB': 'true'},
|
|
);
|
|
print('\n${yellow}Phase 1: Installing dummy caching worker and verifying it caches...$reset');
|
|
final String cleanupWorkerContent = serviceWorkerBuildFile.readAsStringSync();
|
|
serviceWorkerBuildFile.writeAsStringSync(oldCachingWorkerContent);
|
|
|
|
server = await _startServer(headless: headless);
|
|
await _waitForAppToRequest(server, 'flutter_service_worker.js');
|
|
await _waitForAppToRequest(server, 'main.dart.js');
|
|
_requestedPaths.clear();
|
|
print('== RELOADING PAGE ==');
|
|
await server.chrome.reloadPage();
|
|
await _waitForAppToRequest(server, 'CLOSE');
|
|
|
|
expect(
|
|
_requestedPaths,
|
|
isNot(contains('main.dart.js')),
|
|
reason:
|
|
'On a simple reload, main.dart.js should have been served from the cache, so no network request was expected.',
|
|
);
|
|
print('${green}Verification successful: Old caching worker is active.$reset');
|
|
await server.stop();
|
|
|
|
print('\n${yellow}Phase 2: Deploying cleanup worker and verifying cache is removed...$reset');
|
|
serviceWorkerBuildFile.writeAsStringSync(cleanupWorkerContent);
|
|
|
|
_requestedPaths.clear();
|
|
server = await _startServer(headless: headless);
|
|
await _waitForAppToRequest(server, 'flutter_service_worker.js');
|
|
await _waitForAppToRequest(server, 'main.dart.js');
|
|
_requestedPaths.clear();
|
|
print('== RELOADING PAGE ==');
|
|
await server.chrome.reloadPage();
|
|
await _waitForAppToRequest(server, 'main.dart.js');
|
|
|
|
expect(
|
|
_requestedPaths,
|
|
contains('main.dart.js'),
|
|
reason:
|
|
'After cleanup, main.dart.js should be requested from the network because the caching worker is gone.',
|
|
);
|
|
print(
|
|
'${green}Verification successful: Cleanup worker has removed the old caching behavior.$reset',
|
|
);
|
|
} finally {
|
|
await server?.stop();
|
|
print('\n${bold}END: Service Worker Cleanup Verification Test$reset');
|
|
}
|
|
}
|
|
|
|
Future<AppServer> _startServer({required bool headless}) async {
|
|
final int serverPort = await findAvailablePortAndPossiblyCauseFlakyTests();
|
|
final int browserDebugPort = await findAvailablePortAndPossiblyCauseFlakyTests();
|
|
return AppServer.start(
|
|
headless: headless,
|
|
appDirectory: _appBuildDirectory,
|
|
serverPort: serverPort,
|
|
browserDebugPort: browserDebugPort,
|
|
appUrl: 'http://localhost:$serverPort/index.html',
|
|
cacheControl: 'max-age=0',
|
|
additionalRequestHandlers: <Handler>[
|
|
(Request request) {
|
|
final String path = request.url.path.split('/').last;
|
|
print('(requested path: $path)');
|
|
_requestedPaths.add(path);
|
|
return Response.notFound('');
|
|
},
|
|
],
|
|
);
|
|
}
|
|
|
|
Future<void> _waitForAppToRequest(AppServer server, String file) async {
|
|
print('Waiting for app to request "$file"');
|
|
await Future.any(<Future<Object?>>[
|
|
() async {
|
|
int tries = 1;
|
|
while (!_requestedPaths.contains(file)) {
|
|
if (tries++ % 40 == 0) {
|
|
print('-- Still waiting for app to request "$file". Requested so far: $_requestedPaths');
|
|
}
|
|
await Future<void>.delayed(const Duration(milliseconds: 100));
|
|
}
|
|
print('++ App has requested "$file"');
|
|
}(),
|
|
server.onChromeError.then((String error) {
|
|
throw Exception('Chrome error: $error');
|
|
}),
|
|
]);
|
|
}
|
|
|
|
/// A drop-in replacement for `package:test`'s `expect` that can run
|
|
/// outside the standard test runner environment.
|
|
void expect(Object? actual, Object? expected, {String? reason}) {
|
|
final Matcher matcher = wrapMatcher(expected);
|
|
final Map<Object?, Object?> matchState = <Object?, Object?>{};
|
|
if (matcher.matches(actual, matchState)) {
|
|
return;
|
|
}
|
|
final StringDescription mismatchDescription = StringDescription();
|
|
matcher.describeMismatch(actual, mismatchDescription, matchState, true);
|
|
|
|
final String which = mismatchDescription.toString();
|
|
final StringBuffer buffer = StringBuffer();
|
|
buffer.writeln(_indent(_prettyPrint(expected), first: 'Expected: '));
|
|
buffer.writeln(_indent(_prettyPrint(actual), first: ' Actual: '));
|
|
if (which.isNotEmpty) {
|
|
buffer.writeln(_indent(which, first: ' Which: '));
|
|
}
|
|
if (reason != null) {
|
|
buffer.writeln(_indent(reason, first: ' Reason: '));
|
|
}
|
|
foundError(<String>[buffer.toString(), StackTrace.current.toString()]);
|
|
}
|
|
|
|
/// Returns a pretty-printed representation of [value].
|
|
String _prettyPrint(Object? value) => StringDescription().addDescriptionOf(value).toString();
|
|
|
|
/// Indents each line of a [text] string.
|
|
String _indent(String text, {required String first}) {
|
|
final String prefix = ' ' * first.length;
|
|
final List<String> lines = text.split('\n');
|
|
if (lines.length == 1) {
|
|
return '$first$text';
|
|
}
|
|
final StringBuffer buffer = StringBuffer('$first${lines.first}\n');
|
|
for (final String line in lines.skip(1).take(lines.length - 2)) {
|
|
buffer.writeln('$prefix$line');
|
|
}
|
|
buffer.write('$prefix${lines.last}');
|
|
return buffer.toString();
|
|
}
|