diff --git a/lib/web_ui/lib/platform_dispatcher.dart b/lib/web_ui/lib/platform_dispatcher.dart index f463595697e88..b033b32847838 100644 --- a/lib/web_ui/lib/platform_dispatcher.dart +++ b/lib/web_ui/lib/platform_dispatcher.dart @@ -80,8 +80,6 @@ abstract class PlatformDispatcher { void scheduleFrame(); - void render(Scene scene, [FlutterView view]); - AccessibilityFeatures get accessibilityFeatures; VoidCallback? get onAccessibilityFeaturesChanged; diff --git a/lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart b/lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart index d9280f28b6d7e..3a52eb6bc41af 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart @@ -30,8 +30,8 @@ class Rasterizer { /// Creates a new frame from this rasterizer's surface, draws the given /// [LayerTree] into it, and then submits the frame. - void draw(LayerTree layerTree) { - final ui.Size frameSize = view.physicalSize; + void draw(LayerTree layerTree, { ui.Size? size }) { + final ui.Size frameSize = size ?? view.physicalSize; if (frameSize.isEmpty) { // Available drawing area is empty. Skip drawing. return; diff --git a/lib/web_ui/lib/src/engine/canvaskit/renderer.dart b/lib/web_ui/lib/src/engine/canvaskit/renderer.dart index 567fac6849883..db64b70c24cb6 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/renderer.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/renderer.dart @@ -402,7 +402,7 @@ class CanvasKitRenderer implements Renderer { CkParagraphBuilder(style); @override - void renderScene(ui.Scene scene, ui.FlutterView view) { + void renderScene(ui.Scene scene, ui.FlutterView view, { ui.Size? size }) { // "Build finish" and "raster start" happen back-to-back because we // render on the same thread, so there's no overhead from hopping to // another thread. @@ -417,7 +417,7 @@ class CanvasKitRenderer implements Renderer { "Unable to render to a view which hasn't been registered"); final Rasterizer rasterizer = _rasterizers[view.viewId]!; - rasterizer.draw((scene as LayerScene).layerTree); + rasterizer.draw((scene as LayerScene).layerTree, size: size); frameTimingsOnRasterFinish(); } diff --git a/lib/web_ui/lib/src/engine/html/renderer.dart b/lib/web_ui/lib/src/engine/html/renderer.dart index 7e74faefc09a0..a0ba06230224d 100644 --- a/lib/web_ui/lib/src/engine/html/renderer.dart +++ b/lib/web_ui/lib/src/engine/html/renderer.dart @@ -323,7 +323,7 @@ class HtmlRenderer implements Renderer { CanvasParagraphBuilder(style as EngineParagraphStyle); @override - void renderScene(ui.Scene scene, ui.FlutterView view) { + void renderScene(ui.Scene scene, ui.FlutterView view, { ui.Size? size }) { final EngineFlutterView implicitView = EnginePlatformDispatcher.instance.implicitView!; implicitView.dom.setScene((scene as SurfaceScene).webOnlyRootElement!); frameTimingsOnRasterFinish(); diff --git a/lib/web_ui/lib/src/engine/js_interop/js_app.dart b/lib/web_ui/lib/src/engine/js_interop/js_app.dart index 0ca43aef50247..75434f3f24faa 100644 --- a/lib/web_ui/lib/src/engine/js_interop/js_app.dart +++ b/lib/web_ui/lib/src/engine/js_interop/js_app.dart @@ -25,11 +25,31 @@ extension JsFlutterViewOptionsExtension on JsFlutterViewOptions { return _hostElement!; } + @JS('viewConstraints') + external JsViewConstraints? get _viewConstraints; + JsViewConstraints? get viewConstraints { + // Assert constraints are valid? + return _viewConstraints; + } + @JS('initialData') external JSObject? get _initialData; Object? get initialData => _initialData?.toObjectDeep; } +/// The JS bindings for a [ViewConstraints] object. +@JS() +@staticInterop +class JsViewConstraints {} + +/// The attributes of a [JsViewConstraints] object. +extension JsViewConstraintsExtension on JsViewConstraints { + external double? get maxHeight; + external double? get maxWidth; + external double? get minHeight; + external double? get minWidth; +} + /// The public JS API of a running Flutter Web App. @JS() @anonymous diff --git a/lib/web_ui/lib/src/engine/platform_dispatcher.dart b/lib/web_ui/lib/src/engine/platform_dispatcher.dart index 8327b3d3cf26f..54353010fdd77 100644 --- a/lib/web_ui/lib/src/engine/platform_dispatcher.dart +++ b/lib/web_ui/lib/src/engine/platform_dispatcher.dart @@ -727,8 +727,9 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher { scheduleFrameCallback!(); } - /// Updates the application's rendering on the GPU with the newly provided - /// [Scene]. This function must be called within the scope of the + /// Updates the [view]'s rendering on the GPU with the newly provided [scene] of physical [size]. + /// + /// This function must be called within the scope of the /// [onBeginFrame] or [onDrawFrame] callbacks being invoked. If this function /// is called a second time during a single [onBeginFrame]/[onDrawFrame] /// callback sequence or called outside the scope of those callbacks, the call @@ -751,15 +752,18 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher { /// scheduling of frames. /// * [RendererBinding], the Flutter framework class which manages layout and /// painting. - @override - void render(ui.Scene scene, [ui.FlutterView? view]) { - assert(view != null || implicitView != null, - 'Calling render without a FlutterView'); - if (view == null && implicitView == null) { + void render(ui.Scene scene, { ui.FlutterView? view, ui.Size? size }) { + final ui.FlutterView? target = view ?? implicitView; + assert(target != null, 'Calling render without a FlutterView'); + if (target == null) { // If there is no view to render into, then this is a no-op. return; } - renderer.renderScene(scene, view ?? implicitView!); + + if (size != null && view is EngineFlutterView) { + view.dom.resize(size / view.devicePixelRatio); + } + renderer.renderScene(scene, target, size: size); } /// Additional accessibility features that may be enabled by the platform. diff --git a/lib/web_ui/lib/src/engine/renderer.dart b/lib/web_ui/lib/src/engine/renderer.dart index 9f39fcff9ea94..20cd961e2e1f5 100644 --- a/lib/web_ui/lib/src/engine/renderer.dart +++ b/lib/web_ui/lib/src/engine/renderer.dart @@ -222,5 +222,5 @@ abstract class Renderer { ui.ParagraphBuilder createParagraphBuilder(ui.ParagraphStyle style); - FutureOr renderScene(ui.Scene scene, ui.FlutterView view); + FutureOr renderScene(ui.Scene scene, ui.FlutterView view, { ui.Size? size }); } diff --git a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/renderer.dart b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/renderer.dart index 292994ec18122..b6d1d8f371754 100644 --- a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/renderer.dart +++ b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/renderer.dart @@ -401,7 +401,7 @@ class SkwasmRenderer implements Renderer { // TODO(harryterkelsen): Add multiview support, // https://github.com/flutter/flutter/issues/137073. @override - Future renderScene(ui.Scene scene, ui.FlutterView view) => + Future renderScene(ui.Scene scene, ui.FlutterView view, { ui.Size? size }) => sceneView.renderScene(scene as EngineScene); @override diff --git a/lib/web_ui/lib/src/engine/skwasm/skwasm_stub/renderer.dart b/lib/web_ui/lib/src/engine/skwasm/skwasm_stub/renderer.dart index fe63d34ad0462..2284af370b241 100644 --- a/lib/web_ui/lib/src/engine/skwasm/skwasm_stub/renderer.dart +++ b/lib/web_ui/lib/src/engine/skwasm/skwasm_stub/renderer.dart @@ -150,7 +150,7 @@ class SkwasmRenderer implements Renderer { } @override - void renderScene(ui.Scene scene, ui.FlutterView view) { + void renderScene(ui.Scene scene, ui.FlutterView view, { ui.Size? size }) { throw UnimplementedError('Skwasm not implemented on this platform.'); } diff --git a/lib/web_ui/lib/src/engine/view_embedder/dimensions_provider/custom_element_dimensions_provider.dart b/lib/web_ui/lib/src/engine/view_embedder/dimensions_provider/custom_element_dimensions_provider.dart index 7b39ca908e576..ea4b56f499e04 100644 --- a/lib/web_ui/lib/src/engine/view_embedder/dimensions_provider/custom_element_dimensions_provider.dart +++ b/lib/web_ui/lib/src/engine/view_embedder/dimensions_provider/custom_element_dimensions_provider.dart @@ -48,8 +48,13 @@ class CustomElementDimensionsProvider extends DimensionsProvider { final StreamController _onResizeStreamController = StreamController.broadcast(); + // A cache of the last Size reported by the browser for hostElement, so this + // never has to hit the clientWidth/Height metrics from the DOM. + ui.Size? _lastObservedSize; + // Broadcasts the last seen `Size`. void _broadcastSize(ui.Size size) { + _lastObservedSize = size; _onResizeStreamController.add(size); } @@ -68,10 +73,10 @@ class CustomElementDimensionsProvider extends DimensionsProvider { ui.Size computePhysicalSize() { final double devicePixelRatio = getDevicePixelRatio(); - return ui.Size( - _hostElement.clientWidth * devicePixelRatio, - _hostElement.clientHeight * devicePixelRatio, - ); + final ui.Size size = _lastObservedSize ?? + ui.Size(_hostElement.clientWidth, _hostElement.clientHeight); + + return size * devicePixelRatio; } @override diff --git a/lib/web_ui/lib/src/engine/view_embedder/dom_manager.dart b/lib/web_ui/lib/src/engine/view_embedder/dom_manager.dart index 06a65a8873300..96116cccdbdec 100644 --- a/lib/web_ui/lib/src/engine/view_embedder/dom_manager.dart +++ b/lib/web_ui/lib/src/engine/view_embedder/dom_manager.dart @@ -204,6 +204,13 @@ class DomManager { sceneHost.append(sceneElement); } } + + /// Resizes the [rootElement] to [logicalSize] (in px) via CSS. + void resize(ui.Size logicalSize) { + rootElement.style + ..width = '${logicalSize.width}px' + ..height = '${logicalSize.height}px'; + } } DomShadowRoot _attachShadowRoot(DomElement element) { diff --git a/lib/web_ui/lib/src/engine/view_embedder/flutter_view_manager.dart b/lib/web_ui/lib/src/engine/view_embedder/flutter_view_manager.dart index fc6cced9eefe0..e93956f0325a1 100644 --- a/lib/web_ui/lib/src/engine/view_embedder/flutter_view_manager.dart +++ b/lib/web_ui/lib/src/engine/view_embedder/flutter_view_manager.dart @@ -39,8 +39,11 @@ class FlutterViewManager { EngineFlutterView createAndRegisterView( JsFlutterViewOptions jsViewOptions, ) { - final EngineFlutterView view = - EngineFlutterView(_dispatcher, jsViewOptions.hostElement); + final EngineFlutterView view = EngineFlutterView( + _dispatcher, + jsViewOptions.hostElement, + viewConstraints: jsViewOptions.viewConstraints, + ); registerView(view, jsViewOptions: jsViewOptions); return view; } diff --git a/lib/web_ui/lib/src/engine/window.dart b/lib/web_ui/lib/src/engine/window.dart index a57cd1919edeb..f5c57ea02d9c7 100644 --- a/lib/web_ui/lib/src/engine/window.dart +++ b/lib/web_ui/lib/src/engine/window.dart @@ -13,6 +13,7 @@ import '../engine.dart' show DimensionsProvider, registerHotRestartListener, ren import 'browser_detection.dart'; import 'display.dart'; import 'dom.dart'; +import 'js_interop/js_app.dart' show JsViewConstraints, JsViewConstraintsExtension; import 'mouse/context_menu.dart'; import 'mouse/cursor.dart'; import 'navigation/history.dart'; @@ -48,17 +49,26 @@ base class EngineFlutterView implements ui.FlutterView { /// the Flutter view will be rendered. factory EngineFlutterView( EnginePlatformDispatcher platformDispatcher, - DomElement hostElement, + DomElement hostElement, { + JsViewConstraints? viewConstraints, + } ) = _EngineFlutterViewImpl; EngineFlutterView._( - this.viewId, this.platformDispatcher, - // This is nullable to accommodate the legacy `EngineFlutterWindow`. In - // multi-view mode, the host element is required for each view (as reflected - // by the public `EngineFlutterView` constructor). - DomElement? hostElement, - ) : embeddingStrategy = EmbeddingStrategy.create(hostElement: hostElement), + { + // This is configurable for the implicit view that sets it to a specific + // kImplicitViewId value. Otherwise, this could be auto-incremental and + // not configurable. + required this.viewId, + // This is nullable to accommodate the legacy `EngineFlutterWindow`. In + // multi-view mode, the host element is required for each view (as reflected + // by the public `EngineFlutterView` constructor). + DomElement? hostElement, + JsViewConstraints? viewConstraints, + } + ) : _jsViewConstraints = viewConstraints, + embeddingStrategy = EmbeddingStrategy.create(hostElement: hostElement), dimensionsProvider = DimensionsProvider.create(hostElement: hostElement) { // The embeddingStrategy will take care of cleaning up the rootElement on // hot restart. @@ -107,10 +117,9 @@ base class EngineFlutterView implements ui.FlutterView { } @override - void render(ui.Scene scene, {ui.Size? size}) { + void render(ui.Scene scene, { ui.Size? size }) { assert(!isDisposed, 'Trying to render a disposed EngineFlutterView.'); - // TODO(goderbauer): Respect the provided size when "physicalConstraints" are not always tight. See TODO on "physicalConstraints". - platformDispatcher.render(scene, this); + platformDispatcher.render(scene, view: this, size: size); } @override @@ -135,9 +144,13 @@ base class EngineFlutterView implements ui.FlutterView { late final PointerBinding pointerBinding; - // TODO(goderbauer): Provide API to configure constraints. See also TODO in "render". @override - ViewConstraints get physicalConstraints => ViewConstraints.tight(physicalSize); + ViewConstraints get physicalConstraints { + _computePhysicalSize(); + return ViewConstraints.fromJsOptions(_jsViewConstraints, physicalSize); + } + // The configured constraints used to compute the actual physicalConstraints. + final JsViewConstraints? _jsViewConstraints; late final EngineSemanticsOwner semantics = EngineSemanticsOwner(dom.semanticsHost); @@ -267,17 +280,19 @@ base class EngineFlutterView implements ui.FlutterView { final class _EngineFlutterViewImpl extends EngineFlutterView { _EngineFlutterViewImpl( - EnginePlatformDispatcher platformDispatcher, - DomElement hostElement, - ) : super._(_nextViewId++, platformDispatcher, hostElement); + super.platformDispatcher, + DomElement hostElement, { + super.viewConstraints, + } + ) : super._(viewId: _nextViewId++, hostElement: hostElement); } /// The Web implementation of [ui.SingletonFlutterWindow]. final class EngineFlutterWindow extends EngineFlutterView implements ui.SingletonFlutterWindow { EngineFlutterWindow._( - EnginePlatformDispatcher platformDispatcher, + super.platformDispatcher, DomElement? hostElement, - ) : super._(kImplicitViewId, platformDispatcher, hostElement) { + ) : super._(viewId: kImplicitViewId, hostElement: hostElement) { if (ui_web.isCustomUrlStrategySet) { _browserHistory = createHistoryForExistingState(ui_web.urlStrategy); } @@ -643,7 +658,7 @@ final class EngineFlutterWindow extends EngineFlutterView implements ui.Singleto EngineFlutterWindow get window { assert( _window != null, - 'Trying to access the implicit FlutterView, but it is not available.\n' + 'Trying to access the implicit FlutterView, but it is not available.' 'Note: the implicit FlutterView is not available in multi-view mode.', ); return _window!; @@ -692,12 +707,52 @@ class ViewConstraints implements ui.ViewConstraints { this.maxHeight = double.infinity, }); + factory ViewConstraints.fromJsOptions(JsViewConstraints? constraints, ui.Size? size) { + if (size == null) { + return const ViewConstraints(); + } + if (constraints == null) { + return ViewConstraints.tight(size); + } + return ViewConstraints( + minWidth: _computeMinValue(constraints.minWidth, size.width), + minHeight: _computeMinValue(constraints.minHeight, size.height), + maxWidth: _computeMaxValue(constraints.maxWidth, size.width), + maxHeight: _computeMaxValue(constraints.maxHeight, size.height), + ); + } + ViewConstraints.tight(ui.Size size) : minWidth = size.width, maxWidth = size.width, minHeight = size.height, maxHeight = size.height; + // Computes the "min" value for a constraint that takes into account user configuration + // and the actual available size. + // + // Returns the configured userValue, unless it's null (not passed) in which it returns + // the actual physicalSize. + static double _computeMinValue(double? userValue, double physicalSize) { + assert(userValue == null || userValue >= 0, 'Minimum constraint cannot be less than 0'); + return userValue ?? physicalSize; + } + + // Computes the "max" value for a constraint that takes into account user configuration + // and the available size. + // + // Returns the configured userValue unless: + // * It is null, in which case it returns the physicalSize + // * It is `-1`, in which case it returns "infinity" / unconstrained. + static double _computeMaxValue(double? userValue, double physicalSize) { + assert(userValue == null || userValue >= -1, 'Maximum constraint must be greater than 0 (or -1 for unconstrained)'); + return switch (userValue) { + null => physicalSize, + -1 => double.infinity, + _ => userValue, + }; + } + @override final double minWidth; @override diff --git a/lib/web_ui/test/canvaskit/multi_view_test.dart b/lib/web_ui/test/canvaskit/multi_view_test.dart index a5a67f473f4aa..5dcec1bb63588 100644 --- a/lib/web_ui/test/canvaskit/multi_view_test.dart +++ b/lib/web_ui/test/canvaskit/multi_view_test.dart @@ -32,41 +32,58 @@ void testMain() { scene = sb.build(); }); - test('can render into arbitrary views', () async { - CanvasKitRenderer.instance.renderScene(scene, implicitView); + // test('can render into arbitrary views', () async { + // CanvasKitRenderer.instance.renderScene(scene, implicitView); - final EngineFlutterView anotherView = EngineFlutterView( - EnginePlatformDispatcher.instance, createDomElement('another-view')); - EnginePlatformDispatcher.instance.viewManager.registerView(anotherView); + // final EngineFlutterView anotherView = EngineFlutterView( + // EnginePlatformDispatcher.instance, createDomElement('another-view')); + // EnginePlatformDispatcher.instance.viewManager.registerView(anotherView); - CanvasKitRenderer.instance.renderScene(scene, anotherView); - }); + // CanvasKitRenderer.instance.renderScene(scene, anotherView); + // }); - test('will error if trying to render into an unregistered view', () async { - final EngineFlutterView unregisteredView = EngineFlutterView( - EnginePlatformDispatcher.instance, - createDomElement('unregistered-view')); - expect( - () => CanvasKitRenderer.instance.renderScene(scene, unregisteredView), - throwsAssertionError, + test('can render to a specific size', () async { + const ui.Size size = ui.Size(1920, 1080); + final DomElement target = createDomElement('view-to-be-resized'); + final EngineFlutterView view = EngineFlutterView( + EnginePlatformDispatcher.instance, + target, ); - }); - test('will dispose the Rasterizer for a disposed view', () async { - final EngineFlutterView view = EngineFlutterView( - EnginePlatformDispatcher.instance, createDomElement('multi-view')); EnginePlatformDispatcher.instance.viewManager.registerView(view); - expect( - CanvasKitRenderer.instance.debugGetRasterizerForView(view), - isNotNull, - ); + CanvasKitRenderer.instance.renderScene(scene, view, size: size); - EnginePlatformDispatcher.instance.viewManager - .disposeAndUnregisterView(view.viewId); - expect( - CanvasKitRenderer.instance.debugGetRasterizerForView(view), - isNull, - ); + final DomCanvasElement canvas = view.dom.sceneHost.querySelector('canvas')! as DomCanvasElement; + + expect(canvas.width, size.width); + expect(canvas.height, size.height); }); + + // test('will error if trying to render into an unregistered view', () async { + // final EngineFlutterView unregisteredView = EngineFlutterView( + // EnginePlatformDispatcher.instance, + // createDomElement('unregistered-view')); + // expect( + // () => CanvasKitRenderer.instance.renderScene(scene, unregisteredView), + // throwsAssertionError, + // ); + // }); + + // test('will dispose the Rasterizer for a disposed view', () async { + // final EngineFlutterView view = EngineFlutterView( + // EnginePlatformDispatcher.instance, createDomElement('multi-view')); + // EnginePlatformDispatcher.instance.viewManager.registerView(view); + // expect( + // CanvasKitRenderer.instance.debugGetRasterizerForView(view), + // isNotNull, + // ); + + // EnginePlatformDispatcher.instance.viewManager + // .disposeAndUnregisterView(view.viewId); + // expect( + // CanvasKitRenderer.instance.debugGetRasterizerForView(view), + // isNull, + // ); + // }); }); } diff --git a/lib/web_ui/test/common/frame_timings_common.dart b/lib/web_ui/test/common/frame_timings_common.dart index 51087f3094b39..19ae4d1938ced 100644 --- a/lib/web_ui/test/common/frame_timings_common.dart +++ b/lib/web_ui/test/common/frame_timings_common.dart @@ -5,6 +5,7 @@ import 'dart:async'; import 'package:test/test.dart'; +import 'package:ui/src/engine.dart' show EnginePlatformDispatcher; import 'package:ui/ui.dart' as ui; /// Tests frame timings in a renderer-agnostic way. @@ -21,7 +22,7 @@ Future runFrameTimingsTest() async { sceneBuilder ..pushOffset(0, 0) ..pop(); - ui.PlatformDispatcher.instance.render(sceneBuilder.build()); + (ui.PlatformDispatcher.instance as EnginePlatformDispatcher).render(sceneBuilder.build()); frameDone.complete(); }; diff --git a/lib/web_ui/test/engine/semantics/semantics_test.dart b/lib/web_ui/test/engine/semantics/semantics_test.dart index 4cc3c8bd60246..99be774ee969e 100644 --- a/lib/web_ui/test/engine/semantics/semantics_test.dart +++ b/lib/web_ui/test/engine/semantics/semantics_test.dart @@ -2497,7 +2497,7 @@ void _testPlatformView() { width: 20, height: 30, ); - ui.PlatformDispatcher.instance.render(sceneBuilder.build()); + (ui.PlatformDispatcher.instance as EnginePlatformDispatcher).render(sceneBuilder.build()); final ui.SemanticsUpdateBuilder builder = ui.SemanticsUpdateBuilder(); final double dpr = EngineFlutterDisplay.instance.devicePixelRatio; diff --git a/lib/web_ui/test/engine/surface/scene_builder_test.dart b/lib/web_ui/test/engine/surface/scene_builder_test.dart index 3c893ddc2ec6f..5b7d61107b14d 100644 --- a/lib/web_ui/test/engine/surface/scene_builder_test.dart +++ b/lib/web_ui/test/engine/surface/scene_builder_test.dart @@ -477,7 +477,7 @@ void testMain() { // Pump an empty scene to reset it, otherwise the first frame will attempt // to diff left-overs from a previous test, which results in unpredictable // DOM mutations. - ui.PlatformDispatcher.instance.render(SurfaceSceneBuilder().build()); + (ui.PlatformDispatcher.instance as EnginePlatformDispatcher).render(SurfaceSceneBuilder().build()); // Renders a `string` by breaking it up into individual characters and // rendering each character into its own layer. diff --git a/lib/web_ui/test/engine/view_embedder/dimensions_provider/custom_element_dimensions_provider_test.dart b/lib/web_ui/test/engine/view_embedder/dimensions_provider/custom_element_dimensions_provider_test.dart index 9ee7a95083c05..7250bdc6a22ab 100644 --- a/lib/web_ui/test/engine/view_embedder/dimensions_provider/custom_element_dimensions_provider_test.dart +++ b/lib/web_ui/test/engine/view_embedder/dimensions_provider/custom_element_dimensions_provider_test.dart @@ -97,10 +97,10 @@ void doTests() { ..style.height = '10px'; domDocument.body!.append(sizeSource); provider = CustomElementDimensionsProvider(sizeSource); - // 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.delayed(const Duration(milliseconds: 250)); + // The first event should be the 10x10 size "change" when the element + // first renders. We wait until that event is dispatched so we can + // consider "first" events the resizes caused by the test, and not the setup. + await provider.onResize.first; }); tearDown(() { diff --git a/lib/web_ui/test/html/text/canvas_paragraph_test.dart b/lib/web_ui/test/html/text/canvas_paragraph_test.dart index 4a38f4f5cee1b..8a71fc38fc687 100644 --- a/lib/web_ui/test/html/text/canvas_paragraph_test.dart +++ b/lib/web_ui/test/html/text/canvas_paragraph_test.dart @@ -806,7 +806,7 @@ Future testMain() async { builder.addPicture(ui.Offset.zero, picture); final ui.Scene scene = builder.build(); - ui.PlatformDispatcher.instance.render(scene); + (ui.PlatformDispatcher.instance as EnginePlatformDispatcher).render(scene); picture.dispose(); scene.dispose();