blob: f88629ebb22a45a51d2fccb2841995e725ad7e5f [file] [log] [blame]
// 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();
}