Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,31 @@ jobs:
DEVTOOLS_PACKAGE: devtools_extensions
run: ./tool/ci/bots.sh

devtools-webdriver-test:
name: ${{ matrix.os }} devtools webdriver test
needs: flutter-prep
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
bot:
- test_webdriver
os: [ubuntu-latest, windows-latest]
steps:
- name: git clone
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
- name: Load Cached Flutter SDK
uses: actions/cache@d4323d4df104b026a6aa633fdb11d772146be0bf
with:
path: |
./tool/flutter-sdk
key: flutter-sdk-${{ runner.os }}-${{ needs.flutter-prep.outputs.latest_flutter_candidate }}
- name: tool/ci/bots.sh
env:
BOT: ${{ matrix.bot }}
PLATFORM: vm
run: ./tool/ci/bots.sh

benchmark-performance:
name: benchmark-performance
needs: flutter-prep
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,7 @@ Future<int> _findAvailablePort({required int startingAt}) async {

Future<bool> _isPortAvailable(int port) async {
try {
final RawSocket socket = await RawSocket.connect('localhost', port);
final socket = await RawSocket.connect('localhost', port);
socket.shutdown(SocketDirection.both);
await socket.close();
return false;
Expand Down
103 changes: 60 additions & 43 deletions packages/devtools_app/integration_test/test_infra/run/run_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -35,50 +35,10 @@ Future<void> runFlutterIntegrationTest(
// TODO(https://github.com/flutter/devtools/issues/9196): support starting
// DTD and passing the URI to DevTools server. Workspace roots should be set
// on the DTD instance based on the connected test app.

// Start the DevTools server. This will use the DevTools server that is
// shipped with the Dart SDK.
// TODO(https://github.com/flutter/devtools/issues/9197): launch the
// DevTools server from source so that end to end changes (server + app) can
// be tested.
devToolsServerProcess = await Process.start('dart', [
'devtools',
// Do not launch DevTools app in the browser. This DevTools server
// instance will be used to connect to the DevTools app that is run from
// Flutter driver from the integration test runner.
'--no-launch-browser',
// Disable CORS restrictions so that we can connect to the server from
// DevTools app that is served on a different origin.
'--disable-cors',
]);

final addressCompleter = Completer<void>();
final sub = devToolsServerProcess.stdout.transform(utf8.decoder).listen((
line,
) {
if (line.startsWith(_devToolsServerAddressLine)) {
// This will pull the server address from a String like:
// "Serving DevTools at http://127.0.0.1:9104.".
final regexp = RegExp(
r'http:\/\/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:\d+',
);
final match = regexp.firstMatch(line);
if (match != null) {
devToolsServerAddress = match.group(0);
addressCompleter.complete();
}
}
});

await addressCompleter.future.timeout(
const Duration(seconds: 10),
onTimeout: () async {
await sub.cancel();
devToolsServerProcess?.kill();
throw Exception('Timed out waiting for DevTools server to start.');
},
devToolsServerProcess = await startDevToolsServer();
devToolsServerAddress = await listenForDevToolsAddress(
devToolsServerProcess,
);
await sub.cancel();
}

if (!offline) {
Expand Down Expand Up @@ -195,3 +155,60 @@ class DevToolsAppTestRunnerArgs extends IntegrationTestRunnerArgs {
);
}
}

/// Starts the DevTools server.
///
/// Note: This will use the DevTools server that is shipped with the Dart SDK.
///
/// TODO(https://github.com/flutter/devtools/issues/9197): launch the
/// DevTools server from source so that end to end changes (server + app) can
/// be tested.
Future<Process> startDevToolsServer() async {
final devToolsServerProcess = await Process.start('dart', [
'devtools',
// Do not launch DevTools app in the browser. This DevTools server
// instance will be used to connect to the DevTools app that is run from
// Flutter driver from the integration test runner.
'--no-launch-browser',
// Disable CORS restrictions so that we can connect to the server from
// DevTools app that is served on a different origin.
'--disable-cors',
]);
return devToolsServerProcess;
}

/// Listens on the [devToolsServerProcess] stdout for the DevTool's address and
/// returns it.
Future<String> listenForDevToolsAddress(
Process devToolsServerProcess, {
Duration timeout = const Duration(seconds: 10),
}) async {
final devToolsAddressCompleter = Completer<String>();

final sub = devToolsServerProcess.stdout.transform(utf8.decoder).listen((
line,
) {
if (line.startsWith(_devToolsServerAddressLine)) {
// This will pull the server address from a String like:
// "Serving DevTools at http://127.0.0.1:9104.".
final regexp = RegExp(r'http:\/\/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:\d+');
final match = regexp.firstMatch(line);
if (match != null) {
final devToolsServerAddress = match.group(0);
devToolsAddressCompleter.complete(devToolsServerAddress);
}
}
});

await devToolsAddressCompleter.future.timeout(
timeout,
onTimeout: () async {
await sub.cancel();
devToolsServerProcess.kill();
throw Exception('Timed out waiting for DevTools server to start.');
},
);
await sub.cancel();

return devToolsAddressCompleter.future;
}
Original file line number Diff line number Diff line change
Expand Up @@ -744,12 +744,12 @@ class CpuProfileData with Serializable {

List<CpuStackFrame>? _bottomUpRoots;

late final Iterable<String> userTags = {
late final userTags = <String>{
for (final cpuSample in cpuSamples)
if (cpuSample.userTag case final userTag?) userTag,
};

late final Iterable<String> vmTags = {
late final vmTags = <String>{
for (final cpuSample in cpuSamples)
if (cpuSample.vmTag case final vmTag?) vmTag,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ class DartIOHttpInstantEvent {
TimeRange get timeRange => _timeRangeBuilder.build();

// This is modified from within HttpRequestData.
final TimeRangeBuilder _timeRangeBuilder = TimeRangeBuilder();
final _timeRangeBuilder = TimeRangeBuilder();
}

/// An abstraction of an HTTP request made through dart:io.
Expand Down
1 change: 1 addition & 0 deletions packages/devtools_app/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ dev_dependencies:
stream_channel: ^2.1.1
test: ^1.21.0
web_benchmarks: ^4.0.0
webdriver: ^3.1.0
webkit_inspection_protocol: ">=0.5.0 <2.0.0"

flutter:
Expand Down
98 changes: 98 additions & 0 deletions packages/devtools_app/webdriver_test/web_compiler_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// Copyright 2026 The Flutter Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd.

import 'dart:async';
import 'dart:io';

import 'package:devtools_shared/devtools_test_utils.dart';
import 'package:devtools_test/helpers.dart';
import 'package:devtools_test/integration_test.dart';
import 'package:test/test.dart';
import 'package:webdriver/async_io.dart';

import '../integration_test/test_infra/run/run_test.dart';

void main() {
late Process devtoolsProcess;
late WebDriver driver;
late String devToolsServerAddress;

const serverStartupTimeout = Duration(minutes: 1);

setUpAll(() async {
await ChromeDriver().start(debugLogging: true);

// TODO(https://github.com/flutter/devtools/issues/9197): Launch the
// DevTools server from source.
// For now, this test uses the version of DevTools bundled in the Dart SDK
// because building and running from source is too prohibitive until
// DevTools is moved into the Dart SDK. See issue for details.
devtoolsProcess = await startDevToolsServer();
devToolsServerAddress = await listenForDevToolsAddress(
devtoolsProcess,
timeout: serverStartupTimeout,
);

driver = await createDriver(
uri: Uri.parse('http://127.0.0.1:${ChromeDriver.port}'),
desired: {
...Capabilities.chrome,
Capabilities.chromeOptions: {
'args': ['--headless'],
},
},
);
});

tearDownAll(() async {
await driver.quit();
devtoolsProcess.kill();
});

/// Reads the "flt-renderer" attribute on the body element.
///
/// This can be used to determine whether the render is canvaskit or skwasm:
/// https://github.com/flutter/devtools/pull/9406#pullrequestreview-3142210823
Future<String?> readRendererAttribute() => retryAsync<String?>(
() async {
final body = await driver.findElement(const By.tagName('body'));
return body.attributes['flt-renderer'];
},
condition: (result) => result != null,
onRetry: () => Future.delayed(const Duration(milliseconds: 250)),
);

test(
'compiler query param determines skwasm/canvaskit renderer',
timeout: longTimeout,
() async {
// Open the DevTools URL with ?compiler=wasm.
await driver.get(
_addQueryParam(devToolsServerAddress, param: 'compiler', value: 'wasm'),
);

// Verify we are using the skwasm renderer.
expect(await readRendererAttribute(), equals('skwasm'));

// Open the DevTools URL with ?compiler=js.
await driver.get(
_addQueryParam(devToolsServerAddress, param: 'compiler', value: 'js'),
);

// Verify we are using the canvaskit renderer.
expect(await readRendererAttribute(), equals('canvaskit'));
},
);
}

String _addQueryParam(
String url, {
required String param,
required String value,
}) {
final uri = Uri.parse(url);
final newQueryParameters = Map<String, dynamic>.of(uri.queryParameters);
newQueryParameters[param] = value;
return uri.replace(queryParameters: newQueryParameters).toString();
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import 'package:devtools_shared/devtools_shared.dart';

import '../../utils.dart';
import '../utils/enum_utils.dart';

/// Flutter version service registered by Flutter Tools.
///
Expand Down Expand Up @@ -122,7 +122,7 @@ final class FlutterVersion extends SemanticVersion {
String versionStr, {
String? channelStr,
}) {
// Check if channel string is valid.
// Check if channel string is valid.
if (channelStr != null) {
final channel = FlutterChannel.fromName(channelStr);
if (channel != null) return channel;
Expand Down
34 changes: 31 additions & 3 deletions packages/devtools_shared/lib/src/test/chrome_driver.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import 'dart:io';
import 'io_utils.dart';

class ChromeDriver with IOMixin {
static const port = 4444;

Process? _process;

// TODO(kenz): add error messaging if the chromedriver executable is not
Expand All @@ -17,17 +19,20 @@ class ChromeDriver with IOMixin {
Future<void> start({bool debugLogging = false}) async {
try {
const chromedriverExe = 'chromedriver';
const chromedriverArgs = ['--port=4444'];
const chromedriverArgs = ['--port=$port'];
if (debugLogging) {
print('${DateTime.now()}: starting the chromedriver process');
print('${DateTime.now()}: > $chromedriverExe '
'${chromedriverArgs.join(' ')}');
print(
'${DateTime.now()}: > $chromedriverExe '
'${chromedriverArgs.join(' ')}',
);
}
final process = _process = await Process.start(
chromedriverExe,
chromedriverArgs,
);
listenToProcessOutput(process, printTag: 'ChromeDriver');
await _waitForPortOpen(port);
} catch (e) {
// ignore: avoid-throw-in-catch-block, by design
throw Exception('Error starting chromedriver: $e');
Expand All @@ -47,4 +52,27 @@ class ChromeDriver with IOMixin {
}
await killGracefully(process, debugLogging: debugLogging);
}

Future<void> _waitForPortOpen(
int port, {
Duration timeout = const Duration(seconds: 10),
}) async {
final stopwatch = Stopwatch()..start();

while (stopwatch.elapsed < timeout) {
try {
final socket = await Socket.connect('127.0.0.1', port);
socket.destroy();
stopwatch.stop();
return;
} catch (_) {
await Future.delayed(const Duration(milliseconds: 200));
}
}

stopwatch.stop();
throw Exception(
'ChromeDriver failed to start on port $port within ${timeout.inSeconds} seconds.',
);
}
}
Loading
Loading