Unverified Commit 07f7ffde authored by pdblasi-google's avatar pdblasi-google Committed by GitHub

Adds `TestDisplay` API for testing `Display` features (#127525)

* Adds `TestDisplay`
* Updates `TestPlatformDispatcher` to wrap all `Display`s and relate them to their appropriate `TestFlutterView`
* Updates `TestFlutterView` to tie `devicePixelRatio` to its display as per the documentation on `Display`

Closes #127225
parent 51f7fdbc
......@@ -153,7 +153,7 @@ class TestPlatformDispatcher implements PlatformDispatcher {
TestPlatformDispatcher({
required PlatformDispatcher platformDispatcher,
}) : _platformDispatcher = platformDispatcher {
_updateViews();
_updateViewsAndDisplays();
_platformDispatcher.onMetricsChanged = _handleMetricsChanged;
}
......@@ -168,6 +168,7 @@ class TestPlatformDispatcher implements PlatformDispatcher {
}
final Map<Object, TestFlutterView> _testViews = <Object, TestFlutterView>{};
final Map<Object, TestDisplay> _testDisplays = <Object, TestDisplay>{};
@override
VoidCallback? get onMetricsChanged => _platformDispatcher.onMetricsChanged;
......@@ -178,7 +179,7 @@ class TestPlatformDispatcher implements PlatformDispatcher {
}
void _handleMetricsChanged() {
_updateViews();
_updateViewsAndDisplays();
_onMetricsChanged?.call();
}
......@@ -509,16 +510,50 @@ class TestPlatformDispatcher implements PlatformDispatcher {
@override
Iterable<TestFlutterView> get views => _testViews.values;
void _updateViews() {
final List<Object> extraKeys = <Object>[..._testViews.keys];
@override
Iterable<TestDisplay> get displays => _testDisplays.values;
void _updateViewsAndDisplays() {
final List<Object> extraDisplayKeys = <Object>[..._testDisplays.keys];
for (final Display display in _platformDispatcher.displays) {
extraDisplayKeys.remove(display.id);
if (!_testDisplays.containsKey(display.id)) {
_testDisplays[display.id] = TestDisplay(this, display);
}
}
extraDisplayKeys.forEach(_testDisplays.remove);
final List<Object> extraViewKeys = <Object>[..._testViews.keys];
for (final FlutterView view in _platformDispatcher.views) {
extraKeys.remove(view.viewId);
// TODO(pdblasi-google): Remove this try-catch once the Display API is stable and supported on all platforms
late final TestDisplay display;
try {
final Display realDisplay = view.display;
if (_testDisplays.containsKey(realDisplay.id)) {
display = _testDisplays[view.display.id]!;
} else {
display = _UnsupportedDisplay(
this,
view,
'PlatformDispatcher did not contain a Display with id ${realDisplay.id}, '
'which was expected by FlutterView ($view)',
);
}
} catch (error){
display = _UnsupportedDisplay(this, view, error);
}
extraViewKeys.remove(view.viewId);
if (!_testViews.containsKey(view.viewId)) {
_testViews[view.viewId] = TestFlutterView(view: view, platformDispatcher: this);
_testViews[view.viewId] = TestFlutterView(
view: view,
platformDispatcher: this,
display: display,
);
}
}
extraKeys.forEach(_testViews.remove);
extraViewKeys.forEach(_testViews.remove);
}
@override
......@@ -625,7 +660,11 @@ class TestFlutterView implements FlutterView {
TestFlutterView({
required FlutterView view,
required TestPlatformDispatcher platformDispatcher,
}) : _view = view, _platformDispatcher = platformDispatcher;
required TestDisplay display,
}) :
_view = view,
_platformDispatcher = platformDispatcher,
_display = display;
/// The [FlutterView] backing this [TestFlutterView].
final FlutterView _view;
......@@ -634,6 +673,10 @@ class TestFlutterView implements FlutterView {
TestPlatformDispatcher get platformDispatcher => _platformDispatcher;
final TestPlatformDispatcher _platformDispatcher;
@override
TestDisplay get display => _display;
final TestDisplay _display;
@override
Object get viewId => _view.viewId;
......@@ -646,20 +689,21 @@ class TestFlutterView implements FlutterView {
/// See also:
///
/// * [FlutterView.devicePixelRatio] for the standard implementation
/// * [TestDisplay.devicePixelRatio] which will stay in sync with this value
/// * [resetDevicePixelRatio] to reset this value specifically
/// * [reset] to reset all test values for this view
@override
double get devicePixelRatio => _devicePixelRatio ?? _view.devicePixelRatio;
double? _devicePixelRatio;
double get devicePixelRatio => _display._devicePixelRatio ?? _view.devicePixelRatio;
set devicePixelRatio(double value) {
_devicePixelRatio = value;
platformDispatcher.onMetricsChanged?.call();
_display.devicePixelRatio = value;
}
/// Resets [devicePixelRatio] for this test view to the default value for this view.
///
/// This will also reset the [devicePixelRatio] for the [TestDisplay]
/// that is related to this view.
void resetDevicePixelRatio() {
_devicePixelRatio = null;
platformDispatcher.onMetricsChanged?.call();
_display.resetDevicePixelRatio();
}
/// The display features to use for this test.
......@@ -947,6 +991,158 @@ class TestFlutterView implements FlutterView {
}
}
/// A version of [Display] that can be modified to allow for testing various
/// use cases.
///
/// Updates to the [TestDisplay] will be surfaced through
/// [PlatformDispatcher.onMetricsChanged].
class TestDisplay implements Display {
/// Creates a new [TestDisplay] backed by the given [Display].
TestDisplay(TestPlatformDispatcher platformDispatcher, Display display)
: _platformDispatcher = platformDispatcher, _display = display;
final Display _display;
final TestPlatformDispatcher _platformDispatcher;
@override
int get id => _display.id;
/// The device pixel ratio to use for this test.
///
/// Defaults to the value provided by [Display.devicePixelRatio]. This
/// can only be set in a test environment to emulate different display
/// configurations. A standard [Display] is not mutable from the framework.
///
/// See also:
///
/// * [Display.devicePixelRatio] for the standard implementation
/// * [TestFlutterView.devicePixelRatio] which will stay in sync with this value
/// * [resetDevicePixelRatio] to reset this value specifically
/// * [reset] to reset all test values for this display
@override
double get devicePixelRatio => _devicePixelRatio ?? _display.devicePixelRatio;
double? _devicePixelRatio;
set devicePixelRatio(double value) {
_devicePixelRatio = value;
_platformDispatcher.onMetricsChanged?.call();
}
/// Resets [devicePixelRatio] to the default value for this display.
///
/// This will also reset the [devicePixelRatio] for any [TestFlutterView]s
/// that are related to this display.
void resetDevicePixelRatio() {
_devicePixelRatio = null;
_platformDispatcher.onMetricsChanged?.call();
}
/// The refresh rate to use for this test.
///
/// Defaults to the value provided by [Display.refreshRate]. This
/// can only be set in a test environment to emulate different display
/// configurations. A standard [Display] is not mutable from the framework.
///
/// See also:
///
/// * [Display.refreshRate] for the standard implementation
/// * [resetRefreshRate] to reset this value specifically
/// * [reset] to reset all test values for this display
@override
double get refreshRate => _refreshRate ?? _display.refreshRate;
double? _refreshRate;
set refreshRate(double value) {
_refreshRate = value;
_platformDispatcher.onMetricsChanged?.call();
}
/// Resets [refreshRate] to the default value for this display.
void resetRefreshRate() {
_refreshRate = null;
_platformDispatcher.onMetricsChanged?.call();
}
/// The size of the [Display] to use for this test.
///
/// Defaults to the value provided by [Display.refreshRate]. This
/// can only be set in a test environment to emulate different display
/// configurations. A standard [Display] is not mutable from the framework.
///
/// See also:
///
/// * [Display.refreshRate] for the standard implementation
/// * [resetRefreshRate] to reset this value specifically
/// * [reset] to reset all test values for this display
@override
Size get size => _size ?? _display.size;
Size? _size;
set size(Size value) {
_size = value;
_platformDispatcher.onMetricsChanged?.call();
}
/// Resets [size] to the default value for this display.
void resetSize() {
_size = null;
_platformDispatcher.onMetricsChanged?.call();
}
/// Resets all values on this [TestDisplay].
///
/// See also:
/// * [resetDevicePixelRatio] to reset [devicePixelRatio] specifically
/// * [resetRefreshRate] to reset [refreshRate] specifically
/// * [resetSize] to reset [size] specifically
void reset() {
resetDevicePixelRatio();
resetRefreshRate();
resetSize();
}
/// This gives us some grace time when the dart:ui side adds something to
/// [Display], and makes things easier when we do rolls to give
/// us time to catch up.
@override
dynamic noSuchMethod(Invocation invocation) {
return null;
}
}
// TODO(pdblasi-google): Remove this once the Display API is stable and supported on all platforms
class _UnsupportedDisplay implements TestDisplay {
_UnsupportedDisplay(this._platformDispatcher, this._view, this.error);
final FlutterView _view;
final Object? error;
@override
final TestPlatformDispatcher _platformDispatcher;
@override
double get devicePixelRatio => _devicePixelRatio ?? _view.devicePixelRatio;
@override
double? _devicePixelRatio;
@override
set devicePixelRatio(double value) {
_devicePixelRatio = value;
_platformDispatcher.onMetricsChanged?.call();
}
@override
void resetDevicePixelRatio() {
_devicePixelRatio = null;
_platformDispatcher.onMetricsChanged?.call();
}
@override
dynamic noSuchMethod(Invocation invocation) {
throw UnsupportedError(
'The Display API is unsupported in this context. '
'As of the last metrics change on PlatformDispatcher, this was the error '
'given when trying to prepare the display for testing: $error',
);
}
}
/// Deprecated. Will be removed in a future version of Flutter.
///
/// This class has been deprecated to prepare for Flutter's upcoming support
......
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'utils/fake_and_mock_utils.dart';
void main() {
group('TestDisplay', () {
Display trueDisplay() => PlatformDispatcher.instance.displays.single;
TestDisplay boundDisplay() => WidgetsBinding.instance.platformDispatcher.displays.single as TestDisplay;
tearDown(() {
boundDisplay().reset();
});
testWidgets('can handle new methods without breaking', (WidgetTester tester) async {
final dynamic testDisplay = tester.view.display;
//ignore: avoid_dynamic_calls
expect(testDisplay.someNewProperty, null);
});
testWidgets('can fake devicePixelRatio', (WidgetTester tester) async {
verifyPropertyFaked<double>(
tester: tester,
realValue: trueDisplay().devicePixelRatio,
fakeValue: trueDisplay().devicePixelRatio + 1,
propertyRetriever: () => boundDisplay().devicePixelRatio,
propertyFaker: (_, double fake) {
boundDisplay().devicePixelRatio = fake;
},
);
});
testWidgets('can reset devicePixelRatio', (WidgetTester tester) async {
verifyPropertyReset<double>(
tester: tester,
fakeValue: trueDisplay().devicePixelRatio + 1,
propertyRetriever: () => boundDisplay().devicePixelRatio,
propertyResetter: () => boundDisplay().resetDevicePixelRatio(),
propertyFaker: (double fake) {
boundDisplay().devicePixelRatio = fake;
},
);
});
testWidgets('resetting devicePixelRatio also resets view.devicePixelRatio', (WidgetTester tester) async {
verifyPropertyReset(
tester: tester,
fakeValue: trueDisplay().devicePixelRatio + 1,
propertyRetriever: () => tester.view.devicePixelRatio,
propertyResetter: () => boundDisplay().resetDevicePixelRatio(),
propertyFaker: (double dpr) => boundDisplay().devicePixelRatio = dpr,
);
});
testWidgets('updating devicePixelRatio also updates view.devicePixelRatio', (WidgetTester tester) async {
tester.view.display.devicePixelRatio = tester.view.devicePixelRatio + 1;
expect(tester.view.devicePixelRatio, tester.view.display.devicePixelRatio);
});
testWidgets('can fake refreshRate', (WidgetTester tester) async {
verifyPropertyFaked<double>(
tester: tester,
realValue: trueDisplay().refreshRate,
fakeValue: trueDisplay().refreshRate + 1,
propertyRetriever: () => boundDisplay().refreshRate,
propertyFaker: (_, double fake) {
boundDisplay().refreshRate = fake;
},
);
});
testWidgets('can reset refreshRate', (WidgetTester tester) async {
verifyPropertyReset<double>(
tester: tester,
fakeValue: trueDisplay().refreshRate + 1,
propertyRetriever: () => boundDisplay().refreshRate,
propertyResetter: () => boundDisplay().resetRefreshRate(),
propertyFaker: (double fake) {
boundDisplay().refreshRate = fake;
},
);
});
testWidgets('can fake size', (WidgetTester tester) async {
verifyPropertyFaked<Size>(
tester: tester,
realValue: trueDisplay().size,
fakeValue: const Size(354, 856),
propertyRetriever: () => boundDisplay().size,
propertyFaker: (_, Size fake) {
boundDisplay().size = fake;
},
);
});
testWidgets('can reset size', (WidgetTester tester) async {
verifyPropertyReset<Size>(
tester: tester,
fakeValue: const Size(465, 980),
propertyRetriever: () => boundDisplay().size,
propertyResetter: () => boundDisplay().resetSize(),
propertyFaker: (Size fake) {
boundDisplay().size = fake;
},
);
});
testWidgets('can reset all values', (WidgetTester tester) async {
final DisplaySnapshot initial = DisplaySnapshot(tester.view.display);
tester.view.display.devicePixelRatio = 7;
tester.view.display.refreshRate = 40;
tester.view.display.size = const Size(476, 823);
final DisplaySnapshot faked = DisplaySnapshot(tester.view.display);
tester.view.display.reset();
final DisplaySnapshot reset = DisplaySnapshot(tester.view.display);
expect(initial, isNot(matchesSnapshot(faked)));
expect(initial, matchesSnapshot(reset));
});
});
}
class DisplaySnapshot {
DisplaySnapshot(Display display) :
devicePixelRatio = display.devicePixelRatio,
refreshRate = display.refreshRate,
id = display.id,
size = display.size;
final double devicePixelRatio;
final double refreshRate;
final int id;
final Size size;
}
Matcher matchesSnapshot(DisplaySnapshot expected) => _DisplaySnapshotMatcher(expected);
class _DisplaySnapshotMatcher extends Matcher {
_DisplaySnapshotMatcher(this.expected);
final DisplaySnapshot expected;
@override
Description describe(Description description) {
description.add('snapshot of a Display matches');
return description;
}
@override
Description describeMismatch(dynamic item, Description mismatchDescription, Map<dynamic, dynamic> matchState, bool verbose) {
assert(item is DisplaySnapshot, 'Can only match against snapshots of Display.');
final DisplaySnapshot actual = item as DisplaySnapshot;
if (actual.devicePixelRatio != expected.devicePixelRatio) {
mismatchDescription.add('actual.devicePixelRatio (${actual.devicePixelRatio}) did not match expected.devicePixelRatio (${expected.devicePixelRatio})');
}
if (actual.refreshRate != expected.refreshRate) {
mismatchDescription.add('actual.refreshRate (${actual.refreshRate}) did not match expected.refreshRate (${expected.refreshRate})');
}
if (actual.size != expected.size) {
mismatchDescription.add('actual.size (${actual.size}) did not match expected.size (${expected.size})');
}
if (actual.id != expected.id) {
mismatchDescription.add('actual.id (${actual.id}) did not match expected.id (${expected.id})');
}
return mismatchDescription;
}
@override
bool matches(dynamic item, Map<dynamic, dynamic> matchState) {
assert(item is DisplaySnapshot, 'Can only match against snapshots of Display.');
final DisplaySnapshot actual = item as DisplaySnapshot;
return actual.devicePixelRatio == expected.devicePixelRatio &&
actual.refreshRate == expected.refreshRate &&
actual.size == expected.size &&
actual.id == expected.id;
}
}
......@@ -2,7 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:ui' show AccessibilityFeatures, Brightness, Locale, PlatformDispatcher;
import 'dart:ui' show AccessibilityFeatures, Brightness, Display, FlutterView, Locale, PlatformDispatcher, VoidCallback;
import 'package:flutter/widgets.dart' show WidgetsBinding, WidgetsBindingObserver;
import 'package:flutter_test/flutter_test.dart';
......@@ -152,6 +152,102 @@ void main() {
expect(observer.locales, equals(expectedValue));
retrieveTestBinding(tester).platformDispatcher.localesTestValue = defaultLocales;
});
// TODO(pdblasi-google): Removed this group of tests when the Display API is stable and supported on all platforms.
group('TestPlatformDispatcher with unsupported Display API', () {
testWidgets('can initialize with empty displays', (WidgetTester tester) async {
expect(() {
TestPlatformDispatcher(
platformDispatcher: _FakePlatformDispatcher(
displays: <Display>[],
views: <FlutterView>[
_FakeFlutterView(),
],
)
);
}, isNot(throwsA(anything)));
});
testWidgets('can initialize with mismatched displays', (WidgetTester tester) async {
expect(() {
TestPlatformDispatcher(
platformDispatcher: _FakePlatformDispatcher(
displays: <Display>[
_FakeDisplay(id: 2),
],
views: <FlutterView>[
_FakeFlutterView(display: _FakeDisplay(id: 1)),
],
)
);
}, isNot(throwsA(anything)));
});
testWidgets('creates test views for all views', (WidgetTester tester) async {
final PlatformDispatcher backingDispatcher = _FakePlatformDispatcher(
displays: <Display>[],
views: <FlutterView>[
_FakeFlutterView(),
],
);
final TestPlatformDispatcher testDispatcher = TestPlatformDispatcher(
platformDispatcher: backingDispatcher,
);
expect(testDispatcher.views.length, backingDispatcher.views.length);
});
group('creates TestFlutterViews', () {
testWidgets('that defaults to the correct devicePixelRatio', (WidgetTester tester) async {
const double expectedDpr = 2.5;
final TestPlatformDispatcher testDispatcher = TestPlatformDispatcher(
platformDispatcher: _FakePlatformDispatcher(
displays: <Display>[],
views: <FlutterView>[
_FakeFlutterView(devicePixelRatio: expectedDpr),
],
)
);
expect(testDispatcher.views.single.devicePixelRatio, expectedDpr);
});
testWidgets('with working devicePixelRatio setter', (WidgetTester tester) async {
const double expectedDpr = 2.5;
const double defaultDpr = 4;
final TestPlatformDispatcher testDispatcher = TestPlatformDispatcher(
platformDispatcher: _FakePlatformDispatcher(
displays: <Display>[],
views: <FlutterView>[
_FakeFlutterView(devicePixelRatio: defaultDpr),
],
)
);
testDispatcher.views.single.devicePixelRatio = expectedDpr;
expect(testDispatcher.views.single.devicePixelRatio, expectedDpr);
});
testWidgets('with working resetDevicePixelRatio', (WidgetTester tester) async {
const double changedDpr = 2.5;
const double defaultDpr = 4;
final TestPlatformDispatcher testDispatcher = TestPlatformDispatcher(
platformDispatcher: _FakePlatformDispatcher(
displays: <Display>[],
views: <FlutterView>[
_FakeFlutterView(devicePixelRatio: defaultDpr),
],
)
);
testDispatcher.views.single.devicePixelRatio = changedDpr;
testDispatcher.views.single.resetDevicePixelRatio();
expect(testDispatcher.views.single.devicePixelRatio, defaultDpr);
});
});
});
}
class TestObserver with WidgetsBindingObserver {
......@@ -162,3 +258,45 @@ class TestObserver with WidgetsBindingObserver {
this.locales = locales;
}
}
class _FakeDisplay extends Fake implements Display {
_FakeDisplay({this.id = 0});
@override
final int id;
}
class _FakeFlutterView extends Fake implements FlutterView {
_FakeFlutterView({
this.devicePixelRatio = 1,
Display? display,
}) : _display = display;
@override
final double devicePixelRatio;
// This emulates the PlatformDispatcher not having a display on the engine
// side. We don't have access to the `_displayId` used in the engine to try
// to find it and can't directly extend `FlutterView` to emulate it closer.
@override
Display get display {
assert(_display != null);
return _display!;
}
final Display? _display;
@override
final Object viewId = 1;
}
class _FakePlatformDispatcher extends Fake implements PlatformDispatcher {
_FakePlatformDispatcher({required this.displays, required this.views});
@override
final Iterable<Display> displays;
@override
final Iterable<FlutterView> views;
@override
VoidCallback? onMetricsChanged;
}
......@@ -52,6 +52,12 @@ void main() {
);
});
testWidgets('updating devicePixelRatio also updates display.devicePixelRatio', (WidgetTester tester) async {
tester.view.devicePixelRatio = tester.view.devicePixelRatio + 1;
expect(tester.view.display.devicePixelRatio, tester.view.devicePixelRatio);
});
testWidgets('can fake displayFeatures', (WidgetTester tester) async {
verifyPropertyFaked<List<DisplayFeature>>(
tester: tester,
......@@ -288,6 +294,7 @@ void main() {
final TestFlutterView view = TestFlutterView(
view: backingView,
platformDispatcher: tester.binding.platformDispatcher,
display: _FakeDisplay(),
);
view.render(expectedScene);
......@@ -302,6 +309,7 @@ void main() {
final TestFlutterView view = TestFlutterView(
view: backingView,
platformDispatcher: tester.binding.platformDispatcher,
display: _FakeDisplay(),
);
view.updateSemantics(expectedUpdate);
......@@ -312,7 +320,6 @@ void main() {
});
}
Matcher matchesSnapshot(FlutterViewSnapshot expected) => _FlutterViewSnapshotMatcher(expected);
class _FlutterViewSnapshotMatcher extends Matcher {
......@@ -424,7 +431,7 @@ class FlutterViewSnapshot {
final ViewPadding viewPadding;
}
class _FakeFlutterView implements FlutterView {
class _FakeFlutterView extends Fake implements FlutterView {
SemanticsUpdate? lastSemanticsUpdate;
Scene? lastRenderedScene;
......@@ -437,9 +444,6 @@ class _FakeFlutterView implements FlutterView {
void render(Scene scene) {
lastRenderedScene = scene;
}
@override
dynamic noSuchMethod(Invocation invocation) {
return null;
}
}
class _FakeDisplay extends Fake implements TestDisplay { }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment