Commit f5b9d388 authored by Adam Barth's avatar Adam Barth Committed by GitHub

Switch to the new semantics backend (#6259)

This match switches the framework to use the semantics backend in `dart:ui`
rather than the Mojo backend.
parent 0975d049
...@@ -365,7 +365,7 @@ class ThemeData { ...@@ -365,7 +365,7 @@ class ThemeData {
} }
@override @override
bool operator==(Object other) { bool operator ==(Object other) {
if (other.runtimeType != runtimeType) if (other.runtimeType != runtimeType)
return false; return false;
ThemeData otherData = other; ThemeData otherData = other;
......
...@@ -9,8 +9,6 @@ import 'package:flutter/gestures.dart'; ...@@ -9,8 +9,6 @@ import 'package:flutter/gestures.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:mojo/core.dart' as core;
import 'package:flutter_services/semantics.dart' as mojom;
import 'box.dart'; import 'box.dart';
import 'debug.dart'; import 'debug.dart';
...@@ -28,12 +26,15 @@ abstract class RendererBinding extends BindingBase implements SchedulerBinding, ...@@ -28,12 +26,15 @@ abstract class RendererBinding extends BindingBase implements SchedulerBinding,
_instance = this; _instance = this;
_pipelineOwner = new PipelineOwner( _pipelineOwner = new PipelineOwner(
onNeedVisualUpdate: ensureVisualUpdate, onNeedVisualUpdate: ensureVisualUpdate,
onScheduleInitialSemantics: _scheduleInitialSemantics, onSemanticsOwnerCreated: _handleSemanticsOwnerCreated,
onClearSemantics: _clearSemantics, onSemanticsOwnerDisposed: _handleSemanticsOwnerDisposed,
); );
ui.window.onMetricsChanged = handleMetricsChanged; ui.window
..onMetricsChanged = handleMetricsChanged
..onSemanticsEnabledChanged = _handleSemanticsEnabledChanged
..onSemanticsAction = _handleSemanticsAction;
initRenderView(); initRenderView();
initSemantics(); _handleSemanticsEnabledChanged();
assert(renderView != null); assert(renderView != null);
addPersistentFrameCallback(_handlePersistentFrameCallback); addPersistentFrameCallback(_handlePersistentFrameCallback);
} }
...@@ -134,18 +135,27 @@ abstract class RendererBinding extends BindingBase implements SchedulerBinding, ...@@ -134,18 +135,27 @@ abstract class RendererBinding extends BindingBase implements SchedulerBinding,
); );
} }
/// Prepares the rendering library to handle semantics requests from the engine. SemanticsHandle _semanticsHandle;
///
/// Called automatically when the binding is created. void _handleSemanticsEnabledChanged() {
void initSemantics() { if (ui.window.semanticsEnabled) {
shell.provideService(mojom.SemanticsServer.serviceName, (core.MojoMessagePipeEndpoint endpoint) { _semanticsHandle ??= _pipelineOwner.ensureSemantics();
mojom.SemanticsServerStub stub = new mojom.SemanticsServerStub.fromEndpoint(endpoint); } else {
SemanticsServer server = new SemanticsServer(pipelineOwner); _semanticsHandle?.dispose();
stub.impl = server; _semanticsHandle = null;
stub.ctrl.onError = (_) { }
server.dispose(); }
};
}); void _handleSemanticsAction(int id, SemanticsAction action) {
_pipelineOwner.semanticsOwner?.performAction(id, action);
}
void _handleSemanticsOwnerCreated() {
renderView.scheduleInitialSemantics();
}
void _handleSemanticsOwnerDisposed() {
renderView.clearSemantics();
} }
void _handlePersistentFrameCallback(Duration timeStamp) { void _handlePersistentFrameCallback(Duration timeStamp) {
...@@ -208,7 +218,7 @@ abstract class RendererBinding extends BindingBase implements SchedulerBinding, ...@@ -208,7 +218,7 @@ abstract class RendererBinding extends BindingBase implements SchedulerBinding,
pipelineOwner.flushCompositingBits(); pipelineOwner.flushCompositingBits();
pipelineOwner.flushPaint(); pipelineOwner.flushPaint();
renderView.compositeFrame(); // this sends the bits to the GPU renderView.compositeFrame(); // this sends the bits to the GPU
pipelineOwner.flushSemantics(); pipelineOwner.flushSemantics(); // this also sends the semantics to the OS.
} }
@override @override
...@@ -234,14 +244,6 @@ abstract class RendererBinding extends BindingBase implements SchedulerBinding, ...@@ -234,14 +244,6 @@ abstract class RendererBinding extends BindingBase implements SchedulerBinding,
}; };
instance?.renderView?.visitChildren(visitor); instance?.renderView?.visitChildren(visitor);
} }
void _scheduleInitialSemantics() {
renderView.scheduleInitialSemantics();
}
void _clearSemantics() {
renderView.clearSemantics();
}
} }
/// Prints a textual representation of the entire render tree. /// Prints a textual representation of the entire render tree.
...@@ -261,44 +263,6 @@ void debugDumpSemanticsTree() { ...@@ -261,44 +263,6 @@ void debugDumpSemanticsTree() {
debugPrint(RendererBinding.instance?.renderView?.debugSemantics?.toStringDeep() ?? 'Semantics not collected.'); debugPrint(RendererBinding.instance?.renderView?.debugSemantics?.toStringDeep() ?? 'Semantics not collected.');
} }
/// Exposes the [SemanticsNode] tree to the underlying platform.
class SemanticsServer extends mojom.SemanticsServer {
/// Creates a semantics server that listens to semantic informationa about the
/// given [PipelineOwner].
///
/// Call [dispose] to stop listening for semantic updates.
SemanticsServer(PipelineOwner pipelineOwner) {
_semanticsOwner = pipelineOwner.addSemanticsListener(_updateSemanticsTree);
}
SemanticsOwner _semanticsOwner;
final List<mojom.SemanticsListenerProxy> _listeners = <mojom.SemanticsListenerProxy>[];
/// Stops listening for semantic updates and closes all outstanding listeners.
void dispose() {
for (mojom.SemanticsListenerProxy listener in _listeners)
listener.close();
_listeners.clear();
_semanticsOwner.removeListener(_updateSemanticsTree);
_semanticsOwner = null;
}
void _updateSemanticsTree(List<mojom.SemanticsNode> nodes) {
for (mojom.SemanticsListenerProxy listener in _listeners)
listener.updateSemanticsTree(nodes);
}
@override
void addSemanticsListener(@checked mojom.SemanticsListenerProxy listener) {
_listeners.add(listener);
}
@override
void performAction(int id, mojom.SemanticAction encodedAction) {
_semanticsOwner.performAction(id, SemanticsAction.values[encodedAction.mojoEnumValue]);
}
}
/// A concrete binding for applications that use the Rendering framework /// A concrete binding for applications that use the Rendering framework
/// directly. This is the glue that binds the framework to the Flutter engine. /// directly. This is the glue that binds the framework to the Flutter engine.
/// ///
......
...@@ -732,6 +732,36 @@ class _ForkingSemanticsFragment extends _SemanticsFragment { ...@@ -732,6 +732,36 @@ class _ForkingSemanticsFragment extends _SemanticsFragment {
} }
} }
class SemanticsHandle {
SemanticsHandle._(this._owner, this.listener) {
assert(_owner != null);
if (listener != null)
_owner.semanticsOwner.addListener(listener);
}
PipelineOwner _owner;
final VoidCallback listener;
@mustCallSuper
void dispose() {
assert(() {
if (_owner == null) {
throw new FlutterError(
'SemanticsHandle has already been disposed.\n'
'Each SemanticsHandle should be disposed exactly once.'
);
}
return true;
});
if (_owner != null) {
if (listener != null)
_owner.semanticsOwner.removeListener(listener);
_owner._didDisposeSemanticsHandle();
_owner = null;
}
}
}
/// The pipeline owner manages the rendering pipeline. /// The pipeline owner manages the rendering pipeline.
/// ///
/// The pipeline owner provides an interface for driving the rendering pipeline /// The pipeline owner provides an interface for driving the rendering pipeline
...@@ -769,8 +799,8 @@ class PipelineOwner { ...@@ -769,8 +799,8 @@ class PipelineOwner {
/// through the rendering pipeline. /// through the rendering pipeline.
PipelineOwner({ PipelineOwner({
this.onNeedVisualUpdate, this.onNeedVisualUpdate,
this.onScheduleInitialSemantics, this.onSemanticsOwnerCreated,
this.onClearSemantics, this.onSemanticsOwnerDisposed,
}); });
/// Called when a render object associated with this pipeline owner wishes to /// Called when a render object associated with this pipeline owner wishes to
...@@ -782,20 +812,16 @@ class PipelineOwner { ...@@ -782,20 +812,16 @@ class PipelineOwner {
/// duplicate calls quickly. /// duplicate calls quickly.
final VoidCallback onNeedVisualUpdate; final VoidCallback onNeedVisualUpdate;
/// Called when [addSemanticsListener] is called when there was no /// Called whenever this pipeline owner creates as semantics object.
/// [SemanticsOwner] present, to request that the
/// [RenderObject.scheduleInitialSemantics] method be called on the
/// appropriate object(s).
/// ///
/// For example, the [RendererBinding] calls it on the [RenderView] object. /// Typical implementations will schedule the creation of the initial
final VoidCallback onScheduleInitialSemantics; /// semantics tree.
final VoidCallback onSemanticsOwnerCreated;
/// Called when the last [SemanticsListener] is removed from the /// Called whenever this pipeline owner disposes its semantics owner.
/// [SemanticsOwner], to request that the [RenderObject.clearSemantics] method
/// be called on the appropriate object(s).
/// ///
/// For example, the [RendererBinding] calls it on the [RenderView] object. /// Typical implementations will tear down the semantics tree.
final VoidCallback onClearSemantics; final VoidCallback onSemanticsOwnerDisposed;
/// Calls [onNeedVisualUpdate] if [onNeedVisualUpdate] is not null. /// Calls [onNeedVisualUpdate] if [onNeedVisualUpdate] is not null.
/// ///
...@@ -819,32 +845,6 @@ class PipelineOwner { ...@@ -819,32 +845,6 @@ class PipelineOwner {
_rootNode?.attach(this); _rootNode?.attach(this);
} }
/// Calls the given listener whenever the semantics of the render tree change.
///
/// Creates [semanticsOwner] if necessary.
SemanticsOwner addSemanticsListener(SemanticsListener listener) {
if (_semanticsOwner == null) {
_semanticsOwner = new SemanticsOwner(
initialListener: listener,
onLastListenerRemoved: _handleLastSemanticsListenerRemoved
);
if (onScheduleInitialSemantics != null)
onScheduleInitialSemantics();
} else {
_semanticsOwner.addListener(listener);
}
assert(_semanticsOwner != null);
return _semanticsOwner;
}
void _handleLastSemanticsListenerRemoved() {
assert(!_debugDoingSemantics);
if (onClearSemantics != null)
onClearSemantics();
_semanticsOwner.dispose();
_semanticsOwner = null;
}
List<RenderObject> _nodesNeedingLayout = <RenderObject>[]; List<RenderObject> _nodesNeedingLayout = <RenderObject>[];
/// Whether this pipeline is currently in the layout phase. /// Whether this pipeline is currently in the layout phase.
...@@ -953,18 +953,39 @@ class PipelineOwner { ...@@ -953,18 +953,39 @@ class PipelineOwner {
/// The object that is managing semantics for this pipeline owner, if any. /// The object that is managing semantics for this pipeline owner, if any.
/// ///
/// An owner is created by [addSemanticsListener] the first time a listener is /// An owner is created by [ensureSemantics]. The owner is valid for as long
/// added. /// there are [SemanticsHandle] returned by [ensureSemantics] that have not
/// /// yet be disposed. Once the last handle has been disposed, the
/// The owner is valid for as long as there are listeners. Once the last /// [semanticsOwner] field will revert to null, and the previous owner will be
/// listener is removed (by calling [SemanticsOwner.removeListener] on the /// disposed.
/// [semanticsOwner]), the [semanticsOwner] field will revert to null, and the
/// previous owner will be disposed.
/// ///
/// When [semanticsOwner] is null, the [PipelineOwner] skips all steps /// When [semanticsOwner] is null, the [PipelineOwner] skips all steps
/// relating to semantics. /// relating to semantics.
SemanticsOwner get semanticsOwner => _semanticsOwner; SemanticsOwner get semanticsOwner => _semanticsOwner;
SemanticsOwner _semanticsOwner; SemanticsOwner _semanticsOwner;
int _outstandingSemanticsHandle = 0;
SemanticsHandle ensureSemantics({ VoidCallback listener }) {
if (_outstandingSemanticsHandle++ == 0) {
assert(_semanticsOwner == null);
_semanticsOwner = new SemanticsOwner();
if (onSemanticsOwnerCreated != null)
onSemanticsOwnerCreated();
}
return new SemanticsHandle._(this, listener);
}
void _didDisposeSemanticsHandle() {
assert(_semanticsOwner != null);
if (--_outstandingSemanticsHandle == 0) {
_semanticsOwner.dispose();
_semanticsOwner = null;
if (onSemanticsOwnerDisposed != null)
onSemanticsOwnerDisposed();
}
}
bool _debugDoingSemantics = false; bool _debugDoingSemantics = false;
final List<RenderObject> _nodesNeedingSemantics = <RenderObject>[]; final List<RenderObject> _nodesNeedingSemantics = <RenderObject>[];
...@@ -987,12 +1008,12 @@ class PipelineOwner { ...@@ -987,12 +1008,12 @@ class PipelineOwner {
if (node._needsSemanticsUpdate && node.owner == this) if (node._needsSemanticsUpdate && node.owner == this)
node._updateSemantics(); node._updateSemantics();
} }
_semanticsOwner.sendSemanticsUpdate();
} finally { } finally {
_nodesNeedingSemantics.clear(); _nodesNeedingSemantics.clear();
assert(() { _debugDoingSemantics = false; return true; }); assert(() { _debugDoingSemantics = false; return true; });
Timeline.finishSync(); Timeline.finishSync();
} }
_semanticsOwner.sendSemanticsTree();
} }
} }
...@@ -1996,9 +2017,6 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget { ...@@ -1996,9 +2017,6 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget {
/// Removes all semantics from this render object and its descendants. /// Removes all semantics from this render object and its descendants.
/// ///
/// Should only be called in response to the [PipelineOwner] calling its
/// [PipelineOwner.onClearSemantics] callback.
///
/// Should only be called on objects whose [parent] is not a [RenderObject]. /// Should only be called on objects whose [parent] is not a [RenderObject].
void clearSemantics() { void clearSemantics() {
_needsSemanticsUpdate = true; _needsSemanticsUpdate = true;
......
...@@ -2,13 +2,13 @@ ...@@ -2,13 +2,13 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:math' as math; import 'dart:ui' as ui;
import 'dart:ui' show Rect, SemanticsAction, SemanticsFlags; import 'dart:ui' show Rect, SemanticsAction, SemanticsFlags;
import 'dart:typed_data';
import 'package:flutter_services/semantics.dart' as mojom; import 'package:meta/meta.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/painting.dart'; import 'package:flutter/painting.dart';
import 'package:meta/meta.dart';
import 'package:vector_math/vector_math_64.dart'; import 'package:vector_math/vector_math_64.dart';
import 'node.dart'; import 'node.dart';
...@@ -57,14 +57,19 @@ typedef bool SemanticsNodeVisitor(SemanticsNode node); ...@@ -57,14 +57,19 @@ typedef bool SemanticsNodeVisitor(SemanticsNode node);
class SemanticsData { class SemanticsData {
/// Creates a semantics data object. /// Creates a semantics data object.
/// ///
/// The [flags], [actions], [label], and [rect] arguments must not be null. /// The [flags], [actions], [label], and [Rect] arguments must not be null.
const SemanticsData({ SemanticsData({
@required this.flags, @required this.flags,
@required this.actions, @required this.actions,
@required this.label, @required this.label,
@required this.rect, @required this.rect,
this.transform this.transform
}); }) {
assert(flags != null);
assert(actions != null);
assert(label != null);
assert(rect != null);
}
/// A bit field of [SemanticsFlags] that apply to this node. /// A bit field of [SemanticsFlags] that apply to this node.
final int flags; final int flags;
...@@ -90,6 +95,41 @@ class SemanticsData { ...@@ -90,6 +95,41 @@ class SemanticsData {
/// Whether [actions] contains the given action. /// Whether [actions] contains the given action.
bool hasAction(SemanticsAction action) => (actions & action.index) != 0; bool hasAction(SemanticsAction action) => (actions & action.index) != 0;
@override
String toString() {
StringBuffer buffer = new StringBuffer();
buffer.write('$runtimeType($rect');
if (transform != null)
buffer.write('; $transform');
for (SemanticsAction action in SemanticsAction.values.values) {
if ((actions & action.index) != 0)
buffer.write('; $action');
}
for (SemanticsFlags flag in SemanticsFlags.values.values) {
if ((flags & flag.index) != 0)
buffer.write('; $flag');
}
if (label.isNotEmpty)
buffer.write('; "$label"');
buffer.write(')');
return buffer.toString();
}
@override
bool operator ==(dynamic other) {
if (other is! SemanticsData)
return false;
final SemanticsData typedOther = other;
return typedOther.flags == flags
&& typedOther.actions == actions
&& typedOther.label == label
&& typedOther.rect == rect
&& typedOther.transform == transform;
}
@override
int get hashCode => hashValues(flags, actions, label, rect, transform);
} }
/// A node that represents some semantic data. /// A node that represents some semantic data.
...@@ -307,6 +347,9 @@ class SemanticsNode extends AbstractNode { ...@@ -307,6 +347,9 @@ class SemanticsNode extends AbstractNode {
bool get hasChildren => _children?.isNotEmpty ?? false; bool get hasChildren => _children?.isNotEmpty ?? false;
bool _dead = false; bool _dead = false;
/// The number of children this node has.
int get childrenCount => hasChildren ? _children.length : 0;
/// Visits the immediate children of this node. /// Visits the immediate children of this node.
/// ///
/// This function calls visitor for each child in a pre-order travseral /// This function calls visitor for each child in a pre-order travseral
...@@ -477,53 +520,33 @@ class SemanticsNode extends AbstractNode { ...@@ -477,53 +520,33 @@ class SemanticsNode extends AbstractNode {
); );
} }
mojom.SemanticsNode _serialize() { static Float64List _initIdentityTransform() {
mojom.SemanticsNode result = new mojom.SemanticsNode(); return new Matrix4.identity().storage;
result.id = id; }
if (_dirty) {
// We could be even more efficient about not sending data here, by only static final Int32List _kEmptyChildList = new Int32List(0);
// sending the bits that are dirty (tracking the geometry, flags, strings, static final Float64List _kIdentityTransform = _initIdentityTransform();
// and children separately). For now, we send all or nothing.
result.geometry = new mojom.SemanticGeometry(); void _addToUpdate(ui.SemanticsUpdateBuilder builder) {
result.geometry.transform = transform?.storage; assert(_dirty);
result.geometry.top = rect.top; final SemanticsData data = getSemanticsData();
result.geometry.left = rect.left; Int32List children;
result.geometry.width = math.max(rect.width, 0.0); if (!hasChildren || mergeAllDescendantsIntoThisNode) {
result.geometry.height = math.max(rect.height, 0.0); children = _kEmptyChildList;
result.flags = new mojom.SemanticFlags(); } else {
result.flags.hasCheckedState = hasCheckedState; final int childCount = _children.length;
result.flags.isChecked = isChecked; children = new Int32List(childCount);
result.strings = new mojom.SemanticStrings(); for (int i = 0; i < childCount; ++i)
result.strings.label = label; children[i] = _children[i].id;
List<mojom.SemanticsNode> children = <mojom.SemanticsNode>[];
int mergedActions = _actions;
if (_shouldMergeAllDescendantsIntoThisNode) {
_visitDescendants((SemanticsNode node) {
mergedActions |= node._actions;
result.flags.hasCheckedState = result.flags.hasCheckedState || node.hasCheckedState;
result.flags.isChecked = result.flags.isChecked || node.isChecked;
if (node.label != '')
result.strings.label = result.strings.label.isNotEmpty ? '${result.strings.label}\n${node.label}' : node.label;
node._dirty = false;
return true; // continue walk
});
// and we pretend to have no children
} else {
if (_children != null) {
for (SemanticsNode child in _children)
children.add(child._serialize());
}
}
result.children = children;
result.actions = <int>[];
for (mojom.SemanticAction action in mojom.SemanticAction.values) {
int bit = 1 << action.mojoEnumValue;
if ((mergedActions & bit) != 0)
result.actions.add(action.mojoEnumValue);
}
_dirty = false;
} }
return result; builder.updateNode(
id: id,
flags: data.flags,
actions: data.actions,
rect: data.rect,
transform: data.transform?.storage ?? _kIdentityTransform,
);
_dirty = false;
} }
@override @override
...@@ -567,73 +590,31 @@ class SemanticsNode extends AbstractNode { ...@@ -567,73 +590,31 @@ class SemanticsNode extends AbstractNode {
} }
} }
/// Signature for functions that receive updates about render tree semantics.
typedef void SemanticsListener(List<mojom.SemanticsNode> nodes);
/// Owns [SemanticsNode] objects and notifies listeners of changes to the /// Owns [SemanticsNode] objects and notifies listeners of changes to the
/// render tree semantics. /// render tree semantics.
/// ///
/// To listen for semantic updates, call [PipelineOwner.addSemanticsListener], /// To listen for semantic updates, call [PipelineOwner.addSemanticsListener],
/// which will create a [SemanticsOwner] if necessary. /// which will create a [SemanticsOwner] if necessary.
class SemanticsOwner { class SemanticsOwner extends ChangeNotifier {
/// Creates a [SemanticsOwner].
///
/// The `onLastListenerRemoved` argument must not be null and will be called
/// when the last listener is removed from this object.
SemanticsOwner({
@required SemanticsListener initialListener,
@required VoidCallback onLastListenerRemoved
}) : _onLastListenerRemoved = onLastListenerRemoved {
assert(_onLastListenerRemoved != null);
addListener(initialListener);
}
final VoidCallback _onLastListenerRemoved;
final Set<SemanticsNode> _dirtyNodes = new Set<SemanticsNode>(); final Set<SemanticsNode> _dirtyNodes = new Set<SemanticsNode>();
final Map<int, SemanticsNode> _nodes = <int, SemanticsNode>{}; final Map<int, SemanticsNode> _nodes = <int, SemanticsNode>{};
final Set<SemanticsNode> _detachedNodes = new Set<SemanticsNode>(); final Set<SemanticsNode> _detachedNodes = new Set<SemanticsNode>();
final List<SemanticsListener> _listeners = <SemanticsListener>[];
/// The root node of the semantics tree, if any. /// The root node of the semantics tree, if any.
/// ///
/// If the semantics tree is empty, returns null. /// If the semantics tree is empty, returns null.
SemanticsNode get rootSemanticsNode => _nodes[0]; SemanticsNode get rootSemanticsNode => _nodes[0];
/// Releases any resources retained by this object. @override
///
/// Requires that there are no listeners registered with [addListener].
void dispose() { void dispose() {
assert(_listeners.isEmpty);
_dirtyNodes.clear(); _dirtyNodes.clear();
_nodes.clear(); _nodes.clear();
_detachedNodes.clear(); _detachedNodes.clear();
super.dispose();
} }
/// Add a consumer of semantic data. /// Update the semantics using [ui.window.updateSemantics].
/// void sendSemanticsUpdate() {
/// After the [PipelineOwner] updates the semantic data for a given frame, it
/// calls [sendSemanticsTree], which uploads the data to each listener
/// registered with this function.
///
/// Listeners can be removed with [removeListener].
void addListener(SemanticsListener listener) {
_listeners.add(listener);
}
/// Removes a consumer of semantic data.
///
/// Listeners can be added with [addListener].
void removeListener(SemanticsListener listener) {
_listeners.remove(listener);
if (_listeners.isEmpty)
_onLastListenerRemoved();
}
/// Uploads the semantics tree to the listeners registered with [addListener].
void sendSemanticsTree() {
assert(_listeners.isNotEmpty);
for (SemanticsNode oldNode in _detachedNodes) { for (SemanticsNode oldNode in _detachedNodes) {
// The other side will have forgotten this node if we even send // The other side will have forgotten this node if we even send
// it again, so make sure to mark it dirty so that it'll get // it again, so make sure to mark it dirty so that it'll get
...@@ -680,7 +661,7 @@ class SemanticsOwner { ...@@ -680,7 +661,7 @@ class SemanticsOwner {
} }
} }
visitedNodes.sort((SemanticsNode a, SemanticsNode b) => a.depth - b.depth); visitedNodes.sort((SemanticsNode a, SemanticsNode b) => a.depth - b.depth);
List<mojom.SemanticsNode> updatedNodes = <mojom.SemanticsNode>[]; ui.SemanticsUpdateBuilder builder = new ui.SemanticsUpdateBuilder();
for (SemanticsNode node in visitedNodes) { for (SemanticsNode node in visitedNodes) {
assert(node.parent?._dirty != true); // could be null (no parent) or false (not dirty) assert(node.parent?._dirty != true); // could be null (no parent) or false (not dirty)
// The _serialize() method marks the node as not dirty, and // The _serialize() method marks the node as not dirty, and
...@@ -694,11 +675,11 @@ class SemanticsOwner { ...@@ -694,11 +675,11 @@ class SemanticsOwner {
// which happens e.g. when the node is no longer contributing // which happens e.g. when the node is no longer contributing
// semantics). // semantics).
if (node._dirty && node.attached) if (node._dirty && node.attached)
updatedNodes.add(node._serialize()); node._addToUpdate(builder);
} }
for (SemanticsListener listener in new List<SemanticsListener>.from(_listeners))
listener(updatedNodes);
_dirtyNodes.clear(); _dirtyNodes.clear();
ui.window.updateSemantics(builder.build());
notifyListeners();
} }
SemanticsActionHandler _getSemanticsActionHandlerForId(int id, SemanticsAction action) { SemanticsActionHandler _getSemanticsActionHandlerForId(int id, SemanticsAction action) {
......
...@@ -73,7 +73,7 @@ class MediaQueryData { ...@@ -73,7 +73,7 @@ class MediaQueryData {
} }
@override @override
bool operator==(Object other) { bool operator ==(Object other) {
if (other.runtimeType != runtimeType) if (other.runtimeType != runtimeType)
return false; return false;
MediaQueryData typedOther = other; MediaQueryData typedOther = other;
......
...@@ -8,7 +8,6 @@ import 'dart:ui' show SemanticsFlags; ...@@ -8,7 +8,6 @@ import 'dart:ui' show SemanticsFlags;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter_services/semantics.dart' as mojom;
import 'basic.dart'; import 'basic.dart';
import 'binding.dart'; import 'binding.dart';
...@@ -153,21 +152,23 @@ class _SemanticsDebuggerState extends State<SemanticsDebugger> { ...@@ -153,21 +152,23 @@ class _SemanticsDebuggerState extends State<SemanticsDebugger> {
class _SemanticsClient extends ChangeNotifier { class _SemanticsClient extends ChangeNotifier {
_SemanticsClient(PipelineOwner pipelineOwner) { _SemanticsClient(PipelineOwner pipelineOwner) {
_semanticsOwner = pipelineOwner.addSemanticsListener(_updateSemanticsTree); _semanticsHandle = pipelineOwner.ensureSemantics(
listener: _didUpdateSemantics
);
} }
SemanticsOwner _semanticsOwner; SemanticsHandle _semanticsHandle;
@override @override
void dispose() { void dispose() {
_semanticsOwner.removeListener(_updateSemanticsTree); _semanticsHandle.dispose();
_semanticsOwner = null; _semanticsHandle = null;
super.dispose(); super.dispose();
} }
int generation = 0; int generation = 0;
void _updateSemanticsTree(List<mojom.SemanticsNode> nodes) { void _didUpdateSemantics() {
generation += 1; generation += 1;
notifyListeners(); notifyListeners();
} }
......
...@@ -8,7 +8,7 @@ import 'package:flutter/material.dart'; ...@@ -8,7 +8,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import '../rendering/test_semantics_client.dart'; import '../widget/semantics_tester.dart';
// This file uses "as dynamic" in a few places to defeat the static // This file uses "as dynamic" in a few places to defeat the static
// analysis. In general you want to avoid using this style in your // analysis. In general you want to avoid using this style in your
...@@ -431,7 +431,8 @@ void main() { ...@@ -431,7 +431,8 @@ void main() {
}); });
testWidgets('Does tooltip contribute semantics', (WidgetTester tester) async { testWidgets('Does tooltip contribute semantics', (WidgetTester tester) async {
TestSemanticsClient client = new TestSemanticsClient(tester.binding.pipelineOwner); SemanticsTester semantics = new SemanticsTester(tester);
GlobalKey key = new GlobalKey(); GlobalKey key = new GlobalKey();
await tester.pumpWidget( await tester.pumpWidget(
new Overlay( new Overlay(
...@@ -456,37 +457,16 @@ void main() { ...@@ -456,37 +457,16 @@ void main() {
] ]
) )
); );
expect(client.updates.length, equals(1));
expect(client.updates[0].id, equals(0)); expect(semantics, hasSemantics(new TestSemantics(id: 0, label: 'TIP')));
expect(client.updates[0].actions, isEmpty);
expect(client.updates[0].flags.hasCheckedState, isFalse);
expect(client.updates[0].flags.isChecked, isFalse);
expect(client.updates[0].strings.label, equals('TIP'));
expect(client.updates[0].geometry.transform, isNull);
expect(client.updates[0].geometry.left, equals(0.0));
expect(client.updates[0].geometry.top, equals(0.0));
expect(client.updates[0].geometry.width, equals(800.0));
expect(client.updates[0].geometry.height, equals(600.0));
expect(client.updates[0].children.length, equals(0));
client.updates.clear();
// before using "as dynamic" in your code, see note top of file // before using "as dynamic" in your code, see note top of file
(key.currentState as dynamic).ensureTooltipVisible(); // this triggers a rebuild of the semantics because the tree changes (key.currentState as dynamic).ensureTooltipVisible(); // this triggers a rebuild of the semantics because the tree changes
await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0) await tester.pump(const Duration(seconds: 2)); // faded in, show timer started (and at 0.0)
expect(client.updates.length, equals(1));
expect(client.updates[0].id, equals(0)); expect(semantics, hasSemantics(new TestSemantics(id: 0, label: 'TIP')));
expect(client.updates[0].actions, isEmpty);
expect(client.updates[0].flags.hasCheckedState, isFalse); semantics.dispose();
expect(client.updates[0].flags.isChecked, isFalse);
expect(client.updates[0].strings.label, equals('TIP'));
expect(client.updates[0].geometry.transform, isNull);
expect(client.updates[0].geometry.left, equals(0.0));
expect(client.updates[0].geometry.top, equals(0.0));
expect(client.updates[0].geometry.width, equals(800.0));
expect(client.updates[0].geometry.height, equals(600.0));
expect(client.updates[0].children.length, equals(0));
client.updates.clear();
client.dispose();
}); });
} }
...@@ -7,7 +7,6 @@ import 'package:flutter/rendering.dart'; ...@@ -7,7 +7,6 @@ import 'package:flutter/rendering.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
import 'rendering_tester.dart'; import 'rendering_tester.dart';
import 'test_semantics_client.dart';
class TestTree { class TestTree {
TestTree() { TestTree() {
...@@ -126,20 +125,25 @@ void main() { ...@@ -126,20 +125,25 @@ void main() {
}); });
test('objects can be detached and re-attached: semantics', () { test('objects can be detached and re-attached: semantics', () {
TestTree testTree = new TestTree(); TestTree testTree = new TestTree();
TestSemanticsClient client = new TestSemanticsClient(renderer.pipelineOwner); int semanticsUpdateCount = 0;
SemanticsHandle semanticsHandle = renderer.pipelineOwner.ensureSemantics(
listener: () {
++semanticsUpdateCount;
}
);
// Lay out, composite, paint, and update semantics // Lay out, composite, paint, and update semantics
layout(testTree.root, phase: EnginePhase.flushSemantics); layout(testTree.root, phase: EnginePhase.flushSemantics);
expect(client.updates.length, equals(1)); expect(semanticsUpdateCount, 1);
// Remove testTree from the custom render view // Remove testTree from the custom render view
renderer.renderView.child = null; renderer.renderView.child = null;
expect(testTree.child.owner, isNull); expect(testTree.child.owner, isNull);
// Dirty one of the elements // Dirty one of the elements
client.updates.clear(); semanticsUpdateCount = 0;
testTree.child.markNeedsSemanticsUpdate(); testTree.child.markNeedsSemanticsUpdate();
expect(client.updates.length, equals(0)); expect(semanticsUpdateCount, 0);
// Lay out, composite, paint, and update semantics again // Lay out, composite, paint, and update semantics again
layout(testTree.root, phase: EnginePhase.flushSemantics); layout(testTree.root, phase: EnginePhase.flushSemantics);
expect(client.updates.length, equals(1)); expect(semanticsUpdateCount, 1);
client.dispose(); semanticsHandle.dispose();
}); });
} }
// Copyright 2015 The Chromium 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 'package:flutter/rendering.dart';
import 'package:flutter_services/semantics.dart' as mojom;
class TestSemanticsClient {
TestSemanticsClient(PipelineOwner pipelineOwner) {
_semanticsOwner = pipelineOwner.addSemanticsListener(_updateSemanticsTree);
}
SemanticsOwner _semanticsOwner;
void dispose() {
_semanticsOwner.removeListener(_updateSemanticsTree);
_semanticsOwner = null;
}
final List<mojom.SemanticsNode> updates = <mojom.SemanticsNode>[];
void _updateSemanticsTree(List<mojom.SemanticsNode> nodes) {
updates.addAll(nodes);
}
}
...@@ -2,16 +2,16 @@ ...@@ -2,16 +2,16 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'package:flutter/rendering.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_services/semantics.dart' as mojom;
import '../rendering/test_semantics_client.dart'; import 'semantics_tester.dart';
void main() { void main() {
testWidgets('Does FlatButton contribute semantics', (WidgetTester tester) async { testWidgets('Does FlatButton contribute semantics', (WidgetTester tester) async {
TestSemanticsClient client = new TestSemanticsClient(tester.binding.pipelineOwner); SemanticsTester semantics = new SemanticsTester(tester);
await tester.pumpWidget( await tester.pumpWidget(
new Material( new Material(
child: new Center( child: new Center(
...@@ -22,25 +22,22 @@ void main() { ...@@ -22,25 +22,22 @@ void main() {
) )
) )
); );
expect(client.updates.length, equals(1));
expect(client.updates[0].id, equals(0)); expect(semantics, hasSemantics(
expect(client.updates[0].actions, isEmpty); new TestSemantics(
expect(client.updates[0].flags.hasCheckedState, isFalse); id: 0,
expect(client.updates[0].flags.isChecked, isFalse); children: <TestSemantics>[
expect(client.updates[0].strings.label, equals('')); new TestSemantics(
expect(client.updates[0].geometry.transform, isNull); id: 1,
expect(client.updates[0].geometry.left, equals(0.0)); actions: SemanticsAction.tap.index,
expect(client.updates[0].geometry.top, equals(0.0)); label: 'Hello',
expect(client.updates[0].geometry.width, equals(800.0)); rect: new Rect.fromLTRB(0.0, 0.0, 88.0, 36.0),
expect(client.updates[0].geometry.height, equals(600.0)); transform: new Matrix4.translationValues(356.0, 282.0, 0.0)
expect(client.updates[0].children.length, equals(1)); )
expect(client.updates[0].children[0].id, equals(1)); ]
expect(client.updates[0].children[0].actions, equals(<int>[mojom.SemanticAction.tap.mojoEnumValue])); )
expect(client.updates[0].children[0].flags.hasCheckedState, isFalse); ));
expect(client.updates[0].children[0].flags.isChecked, isFalse);
expect(client.updates[0].children[0].strings.label, equals('Hello')); semantics.dispose();
expect(client.updates[0].children[0].children.length, equals(0));
client.updates.clear();
client.dispose();
}); });
} }
...@@ -7,11 +7,11 @@ import 'package:flutter/rendering.dart'; ...@@ -7,11 +7,11 @@ import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import '../rendering/test_semantics_client.dart'; import 'semantics_tester.dart';
void main() { void main() {
testWidgets('Semantics 1', (WidgetTester tester) async { testWidgets('Semantics 1', (WidgetTester tester) async {
TestSemanticsClient client = new TestSemanticsClient(tester.binding.pipelineOwner); SemanticsTester semantics = new SemanticsTester(tester);
// smoketest // smoketest
await tester.pumpWidget( await tester.pumpWidget(
...@@ -22,23 +22,13 @@ void main() { ...@@ -22,23 +22,13 @@ void main() {
) )
) )
); );
expect(client.updates.length, equals(1));
expect(client.updates[0].id, equals(0)); expect(semantics, hasSemantics(new TestSemantics(id: 0, label: 'test1')));
expect(client.updates[0].actions, isEmpty);
expect(client.updates[0].flags.hasCheckedState, isFalse);
expect(client.updates[0].flags.isChecked, isFalse);
expect(client.updates[0].strings.label, equals('test1'));
expect(client.updates[0].geometry.transform, isNull);
expect(client.updates[0].geometry.left, equals(0.0));
expect(client.updates[0].geometry.top, equals(0.0));
expect(client.updates[0].geometry.width, equals(800.0));
expect(client.updates[0].geometry.height, equals(600.0));
expect(client.updates[0].children.length, equals(0));
client.updates.clear();
// control for forking // control for forking
await tester.pumpWidget( await tester.pumpWidget(
new Column( new Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[ children: <Widget>[
new Container( new Container(
height: 10.0, height: 10.0,
...@@ -52,26 +42,15 @@ void main() { ...@@ -52,26 +42,15 @@ void main() {
) )
), ),
], ],
crossAxisAlignment: CrossAxisAlignment.stretch
) )
); );
expect(client.updates.length, equals(1));
expect(client.updates[0].id, equals(0)); expect(semantics, hasSemantics(new TestSemantics(id: 0, label: 'child1')));
expect(client.updates[0].actions, isEmpty);
expect(client.updates[0].flags.hasCheckedState, isFalse);
expect(client.updates[0].flags.isChecked, isFalse);
expect(client.updates[0].strings.label, equals('child1'));
expect(client.updates[0].geometry.transform, isNull);
expect(client.updates[0].geometry.left, equals(0.0));
expect(client.updates[0].geometry.top, equals(0.0));
expect(client.updates[0].geometry.width, equals(800.0));
expect(client.updates[0].geometry.height, equals(600.0));
expect(client.updates[0].children.length, equals(0));
client.updates.clear();
// forking semantics // forking semantics
await tester.pumpWidget( await tester.pumpWidget(
new Column( new Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[ children: <Widget>[
new Container( new Container(
height: 10.0, height: 10.0,
...@@ -85,48 +64,32 @@ void main() { ...@@ -85,48 +64,32 @@ void main() {
) )
), ),
], ],
crossAxisAlignment: CrossAxisAlignment.stretch
) )
); );
expect(client.updates.length, equals(1));
expect(client.updates[0].id, equals(0)); expect(semantics, hasSemantics(
expect(client.updates[0].actions, isEmpty); new TestSemantics(
expect(client.updates[0].flags.hasCheckedState, isFalse); id: 0,
expect(client.updates[0].flags.isChecked, isFalse); children: <TestSemantics>[
expect(client.updates[0].strings.label, equals('')); new TestSemantics(
expect(client.updates[0].geometry.transform, isNull); id: 1,
expect(client.updates[0].geometry.left, equals(0.0)); label: 'child1',
expect(client.updates[0].geometry.top, equals(0.0)); rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 10.0),
expect(client.updates[0].geometry.width, equals(800.0)); ),
expect(client.updates[0].geometry.height, equals(600.0)); new TestSemantics(
expect(client.updates[0].children.length, equals(2)); id: 2,
expect(client.updates[0].children[0].id, equals(1)); label: 'child2',
expect(client.updates[0].children[0].actions, isEmpty); rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 10.0),
expect(client.updates[0].children[0].flags.hasCheckedState, isFalse); transform: new Matrix4.translationValues(0.0, 10.0, 0.0),
expect(client.updates[0].children[0].flags.isChecked, isFalse); ),
expect(client.updates[0].children[0].strings.label, equals('child1')); ],
expect(client.updates[0].children[0].geometry.transform, isNull); )
expect(client.updates[0].children[0].geometry.left, equals(0.0)); ));
expect(client.updates[0].children[0].geometry.top, equals(0.0));
expect(client.updates[0].children[0].geometry.width, equals(800.0));
expect(client.updates[0].children[0].geometry.height, equals(10.0));
expect(client.updates[0].children[0].children.length, equals(0));
expect(client.updates[0].children[1].id, equals(2));
expect(client.updates[0].children[1].actions, isEmpty);
expect(client.updates[0].children[1].flags.hasCheckedState, isFalse);
expect(client.updates[0].children[1].flags.isChecked, isFalse);
expect(client.updates[0].children[1].strings.label, equals('child2'));
expect(client.updates[0].children[1].geometry.transform, equals(<double>[1.0,0.0,0.0,0.0, 0.0,1.0,0.0,0.0, 0.0,0.0,1.0,0.0, 0.0,10.0,0.0,1.0]));
expect(client.updates[0].children[1].geometry.left, equals(0.0));
expect(client.updates[0].children[1].geometry.top, equals(0.0));
expect(client.updates[0].children[1].geometry.width, equals(800.0));
expect(client.updates[0].children[1].geometry.height, equals(10.0));
expect(client.updates[0].children[1].children.length, equals(0));
client.updates.clear();
// toggle a branch off // toggle a branch off
await tester.pumpWidget( await tester.pumpWidget(
new Column( new Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[ children: <Widget>[
new Container( new Container(
height: 10.0, height: 10.0,
...@@ -140,22 +103,10 @@ void main() { ...@@ -140,22 +103,10 @@ void main() {
) )
), ),
], ],
crossAxisAlignment: CrossAxisAlignment.stretch
) )
); );
expect(client.updates.length, equals(1));
expect(client.updates[0].id, equals(0)); expect(semantics, hasSemantics(new TestSemantics(id: 0, label: 'child1')));
expect(client.updates[0].actions, isEmpty);
expect(client.updates[0].flags.hasCheckedState, isFalse);
expect(client.updates[0].flags.isChecked, isFalse);
expect(client.updates[0].strings.label, equals('child1'));
expect(client.updates[0].geometry.transform, isNull);
expect(client.updates[0].geometry.left, equals(0.0));
expect(client.updates[0].geometry.top, equals(0.0));
expect(client.updates[0].geometry.width, equals(800.0));
expect(client.updates[0].geometry.height, equals(600.0));
expect(client.updates[0].children.length, equals(0));
client.updates.clear();
// toggle a branch back on // toggle a branch back on
await tester.pumpWidget( await tester.pumpWidget(
...@@ -176,41 +127,26 @@ void main() { ...@@ -176,41 +127,26 @@ void main() {
crossAxisAlignment: CrossAxisAlignment.stretch crossAxisAlignment: CrossAxisAlignment.stretch
) )
); );
expect(client.updates.length, equals(1));
expect(client.updates[0].id, equals(0)); expect(semantics, hasSemantics(
expect(client.updates[0].actions, isEmpty); new TestSemantics(
expect(client.updates[0].flags.hasCheckedState, isFalse); id: 0,
expect(client.updates[0].flags.isChecked, isFalse); children: <TestSemantics>[
expect(client.updates[0].strings.label, equals('')); new TestSemantics(
expect(client.updates[0].geometry.transform, isNull); id: 3,
expect(client.updates[0].geometry.left, equals(0.0)); label: 'child1',
expect(client.updates[0].geometry.top, equals(0.0)); rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 10.0),
expect(client.updates[0].geometry.width, equals(800.0)); ),
expect(client.updates[0].geometry.height, equals(600.0)); new TestSemantics(
expect(client.updates[0].children.length, equals(2)); id: 2,
expect(client.updates[0].children[0].id, equals(3)); label: 'child2',
expect(client.updates[0].children[0].actions, isEmpty); rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 10.0),
expect(client.updates[0].children[0].flags.hasCheckedState, isFalse); transform: new Matrix4.translationValues(0.0, 10.0, 0.0),
expect(client.updates[0].children[0].flags.isChecked, isFalse); ),
expect(client.updates[0].children[0].strings.label, equals('child1')); ],
expect(client.updates[0].children[0].geometry.transform, isNull); )
expect(client.updates[0].children[0].geometry.left, equals(0.0)); ));
expect(client.updates[0].children[0].geometry.top, equals(0.0));
expect(client.updates[0].children[0].geometry.width, equals(800.0)); semantics.dispose();
expect(client.updates[0].children[0].geometry.height, equals(10.0));
expect(client.updates[0].children[0].children.length, equals(0));
expect(client.updates[0].children[1].id, equals(2));
expect(client.updates[0].children[1].actions, isEmpty);
expect(client.updates[0].children[1].flags.hasCheckedState, isFalse);
expect(client.updates[0].children[1].flags.isChecked, isFalse);
expect(client.updates[0].children[1].strings.label, equals('child2'));
expect(client.updates[0].children[1].geometry.transform, equals(<double>[1.0,0.0,0.0,0.0, 0.0,1.0,0.0,0.0, 0.0,0.0,1.0,0.0, 0.0,10.0,0.0,1.0]));
expect(client.updates[0].children[1].geometry.left, equals(0.0));
expect(client.updates[0].children[1].geometry.top, equals(0.0));
expect(client.updates[0].children[1].geometry.width, equals(800.0));
expect(client.updates[0].children[1].geometry.height, equals(10.0));
expect(client.updates[0].children[1].children.length, equals(0));
client.updates.clear();
client.dispose();
}); });
} }
...@@ -7,11 +7,11 @@ import 'package:flutter/rendering.dart'; ...@@ -7,11 +7,11 @@ import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import '../rendering/test_semantics_client.dart'; import 'semantics_tester.dart';
void main() { void main() {
testWidgets('Semantics 2', (WidgetTester tester) async { testWidgets('Semantics 2', (WidgetTester tester) async {
TestSemanticsClient client = new TestSemanticsClient(tester.binding.pipelineOwner); SemanticsTester semantics = new SemanticsTester(tester);
// this test is the same as the test in Semantics 1, but // this test is the same as the test in Semantics 1, but
// starting with the second branch being ignored and then // starting with the second branch being ignored and then
...@@ -36,41 +36,25 @@ void main() { ...@@ -36,41 +36,25 @@ void main() {
crossAxisAlignment: CrossAxisAlignment.stretch crossAxisAlignment: CrossAxisAlignment.stretch
) )
); );
expect(client.updates.length, equals(1));
expect(client.updates[0].id, equals(0)); expect(semantics, hasSemantics(
expect(client.updates[0].actions, isEmpty); new TestSemantics(
expect(client.updates[0].flags.hasCheckedState, isFalse); id: 0,
expect(client.updates[0].flags.isChecked, isFalse); children: <TestSemantics>[
expect(client.updates[0].strings.label, equals('')); new TestSemantics(
expect(client.updates[0].geometry.transform, isNull); id: 1,
expect(client.updates[0].geometry.left, equals(0.0)); label: 'child1',
expect(client.updates[0].geometry.top, equals(0.0)); rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 10.0),
expect(client.updates[0].geometry.width, equals(800.0)); ),
expect(client.updates[0].geometry.height, equals(600.0)); new TestSemantics(
expect(client.updates[0].children.length, equals(2)); id: 2,
expect(client.updates[0].children[0].id, equals(1)); label: 'child2',
expect(client.updates[0].children[0].actions, isEmpty); rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 10.0),
expect(client.updates[0].children[0].flags.hasCheckedState, isFalse); transform: new Matrix4.translationValues(0.0, 10.0, 0.0),
expect(client.updates[0].children[0].flags.isChecked, isFalse); ),
expect(client.updates[0].children[0].strings.label, equals('child1')); ],
expect(client.updates[0].children[0].geometry.transform, isNull); )
expect(client.updates[0].children[0].geometry.left, equals(0.0)); ));
expect(client.updates[0].children[0].geometry.top, equals(0.0));
expect(client.updates[0].children[0].geometry.width, equals(800.0));
expect(client.updates[0].children[0].geometry.height, equals(10.0));
expect(client.updates[0].children[0].children.length, equals(0));
expect(client.updates[0].children[1].id, equals(2));
expect(client.updates[0].children[1].actions, isEmpty);
expect(client.updates[0].children[1].flags.hasCheckedState, isFalse);
expect(client.updates[0].children[1].flags.isChecked, isFalse);
expect(client.updates[0].children[1].strings.label, equals('child2'));
expect(client.updates[0].children[1].geometry.transform, equals(<double>[1.0,0.0,0.0,0.0, 0.0,1.0,0.0,0.0, 0.0,0.0,1.0,0.0, 0.0,10.0,0.0,1.0]));
expect(client.updates[0].children[1].geometry.left, equals(0.0));
expect(client.updates[0].children[1].geometry.top, equals(0.0));
expect(client.updates[0].children[1].geometry.width, equals(800.0));
expect(client.updates[0].children[1].geometry.height, equals(10.0));
expect(client.updates[0].children[1].children.length, equals(0));
client.updates.clear();
// toggle a branch off // toggle a branch off
await tester.pumpWidget( await tester.pumpWidget(
...@@ -91,19 +75,8 @@ void main() { ...@@ -91,19 +75,8 @@ void main() {
crossAxisAlignment: CrossAxisAlignment.stretch crossAxisAlignment: CrossAxisAlignment.stretch
) )
); );
expect(client.updates.length, equals(1));
expect(client.updates[0].id, equals(0)); expect(semantics, hasSemantics(new TestSemantics(id: 0, label: 'child1')));
expect(client.updates[0].actions, isEmpty);
expect(client.updates[0].flags.hasCheckedState, isFalse);
expect(client.updates[0].flags.isChecked, isFalse);
expect(client.updates[0].strings.label, equals('child1'));
expect(client.updates[0].geometry.transform, isNull);
expect(client.updates[0].geometry.left, equals(0.0));
expect(client.updates[0].geometry.top, equals(0.0));
expect(client.updates[0].geometry.width, equals(800.0));
expect(client.updates[0].geometry.height, equals(600.0));
expect(client.updates[0].children.length, equals(0));
client.updates.clear();
// toggle a branch back on // toggle a branch back on
await tester.pumpWidget( await tester.pumpWidget(
...@@ -124,41 +97,26 @@ void main() { ...@@ -124,41 +97,26 @@ void main() {
crossAxisAlignment: CrossAxisAlignment.stretch crossAxisAlignment: CrossAxisAlignment.stretch
) )
); );
expect(client.updates.length, equals(1));
expect(client.updates[0].id, equals(0)); expect(semantics, hasSemantics(
expect(client.updates[0].actions, isEmpty); new TestSemantics(
expect(client.updates[0].flags.hasCheckedState, isFalse); id: 0,
expect(client.updates[0].flags.isChecked, isFalse); children: <TestSemantics>[
expect(client.updates[0].strings.label, equals('')); new TestSemantics(
expect(client.updates[0].geometry.transform, isNull); id: 3,
expect(client.updates[0].geometry.left, equals(0.0)); label: 'child1',
expect(client.updates[0].geometry.top, equals(0.0)); rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 10.0),
expect(client.updates[0].geometry.width, equals(800.0)); ),
expect(client.updates[0].geometry.height, equals(600.0)); new TestSemantics(
expect(client.updates[0].children.length, equals(2)); id: 2,
expect(client.updates[0].children[0].id, equals(3)); label: 'child2',
expect(client.updates[0].children[0].actions, isEmpty); rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 10.0),
expect(client.updates[0].children[0].flags.hasCheckedState, isFalse); transform: new Matrix4.translationValues(0.0, 10.0, 0.0),
expect(client.updates[0].children[0].flags.isChecked, isFalse); ),
expect(client.updates[0].children[0].strings.label, equals('child1')); ],
expect(client.updates[0].children[0].geometry.transform, isNull); )
expect(client.updates[0].children[0].geometry.left, equals(0.0)); ));
expect(client.updates[0].children[0].geometry.top, equals(0.0));
expect(client.updates[0].children[0].geometry.width, equals(800.0)); semantics.dispose();
expect(client.updates[0].children[0].geometry.height, equals(10.0));
expect(client.updates[0].children[0].children.length, equals(0));
expect(client.updates[0].children[1].id, equals(2));
expect(client.updates[0].children[1].actions, isEmpty);
expect(client.updates[0].children[1].flags.hasCheckedState, isFalse);
expect(client.updates[0].children[1].flags.isChecked, isFalse);
expect(client.updates[0].children[1].strings.label, equals('child2'));
expect(client.updates[0].children[1].geometry.transform, equals(<double>[1.0,0.0,0.0,0.0, 0.0,1.0,0.0,0.0, 0.0,0.0,1.0,0.0, 0.0,10.0,0.0,1.0]));
expect(client.updates[0].children[1].geometry.left, equals(0.0));
expect(client.updates[0].children[1].geometry.top, equals(0.0));
expect(client.updates[0].children[1].geometry.width, equals(800.0));
expect(client.updates[0].children[1].geometry.height, equals(10.0));
expect(client.updates[0].children[1].children.length, equals(0));
client.updates.clear();
client.dispose();
}); });
} }
...@@ -2,15 +2,17 @@ ...@@ -2,15 +2,17 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:ui' show SemanticsFlags;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import '../rendering/test_semantics_client.dart'; import 'semantics_tester.dart';
void main() { void main() {
testWidgets('Semantics 3', (WidgetTester tester) async { testWidgets('Semantics 3', (WidgetTester tester) async {
TestSemanticsClient client = new TestSemanticsClient(tester.binding.pipelineOwner); SemanticsTester semantics = new SemanticsTester(tester);
// implicit annotators // implicit annotators
await tester.pumpWidget( await tester.pumpWidget(
...@@ -25,19 +27,14 @@ void main() { ...@@ -25,19 +27,14 @@ void main() {
) )
) )
); );
expect(client.updates.length, equals(1));
expect(client.updates[0].id, equals(0)); expect(semantics, hasSemantics(
expect(client.updates[0].actions, isEmpty); new TestSemantics(
expect(client.updates[0].flags.hasCheckedState, isTrue); id: 0,
expect(client.updates[0].flags.isChecked, isTrue); flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index,
expect(client.updates[0].strings.label, equals('test')); label: 'test',
expect(client.updates[0].geometry.transform, isNull); )
expect(client.updates[0].geometry.left, equals(0.0)); ));
expect(client.updates[0].geometry.top, equals(0.0));
expect(client.updates[0].geometry.width, equals(800.0));
expect(client.updates[0].geometry.height, equals(600.0));
expect(client.updates[0].children.length, equals(0));
client.updates.clear();
// remove one // remove one
await tester.pumpWidget( await tester.pumpWidget(
...@@ -49,19 +46,13 @@ void main() { ...@@ -49,19 +46,13 @@ void main() {
) )
) )
); );
expect(client.updates.length, equals(1));
expect(client.updates[0].id, equals(0)); expect(semantics, hasSemantics(
expect(client.updates[0].actions, isEmpty); new TestSemantics(
expect(client.updates[0].flags.hasCheckedState, isTrue); id: 0,
expect(client.updates[0].flags.isChecked, isTrue); flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index,
expect(client.updates[0].strings.label, equals('')); )
expect(client.updates[0].geometry.transform, isNull); ));
expect(client.updates[0].geometry.left, equals(0.0));
expect(client.updates[0].geometry.top, equals(0.0));
expect(client.updates[0].geometry.width, equals(800.0));
expect(client.updates[0].geometry.height, equals(600.0));
expect(client.updates[0].children.length, equals(0));
client.updates.clear();
// change what it says // change what it says
await tester.pumpWidget( await tester.pumpWidget(
...@@ -73,19 +64,13 @@ void main() { ...@@ -73,19 +64,13 @@ void main() {
) )
) )
); );
expect(client.updates.length, equals(1));
expect(client.updates[0].id, equals(0)); expect(semantics, hasSemantics(
expect(client.updates[0].actions, isEmpty); new TestSemantics(
expect(client.updates[0].flags.hasCheckedState, isFalse); id: 0,
expect(client.updates[0].flags.isChecked, isFalse); label: 'test',
expect(client.updates[0].strings.label, equals('test')); )
expect(client.updates[0].geometry.transform, isNull); ));
expect(client.updates[0].geometry.left, equals(0.0));
expect(client.updates[0].geometry.top, equals(0.0));
expect(client.updates[0].geometry.width, equals(800.0));
expect(client.updates[0].geometry.height, equals(600.0));
expect(client.updates[0].children.length, equals(0));
client.updates.clear();
// add a node // add a node
await tester.pumpWidget( await tester.pumpWidget(
...@@ -100,19 +85,19 @@ void main() { ...@@ -100,19 +85,19 @@ void main() {
) )
) )
); );
expect(client.updates.length, equals(1));
expect(client.updates[0].id, equals(0)); expect(semantics, hasSemantics(
expect(client.updates[0].actions, isEmpty); new TestSemantics(
expect(client.updates[0].flags.hasCheckedState, isTrue); id: 0,
expect(client.updates[0].flags.isChecked, isTrue); flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index,
expect(client.updates[0].strings.label, equals('test')); label: 'test',
expect(client.updates[0].geometry.transform, isNull); )
expect(client.updates[0].geometry.left, equals(0.0)); ));
expect(client.updates[0].geometry.top, equals(0.0));
expect(client.updates[0].geometry.width, equals(800.0)); int changeCount = 0;
expect(client.updates[0].geometry.height, equals(600.0)); tester.binding.pipelineOwner.semanticsOwner.addListener(() {
expect(client.updates[0].children.length, equals(0)); changeCount += 1;
client.updates.clear(); });
// make no changes // make no changes
await tester.pumpWidget( await tester.pumpWidget(
...@@ -127,7 +112,9 @@ void main() { ...@@ -127,7 +112,9 @@ void main() {
) )
) )
); );
expect(client.updates.length, equals(0));
client.dispose(); expect(changeCount, 0);
semantics.dispose();
}); });
} }
...@@ -2,15 +2,17 @@ ...@@ -2,15 +2,17 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:ui' show SemanticsFlags;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import '../rendering/test_semantics_client.dart'; import 'semantics_tester.dart';
void main() { void main() {
testWidgets('Semantics 4', (WidgetTester tester) async { testWidgets('Semantics 4', (WidgetTester tester) async {
TestSemanticsClient client = new TestSemanticsClient(tester.binding.pipelineOwner); SemanticsTester semantics = new SemanticsTester(tester);
// O // O
// / \ O=root // / \ O=root
...@@ -40,18 +42,32 @@ void main() { ...@@ -40,18 +42,32 @@ void main() {
] ]
) )
); );
expect(client.updates.length, equals(1));
expect(client.updates[0].id, equals(0)); expect(semantics, hasSemantics(
expect(client.updates[0].children.length, equals(2)); new TestSemantics(
expect(client.updates[0].children[0].id, equals(1)); id: 0,
expect(client.updates[0].children[0].children.length, equals(0)); children: <TestSemantics>[
expect(client.updates[0].children[1].id, equals(2)); new TestSemantics(
expect(client.updates[0].children[1].children.length, equals(2)); id: 1,
expect(client.updates[0].children[1].children[0].id, equals(3)); label: 'L1',
expect(client.updates[0].children[1].children[0].children.length, equals(0)); ),
expect(client.updates[0].children[1].children[1].id, equals(4)); new TestSemantics(
expect(client.updates[0].children[1].children[1].children.length, equals(0)); id: 2,
client.updates.clear(); label: 'L2',
children: <TestSemantics>[
new TestSemantics(
id: 3,
flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index,
),
new TestSemantics(
id: 4,
flags: SemanticsFlags.hasCheckedState.index,
),
]
),
],
)
));
// O O=root // O O=root
// / \ L=node with label // / \ L=node with label
...@@ -78,10 +94,23 @@ void main() { ...@@ -78,10 +94,23 @@ void main() {
] ]
) )
); );
expect(client.updates.length, equals(1));
expect(client.updates[0].id, equals(2)); expect(semantics, hasSemantics(
expect(client.updates[0].children.length, equals(0)); new TestSemantics(
client.updates.clear(); id: 0,
children: <TestSemantics>[
new TestSemantics(
id: 1,
label: 'L1',
),
new TestSemantics(
id: 2,
label: 'L2',
flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index,
),
],
)
));
// O=root // O=root
// OLC L=node with label // OLC L=node with label
...@@ -105,10 +134,15 @@ void main() { ...@@ -105,10 +134,15 @@ void main() {
] ]
) )
); );
expect(client.updates.length, equals(1));
expect(client.updates[0].id, equals(0)); expect(semantics, hasSemantics(
expect(client.updates[0].children.length, equals(0)); new TestSemantics(
client.updates.clear(); id: 0,
client.dispose(); label: 'L2',
flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index,
)
));
semantics.dispose();
}); });
} }
...@@ -6,11 +6,11 @@ import 'package:flutter/material.dart'; ...@@ -6,11 +6,11 @@ import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import '../rendering/test_semantics_client.dart'; import 'semantics_tester.dart';
void main() { void main() {
testWidgets('Semantics 5', (WidgetTester tester) async { testWidgets('Semantics 5', (WidgetTester tester) async {
TestSemanticsClient client = new TestSemanticsClient(tester.binding.pipelineOwner); SemanticsTester semantics = new SemanticsTester(tester);
await tester.pumpWidget( await tester.pumpWidget(
new Stack( new Stack(
...@@ -28,18 +28,22 @@ void main() { ...@@ -28,18 +28,22 @@ void main() {
] ]
) )
); );
expect(client.updates.length, equals(1));
expect(client.updates[0].id, equals(0)); expect(semantics, hasSemantics(
expect(client.updates[0].flags.hasCheckedState, isFalse); new TestSemantics(
expect(client.updates[0].strings.label, equals('')); id: 0,
expect(client.updates[0].children.length, equals(2)); children: <TestSemantics>[
expect(client.updates[0].children[0].id, equals(1)); new TestSemantics(
expect(client.updates[0].children[0].flags.hasCheckedState, isFalse); id: 1,
expect(client.updates[0].children[0].strings.label, equals('')); ),
expect(client.updates[0].children[1].id, equals(2)); new TestSemantics(
expect(client.updates[0].children[1].flags.hasCheckedState, isFalse); id: 2,
expect(client.updates[0].children[1].strings.label, equals('label')); label: 'label',
client.updates.clear(); ),
client.dispose(); ]
)
));
semantics.dispose();
}); });
} }
...@@ -2,16 +2,17 @@ ...@@ -2,16 +2,17 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:ui' show SemanticsFlags;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_services/semantics.dart' as mojom;
import '../rendering/test_semantics_client.dart'; import 'semantics_tester.dart';
void main() { void main() {
testWidgets('Semantics 7 - Merging', (WidgetTester tester) async { testWidgets('Semantics 7 - Merging', (WidgetTester tester) async {
TestSemanticsClient client = new TestSemanticsClient(tester.binding.pipelineOwner); SemanticsTester semantics = new SemanticsTester(tester);
String label; String label;
...@@ -44,43 +45,26 @@ void main() { ...@@ -44,43 +45,26 @@ void main() {
] ]
) )
); );
expect(client.updates.length, equals(1));
expect(client.updates[0].id, equals(0)); expect(semantics, hasSemantics(
expect(client.updates[0].actions, isEmpty); new TestSemantics(
expect(client.updates[0].flags.hasCheckedState, isFalse); id: 0,
expect(client.updates[0].flags.isChecked, isFalse); children: <TestSemantics>[
expect(client.updates[0].strings.label, equals('')); new TestSemantics(
expect(client.updates[0].geometry.transform, isNull); id: 1,
expect(client.updates[0].geometry.left, equals(0.0)); flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index,
expect(client.updates[0].geometry.top, equals(0.0)); label: label,
expect(client.updates[0].geometry.width, equals(800.0)); ),
expect(client.updates[0].geometry.height, equals(600.0)); // IDs 2 and 3 are used up by the nodes that get merged in
expect(client.updates[0].children.length, equals(2)); new TestSemantics(
expect(client.updates[0].children[0].id, equals(1)); id: 4,
expect(client.updates[0].children[0].actions, isEmpty); flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index,
expect(client.updates[0].children[0].flags.hasCheckedState, isTrue); label: label,
expect(client.updates[0].children[0].flags.isChecked, isTrue); ),
expect(client.updates[0].children[0].strings.label, equals(label)); // IDs 5 and 6 are used up by the nodes that get merged in
expect(client.updates[0].children[0].geometry.transform, isNull); ],
expect(client.updates[0].children[0].geometry.left, equals(0.0)); )
expect(client.updates[0].children[0].geometry.top, equals(0.0)); ));
expect(client.updates[0].children[0].geometry.width, equals(800.0));
expect(client.updates[0].children[0].geometry.height, equals(600.0));
expect(client.updates[0].children[0].children.length, equals(0));
// IDs 2 and 3 are used up by the nodes that get merged in
expect(client.updates[0].children[1].id, equals(4));
expect(client.updates[0].children[1].actions, isEmpty);
expect(client.updates[0].children[1].flags.hasCheckedState, isTrue);
expect(client.updates[0].children[1].flags.isChecked, isTrue);
expect(client.updates[0].children[1].strings.label, equals(label));
expect(client.updates[0].children[1].geometry.transform, isNull);
expect(client.updates[0].children[1].geometry.left, equals(0.0));
expect(client.updates[0].children[1].geometry.top, equals(0.0));
expect(client.updates[0].children[1].geometry.width, equals(800.0));
expect(client.updates[0].children[1].geometry.height, equals(600.0));
expect(client.updates[0].children[1].children.length, equals(0));
// IDs 5 and 6 are used up by the nodes that get merged in
client.updates.clear();
label = '2'; label = '2';
await tester.pumpWidget( await tester.pumpWidget(
...@@ -111,43 +95,27 @@ void main() { ...@@ -111,43 +95,27 @@ void main() {
] ]
) )
); );
expect(client.updates.length, equals(2));
// The order of the nodes is undefined, so allow both orders. expect(semantics, hasSemantics(
mojom.SemanticsNode a, b; new TestSemantics(
if (client.updates[0].id == 1) { id: 0,
a = client.updates[0]; children: <TestSemantics>[
b = client.updates[1]; new TestSemantics(
} else { id: 1,
a = client.updates[1]; flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index,
b = client.updates[0]; label: label,
} ),
// IDs 2 and 3 are used up by the nodes that get merged in
expect(a.id, equals(1)); new TestSemantics(
expect(a.actions, isEmpty); id: 4,
expect(a.flags.hasCheckedState, isTrue); flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index,
expect(a.flags.isChecked, isTrue); label: label,
expect(a.strings.label, equals(label)); ),
expect(a.geometry.transform, isNull); // IDs 5 and 6 are used up by the nodes that get merged in
expect(a.geometry.left, equals(0.0)); ],
expect(a.geometry.top, equals(0.0)); )
expect(a.geometry.width, equals(800.0)); ));
expect(a.geometry.height, equals(600.0));
expect(a.children.length, equals(0));
expect(b.id, equals(4));
expect(b.actions, isEmpty);
expect(b.flags.hasCheckedState, isTrue);
expect(b.flags.isChecked, isTrue);
expect(b.strings.label, equals(label));
expect(b.geometry.transform, isNull);
expect(b.geometry.left, equals(0.0));
expect(b.geometry.top, equals(0.0));
expect(b.geometry.width, equals(800.0));
expect(b.geometry.height, equals(600.0));
expect(b.children.length, equals(0));
client.updates.clear(); semantics.dispose();
client.dispose();
}); });
} }
...@@ -2,15 +2,17 @@ ...@@ -2,15 +2,17 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:ui' show SemanticsFlags;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import '../rendering/test_semantics_client.dart'; import 'semantics_tester.dart';
void main() { void main() {
testWidgets('Semantics 8 - Merging with reset', (WidgetTester tester) async { testWidgets('Semantics 8 - Merging with reset', (WidgetTester tester) async {
TestSemanticsClient client = new TestSemanticsClient(tester.binding.pipelineOwner); SemanticsTester semantics = new SemanticsTester(tester);
await tester.pumpWidget( await tester.pumpWidget(
new MergeSemantics( new MergeSemantics(
...@@ -32,19 +34,14 @@ void main() { ...@@ -32,19 +34,14 @@ void main() {
) )
) )
); );
expect(client.updates.length, equals(1));
expect(client.updates[0].id, equals(0)); expect(semantics, hasSemantics(
expect(client.updates[0].actions, isEmpty); new TestSemantics(
expect(client.updates[0].flags.hasCheckedState, isTrue); id: 0,
expect(client.updates[0].flags.isChecked, isTrue); flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index,
expect(client.updates[0].strings.label, equals('label')); label: 'label',
expect(client.updates[0].geometry.transform, isNull); )
expect(client.updates[0].geometry.left, equals(0.0)); ));
expect(client.updates[0].geometry.top, equals(0.0));
expect(client.updates[0].geometry.width, equals(800.0));
expect(client.updates[0].geometry.height, equals(600.0));
expect(client.updates[0].children.length, equals(0));
client.updates.clear();
// switch the order of the inner Semantics node to trigger a reset // switch the order of the inner Semantics node to trigger a reset
await tester.pumpWidget( await tester.pumpWidget(
...@@ -67,19 +64,15 @@ void main() { ...@@ -67,19 +64,15 @@ void main() {
) )
) )
); );
expect(client.updates.length, equals(1));
expect(client.updates[0].id, equals(0)); expect(semantics, hasSemantics(
expect(client.updates[0].actions, isEmpty); new TestSemantics(
expect(client.updates[0].flags.hasCheckedState, isTrue); id: 0,
expect(client.updates[0].flags.isChecked, isTrue); flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index,
expect(client.updates[0].strings.label, equals('label')); label: 'label',
expect(client.updates[0].geometry.transform, isNull); )
expect(client.updates[0].geometry.left, equals(0.0)); ));
expect(client.updates[0].geometry.top, equals(0.0));
expect(client.updates[0].geometry.width, equals(800.0)); semantics.dispose();
expect(client.updates[0].geometry.height, equals(600.0));
expect(client.updates[0].children.length, equals(0));
client.updates.clear();
client.dispose();
}); });
} }
...@@ -5,13 +5,17 @@ ...@@ -5,13 +5,17 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_services/semantics.dart' as mojom;
import '../rendering/test_semantics_client.dart'; import 'semantics_tester.dart';
void main() { void main() {
testWidgets('Semantics shutdown and restart', (WidgetTester tester) async { testWidgets('Semantics shutdown and restart', (WidgetTester tester) async {
TestSemanticsClient client = new TestSemanticsClient(tester.binding.pipelineOwner); SemanticsTester semantics = new SemanticsTester(tester);
TestSemantics expectedSemantics = new TestSemantics(
id: 0,
label: 'test1',
);
await tester.pumpWidget( await tester.pumpWidget(
new Container( new Container(
...@@ -22,37 +26,22 @@ void main() { ...@@ -22,37 +26,22 @@ void main() {
) )
); );
void checkUpdates(List<mojom.SemanticsNode> updates) { expect(semantics, hasSemantics(expectedSemantics));
expect(updates.length, equals(1));
expect(updates[0].id, equals(0));
expect(updates[0].actions, isEmpty);
expect(updates[0].flags.hasCheckedState, isFalse);
expect(updates[0].flags.isChecked, isFalse);
expect(updates[0].strings.label, equals('test1'));
expect(updates[0].geometry.transform, isNull);
expect(updates[0].geometry.left, equals(0.0));
expect(updates[0].geometry.top, equals(0.0));
expect(updates[0].geometry.width, equals(800.0));
expect(updates[0].geometry.height, equals(600.0));
expect(updates[0].children.length, equals(0));
}
checkUpdates(client.updates); semantics.dispose();
client.updates.clear(); semantics = null;
client.dispose();
expect(tester.binding.hasScheduledFrame, isFalse); expect(tester.binding.hasScheduledFrame, isFalse);
client = new TestSemanticsClient(tester.binding.pipelineOwner); semantics = new SemanticsTester(tester);
expect(tester.binding.hasScheduledFrame, isTrue); expect(tester.binding.hasScheduledFrame, isTrue);
await tester.pump(); await tester.pump();
checkUpdates(client.updates); expect(semantics, hasSemantics(expectedSemantics));
client.updates.clear(); semantics.dispose();
client.dispose();
}); });
testWidgets('Detach and reattach assert', (WidgetTester tester) async { testWidgets('Detach and reattach assert', (WidgetTester tester) async {
TestSemanticsClient client = new TestSemanticsClient(tester.binding.pipelineOwner); SemanticsTester semantics = new SemanticsTester(tester);
GlobalKey key = new GlobalKey(); GlobalKey key = new GlobalKey();
await tester.pumpWidget( await tester.pumpWidget(
...@@ -69,11 +58,18 @@ void main() { ...@@ -69,11 +58,18 @@ void main() {
) )
); );
expect(client.updates.length, equals(1)); expect(semantics, hasSemantics(
expect(client.updates[0].strings.label, equals('test1')); new TestSemantics(
expect(client.updates[0].children.length, equals(1)); id: 0,
expect(client.updates[0].children[0].strings.label, equals('test2a')); label: 'test1',
client.updates.clear(); children: <TestSemantics>[
new TestSemantics(
id: 1,
label: 'test2a',
)
]
)
));
await tester.pumpWidget( await tester.pumpWidget(
new Container( new Container(
...@@ -93,14 +89,25 @@ void main() { ...@@ -93,14 +89,25 @@ void main() {
) )
); );
expect(client.updates.length, equals(1)); expect(semantics, hasSemantics(
expect(client.updates[0].strings.label, equals('test1')); new TestSemantics(
expect(client.updates[0].children.length, equals(1)); id: 0,
expect(client.updates[0].children[0].strings.label, equals('middle')); label: 'test1',
expect(client.updates[0].children[0].children.length, equals(1)); children: <TestSemantics>[
expect(client.updates[0].children[0].children[0].strings.label, equals('test2b')); new TestSemantics(
expect(client.updates[0].children[0].children[0].children.length, equals(0)); id: 2,
label: 'middle',
children: <TestSemantics>[
new TestSemantics(
id: 1,
label: 'test2b',
)
]
)
]
)
));
client.dispose(); semantics.dispose();
}); });
} }
// Copyright 2015 The Chromium 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 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:meta/meta.dart';
export 'package:flutter/rendering.dart' show SemanticsData;
/// Test semantics data that is compared against real semantics tree.
///
/// Useful with [hasSemantics] and [SemanticsTester] to test the contents of the
/// semantics tree.
class TestSemantics {
/// Creates an object witht some test semantics data.
///
/// If [rect] argument is null, the [rect] field with ve initialized with
/// `new Rect.fromLTRB(0.0, 0.0, 800.0, 600.0)`, which is the default size of
/// the screen during unit testing.
TestSemantics({
this.id,
this.flags: 0,
this.actions: 0,
this.label: '',
Rect rect,
this.transform,
this.children: const <TestSemantics>[],
}) : rect = rect ?? new Rect.fromLTRB(0.0, 0.0, 800.0, 600.0);
/// The unique identifier for this node.
///
/// The root node has an id of zero. Other nodes are given a unique id when
/// they are created.
final int id;
/// A bit field of [SemanticsFlags] that apply to this node.
final int flags;
/// A bit field of [SemanticsActions] that apply to this node.
final int actions;
/// A textual description of this node.
final String label;
/// The bounding box for this node in its coordinate system.
final Rect rect;
/// The transform from this node's coordinate system to its parent's coordinate system.
///
/// By default, the transform is null, which represents the identity
/// transformation (i.e., that this node has the same coorinate system as its
/// parent).
final Matrix4 transform;
/// The children of this node.
final List<TestSemantics> children;
SemanticsData _getSemanticsData() {
return new SemanticsData(
flags: flags,
actions: actions,
label: label,
rect: rect,
transform: transform
);
}
bool _matches(SemanticsNode node, Map<dynamic, dynamic> matchState) {
if (node == null || id != node.id
|| _getSemanticsData() != node.getSemanticsData()
|| children.length != (node.mergeAllDescendantsIntoThisNode ? 0 : node.childrenCount)) {
matchState[TestSemantics] = this;
matchState[SemanticsNode] = node;
return false;
}
if (children.isEmpty)
return true;
bool result = true;
Iterator<TestSemantics> it = children.iterator;
node.visitChildren((SemanticsNode node) {
it.moveNext();
if (!it.current._matches(node, matchState)) {
result = false;
return false;
}
return true;
});
return result;
}
}
/// Ensures that the given widget tester has a semantics tree to test.
///
/// Useful with [hasSemantics] to test the contents of the semantics tree.
class SemanticsTester {
/// Creates a semantics tester for the given widget tester.
///
/// You should call [dispose] at the end of a test that creates a semantics
/// tester.
SemanticsTester(this.tester) {
_semanticsHandle = tester.binding.pipelineOwner.ensureSemantics();
}
/// The widget tester that this object is testing the semantics of.
final WidgetTester tester;
SemanticsHandle _semanticsHandle;
/// Release resources held by this semantics tester.
///
/// Call this function at the end of any test that uses a semantics tester.
@mustCallSuper
void dispose() {
_semanticsHandle.dispose();
_semanticsHandle = null;
}
@override
String toString() => 'SemanticsTester';
}
class _HasSemantics extends Matcher {
const _HasSemantics(this._semantics);
final TestSemantics _semantics;
@override
bool matches(@checked SemanticsTester item, Map<dynamic, dynamic> matchState) {
return _semantics._matches(item.tester.binding.pipelineOwner.semanticsOwner.rootSemanticsNode, matchState);
}
@override
Description describe(Description description) {
return description.add('semantics node id ${_semantics.id}');
}
@override
Description describeMismatch(dynamic item, Description mismatchDescription, Map<dynamic, dynamic> matchState, bool verbose) {
TestSemantics testNode = matchState[TestSemantics];
SemanticsNode node = matchState[SemanticsNode];
if (node == null)
return mismatchDescription.add('could not find node with id ${testNode.id}');
if (testNode.id != node.id)
return mismatchDescription.add('expected node id ${testNode.id} but found id ${node.id}');
final SemanticsData data = node.getSemanticsData();
if (testNode.flags != data.flags)
return mismatchDescription.add('expected node id ${testNode.id} to have flags ${testNode.flags} but found flags ${data.flags}');
if (testNode.actions != data.actions)
return mismatchDescription.add('expected node id ${testNode.id} to have actions ${testNode.actions} but found actions ${data.actions}');
if (testNode.label != data.label)
return mismatchDescription.add('expected node id ${testNode.id} to have label "${testNode.label}" but found label "${data.label}"');
if (testNode.rect != data.rect)
return mismatchDescription.add('expected node id ${testNode.id} to have rect ${testNode.rect} but found rect ${data.rect}');
if (testNode.transform != data.transform)
return mismatchDescription.add('expected node id ${testNode.id} to have transform ${testNode.transform} but found transform ${data.transform}');
final int childrenCount = node.mergeAllDescendantsIntoThisNode ? 0 : node.childrenCount;
if (testNode.children.length != childrenCount)
return mismatchDescription.add('expected node id ${testNode.id} to have ${testNode.children.length} but found $childrenCount children');
return mismatchDescription;
}
}
/// Asserts that a [SemanticsTester] has a semantics tree that exactly matches the given semantics.
Matcher hasSemantics(TestSemantics semantics) => new _HasSemantics(semantics);
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