Skip to content

Commit 40b2a26

Browse files
authored
Add an integration test for the Network screen (#9003)
There's a lot to this change! I wanted precise control over when the "test app" makes requests, and what kind, etc. I've added a capability for a Dart test app to print out what port it's "control server" is listening to, very similar to how it prints out the URL for DevTools to connect to. The "control server" is a basic HTTP server, that can be controlled with very simple requests. In this case, when the server sees a request to '/get/', it calls a dart:io HttpClient to send an HTTP GET request. A request to '/post/' directs the HttpClient to send an HTTP POST request, etc. For the network screen tests to see complete requests, there is a second HTTP server that responds to the test requests. So some changes are made to TestDartCliApp to support the "control port". This int is then passed to the integration test in the 'test args' (see the changes to integration_test/test_infra/run/run_test.dart). OK, then the test itself: This test is simple for now. The following types of requests are validated: * dart:io HttpClient GET * dart:io HttpClient POST * dart:io HttpClient PUT * dart:io HttpClient DELETE * Dio GET * Dio POST There are some TODOs for other requests we want to validate. Work towards #7554.
1 parent fc85a56 commit 40b2a26

6 files changed

Lines changed: 364 additions & 26 deletions

File tree

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
// Copyright 2025 The Flutter Authors
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd.
4+
5+
// Do not delete these arguments. They are parsed by test runner.
6+
// test-argument:appPath="test/test_infra/fixtures/networking_app/bin/main.dart"
7+
8+
import 'package:devtools_app/devtools_app.dart';
9+
import 'package:devtools_app/src/shared/table/table.dart' show DevToolsTable;
10+
import 'package:devtools_test/helpers.dart';
11+
import 'package:devtools_test/integration_test.dart';
12+
import 'package:flutter_test/flutter_test.dart';
13+
import 'package:http/http.dart' as http;
14+
import 'package:integration_test/integration_test.dart';
15+
16+
// To run:
17+
// dart run integration_test/run_tests.dart --target=integration_test/test/live_connection/network_screen_test.dart --test-app-device=cli
18+
19+
void main() {
20+
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
21+
22+
late TestApp testApp;
23+
24+
setUpAll(() {
25+
testApp = TestApp.fromEnvironment();
26+
expect(testApp.vmServiceUri, isNotNull);
27+
});
28+
29+
tearDown(() async {
30+
await resetHistory();
31+
await http.get(Uri.parse('http://localhost:${testApp.controlPort}/exit/'));
32+
});
33+
34+
testWidgets('nnn', (tester) async {
35+
await pumpAndConnectDevTools(tester, testApp);
36+
await _prepareNetworkScreen(tester);
37+
38+
final helper = _NetworkScreenHelper(tester, testApp.controlPort!);
39+
40+
// Instruct the app to make a GET request via the dart:io HttpClient.
41+
await helper.triggerRequest('get/');
42+
_expectInRequestTable('GET');
43+
await helper.clear();
44+
45+
// Instruct the app to make a POST request via the dart:io HttpClient.
46+
await helper.triggerRequest('post/');
47+
_expectInRequestTable('POST');
48+
await helper.clear();
49+
50+
// Instruct the app to make a PUT request via the dart:io HttpClient.
51+
await helper.triggerRequest('put/');
52+
_expectInRequestTable('PUT');
53+
await helper.clear();
54+
55+
// Instruct the app to make a DELETE request via the dart:io HttpClient.
56+
await helper.triggerRequest('delete/');
57+
_expectInRequestTable('DELETE');
58+
await helper.clear();
59+
60+
// Instruct the app to make a GET request via Dio.
61+
await helper.triggerRequest('dio/get/');
62+
_expectInRequestTable('GET');
63+
await helper.clear();
64+
65+
// Instruct the app to make a POST request via Dio.
66+
await helper.triggerRequest('dio/post/');
67+
_expectInRequestTable('POST');
68+
});
69+
}
70+
71+
final class _NetworkScreenHelper {
72+
_NetworkScreenHelper(this._tester, this._controlPort);
73+
74+
final WidgetTester _tester;
75+
76+
final int _controlPort;
77+
78+
Future<void> clear() async {
79+
// Press the 'Clear' button between tests.
80+
await _tester.tap(find.text('Clear'));
81+
await _tester.pump(safePumpDuration);
82+
}
83+
84+
Future<void> triggerRequest(String path) async {
85+
await http.get(Uri.parse('http://localhost:$_controlPort/$path'));
86+
await Future.delayed(const Duration(milliseconds: 200));
87+
await _tester.pump(safePumpDuration);
88+
}
89+
}
90+
91+
void _expectInRequestTable(String text) {
92+
expect(
93+
find.descendant(
94+
of: find.byType(DevToolsTable<NetworkRequest>),
95+
matching: find.text(text),
96+
),
97+
findsOneWidget,
98+
);
99+
}
100+
101+
/// Prepares the UI of the network screen for an integration test.
102+
Future<void> _prepareNetworkScreen(WidgetTester tester) async {
103+
await switchToScreen(
104+
tester,
105+
tabIcon: ScreenMetaData.network.icon,
106+
tabIconAsset: ScreenMetaData.network.iconAsset,
107+
screenId: ScreenMetaData.network.id,
108+
);
109+
}

packages/devtools_app/integration_test/test_infra/run/_test_app_driver.dart

Lines changed: 49 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -39,25 +39,23 @@ class TestFlutterApp extends IntegrationTestApp {
3939
Future<void> waitForAppStart() async {
4040
// Set this up now, but we don't await it yet. We want to make sure we don't
4141
// miss it while waiting for debugPort below.
42-
final started = waitFor(
42+
final started = _waitFor(
4343
event: FlutterDaemonConstants.appStartedKey,
4444
timeout: IntegrationTestApp._appStartTimeout,
4545
);
4646

47-
final debugPort = await waitFor(
47+
final debugPort = await _waitFor(
4848
event: FlutterDaemonConstants.appDebugPortKey,
4949
timeout: IntegrationTestApp._appStartTimeout,
5050
);
5151
final wsUriString =
5252
(debugPort[FlutterDaemonConstants.paramsKey]!
5353
as Map<String, Object?>)[FlutterDaemonConstants.wsUriKey]
5454
as String;
55-
_vmServiceWsUri = Uri.parse(wsUriString);
55+
final vmServiceWsUri = Uri.parse(wsUriString);
5656

5757
// Map to WS URI.
58-
_vmServiceWsUri = convertToWebSocketUrl(
59-
serviceProtocolUrl: _vmServiceWsUri,
60-
);
58+
_vmServiceWsUri = convertToWebSocketUrl(serviceProtocolUrl: vmServiceWsUri);
6159

6260
// Now await the started event; if it had already happened the future will
6361
// have already completed.
@@ -101,7 +99,7 @@ class TestFlutterApp extends IntegrationTestApp {
10199
// Set up the response future before we send the request to avoid any
102100
// races. If the method we're calling is app.stop then we tell waitFor not
103101
// to throw if it sees an app.stop event before the response to this request.
104-
final responseFuture = waitFor(
102+
final responseFuture = _waitFor(
105103
id: requestId,
106104
ignoreAppStopEvent: method == 'app.stop',
107105
);
@@ -113,7 +111,7 @@ class TestFlutterApp extends IntegrationTestApp {
113111
}
114112
}
115113

116-
Future<Map<String, Object?>> waitFor({
114+
Future<Map<String, Object?>> _waitFor({
117115
String? event,
118116
int? id,
119117
Duration? timeout,
@@ -199,6 +197,10 @@ class TestDartCliApp extends IntegrationTestApp {
199197
: super(appPath, TestAppDevice.cli);
200198

201199
static const vmServicePrefix = 'The Dart VM service is listening on ';
200+
static const controlPortKey = 'controlPort';
201+
202+
int? get controlPort => _controlPort;
203+
late final int? _controlPort;
202204

203205
@override
204206
Future<void> startProcess() async {
@@ -215,20 +217,32 @@ class TestDartCliApp extends IntegrationTestApp {
215217

216218
@override
217219
Future<void> waitForAppStart() async {
218-
final vmServiceUri = await waitFor(
220+
final vmServiceUriString = await _waitFor(
219221
message: vmServicePrefix,
220222
timeout: IntegrationTestApp._appStartTimeout,
221223
);
222-
final parsedVmServiceUri = Uri.parse(vmServiceUri);
224+
final vmServiceUri = Uri.parse(vmServiceUriString);
225+
_controlPort = await _waitFor(
226+
message: controlPortKey,
227+
timeout: const Duration(seconds: 1),
228+
optional: true,
229+
);
223230

224231
// Map to WS URI.
225-
_vmServiceWsUri = convertToWebSocketUrl(
226-
serviceProtocolUrl: parsedVmServiceUri,
227-
);
232+
_vmServiceWsUri = convertToWebSocketUrl(serviceProtocolUrl: vmServiceUri);
228233
}
229234

230-
Future<String> waitFor({required String message, Duration? timeout}) {
231-
final response = Completer<String>();
235+
/// Waits for [message] to appear on stdout.
236+
///
237+
/// After [timeout], if no such message has appeared, then either `null` is
238+
/// returned, if [optional] is `true`, or an exception is thrown, if
239+
/// [optional] is `false`.
240+
Future<T> _waitFor<T>({
241+
required String message,
242+
Duration? timeout,
243+
bool optional = false,
244+
}) {
245+
final response = Completer<T>();
232246
late StreamSubscription<String> sub;
233247
sub = stdoutController.stream.listen(
234248
(String line) => _handleStdout(
@@ -239,25 +253,38 @@ class TestDartCliApp extends IntegrationTestApp {
239253
),
240254
);
241255

242-
return _timeoutWithMessages<String>(
256+
if (optional) {
257+
return response.future
258+
.timeout(
259+
timeout ?? IntegrationTestApp._defaultTimeout,
260+
onTimeout: () => null as T,
261+
)
262+
.whenComplete(() => sub.cancel());
263+
}
264+
265+
return _timeoutWithMessages<T>(
243266
() => response.future,
244267
timeout: timeout,
245268
message: 'Did not receive expected message: $message.',
246269
).whenComplete(() => sub.cancel());
247270
}
248271

249-
void _handleStdout(
272+
void _handleStdout<T>(
250273
String line, {
251274
required StreamSubscription<String> subscription,
252-
required Completer<String> response,
275+
required Completer<T> response,
253276
required String message,
254277
}) async {
255278
if (message == vmServicePrefix && line.startsWith(vmServicePrefix)) {
256279
final vmServiceUri = line.substring(
257280
line.indexOf(vmServicePrefix) + vmServicePrefix.length,
258281
);
259282
await subscription.cancel();
260-
response.complete(vmServiceUri);
283+
response.complete(vmServiceUri as T);
284+
} else if (message == controlPortKey && line.contains(controlPortKey)) {
285+
final asJson = jsonDecode(line) as Map;
286+
await subscription.cancel();
287+
response.complete(asJson[controlPortKey] as T);
261288
}
262289
}
263290
}
@@ -286,7 +313,7 @@ abstract class IntegrationTestApp with IOMixin {
286313
final _allMessages = StreamController<String>.broadcast();
287314

288315
Uri get vmServiceUri => _vmServiceWsUri;
289-
late Uri _vmServiceWsUri;
316+
late final Uri _vmServiceWsUri;
290317

291318
Future<void> startProcess();
292319

@@ -416,9 +443,10 @@ enum TestAppDevice {
416443

417444
/// A mapping of test app device to the unsupported tests for that device.
418445
static final _unsupportedTestsForDevice = <TestAppDevice, List<String>>{
419-
TestAppDevice.flutterTester: [],
446+
TestAppDevice.flutterTester: ['network_screen_test.dart'],
420447
TestAppDevice.flutterChrome: [
421448
'eval_and_browse_test.dart',
449+
'network_screen_test.dart',
422450
'perfetto_test.dart',
423451
'performance_screen_event_recording_test.dart',
424452
'service_connection_test.dart',

packages/devtools_app/integration_test/test_infra/run/run_test.dart

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,10 @@ Future<void> runFlutterIntegrationTest(
5858
// Run the flutter integration test.
5959
final testRunner = IntegrationTestRunner();
6060
try {
61-
final testArgs = <String, Object>{if (!offline) 'service_uri': testAppUri};
61+
final testArgs = <String, Object?>{
62+
if (!offline) 'service_uri': testAppUri,
63+
if (testApp is TestDartCliApp) 'control_port': testApp.controlPort,
64+
};
6265
final testTarget = testRunnerArgs.testTarget!;
6366
debugLog('starting test run for $testTarget');
6467
await testRunner.run(

0 commit comments

Comments
 (0)