flutter/dev/bots/service_worker_test.dart
Jackson Gardner 3c30e3cb20
Flutter Web Bootstrapping Improvements (#144434)
This makes several changes to flutter web app bootstrapping.
* The build now produces a `flutter_bootstrap.js` file.
  * By default, this file does the basic streamlined startup of a flutter app with the service worker settings and no user configuration.
  * The user can also put a `flutter_bootstrap.js` file in the `web` subdirectory in the project directory which can have whatever custom bootstrapping logic they'd like to write instead. This file is also templated, and can use any of the tokens  that can be used with the `index.html` (with the exception of `{{flutter_bootstrap_js}}`, see below).
* Introduced a few new templating tokens for `index.html`:
  * `{{flutter_js}}` => inlines the entirety of `flutter.js`
  * `{{flutter_service_worker_version}}` => replaced directly by the service worker version. This can be used instead of the script that sets the `serviceWorkerVersion` local variable that we used to have by default.
  * `{{flutter_bootstrap_js}}` => inlines the entirety of `flutter_bootstrap.js` (this token obviously doesn't apply to `flutter_bootstrap.js` itself).
* Changed `IndexHtml` to be called `WebTemplate` instead, since it is used for more than just the index.html now.
* We now emit warnings at build time for certain deprecated flows:
  * Warn on the old service worker version pattern (i.e.`(const|var) serviceWorkerVersion = null`) and recommends using `{{flutter_service_worker_version}}` token instead
  * Warn on use of `FlutterLoader.loadEntrypoint` and recommend using `FlutterLoader.load` instead
  * Warn on manual loading of `flutter_service_worker.js`.
* The default `index.html` on `flutter create` now uses an async script tag with `flutter_bootstrap.js`.
2024-03-12 22:41:26 +00:00

850 lines
30 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' hide exit;
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 _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 _targetWithBlockedServiceWorkers = path.join('lib', 'service_worker_test_blocked_service_workers.dart');
final String _targetPath = path.join(_testAppDirectory, _target);
enum ServiceWorkerTestType {
// Mocks how FF disables service workers.
blockedServiceWorkers,
// Drops the main.dart.js directly on the page.
withoutFlutterJs,
// Uses the standard, promise-based, flutterJS initialization.
withFlutterJs,
// Uses the shorthand engineInitializer.autoStart();
withFlutterJsShort,
// Uses onEntrypointLoaded callback instead of returned promise.
withFlutterJsEntrypointLoadedEvent,
// Same as withFlutterJsEntrypointLoadedEvent, but with TrustedTypes enabled.
withFlutterJsTrustedTypesOn,
// Same as withFlutterJsEntrypointLoadedEvent, but with nonce required.
withFlutterJsNonceOn,
// Uses custom serviceWorkerVersion.
withFlutterJsCustomServiceWorkerVersion,
// Entrypoint generated by `flutter create`.
generatedEntrypoint,
}
// Run a web service worker test as a standalone Dart program.
Future<void> main() async {
// When updating this list, also update `dev/bots/test.dart`. This `main()`
// function is only here for convenience. Adding tests here will not add them
// to LUCI.
await runWebServiceWorkerTest(headless: false, testType: ServiceWorkerTestType.withoutFlutterJs);
await runWebServiceWorkerTest(headless: false, testType: ServiceWorkerTestType.withFlutterJs);
await runWebServiceWorkerTest(headless: false, testType: ServiceWorkerTestType.withFlutterJsShort);
await runWebServiceWorkerTest(headless: false, testType: ServiceWorkerTestType.withFlutterJsEntrypointLoadedEvent);
await runWebServiceWorkerTest(headless: false, testType: ServiceWorkerTestType.withFlutterJsTrustedTypesOn);
await runWebServiceWorkerTest(headless: false, testType: ServiceWorkerTestType.withFlutterJsNonceOn);
await runWebServiceWorkerTestWithCachingResources(headless: false, testType: ServiceWorkerTestType.withoutFlutterJs);
await runWebServiceWorkerTestWithCachingResources(headless: false, testType: ServiceWorkerTestType.withFlutterJs);
await runWebServiceWorkerTestWithCachingResources(headless: false, testType: ServiceWorkerTestType.withFlutterJsShort);
await runWebServiceWorkerTestWithCachingResources(headless: false, testType: ServiceWorkerTestType.withFlutterJsEntrypointLoadedEvent);
await runWebServiceWorkerTestWithCachingResources(headless: false, testType: ServiceWorkerTestType.withFlutterJsTrustedTypesOn);
await runWebServiceWorkerTestWithGeneratedEntrypoint(headless: false);
await runWebServiceWorkerTestWithBlockedServiceWorkers(headless: false);
await runWebServiceWorkerTestWithCustomServiceWorkerVersion(headless: false);
if (hasError) {
reportErrorsAndExit('${bold}One or more tests failed.$reset');
}
reportSuccessAndExit('${bold}Tests successful.$reset');
}
// Regression test for https://github.com/flutter/flutter/issues/109093.
//
// Tests the entrypoint that's generated by `flutter create`.
Future<void> runWebServiceWorkerTestWithGeneratedEntrypoint({
required bool headless,
}) async {
await _generateEntrypoint();
await runWebServiceWorkerTestWithCachingResources(headless: headless, testType: ServiceWorkerTestType.generatedEntrypoint);
}
Future<void> _generateEntrypoint() async {
final Directory tempDirectory = Directory.systemTemp.createTempSync('flutter_web_generated_entrypoint.');
await runCommand(
_flutter,
<String>[ 'create', 'generated_entrypoint_test' ],
workingDirectory: tempDirectory.path,
);
final File generatedEntrypoint = File(path.join(tempDirectory.path, 'generated_entrypoint_test', 'web', 'index.html'));
final String generatedEntrypointCode = generatedEntrypoint.readAsStringSync();
final File testEntrypoint = File(path.join(
_testAppWebDirectory,
_testTypeToIndexFile(ServiceWorkerTestType.generatedEntrypoint),
));
testEntrypoint.writeAsStringSync(generatedEntrypointCode);
tempDirectory.deleteSync(recursive: true);
}
Future<void> _setAppVersion(int version) async {
final File targetFile = File(_targetPath);
await targetFile.writeAsString(
(await targetFile.readAsString()).replaceFirst(
RegExp(r'CLOSE\?version=\d+'),
'CLOSE?version=$version',
)
);
}
String _testTypeToIndexFile(ServiceWorkerTestType type) {
return switch (type) {
ServiceWorkerTestType.blockedServiceWorkers => 'index_with_blocked_service_workers.html',
ServiceWorkerTestType.withFlutterJs => 'index_with_flutterjs.html',
ServiceWorkerTestType.withoutFlutterJs => 'index_without_flutterjs.html',
ServiceWorkerTestType.withFlutterJsShort => 'index_with_flutterjs_short.html',
ServiceWorkerTestType.withFlutterJsEntrypointLoadedEvent => 'index_with_flutterjs_entrypoint_loaded.html',
ServiceWorkerTestType.withFlutterJsTrustedTypesOn => 'index_with_flutterjs_el_tt_on.html',
ServiceWorkerTestType.withFlutterJsNonceOn => 'index_with_flutterjs_el_nonce.html',
ServiceWorkerTestType.withFlutterJsCustomServiceWorkerVersion => 'index_with_flutterjs_custom_sw_version.html',
ServiceWorkerTestType.generatedEntrypoint => 'generated_entrypoint.html',
};
}
Future<void> _rebuildApp({ required int version, required ServiceWorkerTestType testType, required String target }) async {
await _setAppVersion(version);
await runCommand(
_flutter,
<String>[ 'clean' ],
workingDirectory: _testAppDirectory,
);
await runCommand(
'cp',
<String>[
_testTypeToIndexFile(testType),
'index.html',
],
workingDirectory: _testAppWebDirectory,
);
await runCommand(
_flutter,
<String>['build', 'web', '--web-resources-cdn', '--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 {
int tries = 1;
while (!waitForCounts.entries.every((MapEntry<String, int> entry) => (requestedPathCounts[entry.key] ?? 0) >= entry.value)) {
if (tries++ % 20 == 0) {
print('Still waiting. Requested so far: $requestedPathCounts');
}
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) {
final Matcher matcher = wrapMatcher(expected);
// matchState needs to be of type <Object?, Object?>, see https://github.com/flutter/flutter/issues/99522
final Map<Object?, Object?> matchState = <Object?, Object?>{};
if (matcher.matches(actual, matchState)) {
return;
}
final StringDescription mismatchDescription = StringDescription();
matcher.describeMismatch(actual, mismatchDescription, matchState, true);
throw TestFailure(mismatchDescription.toString());
}
Future<void> runWebServiceWorkerTest({
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);
String? reportedVersion;
Future<void> startAppServer({
required String cacheControl,
}) async {
final int serverPort = await findAvailablePortAndPossiblyCauseFlakyTests();
final int browserDebugPort = await findAvailablePortAndPossiblyCauseFlakyTests();
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 == 'CLOSE') {
reportedVersion = request.url.queryParameters['version'];
return Response.ok('OK');
}
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 runWebServiceWorkerTest(headless: $headless, testType: $testType)');
try {
/////
// Attempt to load a different version of the service worker!
/////
await _rebuildApp(version: 1, testType: testType, target: _target);
print('Call update() on the current web worker');
await startAppServer(cacheControl: 'max-age=0');
await waitForAppToLoad(<String, int> {
if (shouldExpectFlutterJs)
'flutter.js': 1,
'CLOSE': 1,
});
expect(reportedVersion, '1');
reportedVersion = null;
await server!.chrome.reloadPage(ignoreCache: true);
await waitForAppToLoad(<String, int> {
if (shouldExpectFlutterJs)
'flutter.js': 2,
'CLOSE': 2,
});
expect(reportedVersion, '1');
reportedVersion = null;
await _rebuildApp(version: 2, testType: testType, target: _target);
await server!.chrome.reloadPage(ignoreCache: true);
await waitForAppToLoad(<String, int>{
if (shouldExpectFlutterJs)
'flutter.js': 3,
'CLOSE': 3,
});
expect(reportedVersion, '2');
reportedVersion = null;
requestedPathCounts.clear();
await server!.stop();
//////////////////////////////////////////////////////
// Caching server
//////////////////////////////////////////////////////
await _rebuildApp(version: 1, testType: testType, target: _target);
print('With cache: test first page load');
await startAppServer(cacheControl: 'max-age=3600');
await waitForAppToLoad(<String, int>{
'CLOSE': 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,
'flutter_bootstrap.js': 1,
'assets/FontManifest.json': 1,
'assets/AssetManifest.bin.json': 1,
'assets/fonts/MaterialIcons-Regular.otf': 1,
'CLOSE': 1,
// In headless mode Chrome does not load 'manifest.json' and 'favicon.png'.
if (!headless)
...<String, int>{
'manifest.json': 1,
'favicon.png': 1,
},
});
expect(reportedVersion, '1');
reportedVersion = null;
print('With cache: test page reload');
await server!.chrome.reloadPage();
await waitForAppToLoad(<String, int>{
'CLOSE': 1,
'flutter_service_worker.js': 1,
});
expectRequestCounts(<String, int>{
'flutter_service_worker.js': 1,
'CLOSE': 1,
});
expect(reportedVersion, '1');
reportedVersion = null;
print('With cache: test page reload after rebuild');
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);
await waitForAppToLoad(<String, int>{
'CLOSE': 1,
'flutter_service_worker.js': 2,
});
expectRequestCounts(<String, int>{
'index.html': 2,
if (shouldExpectFlutterJs)
'flutter.js': 1,
'flutter_service_worker.js': 2,
'flutter_bootstrap.js': 1,
'main.dart.js': 1,
'assets/AssetManifest.bin.json': 1,
'assets/FontManifest.json': 1,
'CLOSE': 1,
if (!headless)
'favicon.png': 1,
});
expect(reportedVersion, '2');
reportedVersion = null;
await server!.stop();
//////////////////////////////////////////////////////
// Non-caching server
//////////////////////////////////////////////////////
print('No cache: test first page load');
await _rebuildApp(version: 3, testType: testType, target: _target);
await startAppServer(cacheControl: 'max-age=0');
await waitForAppToLoad(<String, int>{
'CLOSE': 1,
'flutter_service_worker.js': 1,
});
expectRequestCounts(<String, int>{
'index.html': 2,
if (shouldExpectFlutterJs)
'flutter.js': 1,
'main.dart.js': 1,
'assets/FontManifest.json': 1,
'flutter_service_worker.js': 1,
'flutter_bootstrap.js': 1,
'assets/AssetManifest.bin.json': 1,
'assets/fonts/MaterialIcons-Regular.otf': 1,
'CLOSE': 1,
// In headless mode Chrome does not load 'manifest.json' and 'favicon.png'.
if (!headless)
...<String, int>{
'manifest.json': 1,
'favicon.png': 1,
},
});
expect(reportedVersion, '3');
reportedVersion = null;
print('No cache: test page reload');
await server!.chrome.reloadPage();
await waitForAppToLoad(<String, int>{
'CLOSE': 1,
if (shouldExpectFlutterJs)
'flutter.js': 1,
'flutter_service_worker.js': 1,
});
expectRequestCounts(<String, int>{
if (shouldExpectFlutterJs)
'flutter.js': 1,
'flutter_service_worker.js': 1,
'CLOSE': 1,
if (!headless)
'manifest.json': 1,
});
expect(reportedVersion, '3');
reportedVersion = null;
print('No cache: test page reload after rebuild');
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
// refresh when running Chrome manually as normal. At the time of writing
// this test I wasn't able to figure out what's wrong with the way we run
// Chrome from tests.
await server!.chrome.reloadPage(ignoreCache: true);
await waitForAppToLoad(<String, int>{
'CLOSE': 1,
'flutter_service_worker.js': 1,
});
expectRequestCounts(<String, int>{
'index.html': 2,
if (shouldExpectFlutterJs)
'flutter.js': 1,
'flutter_service_worker.js': 2,
'flutter_bootstrap.js': 1,
'main.dart.js': 1,
'assets/AssetManifest.bin.json': 1,
'assets/FontManifest.json': 1,
'CLOSE': 1,
if (!headless)
...<String, int>{
'manifest.json': 1,
'favicon.png': 1,
},
});
expect(reportedVersion, '4');
reportedVersion = null;
} finally {
await runCommand(
'mv',
<String>[
'index_og.html',
'index.html',
],
workingDirectory: _testAppWebDirectory,
);
await _setAppVersion(1);
await server?.stop();
}
print('END runWebServiceWorkerTest(headless: $headless, testType: $testType)');
}
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 findAvailablePortAndPossiblyCauseFlakyTests();
final int browserDebugPort = await findAvailablePortAndPossiblyCauseFlakyTests();
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 usesFlutterBootstrapJs = testType == ServiceWorkerTestType.generatedEntrypoint;
final bool shouldExpectFlutterJs = !usesFlutterBootstrapJs && testType != ServiceWorkerTestType.withoutFlutterJs;
print('BEGIN runWebServiceWorkerTestWithCachingResources(headless: $headless, testType: $testType)');
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,
'flutter_bootstrap.js': usesFlutterBootstrapJs ? 2 : 1,
'assets/FontManifest.json': 1,
'assets/AssetManifest.bin.json': 1,
'assets/fonts/MaterialIcons-Regular.otf': 1,
// In headless mode Chrome does not load 'manifest.json' and 'favicon.png'.
if (!headless)
...<String, int>{
'manifest.json': 1,
'favicon.png': 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,
'flutter_bootstrap.js': usesFlutterBootstrapJs ? 2 : 1,
'assets/FontManifest.json': 1,
'assets/AssetManifest.bin.json': 1,
'assets/fonts/MaterialIcons-Regular.otf': 1,
// In headless mode Chrome does not load 'manifest.json' and 'favicon.png'.
if (!headless)
...<String, int>{
'favicon.png': 1,
},
});
} finally {
await runCommand(
'mv',
<String>[
'index_og.html',
'index.html',
],
workingDirectory: _testAppWebDirectory,
);
await server?.stop();
}
print('END runWebServiceWorkerTestWithCachingResources(headless: $headless, testType: $testType)');
}
Future<void> runWebServiceWorkerTestWithBlockedServiceWorkers({
required bool headless
}) 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 findAvailablePortAndPossiblyCauseFlakyTests();
final int browserDebugPort = await findAvailablePortAndPossiblyCauseFlakyTests();
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 == 'CLOSE') {
return Response.ok('OK');
}
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,
);
print('BEGIN runWebServiceWorkerTestWithBlockedServiceWorkers(headless: $headless)');
try {
await _rebuildApp(version: 1, testType: ServiceWorkerTestType.blockedServiceWorkers, target: _targetWithBlockedServiceWorkers);
print('Ensure app starts (when service workers are blocked)');
await startAppServer(cacheControl: 'max-age=3600');
await waitForAppToLoad(<String, int>{
'CLOSE': 1,
});
expectRequestCounts(<String, int>{
'index.html': 1,
'flutter.js': 1,
'main.dart.js': 1,
'assets/FontManifest.json': 1,
'assets/fonts/MaterialIcons-Regular.otf': 1,
'CLOSE': 1,
// In headless mode Chrome does not load 'manifest.json' and 'favicon.png'.
if (!headless)
...<String, int>{
'manifest.json': 1,
'favicon.png': 1,
},
});
} finally {
await runCommand(
'mv',
<String>[
'index_og.html',
'index.html',
],
workingDirectory: _testAppWebDirectory,
);
await server?.stop();
}
print('END runWebServiceWorkerTestWithBlockedServiceWorkers(headless: $headless)');
}
/// Regression test for https://github.com/flutter/flutter/issues/130212.
Future<void> runWebServiceWorkerTestWithCustomServiceWorkerVersion({
required bool headless,
}) 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 findAvailablePortAndPossiblyCauseFlakyTests();
final int browserDebugPort = await findAvailablePortAndPossiblyCauseFlakyTests();
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 == 'CLOSE') {
return Response.ok('OK');
}
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,
);
print('BEGIN runWebServiceWorkerTestWithCustomServiceWorkerVersion(headless: $headless)');
try {
await _rebuildApp(version: 1, testType: ServiceWorkerTestType.withFlutterJsCustomServiceWorkerVersion, target: _target);
print('Test page load');
await startAppServer(cacheControl: 'max-age=0');
await waitForAppToLoad(<String, int>{
'CLOSE': 1,
'flutter_service_worker.js': 1,
'assets/fonts/MaterialIcons-Regular.otf': 1,
});
expectRequestCounts(<String, int>{
'index.html': 2,
'flutter.js': 1,
'main.dart.js': 1,
'CLOSE': 1,
'flutter_service_worker.js': 1,
'flutter_bootstrap.js': 1,
'assets/FontManifest.json': 1,
'assets/AssetManifest.bin.json': 1,
'assets/fonts/MaterialIcons-Regular.otf': 1,
// In headless mode Chrome does not load 'manifest.json' and 'favicon.png'.
if (!headless)
...<String, int>{
'manifest.json': 1,
'favicon.png': 1,
},
});
print('Test page reload, ensure service worker is not reloaded');
await server!.chrome.reloadPage(ignoreCache: true);
await waitForAppToLoad(<String, int>{
'CLOSE': 1,
'flutter.js': 1,
});
expectRequestCounts(<String, int>{
'index.html': 1,
'flutter.js': 1,
'main.dart.js': 1,
'assets/FontManifest.json': 1,
'assets/fonts/MaterialIcons-Regular.otf': 1,
'CLOSE': 1,
// In headless mode Chrome does not load 'manifest.json' and 'favicon.png'.
if (!headless)
...<String, int>{
'manifest.json': 1,
'favicon.png': 1,
},
});
print('Test page reload after rebuild, ensure service worker is not reloaded');
await _rebuildApp(version: 1, testType: ServiceWorkerTestType.withFlutterJsCustomServiceWorkerVersion, target: _target);
await server!.chrome.reloadPage(ignoreCache: true);
await waitForAppToLoad(<String, int>{
'CLOSE': 1,
'flutter.js': 1,
});
expectRequestCounts(<String, int>{
'index.html': 1,
'flutter.js': 1,
'main.dart.js': 1,
'assets/FontManifest.json': 1,
'assets/fonts/MaterialIcons-Regular.otf': 1,
'CLOSE': 1,
// In headless mode Chrome does not load 'manifest.json' and 'favicon.png'.
if (!headless)
...<String, int>{
'manifest.json': 1,
'favicon.png': 1,
},
});
} finally {
await runCommand(
'mv',
<String>[
'index_og.html',
'index.html',
],
workingDirectory: _testAppWebDirectory,
);
await server?.stop();
}
print('END runWebServiceWorkerTestWithCustomServiceWorkerVersion(headless: $headless)');
}