Skip to content

Commit

Permalink
Add some more testing.
Browse files Browse the repository at this point in the history
  • Loading branch information
ditman committed Feb 14, 2024
1 parent a015985 commit ad56900
Show file tree
Hide file tree
Showing 4 changed files with 246 additions and 20 deletions.
12 changes: 11 additions & 1 deletion lib/web_ui/lib/src/engine/js_interop/js_app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,20 @@ extension JsFlutterViewOptionsExtension on JsFlutterViewOptions {

/// The JS bindings for a [ViewConstraints] object.
@JS()
@anonymous
@staticInterop
class JsViewConstraints {}
class JsViewConstraints {
external factory JsViewConstraints({
double? minWidth,
double? maxWidth,
double? minHeight,
double? maxHeight,
});
}

/// The attributes of a [JsViewConstraints] object.
///
/// These attributes are expressed in *logical* pixels.
extension JsViewConstraintsExtension on JsViewConstraints {
external double? get maxHeight;
external double? get maxWidth;
Expand Down
51 changes: 35 additions & 16 deletions lib/web_ui/lib/src/engine/window.dart
Original file line number Diff line number Diff line change
Expand Up @@ -155,8 +155,9 @@ base class EngineFlutterView implements ui.FlutterView {

@override
ViewConstraints get physicalConstraints {
final ui.Size currentSize = _computePhysicalSize();
return ViewConstraints.fromJs(_jsViewConstraints, currentSize);
final double dpr = devicePixelRatio;
final ui.Size currentLogicalSize = physicalSize / dpr;
return ViewConstraints.fromJs(_jsViewConstraints, currentLogicalSize) * dpr;
}

final JsViewConstraints? _jsViewConstraints;
Expand All @@ -169,7 +170,7 @@ base class EngineFlutterView implements ui.FlutterView {
}

void resize(ui.Size newPhysicalSize) {
// The browser wants logical sizes!
// The browser uses CSS, and CSS operates in logical sizes.
final ui.Size logicalSize = newPhysicalSize / devicePixelRatio;
dom.rootElement.style
..width = '${logicalSize.width}px'
Expand Down Expand Up @@ -733,15 +734,22 @@ class ViewConstraints implements ui.ViewConstraints {
minHeight = size.height,
maxHeight = size.height;

factory ViewConstraints.fromJs(JsViewConstraints? constraints, ui.Size size) {
/// Converts JsViewConstraints into ViewConstraints.
///
/// Since JsViewConstraints are expressed by the user, in logical pixels, this
/// conversion uses logical pixels for the available size as well. The resulting
/// ViewConstraints object can be multiplied by devicePixelRatio later to compute
/// the physicalViewConstraints.
factory ViewConstraints.fromJs(
JsViewConstraints? constraints, ui.Size availableLogicalSize) {
if (constraints == null) {
return ViewConstraints.tight(size);
return ViewConstraints.tight(availableLogicalSize);
}
return ViewConstraints(
minWidth: _computeMinConstraintValue(constraints.minWidth, size.width),
minHeight: _computeMinConstraintValue(constraints.minHeight, size.height),
maxWidth: _computeMaxConstraintValue(constraints.maxWidth, size.width),
maxHeight: _computeMaxConstraintValue(constraints.maxHeight, size.height),
minWidth: _computeMinConstraintValue(constraints.minWidth, availableLogicalSize.width),
minHeight: _computeMinConstraintValue(constraints.minHeight, availableLogicalSize.height),
maxWidth: _computeMaxConstraintValue(constraints.maxWidth, availableLogicalSize.width),
maxHeight: _computeMaxConstraintValue(constraints.maxHeight, availableLogicalSize.height),
);
}

Expand All @@ -763,6 +771,15 @@ class ViewConstraints implements ui.ViewConstraints {
@override
bool get isTight => minWidth >= maxWidth && minHeight >= maxHeight;

ViewConstraints operator*(double factor) {
return ViewConstraints(
minWidth: minWidth * factor,
maxWidth: maxWidth * factor,
minHeight: minHeight * factor,
maxHeight: maxHeight * factor,
);
}

@override
ViewConstraints operator/(double factor) {
return ViewConstraints(
Expand Down Expand Up @@ -812,28 +829,30 @@ class ViewConstraints implements ui.ViewConstraints {
}
}

// Computes the "min" value for a constraint that takes into account user configuration
// and the actual available size.
// Computes the "min" value for a constraint that takes into account user `desired`
// configuration and the actual available value.
//
// Returns the configured userValue, unless it's null (not passed) in which it returns
// the actual physicalSize.
// Returns the `desired` value unless it is `null`, in which case it returns the
// `available` value.
double _computeMinConstraintValue(double? desired, double available) {
assert(desired == null || desired >= 0, 'Minimum constraint must be >= 0 if set.');
assert(desired == null || desired.isFinite, 'Minimum constraint must be finite.');
// Do we need to max/min the values, or just trust the user?
return desired ?? available;
}

// Computes the "max" value for a constraint that takes into account user `desired`
// configuration and the `available` size.
//
// Returns the `desired` value unless it is `null`, in which case it returns the
// `available` size.
// `available` value.
//
// A `desired` value of `Infinity` or `Number.POSITIVE_INFINITY` (from JS) means
// "unconstrained".
//
// This method allows returning values larger than `available`, so the Flutter
// app is able to stretch its container up to a certain value, without being
// fully unconstrained.
double _computeMaxConstraintValue(double? desired, double available) {
assert(desired == null || desired >= 0, 'Maximum constraint must be >= 0 if set.');
// Do we need to max/min the values, or just trust the user?
return desired ?? available;
}
155 changes: 155 additions & 0 deletions lib/web_ui/test/engine/view/view_constraints_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
// Copyright 2013 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:async';

import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart' as ui;

import '../../common/matchers.dart';

void main() {
internalBootstrapBrowserTest(() => testMain);
}

Future<void> testMain() async {
const ui.Size size = ui.Size(640, 480);

group('ViewConstraints.fromJs', () {
test('Negative min constraints -> Assertion error.', () async {
expect(
() => ViewConstraints.fromJs(
JsViewConstraints(
minWidth: -1,
),
size),
throwsAssertionError);
expect(
() => ViewConstraints.fromJs(
JsViewConstraints(
minHeight: -1,
),
size),
throwsAssertionError);
});

test('Infinite min constraints -> Assertion error.', () async {
expect(
() => ViewConstraints.fromJs(
JsViewConstraints(
minWidth: double.infinity,
),
size),
throwsAssertionError);
expect(
() => ViewConstraints.fromJs(
JsViewConstraints(
minHeight: double.infinity,
),
size),
throwsAssertionError);
});

test('Negative max constraints -> Assertion error.', () async {
expect(
() => ViewConstraints.fromJs(
JsViewConstraints(
maxWidth: -1,
),
size),
throwsAssertionError);
expect(
() => ViewConstraints.fromJs(
JsViewConstraints(
maxHeight: -1,
),
size),
throwsAssertionError);
});

test('null JS Constraints -> Tight to size', () async {
expect(
ViewConstraints.fromJs(null, size),
const ViewConstraints(
minWidth: 640, maxWidth: 640, //
minHeight: 480, maxHeight: 480, //
));
});

test('non-null JS Constraints -> Computes sizes', () async {
final JsViewConstraints constraints = JsViewConstraints(
minWidth: 500, maxWidth: 600, //
minHeight: 300, maxHeight: 400, //
);
expect(
ViewConstraints.fromJs(constraints, size),
const ViewConstraints(
minWidth: 500, maxWidth: 600, //
minHeight: 300, maxHeight: 400, //
));
});

test('null JS Width -> Tight to width. Computes height.', () async {
final JsViewConstraints constraints = JsViewConstraints(
minHeight: 200,
maxHeight: 320,
);
expect(
ViewConstraints.fromJs(constraints, size),
const ViewConstraints(
minWidth: 640, maxWidth: 640, //
minHeight: 200, maxHeight: 320, //
));
});

test('null JS Height -> Tight to height. Computed width.', () async {
final JsViewConstraints constraints = JsViewConstraints(
minWidth: 200,
maxWidth: 320,
);
expect(
ViewConstraints.fromJs(constraints, size),
const ViewConstraints(
minWidth: 200, maxWidth: 320, //
minHeight: 480, maxHeight: 480, //
));
});

test(
'non-null JS Constraints -> Computes sizes. Max values can be greater than available size.',
() async {
final JsViewConstraints constraints = JsViewConstraints(
minWidth: 500, maxWidth: 1024, //
minHeight: 300, maxHeight: 768, //
);
expect(
ViewConstraints.fromJs(constraints, size),
const ViewConstraints(
minWidth: 500, maxWidth: 1024, //
minHeight: 300, maxHeight: 768, //
));
});

test(
'non-null JS Constraints -> Computes sizes. Max values can be unconstrained.',
() async {
final JsViewConstraints constraints = JsViewConstraints(
minWidth: 500,
maxWidth: double.infinity,
minHeight: 300,
maxHeight: double.infinity,
);
expect(
ViewConstraints.fromJs(constraints, size),
const ViewConstraints(
// ignore: avoid_redundant_argument_values
minWidth: 500, maxWidth: double.infinity,
// ignore: avoid_redundant_argument_values
minHeight: 300, maxHeight: double.infinity,
));
});
});
}
48 changes: 45 additions & 3 deletions lib/web_ui/test/engine/window_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -584,10 +584,11 @@ Future<void> testMain() async {
..width = '10px'
..height = '10px';
domDocument.body!.append(host);

// Let the DOM settle before starting the test, so we don't get the first
// 10,10 Size in the test. Otherwise, the ResizeObserver may trigger
// unexpectedly after the test has started, and break our "first" result.
await Future<void>.delayed(const Duration(milliseconds: 250));
await view.onResize.first;

metricsChangedCount = 0;
view.platformDispatcher.onMetricsChanged = () {
Expand Down Expand Up @@ -651,8 +652,49 @@ Future<void> testMain() async {
expect(view.physicalSize, const ui.Size(50.0, 50.0));

// Inspect the rootElement directly:
expect(view.dom.rootElement.clientWidth, 50 / 2.5);
expect(view.dom.rootElement.clientHeight, 50 / 2.5);
expect(view.dom.rootElement.clientWidth, 50 / view.devicePixelRatio);
expect(view.dom.rootElement.clientHeight, 50 / view.devicePixelRatio);
});
});

group('physicalConstraints', () {
const double dpr = 2.5;
late DomHTMLDivElement host;
late EngineFlutterView view;

setUp(() async {
EngineFlutterDisplay.instance.debugOverrideDevicePixelRatio(dpr);
host = createDomHTMLDivElement()
..style.width = '640px'
..style.height = '480px';
domDocument.body!.append(host);
});

tearDown(() {
host.remove();
EngineFlutterDisplay.instance.debugOverrideDevicePixelRatio(null);
});

test('JsViewConstraints are passed and used to compute physicalConstraints', () async {
view = EngineFlutterView(
EnginePlatformDispatcher.instance,
host,
viewConstraints: JsViewConstraints(
minHeight: 320,
maxHeight: double.infinity,
));

// All the metrics until now have been expressed in logical pixels, because
// they're coming from CSS/the browser, which works in logical pixels.
expect(view.physicalConstraints, const ViewConstraints(
minHeight: 320,
// ignore: avoid_redundant_argument_values
maxHeight: double.infinity,
minWidth: 640,
maxWidth: 640,
// However the framework expects physical pixels, so we multiply our expectations
// by the current DPR (2.5)
) * dpr);
});
});
}

0 comments on commit ad56900

Please sign in to comment.