| // 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 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 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 { | |
| var 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 matchState = <Object?, Object?>{}; | |
| if (matcher.matches(actual, matchState)) { | |
| return; | |
| } | |
| final mismatchDescription = StringDescription(); | |
| matcher.describeMismatch(actual, mismatchDescription, matchState, true); | |
| final which = mismatchDescription.toString(); | |
| final 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 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(); | |
| } |