[web] do not send SemanticsAction.focus inside frame (#162554)
When a `SemanticsAction` fires while rendering a frame, delay it by a zero-length timer. ## Explanation A concrete situation where this happens is when a semantics update causes DOM focus shift. DOM focus events are delivered synchronously when induced programmatically. We _want_ to notify the framework about the shift. Since it wasn't the framework that decided where the focus moved, the framework may end up out-of-sync with the engine about which widget is currently focused. However, if the framework is still in the middle of rendering a frame, the notification may induce an illegal `setState`. We have to wait until the framework is done before delivering the notification. ## How * Introduce `FrameService` and consolidate all frame scheduling logic into it (this also makes it way more testable). * Update all code that needs to schedule frames to use `FrameService`. * Introduce `isRenderingFrame` boolean that can be used to tell if a frame is being rendered. * Change `invokeOnSemanticsAction` to use `isRenderingFrame` to decide if the action can be delivered immediately, or delayed by a zero-length timer. Fixes https://github.com/flutter/flutter/issues/162472
This commit is contained in:
parent
015cfa88d4
commit
3c43a9939b
@ -41215,6 +41215,7 @@ ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/font_fallback_data.dart + ../
|
||||
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/font_fallbacks.dart + ../../../flutter/LICENSE
|
||||
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/fonts.dart + ../../../flutter/LICENSE
|
||||
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/frame_reference.dart + ../../../flutter/LICENSE
|
||||
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/frame_service.dart + ../../../flutter/LICENSE
|
||||
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/frame_timing_recorder.dart + ../../../flutter/LICENSE
|
||||
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/html/backdrop_filter.dart + ../../../flutter/LICENSE
|
||||
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/html/bitmap_canvas.dart + ../../../flutter/LICENSE
|
||||
@ -44166,6 +44167,7 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/font_fallback_data.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/font_fallbacks.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/fonts.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/frame_reference.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/frame_service.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/frame_timing_recorder.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/backdrop_filter.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/html/bitmap_canvas.dart
|
||||
|
@ -66,6 +66,7 @@ export 'engine/font_fallback_data.dart';
|
||||
export 'engine/font_fallbacks.dart';
|
||||
export 'engine/fonts.dart';
|
||||
export 'engine/frame_reference.dart';
|
||||
export 'engine/frame_service.dart';
|
||||
export 'engine/frame_timing_recorder.dart';
|
||||
export 'engine/html/backdrop_filter.dart';
|
||||
export 'engine/html/bitmap_canvas.dart';
|
||||
|
@ -2,10 +2,8 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
/// A monotonically increasing frame number being rendered.
|
||||
///
|
||||
/// Used for debugging only.
|
||||
int debugFrameNumber = 1;
|
||||
// TODO(yjbanov): this file should be deleted as part of https://github.com/flutter/flutter/issues/145954
|
||||
// because it's only used by the HTML renderer.
|
||||
|
||||
List<FrameReference<dynamic>> frameReferences = <FrameReference<dynamic>>[];
|
||||
|
||||
|
195
engine/src/flutter/lib/web_ui/lib/src/engine/frame_service.dart
Normal file
195
engine/src/flutter/lib/web_ui/lib/src/engine/frame_service.dart
Normal file
@ -0,0 +1,195 @@
|
||||
// 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 'dart:js_interop';
|
||||
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:ui/ui.dart' as ui;
|
||||
|
||||
import 'dom.dart';
|
||||
import 'frame_timing_recorder.dart';
|
||||
import 'platform_dispatcher.dart';
|
||||
|
||||
/// Provides frame scheduling functionality and frame lifecycle information to
|
||||
/// all of the web engine.
|
||||
///
|
||||
/// If new frame-related functionality needs to be added to the web engine,
|
||||
/// prefer to add it here instead of implementing it ad hoc.
|
||||
class FrameService {
|
||||
/// The singleton instance of the [FrameService] used to schedule frames.
|
||||
///
|
||||
/// This may be overridden in tests, for example, to pump fake frames, using
|
||||
/// [debugOverrideFrameService].
|
||||
static FrameService get instance => _instance ??= FrameService();
|
||||
static FrameService? _instance;
|
||||
|
||||
/// Overrides the value returned by [instance].
|
||||
///
|
||||
/// If [mock] is null, resets the value of [instance] back to real
|
||||
/// implementation.
|
||||
///
|
||||
/// This is intended for tests only.
|
||||
@visibleForTesting
|
||||
static void debugOverrideFrameService(FrameService? mock) {
|
||||
_instance = mock;
|
||||
}
|
||||
|
||||
/// A monotonically increasing frame number being rendered.
|
||||
///
|
||||
/// This is intended for tests only.
|
||||
int get debugFrameNumber => _debugFrameNumber;
|
||||
int _debugFrameNumber = 0;
|
||||
|
||||
/// Resets [debugFrameNumber] back to zero.
|
||||
///
|
||||
/// This is intended for tests only.
|
||||
@visibleForTesting
|
||||
void debugResetFrameNumber() {
|
||||
_debugFrameNumber = 0;
|
||||
}
|
||||
|
||||
/// Whether a frame has already been scheduled.
|
||||
///
|
||||
/// If this value is currently true, then calling [scheduleFrame] has no effect.
|
||||
bool get isFrameScheduled => _isFrameScheduled;
|
||||
bool _isFrameScheduled = false;
|
||||
|
||||
/// Whether the engine and framework are in the middle of rendering a frame.
|
||||
///
|
||||
/// Some DOM events can be triggered synchronously with DOM mutations, such as
|
||||
/// the DOM "focus" event. Handlers of such events may wish to be aware of the
|
||||
/// fact that the engine is actively rendering a frame. This is especially
|
||||
/// true for DOM event handlers that send notifications to the framework. It
|
||||
/// goes against the framework's design to receive events that lead to widget
|
||||
/// state changes invalidating the current frame. That must be done in the
|
||||
/// next frame.
|
||||
///
|
||||
/// DOM event handlers whose notifications to the framework result in state
|
||||
/// changes may want to delay their notifications, e.g. by scheduling them in
|
||||
/// a timer.
|
||||
bool get isRenderingFrame => _isRenderingFrame;
|
||||
bool _isRenderingFrame = false;
|
||||
|
||||
/// If not null, called immediately and synchronously after rendering a frame.
|
||||
///
|
||||
/// At the time this callback is called, the framework completed responding to
|
||||
/// `onBeginFrame` and `onDrawFrame`, and [isRenderingFrame] is set to false.
|
||||
///
|
||||
/// Any microtasks scheduled while rendering the frame execute after this
|
||||
/// callback.
|
||||
ui.VoidCallback? onFinishedRenderingFrame;
|
||||
|
||||
void scheduleFrame() {
|
||||
// A frame is already scheduled. Do nothing.
|
||||
if (_isFrameScheduled) {
|
||||
return;
|
||||
}
|
||||
|
||||
_isFrameScheduled = true;
|
||||
|
||||
domWindow.requestAnimationFrame((JSNumber highResTime) {
|
||||
// Reset immediately for two reasons:
|
||||
//
|
||||
// * While drawing a frame the framework may attempt to schedule a new
|
||||
// frame, e.g. when there's a continuous animation.
|
||||
// * If this value is stuck in `true` state, there will be no way to
|
||||
// schedule new frames and the app will freeze. It is therefore the
|
||||
// safest to reset this value before running any significant amount of
|
||||
// functionality that may throw exceptions, or produce wasm traps.
|
||||
_isFrameScheduled = false;
|
||||
|
||||
try {
|
||||
_isRenderingFrame = true;
|
||||
_debugFrameNumber += 1;
|
||||
_renderFrame(highResTime.toDartDouble);
|
||||
} finally {
|
||||
_isRenderingFrame = false;
|
||||
onFinishedRenderingFrame?.call();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// The framework has special handling for the warm-up frame. It uses timers,
|
||||
/// ensures that there's no regular frame scheduling happening before or
|
||||
/// between timers. So this logic here trusts the the framework fulfills its
|
||||
/// promises. For example, there's no check if _isFrameScheduled is already
|
||||
/// true. The assumption that no prior frames were scheduled.
|
||||
void scheduleWarmUpFrame({
|
||||
required ui.VoidCallback beginFrame,
|
||||
required ui.VoidCallback drawFrame,
|
||||
}) {
|
||||
_isFrameScheduled = true;
|
||||
|
||||
// A note from dkwingsmt:
|
||||
//
|
||||
// We use timers here to ensure that microtasks flush in between.
|
||||
//
|
||||
// TODO(dkwingsmt): This logic was moved from the framework and is different
|
||||
// from how Web renders a regular frame, which doesn't flush microtasks
|
||||
// between the callbacks at all (see `initializeEngineServices`). We might
|
||||
// want to change this. See the to-do in `initializeEngineServices` and
|
||||
// https://github.com/flutter/engine/pull/50570#discussion_r1496671676
|
||||
|
||||
Timer.run(() {
|
||||
_isFrameScheduled = false;
|
||||
_isRenderingFrame = true;
|
||||
_debugFrameNumber += 1;
|
||||
// TODO(yjbanov): it's funky that if beginFrame crashes, the drawFrame
|
||||
// fires anyway. We should clean this up, or better explain
|
||||
// what the expectations are for various situations. The
|
||||
// "we did this before so let's continue doing it" excuse
|
||||
// only works so far (referring to the discussion linked
|
||||
// above).
|
||||
beginFrame();
|
||||
});
|
||||
|
||||
Timer.run(() {
|
||||
try {
|
||||
drawFrame();
|
||||
} finally {
|
||||
_isRenderingFrame = false;
|
||||
onFinishedRenderingFrame?.call();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _renderFrame(double highResTime) {
|
||||
FrameTimingRecorder.recordCurrentFrameVsync();
|
||||
|
||||
// In Flutter terminology "building a frame" consists of "beginning
|
||||
// frame" and "drawing frame".
|
||||
//
|
||||
// We do not call `recordBuildFinish` from here because
|
||||
// part of the rasterization process, particularly in the HTML
|
||||
// renderer, takes place in the `SceneBuilder.build()`.
|
||||
FrameTimingRecorder.recordCurrentFrameBuildStart();
|
||||
|
||||
// We have to convert high-resolution time to `int` so we can construct
|
||||
// a `Duration` out of it. However, high-res time is supplied in
|
||||
// milliseconds as a double value, with sub-millisecond information
|
||||
// hidden in the fraction. So we first multiply it by 1000 to uncover
|
||||
// microsecond precision, and only then convert to `int`.
|
||||
final int highResTimeMicroseconds = (1000 * highResTime).toInt();
|
||||
|
||||
if (EnginePlatformDispatcher.instance.onBeginFrame != null) {
|
||||
EnginePlatformDispatcher.instance.invokeOnBeginFrame(
|
||||
Duration(microseconds: highResTimeMicroseconds),
|
||||
);
|
||||
}
|
||||
|
||||
if (EnginePlatformDispatcher.instance.onDrawFrame != null) {
|
||||
// On mobile Flutter flushes microtasks between onBeginFrame and
|
||||
// onDrawFrame. The web doesn't because there's no way to hook into the
|
||||
// event loop, which is controlled by the browser (mobile Flutter hooks
|
||||
// into the event loop using C++ code behind-the-scenes). This hasn't
|
||||
// been an issue yet. However, if in the future someone can find a way
|
||||
// to implement it exactly like mobile does, that would be great.
|
||||
//
|
||||
// (Also see the to-do in
|
||||
// `EnginePlatformDispatcher.scheduleWarmUpFrame`).
|
||||
EnginePlatformDispatcher.instance.invokeOnDrawFrame();
|
||||
}
|
||||
}
|
||||
}
|
@ -7,6 +7,7 @@ import 'package:ui/ui.dart' as ui;
|
||||
|
||||
import '../dom.dart';
|
||||
import '../frame_reference.dart';
|
||||
import '../frame_service.dart';
|
||||
import '../onscreen_logging.dart';
|
||||
import '../semantics.dart';
|
||||
import '../util.dart';
|
||||
@ -73,7 +74,7 @@ void commitScene(PersistedScene scene) {
|
||||
retainedSurfaces = <PersistedSurface>[];
|
||||
}
|
||||
if (debugExplainSurfaceStats) {
|
||||
debugPrintSurfaceStats(scene, debugFrameNumber);
|
||||
debugPrintSurfaceStats(scene, FrameService.instance.debugFrameNumber);
|
||||
debugRepaintSurfaceStatsOverlay(scene);
|
||||
}
|
||||
|
||||
@ -97,10 +98,6 @@ void commitScene(PersistedScene scene) {
|
||||
if (debugExplainSurfaceStats) {
|
||||
surfaceStats = <PersistedSurface, DebugSurfaceStats>{};
|
||||
}
|
||||
assert(() {
|
||||
debugFrameNumber++;
|
||||
return true;
|
||||
}());
|
||||
}
|
||||
|
||||
/// Signature of a function that receives a [PersistedSurface].
|
||||
|
@ -4,7 +4,6 @@
|
||||
|
||||
import 'dart:async';
|
||||
import 'dart:developer' as developer;
|
||||
import 'dart:js_interop';
|
||||
|
||||
import 'package:ui/src/engine.dart';
|
||||
import 'package:ui/ui.dart' as ui;
|
||||
@ -154,51 +153,6 @@ Future<void> initializeEngineServices({
|
||||
Profiler.ensureInitialized();
|
||||
}
|
||||
|
||||
bool waitingForAnimation = false;
|
||||
scheduleFrameCallback = () {
|
||||
// We're asked to schedule a frame and call `frameHandler` when the frame
|
||||
// fires.
|
||||
if (!waitingForAnimation) {
|
||||
waitingForAnimation = true;
|
||||
domWindow.requestAnimationFrame((JSNumber highResTime) {
|
||||
FrameTimingRecorder.recordCurrentFrameVsync();
|
||||
|
||||
// In Flutter terminology "building a frame" consists of "beginning
|
||||
// frame" and "drawing frame".
|
||||
//
|
||||
// We do not call `recordBuildFinish` from here because
|
||||
// part of the rasterization process, particularly in the HTML
|
||||
// renderer, takes place in the `SceneBuilder.build()`.
|
||||
FrameTimingRecorder.recordCurrentFrameBuildStart();
|
||||
|
||||
// Reset immediately, because `frameHandler` can schedule more frames.
|
||||
waitingForAnimation = false;
|
||||
|
||||
// We have to convert high-resolution time to `int` so we can construct
|
||||
// a `Duration` out of it. However, high-res time is supplied in
|
||||
// milliseconds as a double value, with sub-millisecond information
|
||||
// hidden in the fraction. So we first multiply it by 1000 to uncover
|
||||
// microsecond precision, and only then convert to `int`.
|
||||
final int highResTimeMicroseconds = (1000 * highResTime.toDartDouble).toInt();
|
||||
|
||||
if (EnginePlatformDispatcher.instance.onBeginFrame != null) {
|
||||
EnginePlatformDispatcher.instance.invokeOnBeginFrame(
|
||||
Duration(microseconds: highResTimeMicroseconds),
|
||||
);
|
||||
}
|
||||
|
||||
if (EnginePlatformDispatcher.instance.onDrawFrame != null) {
|
||||
// TODO(yjbanov): technically Flutter flushes microtasks between
|
||||
// onBeginFrame and onDrawFrame. We don't, which hasn't
|
||||
// been an issue yet, but eventually we'll have to
|
||||
// implement it properly. (Also see the to-do in
|
||||
// `EnginePlatformDispatcher.scheduleWarmUpFrame`).
|
||||
EnginePlatformDispatcher.instance.invokeOnDrawFrame();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
assetManager ??= ui_web.AssetManager(assetBase: configuration.assetBase);
|
||||
_setAssetManager(assetManager);
|
||||
|
||||
|
@ -13,11 +13,6 @@ import 'package:ui/ui_web/src/ui_web.dart' as ui_web;
|
||||
|
||||
import '../engine.dart';
|
||||
|
||||
/// Requests that the browser schedule a frame.
|
||||
///
|
||||
/// This may be overridden in tests, for example, to pump fake frames.
|
||||
ui.VoidCallback? scheduleFrameCallback;
|
||||
|
||||
/// Signature of functions added as a listener to high contrast changes
|
||||
typedef HighContrastListener = void Function(bool enabled);
|
||||
typedef _KeyDataResponseCallback = void Function(bool handled);
|
||||
@ -735,10 +730,7 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher {
|
||||
/// scheduling of frames.
|
||||
@override
|
||||
void scheduleFrame() {
|
||||
if (scheduleFrameCallback == null) {
|
||||
throw Exception('scheduleFrameCallback must be initialized first.');
|
||||
}
|
||||
scheduleFrameCallback!();
|
||||
FrameService.instance.scheduleFrame();
|
||||
}
|
||||
|
||||
@override
|
||||
@ -746,15 +738,7 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher {
|
||||
required ui.VoidCallback beginFrame,
|
||||
required ui.VoidCallback drawFrame,
|
||||
}) {
|
||||
Timer.run(beginFrame);
|
||||
// We use timers here to ensure that microtasks flush in between.
|
||||
//
|
||||
// TODO(dkwingsmt): This logic was moved from the framework and is different
|
||||
// from how Web renders a regular frame, which doesn't flush microtasks
|
||||
// between the callbacks at all (see `initializeEngineServices`). We might
|
||||
// want to change this. See the to-do in `initializeEngineServices` and
|
||||
// https://github.com/flutter/engine/pull/50570#discussion_r1496671676
|
||||
Timer.run(drawFrame);
|
||||
FrameService.instance.scheduleWarmUpFrame(beginFrame: beginFrame, drawFrame: drawFrame);
|
||||
}
|
||||
|
||||
/// Updates the application's rendering on the GPU with the newly provided
|
||||
@ -1248,11 +1232,28 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher {
|
||||
/// Engine code should use this method instead of the callback directly.
|
||||
/// Otherwise zones won't work properly.
|
||||
void invokeOnSemanticsAction(int viewId, int nodeId, ui.SemanticsAction action, ByteData? args) {
|
||||
invoke1<ui.SemanticsActionEvent>(
|
||||
_onSemanticsActionEvent,
|
||||
_onSemanticsActionEventZone,
|
||||
ui.SemanticsActionEvent(type: action, nodeId: nodeId, viewId: viewId, arguments: args),
|
||||
);
|
||||
void sendActionToFramework() {
|
||||
invoke1<ui.SemanticsActionEvent>(
|
||||
_onSemanticsActionEvent,
|
||||
_onSemanticsActionEventZone,
|
||||
ui.SemanticsActionEvent(type: action, nodeId: nodeId, viewId: viewId, arguments: args),
|
||||
);
|
||||
}
|
||||
|
||||
// Semantic actions should not be sent to the framework while the framework
|
||||
// is rendering a frame, even if the action is induced as a result of
|
||||
// rendering it. An example of when the framework might need to be notified
|
||||
// about an action as a result of rendering a new frame is a semantics
|
||||
// update which results in the screen reader shifting focus (DOM "focus"
|
||||
// events are delivered synchronously). In this situation a
|
||||
// `SemanticsAction.focus` might be induced, and while it should be
|
||||
// delivered to the framework asap, it must be done after the frame is done
|
||||
// rendering at the earliest.
|
||||
if (FrameService.instance.isRenderingFrame) {
|
||||
Timer.run(sendActionToFramework);
|
||||
} else {
|
||||
sendActionToFramework();
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(dnfield): make this work on web.
|
||||
|
@ -15,7 +15,6 @@ void main() {
|
||||
void testMain() {
|
||||
test('services are initalized separately from UI', () async {
|
||||
final JsFlutterConfiguration? config = await bootstrapAndExtractConfig();
|
||||
expect(scheduleFrameCallback, isNull);
|
||||
|
||||
expect(findGlassPane(), isNull);
|
||||
expect(RawKeyboard.instance, isNull);
|
||||
@ -24,7 +23,6 @@ void testMain() {
|
||||
|
||||
// After initializing services the UI should remain intact.
|
||||
await initializeEngineServices(jsConfiguration: config);
|
||||
expect(scheduleFrameCallback, isNotNull);
|
||||
expect(windowFlutterCanvasKit, isNotNull);
|
||||
|
||||
expect(findGlassPane(), isNull);
|
||||
|
@ -7,6 +7,7 @@ import 'dart:js_interop';
|
||||
|
||||
import 'package:test/test.dart';
|
||||
import 'package:ui/src/engine.dart' as engine;
|
||||
import 'package:ui/src/engine/frame_service.dart';
|
||||
import 'package:ui/src/engine/initialization.dart';
|
||||
import 'package:ui/ui.dart' as ui;
|
||||
import 'package:ui/ui_web/src/ui_web.dart' as ui_web;
|
||||
@ -41,7 +42,7 @@ void setUpUnitTests({
|
||||
engine.EngineFlutterDisplay.instance.debugOverrideDevicePixelRatio(devicePixelRatio);
|
||||
engine.EnginePlatformDispatcher.instance.implicitView?.debugPhysicalSizeOverride =
|
||||
const ui.Size(800 * devicePixelRatio, 600 * devicePixelRatio);
|
||||
engine.scheduleFrameCallback = () {};
|
||||
FrameService.debugOverrideFrameService(FakeFrameService());
|
||||
}
|
||||
|
||||
setUpRenderingForTests();
|
||||
@ -90,3 +91,8 @@ void _disableImplicitView() {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FakeFrameService extends FrameService {
|
||||
@override
|
||||
void scheduleFrame() {}
|
||||
}
|
||||
|
@ -0,0 +1,152 @@
|
||||
// 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';
|
||||
|
||||
void main() {
|
||||
internalBootstrapBrowserTest(() => testMain);
|
||||
}
|
||||
|
||||
void testMain() {
|
||||
group('FrameService', () {
|
||||
setUp(() {
|
||||
FrameService.debugOverrideFrameService(null);
|
||||
expect(FrameService.instance.runtimeType, FrameService);
|
||||
EnginePlatformDispatcher.instance.onBeginFrame = null;
|
||||
EnginePlatformDispatcher.instance.onDrawFrame = null;
|
||||
});
|
||||
|
||||
test('instance is valid and can be overridden', () {
|
||||
final defaultInstance = FrameService.instance;
|
||||
expect(defaultInstance.runtimeType, FrameService);
|
||||
|
||||
FrameService.debugOverrideFrameService(DummyFrameService());
|
||||
expect(FrameService.instance.runtimeType, DummyFrameService);
|
||||
|
||||
FrameService.debugOverrideFrameService(null);
|
||||
expect(FrameService.instance.runtimeType, FrameService);
|
||||
});
|
||||
|
||||
test('counts frames', () async {
|
||||
final instance = FrameService.instance;
|
||||
instance.debugResetFrameNumber();
|
||||
|
||||
final frameCompleter = Completer<void>();
|
||||
instance.onFinishedRenderingFrame = () {
|
||||
frameCompleter.complete();
|
||||
};
|
||||
|
||||
expect(instance.debugFrameNumber, 0);
|
||||
instance.scheduleFrame();
|
||||
await frameCompleter.future;
|
||||
expect(instance.debugFrameNumber, 1);
|
||||
});
|
||||
|
||||
test('isFrameScheduled is true iff the frame is scheduled', () async {
|
||||
final instance = FrameService.instance;
|
||||
instance.debugResetFrameNumber();
|
||||
|
||||
var frameCompleter = Completer<void>();
|
||||
instance.onFinishedRenderingFrame = () {
|
||||
frameCompleter.complete();
|
||||
};
|
||||
|
||||
// Normal case: pump one frame
|
||||
expect(instance.isFrameScheduled, isFalse);
|
||||
instance.scheduleFrame();
|
||||
expect(instance.isFrameScheduled, isTrue);
|
||||
await frameCompleter.future;
|
||||
expect(instance.isFrameScheduled, isFalse);
|
||||
expect(instance.debugFrameNumber, 1);
|
||||
|
||||
// Test idempotency
|
||||
instance.debugResetFrameNumber();
|
||||
frameCompleter = Completer<void>();
|
||||
instance.scheduleFrame();
|
||||
instance.scheduleFrame();
|
||||
instance.scheduleFrame();
|
||||
instance.scheduleFrame();
|
||||
|
||||
expect(instance.isFrameScheduled, isTrue);
|
||||
await frameCompleter.future;
|
||||
expect(instance.isFrameScheduled, isFalse);
|
||||
expect(instance.debugFrameNumber, 1);
|
||||
});
|
||||
|
||||
test('onBeginFrame and onDrawFrame are called with isRenderingFrame set to true', () async {
|
||||
final instance = FrameService.instance;
|
||||
|
||||
bool? isRenderingInOnBeginFrame;
|
||||
EnginePlatformDispatcher.instance.onBeginFrame = (_) {
|
||||
isRenderingInOnBeginFrame = instance.isRenderingFrame;
|
||||
};
|
||||
|
||||
bool? isRenderingInOnDrawFrame;
|
||||
EnginePlatformDispatcher.instance.onDrawFrame = () {
|
||||
isRenderingInOnDrawFrame = instance.isRenderingFrame;
|
||||
};
|
||||
|
||||
final frameCompleter = Completer<void>();
|
||||
bool? valueInOnFinishedRenderingFrame;
|
||||
instance.onFinishedRenderingFrame = () {
|
||||
valueInOnFinishedRenderingFrame = instance.isRenderingFrame;
|
||||
frameCompleter.complete();
|
||||
};
|
||||
|
||||
expect(instance.isRenderingFrame, isFalse);
|
||||
instance.scheduleFrame();
|
||||
|
||||
// IMPORTANT: scheduled, but not yet rendering
|
||||
expect(instance.isRenderingFrame, isFalse);
|
||||
await frameCompleter.future;
|
||||
expect(instance.isFrameScheduled, isFalse);
|
||||
|
||||
expect(isRenderingInOnBeginFrame, isTrue);
|
||||
expect(isRenderingInOnDrawFrame, isTrue);
|
||||
expect(valueInOnFinishedRenderingFrame, isFalse);
|
||||
});
|
||||
|
||||
test('scheduleWarmUpFrame', () async {
|
||||
final instance = FrameService.instance;
|
||||
|
||||
final frameCompleter = Completer<void>();
|
||||
bool? valueInOnFinishedRenderingFrame;
|
||||
instance.onFinishedRenderingFrame = () {
|
||||
valueInOnFinishedRenderingFrame = instance.isRenderingFrame;
|
||||
frameCompleter.complete();
|
||||
};
|
||||
|
||||
bool? isRenderingInOnBeginFrame;
|
||||
bool? isRenderingInOnDrawFrame;
|
||||
|
||||
expect(instance.isRenderingFrame, isFalse);
|
||||
expect(instance.isFrameScheduled, isFalse);
|
||||
|
||||
instance.scheduleWarmUpFrame(
|
||||
beginFrame: () {
|
||||
isRenderingInOnBeginFrame = instance.isRenderingFrame;
|
||||
},
|
||||
drawFrame: () {
|
||||
isRenderingInOnDrawFrame = instance.isRenderingFrame;
|
||||
},
|
||||
);
|
||||
|
||||
// IMPORTANT: scheduled, but not yet rendering
|
||||
expect(instance.isFrameScheduled, isTrue);
|
||||
expect(instance.isRenderingFrame, isFalse);
|
||||
await frameCompleter.future;
|
||||
expect(instance.isFrameScheduled, isFalse);
|
||||
|
||||
expect(isRenderingInOnBeginFrame, isTrue);
|
||||
expect(isRenderingInOnDrawFrame, isTrue);
|
||||
expect(valueInOnFinishedRenderingFrame, isFalse);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
class DummyFrameService extends FrameService {}
|
@ -250,6 +250,73 @@ Future<void> testMain() async {
|
||||
);
|
||||
});
|
||||
|
||||
test('onSemanticsActionEvent delays action until after frame', () async {
|
||||
final eventLog = <ui.SemanticsAction>[];
|
||||
|
||||
void callback(ui.SemanticsActionEvent event) {
|
||||
eventLog.add(event.type);
|
||||
}
|
||||
|
||||
ui.PlatformDispatcher.instance.onSemanticsActionEvent = callback;
|
||||
|
||||
// Outside frame: action must be sent immediately
|
||||
EnginePlatformDispatcher.instance.invokeOnSemanticsAction(
|
||||
myWindow.viewId,
|
||||
0,
|
||||
ui.SemanticsAction.focus,
|
||||
null,
|
||||
);
|
||||
|
||||
expect(eventLog, [ui.SemanticsAction.focus]);
|
||||
eventLog.clear();
|
||||
|
||||
bool tapCalled = false;
|
||||
EnginePlatformDispatcher.instance.onBeginFrame = (_) {
|
||||
// Inside onBeginFrame: should be delayed
|
||||
EnginePlatformDispatcher.instance.invokeOnSemanticsAction(
|
||||
myWindow.viewId,
|
||||
0,
|
||||
ui.SemanticsAction.tap,
|
||||
null,
|
||||
);
|
||||
tapCalled = true;
|
||||
};
|
||||
|
||||
bool increaseCalled = false;
|
||||
EnginePlatformDispatcher.instance.onDrawFrame = () {
|
||||
// Inside onDrawFrame: should be delayed
|
||||
EnginePlatformDispatcher.instance.invokeOnSemanticsAction(
|
||||
myWindow.viewId,
|
||||
0,
|
||||
ui.SemanticsAction.increase,
|
||||
null,
|
||||
);
|
||||
increaseCalled = true;
|
||||
};
|
||||
|
||||
final frameCompleter = Completer<void>();
|
||||
FrameService.instance.onFinishedRenderingFrame = () {
|
||||
frameCompleter.complete();
|
||||
};
|
||||
|
||||
FrameService.instance.scheduleFrame();
|
||||
await frameCompleter.future;
|
||||
|
||||
// Even though invokeOnSemanticsAction was called for tap and increase
|
||||
// actions the actions have not yet been delivered to the framework, because
|
||||
// the actions happened inside onBeginFrame and onDrawFrame. The events are
|
||||
// queues in zero-length timers.
|
||||
expect(tapCalled, isTrue);
|
||||
expect(increaseCalled, isTrue);
|
||||
expect(eventLog, isEmpty);
|
||||
|
||||
// Flush the timers after the frame.
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
|
||||
// Now the events should be delivered.
|
||||
expect(eventLog, [ui.SemanticsAction.tap, ui.SemanticsAction.increase]);
|
||||
});
|
||||
|
||||
test('onAccessibilityFeaturesChanged preserves the zone', () {
|
||||
final Zone innerZone = Zone.current.fork();
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user