Skip to content

Commit 04b74a8

Browse files
authored
Add webdriver test for compiler query parameter (#9608)
1 parent 92913e2 commit 04b74a8

12 files changed

Lines changed: 254 additions & 64 deletions

File tree

.github/workflows/build.yaml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,31 @@ jobs:
290290
DEVTOOLS_PACKAGE: devtools_extensions
291291
run: ./tool/ci/bots.sh
292292

293+
devtools-webdriver-test:
294+
name: ${{ matrix.os }} devtools webdriver test
295+
needs: flutter-prep
296+
runs-on: ${{ matrix.os }}
297+
strategy:
298+
fail-fast: false
299+
matrix:
300+
bot:
301+
- test_webdriver
302+
os: [ubuntu-latest, windows-latest]
303+
steps:
304+
- name: git clone
305+
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
306+
- name: Load Cached Flutter SDK
307+
uses: actions/cache@d4323d4df104b026a6aa633fdb11d772146be0bf
308+
with:
309+
path: |
310+
./tool/flutter-sdk
311+
key: flutter-sdk-${{ runner.os }}-${{ needs.flutter-prep.outputs.latest_flutter_candidate }}
312+
- name: tool/ci/bots.sh
313+
env:
314+
BOT: ${{ matrix.bot }}
315+
PLATFORM: vm
316+
run: ./tool/ci/bots.sh
317+
293318
benchmark-performance:
294319
name: benchmark-performance
295320
needs: flutter-prep

packages/devtools_app/benchmark/devtools_benchmarks_test.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,7 @@ Future<int> _findAvailablePort({required int startingAt}) async {
277277

278278
Future<bool> _isPortAvailable(int port) async {
279279
try {
280-
final RawSocket socket = await RawSocket.connect('localhost', port);
280+
final socket = await RawSocket.connect('localhost', port);
281281
socket.shutdown(SocketDirection.both);
282282
await socket.close();
283283
return false;

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

Lines changed: 60 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -35,50 +35,10 @@ Future<void> runFlutterIntegrationTest(
3535
// TODO(https://github.com/flutter/devtools/issues/9196): support starting
3636
// DTD and passing the URI to DevTools server. Workspace roots should be set
3737
// on the DTD instance based on the connected test app.
38-
39-
// Start the DevTools server. This will use the DevTools server that is
40-
// shipped with the Dart SDK.
41-
// TODO(https://github.com/flutter/devtools/issues/9197): launch the
42-
// DevTools server from source so that end to end changes (server + app) can
43-
// be tested.
44-
devToolsServerProcess = await Process.start('dart', [
45-
'devtools',
46-
// Do not launch DevTools app in the browser. This DevTools server
47-
// instance will be used to connect to the DevTools app that is run from
48-
// Flutter driver from the integration test runner.
49-
'--no-launch-browser',
50-
// Disable CORS restrictions so that we can connect to the server from
51-
// DevTools app that is served on a different origin.
52-
'--disable-cors',
53-
]);
54-
55-
final addressCompleter = Completer<void>();
56-
final sub = devToolsServerProcess.stdout.transform(utf8.decoder).listen((
57-
line,
58-
) {
59-
if (line.startsWith(_devToolsServerAddressLine)) {
60-
// This will pull the server address from a String like:
61-
// "Serving DevTools at http://127.0.0.1:9104.".
62-
final regexp = RegExp(
63-
r'http:\/\/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:\d+',
64-
);
65-
final match = regexp.firstMatch(line);
66-
if (match != null) {
67-
devToolsServerAddress = match.group(0);
68-
addressCompleter.complete();
69-
}
70-
}
71-
});
72-
73-
await addressCompleter.future.timeout(
74-
const Duration(seconds: 10),
75-
onTimeout: () async {
76-
await sub.cancel();
77-
devToolsServerProcess?.kill();
78-
throw Exception('Timed out waiting for DevTools server to start.');
79-
},
38+
devToolsServerProcess = await startDevToolsServer();
39+
devToolsServerAddress = await listenForDevToolsAddress(
40+
devToolsServerProcess,
8041
);
81-
await sub.cancel();
8242
}
8343

8444
if (!offline) {
@@ -195,3 +155,60 @@ class DevToolsAppTestRunnerArgs extends IntegrationTestRunnerArgs {
195155
);
196156
}
197157
}
158+
159+
/// Starts the DevTools server.
160+
///
161+
/// Note: This will use the DevTools server that is shipped with the Dart SDK.
162+
///
163+
/// TODO(https://github.com/flutter/devtools/issues/9197): launch the
164+
/// DevTools server from source so that end to end changes (server + app) can
165+
/// be tested.
166+
Future<Process> startDevToolsServer() async {
167+
final devToolsServerProcess = await Process.start('dart', [
168+
'devtools',
169+
// Do not launch DevTools app in the browser. This DevTools server
170+
// instance will be used to connect to the DevTools app that is run from
171+
// Flutter driver from the integration test runner.
172+
'--no-launch-browser',
173+
// Disable CORS restrictions so that we can connect to the server from
174+
// DevTools app that is served on a different origin.
175+
'--disable-cors',
176+
]);
177+
return devToolsServerProcess;
178+
}
179+
180+
/// Listens on the [devToolsServerProcess] stdout for the DevTool's address and
181+
/// returns it.
182+
Future<String> listenForDevToolsAddress(
183+
Process devToolsServerProcess, {
184+
Duration timeout = const Duration(seconds: 10),
185+
}) async {
186+
final devToolsAddressCompleter = Completer<String>();
187+
188+
final sub = devToolsServerProcess.stdout.transform(utf8.decoder).listen((
189+
line,
190+
) {
191+
if (line.startsWith(_devToolsServerAddressLine)) {
192+
// This will pull the server address from a String like:
193+
// "Serving DevTools at http://127.0.0.1:9104.".
194+
final regexp = RegExp(r'http:\/\/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:\d+');
195+
final match = regexp.firstMatch(line);
196+
if (match != null) {
197+
final devToolsServerAddress = match.group(0);
198+
devToolsAddressCompleter.complete(devToolsServerAddress);
199+
}
200+
}
201+
});
202+
203+
await devToolsAddressCompleter.future.timeout(
204+
timeout,
205+
onTimeout: () async {
206+
await sub.cancel();
207+
devToolsServerProcess.kill();
208+
throw Exception('Timed out waiting for DevTools server to start.');
209+
},
210+
);
211+
await sub.cancel();
212+
213+
return devToolsAddressCompleter.future;
214+
}

packages/devtools_app/lib/src/screens/profiler/cpu_profile_model.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -744,12 +744,12 @@ class CpuProfileData with Serializable {
744744

745745
List<CpuStackFrame>? _bottomUpRoots;
746746

747-
late final Iterable<String> userTags = {
747+
late final userTags = <String>{
748748
for (final cpuSample in cpuSamples)
749749
if (cpuSample.userTag case final userTag?) userTag,
750750
};
751751

752-
late final Iterable<String> vmTags = {
752+
late final vmTags = <String>{
753753
for (final cpuSample in cpuSamples)
754754
if (cpuSample.vmTag case final vmTag?) vmTag,
755755
};

packages/devtools_app/lib/src/shared/http/http_request_data.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ class DartIOHttpInstantEvent {
3333
TimeRange get timeRange => _timeRangeBuilder.build();
3434

3535
// This is modified from within HttpRequestData.
36-
final TimeRangeBuilder _timeRangeBuilder = TimeRangeBuilder();
36+
final _timeRangeBuilder = TimeRangeBuilder();
3737
}
3838

3939
/// An abstraction of an HTTP request made through dart:io.

packages/devtools_app/pubspec.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ dev_dependencies:
7777
stream_channel: ^2.1.1
7878
test: ^1.21.0
7979
web_benchmarks: ^4.0.0
80+
webdriver: ^3.1.0
8081
webkit_inspection_protocol: ">=0.5.0 <2.0.0"
8182

8283
flutter:
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
// Copyright 2026 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+
import 'dart:async';
6+
import 'dart:io';
7+
8+
import 'package:devtools_shared/devtools_test_utils.dart';
9+
import 'package:devtools_test/helpers.dart';
10+
import 'package:devtools_test/integration_test.dart';
11+
import 'package:test/test.dart';
12+
import 'package:webdriver/async_io.dart';
13+
14+
import '../integration_test/test_infra/run/run_test.dart';
15+
16+
void main() {
17+
late Process devtoolsProcess;
18+
late WebDriver driver;
19+
late String devToolsServerAddress;
20+
21+
const serverStartupTimeout = Duration(minutes: 1);
22+
23+
setUpAll(() async {
24+
await ChromeDriver().start(debugLogging: true);
25+
26+
// TODO(https://github.com/flutter/devtools/issues/9197): Launch the
27+
// DevTools server from source.
28+
// For now, this test uses the version of DevTools bundled in the Dart SDK
29+
// because building and running from source is too prohibitive until
30+
// DevTools is moved into the Dart SDK. See issue for details.
31+
devtoolsProcess = await startDevToolsServer();
32+
devToolsServerAddress = await listenForDevToolsAddress(
33+
devtoolsProcess,
34+
timeout: serverStartupTimeout,
35+
);
36+
37+
driver = await createDriver(
38+
uri: Uri.parse('http://127.0.0.1:${ChromeDriver.port}'),
39+
desired: {
40+
...Capabilities.chrome,
41+
Capabilities.chromeOptions: {
42+
'args': ['--headless'],
43+
},
44+
},
45+
);
46+
});
47+
48+
tearDownAll(() async {
49+
await driver.quit();
50+
devtoolsProcess.kill();
51+
});
52+
53+
/// Reads the "flt-renderer" attribute on the body element.
54+
///
55+
/// This can be used to determine whether the render is canvaskit or skwasm:
56+
/// https://github.com/flutter/devtools/pull/9406#pullrequestreview-3142210823
57+
Future<String?> readRendererAttribute() => retryAsync<String?>(
58+
() async {
59+
final body = await driver.findElement(const By.tagName('body'));
60+
return body.attributes['flt-renderer'];
61+
},
62+
condition: (result) => result != null,
63+
onRetry: () => Future.delayed(const Duration(milliseconds: 250)),
64+
);
65+
66+
test(
67+
'compiler query param determines skwasm/canvaskit renderer',
68+
timeout: longTimeout,
69+
() async {
70+
// Open the DevTools URL with ?compiler=wasm.
71+
await driver.get(
72+
_addQueryParam(devToolsServerAddress, param: 'compiler', value: 'wasm'),
73+
);
74+
75+
// Verify we are using the skwasm renderer.
76+
expect(await readRendererAttribute(), equals('skwasm'));
77+
78+
// Open the DevTools URL with ?compiler=js.
79+
await driver.get(
80+
_addQueryParam(devToolsServerAddress, param: 'compiler', value: 'js'),
81+
);
82+
83+
// Verify we are using the canvaskit renderer.
84+
expect(await readRendererAttribute(), equals('canvaskit'));
85+
},
86+
);
87+
}
88+
89+
String _addQueryParam(
90+
String url, {
91+
required String param,
92+
required String value,
93+
}) {
94+
final uri = Uri.parse(url);
95+
final newQueryParameters = Map<String, dynamic>.of(uri.queryParameters);
96+
newQueryParameters[param] = value;
97+
return uri.replace(queryParameters: newQueryParameters).toString();
98+
}

packages/devtools_app_shared/lib/src/service/flutter_version.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import 'package:devtools_shared/devtools_shared.dart';
66

7-
import '../../utils.dart';
7+
import '../utils/enum_utils.dart';
88

99
/// Flutter version service registered by Flutter Tools.
1010
///
@@ -122,7 +122,7 @@ final class FlutterVersion extends SemanticVersion {
122122
String versionStr, {
123123
String? channelStr,
124124
}) {
125-
// Check if channel string is valid.
125+
// Check if channel string is valid.
126126
if (channelStr != null) {
127127
final channel = FlutterChannel.fromName(channelStr);
128128
if (channel != null) return channel;

packages/devtools_shared/lib/src/test/chrome_driver.dart

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import 'dart:io';
99
import 'io_utils.dart';
1010

1111
class ChromeDriver with IOMixin {
12+
static const port = 4444;
13+
1214
Process? _process;
1315

1416
// TODO(kenz): add error messaging if the chromedriver executable is not
@@ -17,17 +19,20 @@ class ChromeDriver with IOMixin {
1719
Future<void> start({bool debugLogging = false}) async {
1820
try {
1921
const chromedriverExe = 'chromedriver';
20-
const chromedriverArgs = ['--port=4444'];
22+
const chromedriverArgs = ['--port=$port'];
2123
if (debugLogging) {
2224
print('${DateTime.now()}: starting the chromedriver process');
23-
print('${DateTime.now()}: > $chromedriverExe '
24-
'${chromedriverArgs.join(' ')}');
25+
print(
26+
'${DateTime.now()}: > $chromedriverExe '
27+
'${chromedriverArgs.join(' ')}',
28+
);
2529
}
2630
final process = _process = await Process.start(
2731
chromedriverExe,
2832
chromedriverArgs,
2933
);
3034
listenToProcessOutput(process, printTag: 'ChromeDriver');
35+
await _waitForPortOpen(port);
3136
} catch (e) {
3237
// ignore: avoid-throw-in-catch-block, by design
3338
throw Exception('Error starting chromedriver: $e');
@@ -47,4 +52,27 @@ class ChromeDriver with IOMixin {
4752
}
4853
await killGracefully(process, debugLogging: debugLogging);
4954
}
55+
56+
Future<void> _waitForPortOpen(
57+
int port, {
58+
Duration timeout = const Duration(seconds: 10),
59+
}) async {
60+
final stopwatch = Stopwatch()..start();
61+
62+
while (stopwatch.elapsed < timeout) {
63+
try {
64+
final socket = await Socket.connect('127.0.0.1', port);
65+
socket.destroy();
66+
stopwatch.stop();
67+
return;
68+
} catch (_) {
69+
await Future.delayed(const Duration(milliseconds: 200));
70+
}
71+
}
72+
73+
stopwatch.stop();
74+
throw Exception(
75+
'ChromeDriver failed to start on port $port within ${timeout.inSeconds} seconds.',
76+
);
77+
}
5078
}

0 commit comments

Comments
 (0)