Skip to content

Commit

Permalink
[web] [fix] Cache resource data only if the fetching succeed (#103816)
Browse files Browse the repository at this point in the history
  • Loading branch information
dacianf authored May 24, 2022
1 parent f2e6d47 commit 680bc17
Show file tree
Hide file tree
Showing 5 changed files with 232 additions and 28 deletions.
232 changes: 206 additions & 26 deletions dev/bots/service_worker_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ final String _testAppDirectory = path.join(_flutterRoot, 'dev', 'integration_tes
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 _targetPath = path.join(_testAppDirectory, _target);

enum ServiceWorkerTestType {
Expand All @@ -30,9 +31,12 @@ enum ServiceWorkerTestType {

// Run a web service worker test as a standalone Dart program.
Future<void> main() async {
await runWebServiceWorkerTest(headless: false, testType: ServiceWorkerTestType.withFlutterJs);
await runWebServiceWorkerTest(headless: false, testType: ServiceWorkerTestType.withoutFlutterJs);
await runWebServiceWorkerTest(headless: false, testType: ServiceWorkerTestType.withFlutterJs);
await runWebServiceWorkerTest(headless: false, testType: ServiceWorkerTestType.withFlutterJsShort);
await runWebServiceWorkerTestWithCachingResources(headless: false, testType: ServiceWorkerTestType.withoutFlutterJs);
await runWebServiceWorkerTestWithCachingResources(headless: false, testType: ServiceWorkerTestType.withFlutterJs);
await runWebServiceWorkerTestWithCachingResources(headless: false, testType: ServiceWorkerTestType.withFlutterJsShort);
}

Future<void> _setAppVersion(int version) async {
Expand Down Expand Up @@ -61,7 +65,7 @@ String _testTypeToIndexFile(ServiceWorkerTestType type) {
return indexFile;
}

Future<void> _rebuildApp({ required int version, required ServiceWorkerTestType testType }) async {
Future<void> _rebuildApp({ required int version, required ServiceWorkerTestType testType, required String target }) async {
await _setAppVersion(version);
await runCommand(
_flutter,
Expand All @@ -78,14 +82,40 @@ Future<void> _rebuildApp({ required int version, required ServiceWorkerTestType
);
await runCommand(
_flutter,
<String>['build', 'web', '--profile', '-t', _target],
<String>['build', 'web', '--profile', '-t', target],
workingDirectory: _testAppDirectory,
environment: <String, String>{
'FLUTTER_WEB': 'true',
},
);
}

void _expectRequestCounts(
Map<String, int> expectedCounts,
Map<String, int> requestedPathCounts,
) {
expect(requestedPathCounts, expectedCounts);
requestedPathCounts.clear();
}

Future<void> _waitForAppToLoad(
Map<String, int> waitForCounts,
Map<String, int> requestedPathCounts,
AppServer? server
) async {
print('Waiting for app to load $waitForCounts');
await Future.any(<Future<Object?>>[
() async {
while (!waitForCounts.entries.every((MapEntry<String, int> entry) => (requestedPathCounts[entry.key] ?? 0) >= entry.value)) {
await Future<void>.delayed(const Duration(milliseconds: 100));
}
}(),
server!.onChromeError.then((String error) {
throw Exception('Chrome error: $error');
}),
]);
}

/// A drop-in replacement for `package:test` expect that can run outside the
/// test zone.
void expect(Object? actual, Object? expected) {
Expand All @@ -105,25 +135,12 @@ Future<void> runWebServiceWorkerTest({
required ServiceWorkerTestType testType,
}) async {
final Map<String, int> requestedPathCounts = <String, int>{};
void expectRequestCounts(Map<String, int> expectedCounts) {
expect(requestedPathCounts, expectedCounts);
requestedPathCounts.clear();
}
void expectRequestCounts(Map<String, int> expectedCounts) =>
_expectRequestCounts(expectedCounts, requestedPathCounts);

AppServer? server;
Future<void> waitForAppToLoad(Map<String, int> waitForCounts) async {
print('Waiting for app to load $waitForCounts');
await Future.any(<Future<Object?>>[
() async {
while (!waitForCounts.entries.every((MapEntry<String, int> entry) => (requestedPathCounts[entry.key] ?? 0) >= entry.value)) {
await Future<void>.delayed(const Duration(milliseconds: 100));
}
}(),
server!.onChromeError.then((String error) {
throw Exception('Chrome error: $error');
}),
]);
}
Future<void> waitForAppToLoad(Map<String, int> waitForCounts) async =>
_waitForAppToLoad(waitForCounts, requestedPathCounts, server);

String? reportedVersion;

Expand Down Expand Up @@ -174,7 +191,7 @@ Future<void> runWebServiceWorkerTest({
/////
// Attempt to load a different version of the service worker!
/////
await _rebuildApp(version: 1, testType: testType);
await _rebuildApp(version: 1, testType: testType, target: _target);

print('Call update() on the current web worker');
await startAppServer(cacheControl: 'max-age=0');
Expand All @@ -195,7 +212,7 @@ Future<void> runWebServiceWorkerTest({
expect(reportedVersion, '1');
reportedVersion = null;

await _rebuildApp(version: 2, testType: testType);
await _rebuildApp(version: 2, testType: testType, target: _target);

await server!.chrome.reloadPage(ignoreCache: true);
await waitForAppToLoad(<String, int>{
Expand All @@ -212,7 +229,7 @@ Future<void> runWebServiceWorkerTest({
//////////////////////////////////////////////////////
// Caching server
//////////////////////////////////////////////////////
await _rebuildApp(version: 1, testType: testType);
await _rebuildApp(version: 1, testType: testType, target: _target);

print('With cache: test first page load');
await startAppServer(cacheControl: 'max-age=3600');
Expand All @@ -232,6 +249,7 @@ Future<void> runWebServiceWorkerTest({
'flutter_service_worker.js': 1,
'assets/FontManifest.json': 1,
'assets/AssetManifest.json': 1,
'assets/fonts/MaterialIcons-Regular.otf': 1,
'CLOSE': 1,
// In headless mode Chrome does not load 'manifest.json' and 'favicon.ico'.
if (!headless)
Expand All @@ -258,7 +276,7 @@ Future<void> runWebServiceWorkerTest({
reportedVersion = null;

print('With cache: test page reload after rebuild');
await _rebuildApp(version: 2, testType: testType);
await _rebuildApp(version: 2, testType: testType, target: _target);

// Since we're caching, we need to ignore cache when reloading the page.
await server!.chrome.reloadPage(ignoreCache: true);
Expand Down Expand Up @@ -288,7 +306,7 @@ Future<void> runWebServiceWorkerTest({
// Non-caching server
//////////////////////////////////////////////////////
print('No cache: test first page load');
await _rebuildApp(version: 3, testType: testType);
await _rebuildApp(version: 3, testType: testType, target: _target);
await startAppServer(cacheControl: 'max-age=0');
await waitForAppToLoad(<String, int>{
'CLOSE': 1,
Expand All @@ -304,6 +322,7 @@ Future<void> runWebServiceWorkerTest({
'assets/FontManifest.json': 2,
'flutter_service_worker.js': 1,
'assets/AssetManifest.json': 1,
'assets/fonts/MaterialIcons-Regular.otf': 1,
'CLOSE': 1,
// In headless mode Chrome does not load 'manifest.json' and 'favicon.ico'.
if (!headless)
Expand All @@ -329,6 +348,7 @@ Future<void> runWebServiceWorkerTest({
if (shouldExpectFlutterJs)
'flutter.js': 1,
'flutter_service_worker.js': 1,
'assets/fonts/MaterialIcons-Regular.otf': 1,
'CLOSE': 1,
if (!headless)
'manifest.json': 1,
Expand All @@ -337,7 +357,7 @@ Future<void> runWebServiceWorkerTest({
reportedVersion = null;

print('No cache: test page reload after rebuild');
await _rebuildApp(version: 4, testType: testType);
await _rebuildApp(version: 4, testType: testType, target: _target);

// TODO(yjbanov): when running Chrome with DevTools protocol, for some
// reason a hard refresh is still required. This works without a hard
Expand All @@ -357,6 +377,7 @@ Future<void> runWebServiceWorkerTest({
'main.dart.js': 2,
'assets/AssetManifest.json': 1,
'assets/FontManifest.json': 2,
'assets/fonts/MaterialIcons-Regular.otf': 1,
'CLOSE': 1,
if (!headless)
...<String, int>{
Expand All @@ -382,3 +403,162 @@ Future<void> runWebServiceWorkerTest({

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

Future<void> runWebServiceWorkerTestWithCachingResources({
required bool headless,
required ServiceWorkerTestType testType
}) 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 == 'assets/fonts/MaterialIcons-Regular.otf') {
return Response.internalServerError();
}
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,
);

final bool shouldExpectFlutterJs = testType != ServiceWorkerTestType.withoutFlutterJs;

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

try {
//////////////////////////////////////////////////////
// Caching server
//////////////////////////////////////////////////////
await _rebuildApp(version: 1, testType: testType, target: _targetWithCachedResources);

print('With cache: test first page load');
await startAppServer(cacheControl: 'max-age=3600');
await waitForAppToLoad(<String, int>{
'assets/fonts/MaterialIcons-Regular.otf': 1,
'flutter_service_worker.js': 1,
});

expectRequestCounts(<String, int>{
// Even though the server is caching index.html is downloaded twice,
// 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,
'assets/AssetManifest.json': 1,
'assets/fonts/MaterialIcons-Regular.otf': 1,
// In headless mode Chrome does not load 'manifest.json' and 'favicon.ico'.
if (!headless)
...<String, int>{
'manifest.json': 1,
'favicon.ico': 1,
},
});

print('With cache: test first page reload');
await server!.chrome.reloadPage();
await waitForAppToLoad(<String, int>{
'assets/fonts/MaterialIcons-Regular.otf': 1,
'flutter_service_worker.js': 1,
});
expectRequestCounts(<String, int>{
'assets/fonts/MaterialIcons-Regular.otf': 1,
'flutter_service_worker.js': 1,
});

print('With cache: test second page reload');
await server!.chrome.reloadPage();
await waitForAppToLoad(<String, int>{
'assets/fonts/MaterialIcons-Regular.otf': 1,
'flutter_service_worker.js': 1,
});
expectRequestCounts(<String, int>{
'assets/fonts/MaterialIcons-Regular.otf': 1,
'flutter_service_worker.js': 1,
});

print('With cache: test third page reload');
await server!.chrome.reloadPage();
await waitForAppToLoad(<String, int>{
'assets/fonts/MaterialIcons-Regular.otf': 1,
'flutter_service_worker.js': 1,
});
expectRequestCounts(<String, int>{
'assets/fonts/MaterialIcons-Regular.otf': 1,
'flutter_service_worker.js': 1,
});

print('With cache: test page reload after rebuild');
await _rebuildApp(version: 1, testType: testType, target: _targetWithCachedResources);

// Since we're caching, we need to ignore cache when reloading the page.
await server!.chrome.reloadPage(ignoreCache: true);
await waitForAppToLoad(<String, int>{
'assets/fonts/MaterialIcons-Regular.otf': 1,
'flutter_service_worker.js': 1,
});
expectRequestCounts(<String, int>{
'index.html': 2,
if (shouldExpectFlutterJs)
'flutter.js': 1,
'main.dart.js': 1,
'flutter_service_worker.js': 2,
'assets/FontManifest.json': 1,
'assets/AssetManifest.json': 1,
'assets/fonts/MaterialIcons-Regular.otf': 1,
// In headless mode Chrome does not load 'manifest.json' and 'favicon.ico'.
if (!headless)
...<String, int>{
'favicon.ico': 1,
},
});
} finally {
await runCommand(
'mv',
<String>[
'index_og.html',
'index.html',
],
workingDirectory: _testAppWebDirectory,
);
await server?.stop();
}

print('END runWebServiceWorkerTestWithCachingResources(headless: $headless, testType: $testType)\n');
}
3 changes: 3 additions & 0 deletions dev/bots/test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1083,6 +1083,9 @@ Future<void> _runWebLongRunningTests() async {
() => runWebServiceWorkerTest(headless: true, testType: ServiceWorkerTestType.withoutFlutterJs),
() => runWebServiceWorkerTest(headless: true, testType: ServiceWorkerTestType.withFlutterJs),
() => runWebServiceWorkerTest(headless: true, testType: ServiceWorkerTestType.withFlutterJsShort),
() => runWebServiceWorkerTestWithCachingResources(headless: true, testType: ServiceWorkerTestType.withoutFlutterJs),
() => runWebServiceWorkerTestWithCachingResources(headless: true, testType: ServiceWorkerTestType.withFlutterJs),
() => runWebServiceWorkerTestWithCachingResources(headless: true, testType: ServiceWorkerTestType.withFlutterJsShort),
() => _runWebStackTraceTest('profile', 'lib/stack_trace.dart'),
() => _runWebStackTraceTest('release', 'lib/stack_trace.dart'),
() => _runWebStackTraceTest('profile', 'lib/framework_stack_trace.dart'),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// 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 'package:flutter/material.dart';

Future<void> main() async {
runApp(Scaffold(
body: Center(
child: Column(
children: const <Widget>[
Icon(Icons.ac_unit),
Text('Hello, World', textDirection: TextDirection.ltr),
],
),
),
));
}
1 change: 1 addition & 0 deletions dev/integration_tests/web/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ flutter:
assets:
- lib/a.dart
- lib/b.dart
uses-material-design: true

dependencies:
flutter:
Expand Down
Loading

0 comments on commit 680bc17

Please sign in to comment.