mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
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:
parent
1cee814c96
commit
337bbfbe04
@ -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(
|
||||
|
||||
@ -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);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user