Skip to content

Commit 680bc17

Browse files
authored
[web] [fix] Cache resource data only if the fetching succeed (#103816)
1 parent f2e6d47 commit 680bc17

File tree

5 files changed

+232
-28
lines changed

5 files changed

+232
-28
lines changed

dev/bots/service_worker_test.dart

Lines changed: 206 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ final String _testAppDirectory = path.join(_flutterRoot, 'dev', 'integration_tes
2020
final String _testAppWebDirectory = path.join(_testAppDirectory, 'web');
2121
final String _appBuildDirectory = path.join(_testAppDirectory, 'build', 'web');
2222
final String _target = path.join('lib', 'service_worker_test.dart');
23+
final String _targetWithCachedResources = path.join('lib', 'service_worker_test_cached_resources.dart');
2324
final String _targetPath = path.join(_testAppDirectory, _target);
2425

2526
enum ServiceWorkerTestType {
@@ -30,9 +31,12 @@ enum ServiceWorkerTestType {
3031

3132
// Run a web service worker test as a standalone Dart program.
3233
Future<void> main() async {
33-
await runWebServiceWorkerTest(headless: false, testType: ServiceWorkerTestType.withFlutterJs);
3434
await runWebServiceWorkerTest(headless: false, testType: ServiceWorkerTestType.withoutFlutterJs);
35+
await runWebServiceWorkerTest(headless: false, testType: ServiceWorkerTestType.withFlutterJs);
3536
await runWebServiceWorkerTest(headless: false, testType: ServiceWorkerTestType.withFlutterJsShort);
37+
await runWebServiceWorkerTestWithCachingResources(headless: false, testType: ServiceWorkerTestType.withoutFlutterJs);
38+
await runWebServiceWorkerTestWithCachingResources(headless: false, testType: ServiceWorkerTestType.withFlutterJs);
39+
await runWebServiceWorkerTestWithCachingResources(headless: false, testType: ServiceWorkerTestType.withFlutterJsShort);
3640
}
3741

3842
Future<void> _setAppVersion(int version) async {
@@ -61,7 +65,7 @@ String _testTypeToIndexFile(ServiceWorkerTestType type) {
6165
return indexFile;
6266
}
6367

64-
Future<void> _rebuildApp({ required int version, required ServiceWorkerTestType testType }) async {
68+
Future<void> _rebuildApp({ required int version, required ServiceWorkerTestType testType, required String target }) async {
6569
await _setAppVersion(version);
6670
await runCommand(
6771
_flutter,
@@ -78,14 +82,40 @@ Future<void> _rebuildApp({ required int version, required ServiceWorkerTestType
7882
);
7983
await runCommand(
8084
_flutter,
81-
<String>['build', 'web', '--profile', '-t', _target],
85+
<String>['build', 'web', '--profile', '-t', target],
8286
workingDirectory: _testAppDirectory,
8387
environment: <String, String>{
8488
'FLUTTER_WEB': 'true',
8589
},
8690
);
8791
}
8892

93+
void _expectRequestCounts(
94+
Map<String, int> expectedCounts,
95+
Map<String, int> requestedPathCounts,
96+
) {
97+
expect(requestedPathCounts, expectedCounts);
98+
requestedPathCounts.clear();
99+
}
100+
101+
Future<void> _waitForAppToLoad(
102+
Map<String, int> waitForCounts,
103+
Map<String, int> requestedPathCounts,
104+
AppServer? server
105+
) async {
106+
print('Waiting for app to load $waitForCounts');
107+
await Future.any(<Future<Object?>>[
108+
() async {
109+
while (!waitForCounts.entries.every((MapEntry<String, int> entry) => (requestedPathCounts[entry.key] ?? 0) >= entry.value)) {
110+
await Future<void>.delayed(const Duration(milliseconds: 100));
111+
}
112+
}(),
113+
server!.onChromeError.then((String error) {
114+
throw Exception('Chrome error: $error');
115+
}),
116+
]);
117+
}
118+
89119
/// A drop-in replacement for `package:test` expect that can run outside the
90120
/// test zone.
91121
void expect(Object? actual, Object? expected) {
@@ -105,25 +135,12 @@ Future<void> runWebServiceWorkerTest({
105135
required ServiceWorkerTestType testType,
106136
}) async {
107137
final Map<String, int> requestedPathCounts = <String, int>{};
108-
void expectRequestCounts(Map<String, int> expectedCounts) {
109-
expect(requestedPathCounts, expectedCounts);
110-
requestedPathCounts.clear();
111-
}
138+
void expectRequestCounts(Map<String, int> expectedCounts) =>
139+
_expectRequestCounts(expectedCounts, requestedPathCounts);
112140

113141
AppServer? server;
114-
Future<void> waitForAppToLoad(Map<String, int> waitForCounts) async {
115-
print('Waiting for app to load $waitForCounts');
116-
await Future.any(<Future<Object?>>[
117-
() async {
118-
while (!waitForCounts.entries.every((MapEntry<String, int> entry) => (requestedPathCounts[entry.key] ?? 0) >= entry.value)) {
119-
await Future<void>.delayed(const Duration(milliseconds: 100));
120-
}
121-
}(),
122-
server!.onChromeError.then((String error) {
123-
throw Exception('Chrome error: $error');
124-
}),
125-
]);
126-
}
142+
Future<void> waitForAppToLoad(Map<String, int> waitForCounts) async =>
143+
_waitForAppToLoad(waitForCounts, requestedPathCounts, server);
127144

128145
String? reportedVersion;
129146

@@ -174,7 +191,7 @@ Future<void> runWebServiceWorkerTest({
174191
/////
175192
// Attempt to load a different version of the service worker!
176193
/////
177-
await _rebuildApp(version: 1, testType: testType);
194+
await _rebuildApp(version: 1, testType: testType, target: _target);
178195

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

198-
await _rebuildApp(version: 2, testType: testType);
215+
await _rebuildApp(version: 2, testType: testType, target: _target);
199216

200217
await server!.chrome.reloadPage(ignoreCache: true);
201218
await waitForAppToLoad(<String, int>{
@@ -212,7 +229,7 @@ Future<void> runWebServiceWorkerTest({
212229
//////////////////////////////////////////////////////
213230
// Caching server
214231
//////////////////////////////////////////////////////
215-
await _rebuildApp(version: 1, testType: testType);
232+
await _rebuildApp(version: 1, testType: testType, target: _target);
216233

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

260278
print('With cache: test page reload after rebuild');
261-
await _rebuildApp(version: 2, testType: testType);
279+
await _rebuildApp(version: 2, testType: testType, target: _target);
262280

263281
// Since we're caching, we need to ignore cache when reloading the page.
264282
await server!.chrome.reloadPage(ignoreCache: true);
@@ -288,7 +306,7 @@ Future<void> runWebServiceWorkerTest({
288306
// Non-caching server
289307
//////////////////////////////////////////////////////
290308
print('No cache: test first page load');
291-
await _rebuildApp(version: 3, testType: testType);
309+
await _rebuildApp(version: 3, testType: testType, target: _target);
292310
await startAppServer(cacheControl: 'max-age=0');
293311
await waitForAppToLoad(<String, int>{
294312
'CLOSE': 1,
@@ -304,6 +322,7 @@ Future<void> runWebServiceWorkerTest({
304322
'assets/FontManifest.json': 2,
305323
'flutter_service_worker.js': 1,
306324
'assets/AssetManifest.json': 1,
325+
'assets/fonts/MaterialIcons-Regular.otf': 1,
307326
'CLOSE': 1,
308327
// In headless mode Chrome does not load 'manifest.json' and 'favicon.ico'.
309328
if (!headless)
@@ -329,6 +348,7 @@ Future<void> runWebServiceWorkerTest({
329348
if (shouldExpectFlutterJs)
330349
'flutter.js': 1,
331350
'flutter_service_worker.js': 1,
351+
'assets/fonts/MaterialIcons-Regular.otf': 1,
332352
'CLOSE': 1,
333353
if (!headless)
334354
'manifest.json': 1,
@@ -337,7 +357,7 @@ Future<void> runWebServiceWorkerTest({
337357
reportedVersion = null;
338358

339359
print('No cache: test page reload after rebuild');
340-
await _rebuildApp(version: 4, testType: testType);
360+
await _rebuildApp(version: 4, testType: testType, target: _target);
341361

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

383404
print('END runWebServiceWorkerTest(headless: $headless, testType: $testType)\n');
384405
}
406+
407+
Future<void> runWebServiceWorkerTestWithCachingResources({
408+
required bool headless,
409+
required ServiceWorkerTestType testType
410+
}) async {
411+
final Map<String, int> requestedPathCounts = <String, int>{};
412+
void expectRequestCounts(Map<String, int> expectedCounts) =>
413+
_expectRequestCounts(expectedCounts, requestedPathCounts);
414+
415+
AppServer? server;
416+
Future<void> waitForAppToLoad(Map<String, int> waitForCounts) async =>
417+
_waitForAppToLoad(waitForCounts, requestedPathCounts, server);
418+
419+
Future<void> startAppServer({
420+
required String cacheControl,
421+
}) async {
422+
final int serverPort = await findAvailablePort();
423+
final int browserDebugPort = await findAvailablePort();
424+
server = await AppServer.start(
425+
headless: headless,
426+
cacheControl: cacheControl,
427+
// TODO(yjbanov): use a better port disambiguation strategy than trying
428+
// to guess what ports other tests use.
429+
appUrl: 'http://localhost:$serverPort/index.html',
430+
serverPort: serverPort,
431+
browserDebugPort: browserDebugPort,
432+
appDirectory: _appBuildDirectory,
433+
additionalRequestHandlers: <Handler>[
434+
(Request request) {
435+
final String requestedPath = request.url.path;
436+
requestedPathCounts.putIfAbsent(requestedPath, () => 0);
437+
requestedPathCounts[requestedPath] = requestedPathCounts[requestedPath]! + 1;
438+
if (requestedPath == 'assets/fonts/MaterialIcons-Regular.otf') {
439+
return Response.internalServerError();
440+
}
441+
return Response.notFound('');
442+
},
443+
],
444+
);
445+
}
446+
447+
// Preserve old index.html as index_og.html so we can restore it later for other tests
448+
await runCommand(
449+
'mv',
450+
<String>[
451+
'index.html',
452+
'index_og.html',
453+
],
454+
workingDirectory: _testAppWebDirectory,
455+
);
456+
457+
final bool shouldExpectFlutterJs = testType != ServiceWorkerTestType.withoutFlutterJs;
458+
459+
print('BEGIN runWebServiceWorkerTestWithCachingResources(headless: $headless, testType: $testType)\n');
460+
461+
try {
462+
//////////////////////////////////////////////////////
463+
// Caching server
464+
//////////////////////////////////////////////////////
465+
await _rebuildApp(version: 1, testType: testType, target: _targetWithCachedResources);
466+
467+
print('With cache: test first page load');
468+
await startAppServer(cacheControl: 'max-age=3600');
469+
await waitForAppToLoad(<String, int>{
470+
'assets/fonts/MaterialIcons-Regular.otf': 1,
471+
'flutter_service_worker.js': 1,
472+
});
473+
474+
expectRequestCounts(<String, int>{
475+
// Even though the server is caching index.html is downloaded twice,
476+
// once by the initial page load, and once by the service worker.
477+
// Other resources are loaded once only by the service worker.
478+
'index.html': 2,
479+
if (shouldExpectFlutterJs)
480+
'flutter.js': 1,
481+
'main.dart.js': 1,
482+
'flutter_service_worker.js': 1,
483+
'assets/FontManifest.json': 1,
484+
'assets/AssetManifest.json': 1,
485+
'assets/fonts/MaterialIcons-Regular.otf': 1,
486+
// In headless mode Chrome does not load 'manifest.json' and 'favicon.ico'.
487+
if (!headless)
488+
...<String, int>{
489+
'manifest.json': 1,
490+
'favicon.ico': 1,
491+
},
492+
});
493+
494+
print('With cache: test first page reload');
495+
await server!.chrome.reloadPage();
496+
await waitForAppToLoad(<String, int>{
497+
'assets/fonts/MaterialIcons-Regular.otf': 1,
498+
'flutter_service_worker.js': 1,
499+
});
500+
expectRequestCounts(<String, int>{
501+
'assets/fonts/MaterialIcons-Regular.otf': 1,
502+
'flutter_service_worker.js': 1,
503+
});
504+
505+
print('With cache: test second page reload');
506+
await server!.chrome.reloadPage();
507+
await waitForAppToLoad(<String, int>{
508+
'assets/fonts/MaterialIcons-Regular.otf': 1,
509+
'flutter_service_worker.js': 1,
510+
});
511+
expectRequestCounts(<String, int>{
512+
'assets/fonts/MaterialIcons-Regular.otf': 1,
513+
'flutter_service_worker.js': 1,
514+
});
515+
516+
print('With cache: test third page reload');
517+
await server!.chrome.reloadPage();
518+
await waitForAppToLoad(<String, int>{
519+
'assets/fonts/MaterialIcons-Regular.otf': 1,
520+
'flutter_service_worker.js': 1,
521+
});
522+
expectRequestCounts(<String, int>{
523+
'assets/fonts/MaterialIcons-Regular.otf': 1,
524+
'flutter_service_worker.js': 1,
525+
});
526+
527+
print('With cache: test page reload after rebuild');
528+
await _rebuildApp(version: 1, testType: testType, target: _targetWithCachedResources);
529+
530+
// Since we're caching, we need to ignore cache when reloading the page.
531+
await server!.chrome.reloadPage(ignoreCache: true);
532+
await waitForAppToLoad(<String, int>{
533+
'assets/fonts/MaterialIcons-Regular.otf': 1,
534+
'flutter_service_worker.js': 1,
535+
});
536+
expectRequestCounts(<String, int>{
537+
'index.html': 2,
538+
if (shouldExpectFlutterJs)
539+
'flutter.js': 1,
540+
'main.dart.js': 1,
541+
'flutter_service_worker.js': 2,
542+
'assets/FontManifest.json': 1,
543+
'assets/AssetManifest.json': 1,
544+
'assets/fonts/MaterialIcons-Regular.otf': 1,
545+
// In headless mode Chrome does not load 'manifest.json' and 'favicon.ico'.
546+
if (!headless)
547+
...<String, int>{
548+
'favicon.ico': 1,
549+
},
550+
});
551+
} finally {
552+
await runCommand(
553+
'mv',
554+
<String>[
555+
'index_og.html',
556+
'index.html',
557+
],
558+
workingDirectory: _testAppWebDirectory,
559+
);
560+
await server?.stop();
561+
}
562+
563+
print('END runWebServiceWorkerTestWithCachingResources(headless: $headless, testType: $testType)\n');
564+
}

dev/bots/test.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1083,6 +1083,9 @@ Future<void> _runWebLongRunningTests() async {
10831083
() => runWebServiceWorkerTest(headless: true, testType: ServiceWorkerTestType.withoutFlutterJs),
10841084
() => runWebServiceWorkerTest(headless: true, testType: ServiceWorkerTestType.withFlutterJs),
10851085
() => runWebServiceWorkerTest(headless: true, testType: ServiceWorkerTestType.withFlutterJsShort),
1086+
() => runWebServiceWorkerTestWithCachingResources(headless: true, testType: ServiceWorkerTestType.withoutFlutterJs),
1087+
() => runWebServiceWorkerTestWithCachingResources(headless: true, testType: ServiceWorkerTestType.withFlutterJs),
1088+
() => runWebServiceWorkerTestWithCachingResources(headless: true, testType: ServiceWorkerTestType.withFlutterJsShort),
10861089
() => _runWebStackTraceTest('profile', 'lib/stack_trace.dart'),
10871090
() => _runWebStackTraceTest('release', 'lib/stack_trace.dart'),
10881091
() => _runWebStackTraceTest('profile', 'lib/framework_stack_trace.dart'),
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Copyright 2014 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'package:flutter/material.dart';
6+
7+
Future<void> main() async {
8+
runApp(Scaffold(
9+
body: Center(
10+
child: Column(
11+
children: const <Widget>[
12+
Icon(Icons.ac_unit),
13+
Text('Hello, World', textDirection: TextDirection.ltr),
14+
],
15+
),
16+
),
17+
));
18+
}

dev/integration_tests/web/pubspec.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ flutter:
88
assets:
99
- lib/a.dart
1010
- lib/b.dart
11+
uses-material-design: true
1112

1213
dependencies:
1314
flutter:

0 commit comments

Comments
 (0)