Potentially fixing the flakiness in win32 windowing tests, but it needs some running (#178499)

Adding some more logging and retrying mechanisms to the win32 windowing
tests to see if it fixes the flakiness.

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [x] All existing and new tests are passing.
This commit is contained in:
Matthew Kosarek 2025-11-24 08:47:20 -05:00 committed by GitHub
parent 1cee814c96
commit 337bbfbe04
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 244 additions and 158 deletions

View File

@ -32,138 +32,149 @@ void main() {
final Completer<void> windowCreated = Completer();
enableFlutterDriverExtension(
handler: (String? message) async {
await windowCreated.future;
if (message == null) {
return '';
}
final jsonMap = jsonDecode(message);
if (!jsonMap.containsKey('type')) {
throw ArgumentError('Message must contain a "type" field.');
}
/// This helper method registers a listener on the controller,
/// calls [act] to perform some action on the controller, waits for
/// the [predicate] to be satisified, and finally cleans up the listener.
Future<void> awaitNotification(
VoidCallback act,
bool Function() predicate,
) async {
final StreamController<bool> streamController = StreamController();
void notificationHandler() {
streamController.add(true);
try {
await windowCreated.future;
if (message == null) {
return '';
}
controller.addListener(notificationHandler);
act();
// On macOS, the predicate is immediately true, even though
// the animation is in progress and next request for state change
// will be ignored. Easiest way to handle this is to just wait.
if (defaultTargetPlatform == TargetPlatform.macOS) {
await Future.delayed(Duration(seconds: 1));
final jsonMap = jsonDecode(message);
if (!jsonMap.containsKey('type')) {
throw ArgumentError('Message must contain a "type" field.');
}
await for (final _ in streamController.stream) {
if (predicate()) {
break;
/// This helper method registers a listener on the controller,
/// calls [act] to perform some action on the controller, waits for
/// the [predicate] to be satisified, and finally cleans up the listener.
Future<void> awaitNotification(
VoidCallback act,
bool Function() predicate,
) async {
final StreamController<bool> streamController = StreamController();
void notificationHandler() {
streamController.add(true);
}
controller.addListener(notificationHandler);
try {
act();
// On macOS, the predicate is immediately true, even though
// the animation is in progress and next request for state change
// will be ignored. Easiest way to handle this is to just wait.
if (defaultTargetPlatform == TargetPlatform.macOS) {
await Future.delayed(Duration(seconds: 1));
}
// Add a timeout to avoid hanging indefinitely
await for (final _ in streamController.stream.timeout(
Duration(seconds: 10),
)) {
if (predicate()) {
break;
}
}
} finally {
controller.removeListener(notificationHandler);
await streamController.close();
}
}
controller.removeListener(notificationHandler);
}
if (jsonMap['type'] == 'ping') {
return jsonEncode({'type': 'pong'});
} else if (jsonMap['type'] == 'get_size') {
return jsonEncode({
'width': controller.contentSize.width,
'height': controller.contentSize.height,
});
} else if (jsonMap['type'] == 'set_size') {
final Size size = Size(
jsonMap['width'].toDouble(),
jsonMap['height'].toDouble(),
);
await awaitNotification(() {
controller.setSize(size);
}, () => controller.contentSize == size);
} else if (jsonMap['type'] == 'set_constraints') {
final BoxConstraints constraints = BoxConstraints(
minWidth: jsonMap['min_width'].toDouble(),
minHeight: jsonMap['min_height'].toDouble(),
maxWidth: jsonMap['max_width'].toDouble(),
maxHeight: jsonMap['max_height'].toDouble(),
);
// We assume that this will cause a resize, which the current tests do.
final initialSize = controller.contentSize;
await awaitNotification(() {
controller.setConstraints(constraints);
}, () => controller.contentSize != initialSize);
} else if (jsonMap['type'] == 'set_fullscreen') {
await awaitNotification(() {
controller.setFullscreen(true);
}, () => controller.isFullscreen);
} else if (jsonMap['type'] == 'unset_fullscreen') {
await awaitNotification(() {
controller.setFullscreen(false);
}, () => !controller.isFullscreen);
} else if (jsonMap['type'] == 'get_fullscreen') {
return jsonEncode({'isFullscreen': controller.isFullscreen});
} else if (jsonMap['type'] == 'set_maximized') {
await awaitNotification(() {
controller.setMaximized(true);
}, () => controller.isMaximized);
} else if (jsonMap['type'] == 'unset_maximized') {
await awaitNotification(() {
controller.setMaximized(false);
}, () => !controller.isMaximized);
} else if (jsonMap['type'] == 'get_maximized') {
return jsonEncode({'isMaximized': controller.isMaximized});
} else if (jsonMap['type'] == 'set_minimized') {
await awaitNotification(() {
controller.setMinimized(true);
}, () => controller.isMinimized);
} else if (jsonMap['type'] == 'unset_minimized') {
await awaitNotification(() {
controller.setMinimized(false);
}, () => !controller.isMinimized);
} else if (jsonMap['type'] == 'get_minimized') {
return jsonEncode({'isMinimized': controller.isMinimized});
} else if (jsonMap['type'] == 'set_title') {
final String title = jsonMap['title'];
await awaitNotification(() {
controller.setTitle(title);
}, () => controller.title == title);
} else if (jsonMap['type'] == 'get_title') {
return jsonEncode({'title': controller.title});
} else if (jsonMap['type'] == 'set_activated') {
await awaitNotification(() {
controller.activate();
}, () => controller.isActivated);
} else if (jsonMap['type'] == 'get_activated') {
return jsonEncode({'isActivated': controller.isActivated});
} else if (jsonMap['type'] == 'open_dialog') {
if (dialogController.value != null) {
return jsonEncode({'result': false});
if (jsonMap['type'] == 'ping') {
return jsonEncode({'type': 'pong'});
} else if (jsonMap['type'] == 'get_size') {
return jsonEncode({
'width': controller.contentSize.width,
'height': controller.contentSize.height,
});
} else if (jsonMap['type'] == 'set_size') {
final Size size = Size(
jsonMap['width'].toDouble(),
jsonMap['height'].toDouble(),
);
await awaitNotification(() {
controller.setSize(size);
}, () => controller.contentSize == size);
} else if (jsonMap['type'] == 'set_constraints') {
final BoxConstraints constraints = BoxConstraints(
minWidth: jsonMap['min_width'].toDouble(),
minHeight: jsonMap['min_height'].toDouble(),
maxWidth: jsonMap['max_width'].toDouble(),
maxHeight: jsonMap['max_height'].toDouble(),
);
// We assume that this will cause a resize, which the current tests do.
final initialSize = controller.contentSize;
await awaitNotification(() {
controller.setConstraints(constraints);
}, () => controller.contentSize != initialSize);
} else if (jsonMap['type'] == 'set_fullscreen') {
await awaitNotification(() {
controller.setFullscreen(true);
}, () => controller.isFullscreen);
} else if (jsonMap['type'] == 'unset_fullscreen') {
await awaitNotification(() {
controller.setFullscreen(false);
}, () => !controller.isFullscreen);
} else if (jsonMap['type'] == 'get_fullscreen') {
return jsonEncode({'isFullscreen': controller.isFullscreen});
} else if (jsonMap['type'] == 'set_maximized') {
await awaitNotification(() {
controller.setMaximized(true);
}, () => controller.isMaximized);
} else if (jsonMap['type'] == 'unset_maximized') {
await awaitNotification(() {
controller.setMaximized(false);
}, () => !controller.isMaximized);
} else if (jsonMap['type'] == 'get_maximized') {
return jsonEncode({'isMaximized': controller.isMaximized});
} else if (jsonMap['type'] == 'set_minimized') {
await awaitNotification(() {
controller.setMinimized(true);
}, () => controller.isMinimized);
} else if (jsonMap['type'] == 'unset_minimized') {
await awaitNotification(() {
controller.setMinimized(false);
}, () => !controller.isMinimized);
} else if (jsonMap['type'] == 'get_minimized') {
return jsonEncode({'isMinimized': controller.isMinimized});
} else if (jsonMap['type'] == 'set_title') {
final String title = jsonMap['title'];
await awaitNotification(() {
controller.setTitle(title);
}, () => controller.title == title);
} else if (jsonMap['type'] == 'get_title') {
return jsonEncode({'title': controller.title});
} else if (jsonMap['type'] == 'set_activated') {
await awaitNotification(() {
controller.activate();
}, () => controller.isActivated);
} else if (jsonMap['type'] == 'get_activated') {
return jsonEncode({'isActivated': controller.isActivated});
} else if (jsonMap['type'] == 'open_dialog') {
if (dialogController.value != null) {
return jsonEncode({'result': false});
}
dialogController.value = DialogWindowController(
preferredSize: const Size(200, 200),
parent: controller,
delegate: MyDialogWindowControllerDelegate(
onDestroyed: () {
dialogController.value = null;
},
),
);
return jsonEncode({'result': true});
} else if (jsonMap['type'] == 'close_dialog') {
dialogController.value?.destroy();
return jsonEncode({'result': true});
} else {
throw ArgumentError('Unknown message type: ${jsonMap['type']}');
}
dialogController.value = DialogWindowController(
preferredSize: const Size(200, 200),
parent: controller,
delegate: MyDialogWindowControllerDelegate(
onDestroyed: () {
dialogController.value = null;
},
),
);
return jsonEncode({'result': true});
} else if (jsonMap['type'] == 'close_dialog') {
dialogController.value?.destroy();
return jsonEncode({'result': true});
} else {
throw ArgumentError('Unknown message type: ${jsonMap['type']}');
return '';
} catch (e) {
return jsonEncode({'error': e.toString()});
}
return '';
},
);
controller = RegularWindowController(

View File

@ -7,13 +7,47 @@ import 'dart:convert';
import 'package:flutter_driver/flutter_driver.dart';
import 'package:test/test.dart' hide TypeMatcher, isInstanceOf;
Future<String> _requestDataWithRetry(
FlutterDriver driver,
String message, {
int maxRetries = 3,
}) async {
for (int attempt = 1; attempt <= maxRetries; attempt++) {
try {
final String response = await driver.requestData(message);
if (response.isNotEmpty) {
// Check for an error returned from the driver extension handler.
try {
final Map<String, dynamic> data =
jsonDecode(response) as Map<String, dynamic>;
if (data.containsKey('error')) {
throw Exception(
'Driver extension handler reported an error: ${data['error']}',
);
}
} on FormatException {
// Not a JSON map, which is fine for some responses.
}
}
return response;
} catch (e) {
if (attempt == maxRetries) {
rethrow;
}
await Future.delayed(Duration(milliseconds: 100 * attempt));
}
}
throw StateError('Should not reach here');
}
void main() {
group('end-to-end test', () {
late final FlutterDriver driver;
setUpAll(() async {
driver = await FlutterDriver.connect();
await driver.requestData(jsonEncode({'type': 'ping'}));
await _requestDataWithRetry(driver, jsonEncode({'type': 'ping'}));
});
tearDownAll(() async {
@ -21,10 +55,12 @@ void main() {
});
test('Can set and get title', () async {
await driver.requestData(
await _requestDataWithRetry(
driver,
jsonEncode({'type': 'set_title', 'title': 'Hello World'}),
);
final response = await driver.requestData(
final response = await _requestDataWithRetry(
driver,
jsonEncode({'type': 'get_title'}),
);
final data = jsonDecode(response);
@ -32,7 +68,8 @@ void main() {
}, timeout: Timeout.none);
test('Initial controller size is correct', () async {
final response = await driver.requestData(
final response = await _requestDataWithRetry(
driver,
jsonEncode({'type': 'get_size'}),
);
final data = jsonDecode(response);
@ -41,10 +78,12 @@ void main() {
}, timeout: Timeout.none);
test('Can set and get size', () async {
await driver.requestData(
await _requestDataWithRetry(
driver,
jsonEncode({'type': 'set_size', 'width': 800, 'height': 600}),
);
final response = await driver.requestData(
final response = await _requestDataWithRetry(
driver,
jsonEncode({'type': 'get_size'}),
);
final data = jsonDecode(response);
@ -53,53 +92,77 @@ void main() {
}, timeout: Timeout.none);
test('Can set and get fullscreen', () async {
await driver.requestData(jsonEncode({'type': 'set_fullscreen'}));
var response = await driver.requestData(
await _requestDataWithRetry(
driver,
jsonEncode({'type': 'set_fullscreen'}),
);
var response = await _requestDataWithRetry(
driver,
jsonEncode({'type': 'get_fullscreen'}),
);
var data = jsonDecode(response);
expect(data["isFullscreen"], true);
expect(data['isFullscreen'], true);
await driver.requestData(jsonEncode({'type': 'unset_fullscreen'}));
response = await driver.requestData(
await _requestDataWithRetry(
driver,
jsonEncode({'type': 'unset_fullscreen'}),
);
response = await _requestDataWithRetry(
driver,
jsonEncode({'type': 'get_fullscreen'}),
);
data = jsonDecode(response);
expect(data["isFullscreen"], false);
expect(data['isFullscreen'], false);
}, timeout: Timeout.none);
test('Can set and get maximized', () async {
await driver.requestData(jsonEncode({'type': 'set_maximized'}));
var response = await driver.requestData(
await _requestDataWithRetry(
driver,
jsonEncode({'type': 'set_maximized'}),
);
var response = await _requestDataWithRetry(
driver,
jsonEncode({'type': 'get_maximized'}),
);
var data = jsonDecode(response);
expect(data["isMaximized"], true);
expect(data['isMaximized'], true);
await driver.requestData(jsonEncode({'type': 'unset_maximized'}));
response = await driver.requestData(
await _requestDataWithRetry(
driver,
jsonEncode({'type': 'unset_maximized'}),
);
response = await _requestDataWithRetry(
driver,
jsonEncode({'type': 'get_maximized'}),
);
data = jsonDecode(response);
expect(data["isMaximized"], false);
expect(data['isMaximized'], false);
}, timeout: Timeout.none);
test(
'Can set and get minimized',
() async {
await driver.requestData(jsonEncode({'type': 'set_minimized'}));
var response = await driver.requestData(
await _requestDataWithRetry(
driver,
jsonEncode({'type': 'set_minimized'}),
);
var response = await _requestDataWithRetry(
driver,
jsonEncode({'type': 'get_minimized'}),
);
var data = jsonDecode(response);
expect(data["isMinimized"], true);
expect(data['isMinimized'], true);
await driver.requestData(jsonEncode({'type': 'unset_minimized'}));
response = await driver.requestData(
await _requestDataWithRetry(
driver,
jsonEncode({'type': 'unset_minimized'}),
);
response = await _requestDataWithRetry(
driver,
jsonEncode({'type': 'get_minimized'}),
);
data = jsonDecode(response);
expect(data["isMinimized"], false);
expect(data['isMinimized'], false);
},
timeout: Timeout.none,
onPlatform: {'linux': Skip('isMinimized is not supported on Wayland')},
@ -108,30 +171,39 @@ void main() {
test(
'Can set and get activated',
() async {
await driver.requestData(
await _requestDataWithRetry(
driver,
jsonEncode({'type': 'set_minimized'}),
); // Minimize first so that the window is not active
await driver.requestData(jsonEncode({'type': 'set_activated'}));
final response = await driver.requestData(
await _requestDataWithRetry(
driver,
jsonEncode({'type': 'set_activated'}),
);
final response = await _requestDataWithRetry(
driver,
jsonEncode({'type': 'get_activated'}),
);
final data = jsonDecode(response);
expect(data["isActivated"], true);
expect(data['isActivated'], true);
},
timeout: Timeout.none,
onPlatform: {'linux': Skip('isMinimized is not supported on Wayland')},
);
test('Can open dialog', () async {
await driver.requestData(jsonEncode({'type': 'open_dialog'}));
await driver.waitFor(find.byValueKey('close_dialog'));
await driver.requestData(jsonEncode({'type': 'close_dialog'}));
await _requestDataWithRetry(driver, jsonEncode({'type': 'open_dialog'}));
await driver.waitFor(
find.byValueKey('close_dialog'),
timeout: Duration(seconds: 10),
);
await _requestDataWithRetry(driver, jsonEncode({'type': 'close_dialog'}));
}, timeout: Timeout.none);
test(
'Can set constraints and see the resize',
() async {
await driver.requestData(
await _requestDataWithRetry(
driver,
jsonEncode({
'type': 'set_constraints',
'min_width': 0,
@ -140,7 +212,8 @@ void main() {
'max_height': 501,
}),
);
final response = await driver.requestData(
final response = await _requestDataWithRetry(
driver,
jsonEncode({'type': 'get_size'}),
);
final data = jsonDecode(response);
@ -154,7 +227,8 @@ void main() {
test(
'Can set constraints and see the resize (Linux)',
() async {
await driver.requestData(
await _requestDataWithRetry(
driver,
jsonEncode({
'type': 'set_constraints',
'min_width': 0,
@ -163,7 +237,8 @@ void main() {
'max_height': 501,
}),
);
final response = await driver.requestData(
final response = await _requestDataWithRetry(
driver,
jsonEncode({'type': 'get_size'}),
);
final data = jsonDecode(response);