flutter_flutter/dev/bots/service_worker_test.dart
Kate Lovett 9d96df2364
Modernize framework lints (#179089)
WIP

Commits separated as follows:
- Update lints in analysis_options files
- Run `dart fix --apply`
- Clean up leftover analysis issues 
- Run `dart format .` in the right places.

Local analysis and testing passes. Checking CI now.

Part of https://github.com/flutter/flutter/issues/178827
- Adoption of flutter_lints in examples/api coming in a separate change
(cc @loic-sharma)

## Pre-launch Checklist

- [ ] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [ ] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [ ] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [ ] I signed the [CLA].
- [ ] I listed at least one issue that this PR fixes in the description
above.
- [ ] I updated/added relevant documentation (doc comments with `///`).
- [ ] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [ ] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [ ] All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel
on [Discord].

**Note**: The Flutter team is currently trialing the use of [Gemini Code
Assist for
GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code).
Comments from the `gemini-code-assist` bot should not be taken as
authoritative feedback from the Flutter team. If you find its comments
useful you can update your code accordingly, but if you are unsure or
disagree with the feedback, please feel free to wait for a Flutter team
member's review for guidance on which automated comments should be
addressed.

<!-- Links -->
[Contributor Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview
[Tree Hygiene]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md
[test-exempt]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests
[Flutter Style Guide]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md
[Features we expect every widget to implement]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement
[CLA]: https://cla.developers.google.com/
[flutter/tests]: https://github.com/flutter/tests
[breaking change policy]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes
[Discord]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md
[Data Driven Fixes]:
https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
2025-11-26 01:10:39 +00:00

207 lines
7.3 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';
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();
}