diff --git a/packages/flutter/lib/src/material/theme_data.dart b/packages/flutter/lib/src/material/theme_data.dart index 3a9efc3bc7a..f0723bcccd6 100644 --- a/packages/flutter/lib/src/material/theme_data.dart +++ b/packages/flutter/lib/src/material/theme_data.dart @@ -365,7 +365,7 @@ class ThemeData { } @override - bool operator==(Object other) { + bool operator ==(Object other) { if (other.runtimeType != runtimeType) return false; ThemeData otherData = other; diff --git a/packages/flutter/lib/src/rendering/binding.dart b/packages/flutter/lib/src/rendering/binding.dart index 96b0b3f9636..2ebb7c68866 100644 --- a/packages/flutter/lib/src/rendering/binding.dart +++ b/packages/flutter/lib/src/rendering/binding.dart @@ -9,8 +9,6 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.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 'debug.dart'; @@ -28,12 +26,15 @@ abstract class RendererBinding extends BindingBase implements SchedulerBinding, _instance = this; _pipelineOwner = new PipelineOwner( onNeedVisualUpdate: ensureVisualUpdate, - onScheduleInitialSemantics: _scheduleInitialSemantics, - onClearSemantics: _clearSemantics, + onSemanticsOwnerCreated: _handleSemanticsOwnerCreated, + onSemanticsOwnerDisposed: _handleSemanticsOwnerDisposed, ); - ui.window.onMetricsChanged = handleMetricsChanged; + ui.window + ..onMetricsChanged = handleMetricsChanged + ..onSemanticsEnabledChanged = _handleSemanticsEnabledChanged + ..onSemanticsAction = _handleSemanticsAction; initRenderView(); - initSemantics(); + _handleSemanticsEnabledChanged(); assert(renderView != null); addPersistentFrameCallback(_handlePersistentFrameCallback); } @@ -134,18 +135,27 @@ abstract class RendererBinding extends BindingBase implements SchedulerBinding, ); } - /// Prepares the rendering library to handle semantics requests from the engine. - /// - /// Called automatically when the binding is created. - void initSemantics() { - shell.provideService(mojom.SemanticsServer.serviceName, (core.MojoMessagePipeEndpoint endpoint) { - mojom.SemanticsServerStub stub = new mojom.SemanticsServerStub.fromEndpoint(endpoint); - SemanticsServer server = new SemanticsServer(pipelineOwner); - stub.impl = server; - stub.ctrl.onError = (_) { - server.dispose(); - }; - }); + SemanticsHandle _semanticsHandle; + + void _handleSemanticsEnabledChanged() { + if (ui.window.semanticsEnabled) { + _semanticsHandle ??= _pipelineOwner.ensureSemantics(); + } else { + _semanticsHandle?.dispose(); + _semanticsHandle = null; + } + } + + void _handleSemanticsAction(int id, SemanticsAction action) { + _pipelineOwner.semanticsOwner?.performAction(id, action); + } + + void _handleSemanticsOwnerCreated() { + renderView.scheduleInitialSemantics(); + } + + void _handleSemanticsOwnerDisposed() { + renderView.clearSemantics(); } void _handlePersistentFrameCallback(Duration timeStamp) { @@ -208,7 +218,7 @@ abstract class RendererBinding extends BindingBase implements SchedulerBinding, pipelineOwner.flushCompositingBits(); pipelineOwner.flushPaint(); renderView.compositeFrame(); // this sends the bits to the GPU - pipelineOwner.flushSemantics(); + pipelineOwner.flushSemantics(); // this also sends the semantics to the OS. } @override @@ -234,14 +244,6 @@ abstract class RendererBinding extends BindingBase implements SchedulerBinding, }; instance?.renderView?.visitChildren(visitor); } - - void _scheduleInitialSemantics() { - renderView.scheduleInitialSemantics(); - } - - void _clearSemantics() { - renderView.clearSemantics(); - } } /// Prints a textual representation of the entire render tree. @@ -261,44 +263,6 @@ void debugDumpSemanticsTree() { 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 _listeners = []; - - /// 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 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 /// directly. This is the glue that binds the framework to the Flutter engine. /// diff --git a/packages/flutter/lib/src/rendering/object.dart b/packages/flutter/lib/src/rendering/object.dart index edaaf153a26..e36db3e25b8 100644 --- a/packages/flutter/lib/src/rendering/object.dart +++ b/packages/flutter/lib/src/rendering/object.dart @@ -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 provides an interface for driving the rendering pipeline @@ -769,8 +799,8 @@ class PipelineOwner { /// through the rendering pipeline. PipelineOwner({ this.onNeedVisualUpdate, - this.onScheduleInitialSemantics, - this.onClearSemantics, + this.onSemanticsOwnerCreated, + this.onSemanticsOwnerDisposed, }); /// Called when a render object associated with this pipeline owner wishes to @@ -782,20 +812,16 @@ class PipelineOwner { /// duplicate calls quickly. final VoidCallback onNeedVisualUpdate; - /// Called when [addSemanticsListener] is called when there was no - /// [SemanticsOwner] present, to request that the - /// [RenderObject.scheduleInitialSemantics] method be called on the - /// appropriate object(s). + /// Called whenever this pipeline owner creates as semantics object. /// - /// For example, the [RendererBinding] calls it on the [RenderView] object. - final VoidCallback onScheduleInitialSemantics; + /// Typical implementations will schedule the creation of the initial + /// semantics tree. + final VoidCallback onSemanticsOwnerCreated; - /// Called when the last [SemanticsListener] is removed from the - /// [SemanticsOwner], to request that the [RenderObject.clearSemantics] method - /// be called on the appropriate object(s). + /// Called whenever this pipeline owner disposes its semantics owner. /// - /// For example, the [RendererBinding] calls it on the [RenderView] object. - final VoidCallback onClearSemantics; + /// Typical implementations will tear down the semantics tree. + final VoidCallback onSemanticsOwnerDisposed; /// Calls [onNeedVisualUpdate] if [onNeedVisualUpdate] is not null. /// @@ -819,32 +845,6 @@ class PipelineOwner { _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 _nodesNeedingLayout = []; /// Whether this pipeline is currently in the layout phase. @@ -953,18 +953,39 @@ class PipelineOwner { /// The object that is managing semantics for this pipeline owner, if any. /// - /// An owner is created by [addSemanticsListener] the first time a listener is - /// added. - /// - /// The owner is valid for as long as there are listeners. Once the last - /// listener is removed (by calling [SemanticsOwner.removeListener] on the - /// [semanticsOwner]), the [semanticsOwner] field will revert to null, and the - /// previous owner will be disposed. + /// An owner is created by [ensureSemantics]. The owner is valid for as long + /// there are [SemanticsHandle] returned by [ensureSemantics] that have not + /// yet be disposed. Once the last handle has been disposed, the + /// [semanticsOwner] field will revert to null, and the previous owner will be + /// disposed. /// /// When [semanticsOwner] is null, the [PipelineOwner] skips all steps /// relating to semantics. SemanticsOwner get 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; final List _nodesNeedingSemantics = []; @@ -987,12 +1008,12 @@ class PipelineOwner { if (node._needsSemanticsUpdate && node.owner == this) node._updateSemantics(); } + _semanticsOwner.sendSemanticsUpdate(); } finally { _nodesNeedingSemantics.clear(); assert(() { _debugDoingSemantics = false; return true; }); Timeline.finishSync(); } - _semanticsOwner.sendSemanticsTree(); } } @@ -1996,9 +2017,6 @@ abstract class RenderObject extends AbstractNode implements HitTestTarget { /// 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]. void clearSemantics() { _needsSemanticsUpdate = true; diff --git a/packages/flutter/lib/src/rendering/semantics.dart b/packages/flutter/lib/src/rendering/semantics.dart index 7578c640ce6..747262ad90e 100644 --- a/packages/flutter/lib/src/rendering/semantics.dart +++ b/packages/flutter/lib/src/rendering/semantics.dart @@ -2,13 +2,13 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'dart:math' as math; +import 'dart:ui' as ui; 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/painting.dart'; -import 'package:meta/meta.dart'; import 'package:vector_math/vector_math_64.dart'; import 'node.dart'; @@ -57,14 +57,19 @@ typedef bool SemanticsNodeVisitor(SemanticsNode node); class SemanticsData { /// Creates a semantics data object. /// - /// The [flags], [actions], [label], and [rect] arguments must not be null. - const SemanticsData({ + /// The [flags], [actions], [label], and [Rect] arguments must not be null. + SemanticsData({ @required this.flags, @required this.actions, @required this.label, @required this.rect, this.transform - }); + }) { + assert(flags != null); + assert(actions != null); + assert(label != null); + assert(rect != null); + } /// A bit field of [SemanticsFlags] that apply to this node. final int flags; @@ -90,6 +95,41 @@ class SemanticsData { /// Whether [actions] contains the given action. 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. @@ -307,6 +347,9 @@ class SemanticsNode extends AbstractNode { bool get hasChildren => _children?.isNotEmpty ?? 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. /// /// This function calls visitor for each child in a pre-order travseral @@ -477,53 +520,33 @@ class SemanticsNode extends AbstractNode { ); } - mojom.SemanticsNode _serialize() { - mojom.SemanticsNode result = new mojom.SemanticsNode(); - result.id = id; - if (_dirty) { - // We could be even more efficient about not sending data here, by only - // sending the bits that are dirty (tracking the geometry, flags, strings, - // and children separately). For now, we send all or nothing. - result.geometry = new mojom.SemanticGeometry(); - result.geometry.transform = transform?.storage; - result.geometry.top = rect.top; - result.geometry.left = rect.left; - result.geometry.width = math.max(rect.width, 0.0); - result.geometry.height = math.max(rect.height, 0.0); - result.flags = new mojom.SemanticFlags(); - result.flags.hasCheckedState = hasCheckedState; - result.flags.isChecked = isChecked; - result.strings = new mojom.SemanticStrings(); - result.strings.label = label; - List children = []; - 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 = []; - for (mojom.SemanticAction action in mojom.SemanticAction.values) { - int bit = 1 << action.mojoEnumValue; - if ((mergedActions & bit) != 0) - result.actions.add(action.mojoEnumValue); - } - _dirty = false; + static Float64List _initIdentityTransform() { + return new Matrix4.identity().storage; + } + + static final Int32List _kEmptyChildList = new Int32List(0); + static final Float64List _kIdentityTransform = _initIdentityTransform(); + + void _addToUpdate(ui.SemanticsUpdateBuilder builder) { + assert(_dirty); + final SemanticsData data = getSemanticsData(); + Int32List children; + if (!hasChildren || mergeAllDescendantsIntoThisNode) { + children = _kEmptyChildList; + } else { + final int childCount = _children.length; + children = new Int32List(childCount); + for (int i = 0; i < childCount; ++i) + children[i] = _children[i].id; } - return result; + builder.updateNode( + id: id, + flags: data.flags, + actions: data.actions, + rect: data.rect, + transform: data.transform?.storage ?? _kIdentityTransform, + ); + _dirty = false; } @override @@ -567,73 +590,31 @@ class SemanticsNode extends AbstractNode { } } -/// Signature for functions that receive updates about render tree semantics. -typedef void SemanticsListener(List nodes); - /// Owns [SemanticsNode] objects and notifies listeners of changes to the /// render tree semantics. /// /// To listen for semantic updates, call [PipelineOwner.addSemanticsListener], /// which will create a [SemanticsOwner] if necessary. -class SemanticsOwner { - /// 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; - +class SemanticsOwner extends ChangeNotifier { final Set _dirtyNodes = new Set(); final Map _nodes = {}; final Set _detachedNodes = new Set(); - final List _listeners = []; - /// The root node of the semantics tree, if any. /// /// If the semantics tree is empty, returns null. SemanticsNode get rootSemanticsNode => _nodes[0]; - /// Releases any resources retained by this object. - /// - /// Requires that there are no listeners registered with [addListener]. + @override void dispose() { - assert(_listeners.isEmpty); _dirtyNodes.clear(); _nodes.clear(); _detachedNodes.clear(); + super.dispose(); } - /// Add a consumer of semantic data. - /// - /// 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); + /// Update the semantics using [ui.window.updateSemantics]. + void sendSemanticsUpdate() { for (SemanticsNode oldNode in _detachedNodes) { // 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 @@ -680,7 +661,7 @@ class SemanticsOwner { } } visitedNodes.sort((SemanticsNode a, SemanticsNode b) => a.depth - b.depth); - List updatedNodes = []; + ui.SemanticsUpdateBuilder builder = new ui.SemanticsUpdateBuilder(); for (SemanticsNode node in visitedNodes) { assert(node.parent?._dirty != true); // could be null (no parent) or false (not dirty) // The _serialize() method marks the node as not dirty, and @@ -694,11 +675,11 @@ class SemanticsOwner { // which happens e.g. when the node is no longer contributing // semantics). if (node._dirty && node.attached) - updatedNodes.add(node._serialize()); + node._addToUpdate(builder); } - for (SemanticsListener listener in new List.from(_listeners)) - listener(updatedNodes); _dirtyNodes.clear(); + ui.window.updateSemantics(builder.build()); + notifyListeners(); } SemanticsActionHandler _getSemanticsActionHandlerForId(int id, SemanticsAction action) { diff --git a/packages/flutter/lib/src/widgets/media_query.dart b/packages/flutter/lib/src/widgets/media_query.dart index c2812e21c84..65446610c00 100644 --- a/packages/flutter/lib/src/widgets/media_query.dart +++ b/packages/flutter/lib/src/widgets/media_query.dart @@ -73,7 +73,7 @@ class MediaQueryData { } @override - bool operator==(Object other) { + bool operator ==(Object other) { if (other.runtimeType != runtimeType) return false; MediaQueryData typedOther = other; diff --git a/packages/flutter/lib/src/widgets/semantics_debugger.dart b/packages/flutter/lib/src/widgets/semantics_debugger.dart index 37c2c7ef132..d59b86a90f7 100644 --- a/packages/flutter/lib/src/widgets/semantics_debugger.dart +++ b/packages/flutter/lib/src/widgets/semantics_debugger.dart @@ -8,7 +8,6 @@ import 'dart:ui' show SemanticsFlags; import 'package:flutter/foundation.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/rendering.dart'; -import 'package:flutter_services/semantics.dart' as mojom; import 'basic.dart'; import 'binding.dart'; @@ -153,21 +152,23 @@ class _SemanticsDebuggerState extends State { class _SemanticsClient extends ChangeNotifier { _SemanticsClient(PipelineOwner pipelineOwner) { - _semanticsOwner = pipelineOwner.addSemanticsListener(_updateSemanticsTree); + _semanticsHandle = pipelineOwner.ensureSemantics( + listener: _didUpdateSemantics + ); } - SemanticsOwner _semanticsOwner; + SemanticsHandle _semanticsHandle; @override void dispose() { - _semanticsOwner.removeListener(_updateSemanticsTree); - _semanticsOwner = null; + _semanticsHandle.dispose(); + _semanticsHandle = null; super.dispose(); } int generation = 0; - void _updateSemanticsTree(List nodes) { + void _didUpdateSemantics() { generation += 1; notifyListeners(); } diff --git a/packages/flutter/test/material/tooltip_test.dart b/packages/flutter/test/material/tooltip_test.dart index 1d00df724a3..17c380aec39 100644 --- a/packages/flutter/test/material/tooltip_test.dart +++ b/packages/flutter/test/material/tooltip_test.dart @@ -8,7 +8,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.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 // analysis. In general you want to avoid using this style in your @@ -431,7 +431,8 @@ void main() { }); testWidgets('Does tooltip contribute semantics', (WidgetTester tester) async { - TestSemanticsClient client = new TestSemanticsClient(tester.binding.pipelineOwner); + SemanticsTester semantics = new SemanticsTester(tester); + GlobalKey key = new GlobalKey(); await tester.pumpWidget( new Overlay( @@ -456,37 +457,16 @@ void main() { ] ) ); - expect(client.updates.length, equals(1)); - expect(client.updates[0].id, equals(0)); - 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(); + + expect(semantics, hasSemantics(new TestSemantics(id: 0, label: 'TIP'))); // 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 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(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(); - client.dispose(); + + expect(semantics, hasSemantics(new TestSemantics(id: 0, label: 'TIP'))); + + semantics.dispose(); }); } diff --git a/packages/flutter/test/rendering/reattach_test.dart b/packages/flutter/test/rendering/reattach_test.dart index 216d91e6380..a3935dd7933 100644 --- a/packages/flutter/test/rendering/reattach_test.dart +++ b/packages/flutter/test/rendering/reattach_test.dart @@ -7,7 +7,6 @@ import 'package:flutter/rendering.dart'; import 'package:test/test.dart'; import 'rendering_tester.dart'; -import 'test_semantics_client.dart'; class TestTree { TestTree() { @@ -126,20 +125,25 @@ void main() { }); test('objects can be detached and re-attached: semantics', () { 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 layout(testTree.root, phase: EnginePhase.flushSemantics); - expect(client.updates.length, equals(1)); + expect(semanticsUpdateCount, 1); // Remove testTree from the custom render view renderer.renderView.child = null; expect(testTree.child.owner, isNull); // Dirty one of the elements - client.updates.clear(); + semanticsUpdateCount = 0; testTree.child.markNeedsSemanticsUpdate(); - expect(client.updates.length, equals(0)); + expect(semanticsUpdateCount, 0); // Lay out, composite, paint, and update semantics again layout(testTree.root, phase: EnginePhase.flushSemantics); - expect(client.updates.length, equals(1)); - client.dispose(); + expect(semanticsUpdateCount, 1); + semanticsHandle.dispose(); }); } diff --git a/packages/flutter/test/rendering/test_semantics_client.dart b/packages/flutter/test/rendering/test_semantics_client.dart deleted file mode 100644 index 2d24f892472..00000000000 --- a/packages/flutter/test/rendering/test_semantics_client.dart +++ /dev/null @@ -1,25 +0,0 @@ -// 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 updates = []; - - void _updateSemanticsTree(List nodes) { - updates.addAll(nodes); - } -} diff --git a/packages/flutter/test/widget/buttons_test.dart b/packages/flutter/test/widget/buttons_test.dart index 59238e71ede..9fd62e7c49c 100644 --- a/packages/flutter/test/widget/buttons_test.dart +++ b/packages/flutter/test/widget/buttons_test.dart @@ -2,16 +2,16 @@ // 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/material.dart'; import 'package:flutter/widgets.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() { testWidgets('Does FlatButton contribute semantics', (WidgetTester tester) async { - TestSemanticsClient client = new TestSemanticsClient(tester.binding.pipelineOwner); + SemanticsTester semantics = new SemanticsTester(tester); await tester.pumpWidget( new Material( child: new Center( @@ -22,25 +22,22 @@ void main() { ) ) ); - expect(client.updates.length, equals(1)); - expect(client.updates[0].id, equals(0)); - 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('')); - 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(1)); - expect(client.updates[0].children[0].id, equals(1)); - expect(client.updates[0].children[0].actions, equals([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')); - expect(client.updates[0].children[0].children.length, equals(0)); - client.updates.clear(); - client.dispose(); + + expect(semantics, hasSemantics( + new TestSemantics( + id: 0, + children: [ + new TestSemantics( + id: 1, + actions: SemanticsAction.tap.index, + label: 'Hello', + rect: new Rect.fromLTRB(0.0, 0.0, 88.0, 36.0), + transform: new Matrix4.translationValues(356.0, 282.0, 0.0) + ) + ] + ) + )); + + semantics.dispose(); }); } diff --git a/packages/flutter/test/widget/semantics_1_test.dart b/packages/flutter/test/widget/semantics_1_test.dart index 96ba3a720af..f245813b581 100644 --- a/packages/flutter/test/widget/semantics_1_test.dart +++ b/packages/flutter/test/widget/semantics_1_test.dart @@ -7,11 +7,11 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; -import '../rendering/test_semantics_client.dart'; +import 'semantics_tester.dart'; void main() { testWidgets('Semantics 1', (WidgetTester tester) async { - TestSemanticsClient client = new TestSemanticsClient(tester.binding.pipelineOwner); + SemanticsTester semantics = new SemanticsTester(tester); // smoketest await tester.pumpWidget( @@ -22,23 +22,13 @@ void main() { ) ) ); - expect(client.updates.length, equals(1)); - expect(client.updates[0].id, equals(0)); - 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(); + + expect(semantics, hasSemantics(new TestSemantics(id: 0, label: 'test1'))); // control for forking await tester.pumpWidget( new Column( + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ new Container( height: 10.0, @@ -52,26 +42,15 @@ void main() { ) ), ], - crossAxisAlignment: CrossAxisAlignment.stretch ) ); - expect(client.updates.length, equals(1)); - expect(client.updates[0].id, equals(0)); - 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(); + + expect(semantics, hasSemantics(new TestSemantics(id: 0, label: 'child1'))); // forking semantics await tester.pumpWidget( new Column( + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ new Container( height: 10.0, @@ -85,48 +64,32 @@ void main() { ) ), ], - crossAxisAlignment: CrossAxisAlignment.stretch ) ); - expect(client.updates.length, equals(1)); - expect(client.updates[0].id, equals(0)); - 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('')); - 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(2)); - expect(client.updates[0].children[0].id, equals(1)); - expect(client.updates[0].children[0].actions, isEmpty); - 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('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([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(); + + expect(semantics, hasSemantics( + new TestSemantics( + id: 0, + children: [ + new TestSemantics( + id: 1, + label: 'child1', + rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 10.0), + ), + new TestSemantics( + id: 2, + label: 'child2', + rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 10.0), + transform: new Matrix4.translationValues(0.0, 10.0, 0.0), + ), + ], + ) + )); // toggle a branch off await tester.pumpWidget( new Column( + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ new Container( height: 10.0, @@ -140,22 +103,10 @@ void main() { ) ), ], - crossAxisAlignment: CrossAxisAlignment.stretch ) ); - expect(client.updates.length, equals(1)); - expect(client.updates[0].id, equals(0)); - 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(); + + expect(semantics, hasSemantics(new TestSemantics(id: 0, label: 'child1'))); // toggle a branch back on await tester.pumpWidget( @@ -176,41 +127,26 @@ void main() { crossAxisAlignment: CrossAxisAlignment.stretch ) ); - expect(client.updates.length, equals(1)); - expect(client.updates[0].id, equals(0)); - 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('')); - 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(2)); - expect(client.updates[0].children[0].id, equals(3)); - expect(client.updates[0].children[0].actions, isEmpty); - 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('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([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(); + + expect(semantics, hasSemantics( + new TestSemantics( + id: 0, + children: [ + new TestSemantics( + id: 3, + label: 'child1', + rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 10.0), + ), + new TestSemantics( + id: 2, + label: 'child2', + rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 10.0), + transform: new Matrix4.translationValues(0.0, 10.0, 0.0), + ), + ], + ) + )); + + semantics.dispose(); }); } diff --git a/packages/flutter/test/widget/semantics_2_test.dart b/packages/flutter/test/widget/semantics_2_test.dart index 9e666852e78..766001eaf0b 100644 --- a/packages/flutter/test/widget/semantics_2_test.dart +++ b/packages/flutter/test/widget/semantics_2_test.dart @@ -7,11 +7,11 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; -import '../rendering/test_semantics_client.dart'; +import 'semantics_tester.dart'; void main() { 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 // starting with the second branch being ignored and then @@ -36,41 +36,25 @@ void main() { crossAxisAlignment: CrossAxisAlignment.stretch ) ); - expect(client.updates.length, equals(1)); - expect(client.updates[0].id, equals(0)); - 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('')); - 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(2)); - expect(client.updates[0].children[0].id, equals(1)); - expect(client.updates[0].children[0].actions, isEmpty); - 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('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([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(); + + expect(semantics, hasSemantics( + new TestSemantics( + id: 0, + children: [ + new TestSemantics( + id: 1, + label: 'child1', + rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 10.0), + ), + new TestSemantics( + id: 2, + label: 'child2', + rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 10.0), + transform: new Matrix4.translationValues(0.0, 10.0, 0.0), + ), + ], + ) + )); // toggle a branch off await tester.pumpWidget( @@ -91,19 +75,8 @@ void main() { crossAxisAlignment: CrossAxisAlignment.stretch ) ); - expect(client.updates.length, equals(1)); - expect(client.updates[0].id, equals(0)); - 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(); + + expect(semantics, hasSemantics(new TestSemantics(id: 0, label: 'child1'))); // toggle a branch back on await tester.pumpWidget( @@ -124,41 +97,26 @@ void main() { crossAxisAlignment: CrossAxisAlignment.stretch ) ); - expect(client.updates.length, equals(1)); - expect(client.updates[0].id, equals(0)); - 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('')); - 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(2)); - expect(client.updates[0].children[0].id, equals(3)); - expect(client.updates[0].children[0].actions, isEmpty); - 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('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([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(); + + expect(semantics, hasSemantics( + new TestSemantics( + id: 0, + children: [ + new TestSemantics( + id: 3, + label: 'child1', + rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 10.0), + ), + new TestSemantics( + id: 2, + label: 'child2', + rect: new Rect.fromLTRB(0.0, 0.0, 800.0, 10.0), + transform: new Matrix4.translationValues(0.0, 10.0, 0.0), + ), + ], + ) + )); + + semantics.dispose(); }); } diff --git a/packages/flutter/test/widget/semantics_3_test.dart b/packages/flutter/test/widget/semantics_3_test.dart index 3302d0d6a0d..848fa603913 100644 --- a/packages/flutter/test/widget/semantics_3_test.dart +++ b/packages/flutter/test/widget/semantics_3_test.dart @@ -2,15 +2,17 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:ui' show SemanticsFlags; + import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; -import '../rendering/test_semantics_client.dart'; +import 'semantics_tester.dart'; void main() { testWidgets('Semantics 3', (WidgetTester tester) async { - TestSemanticsClient client = new TestSemanticsClient(tester.binding.pipelineOwner); + SemanticsTester semantics = new SemanticsTester(tester); // implicit annotators await tester.pumpWidget( @@ -25,19 +27,14 @@ void main() { ) ) ); - expect(client.updates.length, equals(1)); - expect(client.updates[0].id, equals(0)); - expect(client.updates[0].actions, isEmpty); - expect(client.updates[0].flags.hasCheckedState, isTrue); - expect(client.updates[0].flags.isChecked, isTrue); - 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(); + + expect(semantics, hasSemantics( + new TestSemantics( + id: 0, + flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index, + label: 'test', + ) + )); // remove one await tester.pumpWidget( @@ -49,19 +46,13 @@ void main() { ) ) ); - expect(client.updates.length, equals(1)); - expect(client.updates[0].id, equals(0)); - expect(client.updates[0].actions, isEmpty); - expect(client.updates[0].flags.hasCheckedState, isTrue); - expect(client.updates[0].flags.isChecked, isTrue); - 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(); + + expect(semantics, hasSemantics( + new TestSemantics( + id: 0, + flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index, + ) + )); // change what it says await tester.pumpWidget( @@ -73,19 +64,13 @@ void main() { ) ) ); - expect(client.updates.length, equals(1)); - expect(client.updates[0].id, equals(0)); - 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('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(); + + expect(semantics, hasSemantics( + new TestSemantics( + id: 0, + label: 'test', + ) + )); // add a node await tester.pumpWidget( @@ -100,19 +85,19 @@ void main() { ) ) ); - expect(client.updates.length, equals(1)); - expect(client.updates[0].id, equals(0)); - expect(client.updates[0].actions, isEmpty); - expect(client.updates[0].flags.hasCheckedState, isTrue); - expect(client.updates[0].flags.isChecked, isTrue); - 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(); + + expect(semantics, hasSemantics( + new TestSemantics( + id: 0, + flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index, + label: 'test', + ) + )); + + int changeCount = 0; + tester.binding.pipelineOwner.semanticsOwner.addListener(() { + changeCount += 1; + }); // make no changes await tester.pumpWidget( @@ -127,7 +112,9 @@ void main() { ) ) ); - expect(client.updates.length, equals(0)); - client.dispose(); + + expect(changeCount, 0); + + semantics.dispose(); }); } diff --git a/packages/flutter/test/widget/semantics_4_test.dart b/packages/flutter/test/widget/semantics_4_test.dart index b99d7284872..6fef7a80ed3 100644 --- a/packages/flutter/test/widget/semantics_4_test.dart +++ b/packages/flutter/test/widget/semantics_4_test.dart @@ -2,15 +2,17 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:ui' show SemanticsFlags; + import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; -import '../rendering/test_semantics_client.dart'; +import 'semantics_tester.dart'; void main() { testWidgets('Semantics 4', (WidgetTester tester) async { - TestSemanticsClient client = new TestSemanticsClient(tester.binding.pipelineOwner); + SemanticsTester semantics = new SemanticsTester(tester); // O // / \ O=root @@ -40,18 +42,32 @@ void main() { ] ) ); - expect(client.updates.length, equals(1)); - expect(client.updates[0].id, equals(0)); - expect(client.updates[0].children.length, equals(2)); - expect(client.updates[0].children[0].id, equals(1)); - 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].children.length, equals(2)); - expect(client.updates[0].children[1].children[0].id, equals(3)); - expect(client.updates[0].children[1].children[0].children.length, equals(0)); - expect(client.updates[0].children[1].children[1].id, equals(4)); - expect(client.updates[0].children[1].children[1].children.length, equals(0)); - client.updates.clear(); + + expect(semantics, hasSemantics( + new TestSemantics( + id: 0, + children: [ + new TestSemantics( + id: 1, + label: 'L1', + ), + new TestSemantics( + id: 2, + label: 'L2', + children: [ + new TestSemantics( + id: 3, + flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index, + ), + new TestSemantics( + id: 4, + flags: SemanticsFlags.hasCheckedState.index, + ), + ] + ), + ], + ) + )); // O O=root // / \ L=node with label @@ -78,10 +94,23 @@ void main() { ] ) ); - expect(client.updates.length, equals(1)); - expect(client.updates[0].id, equals(2)); - expect(client.updates[0].children.length, equals(0)); - client.updates.clear(); + + expect(semantics, hasSemantics( + new TestSemantics( + id: 0, + children: [ + new TestSemantics( + id: 1, + label: 'L1', + ), + new TestSemantics( + id: 2, + label: 'L2', + flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index, + ), + ], + ) + )); // O=root // OLC L=node with label @@ -105,10 +134,15 @@ void main() { ] ) ); - expect(client.updates.length, equals(1)); - expect(client.updates[0].id, equals(0)); - expect(client.updates[0].children.length, equals(0)); - client.updates.clear(); - client.dispose(); + + expect(semantics, hasSemantics( + new TestSemantics( + id: 0, + label: 'L2', + flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index, + ) + )); + + semantics.dispose(); }); } diff --git a/packages/flutter/test/widget/semantics_5_test.dart b/packages/flutter/test/widget/semantics_5_test.dart index b0b2f8be8fa..f4c5a842d2f 100644 --- a/packages/flutter/test/widget/semantics_5_test.dart +++ b/packages/flutter/test/widget/semantics_5_test.dart @@ -6,11 +6,11 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; -import '../rendering/test_semantics_client.dart'; +import 'semantics_tester.dart'; void main() { testWidgets('Semantics 5', (WidgetTester tester) async { - TestSemanticsClient client = new TestSemanticsClient(tester.binding.pipelineOwner); + SemanticsTester semantics = new SemanticsTester(tester); await tester.pumpWidget( new Stack( @@ -28,18 +28,22 @@ void main() { ] ) ); - expect(client.updates.length, equals(1)); - expect(client.updates[0].id, equals(0)); - expect(client.updates[0].flags.hasCheckedState, isFalse); - expect(client.updates[0].strings.label, equals('')); - expect(client.updates[0].children.length, equals(2)); - expect(client.updates[0].children[0].id, equals(1)); - expect(client.updates[0].children[0].flags.hasCheckedState, isFalse); - expect(client.updates[0].children[0].strings.label, equals('')); - expect(client.updates[0].children[1].id, equals(2)); - expect(client.updates[0].children[1].flags.hasCheckedState, isFalse); - expect(client.updates[0].children[1].strings.label, equals('label')); - client.updates.clear(); - client.dispose(); + + expect(semantics, hasSemantics( + new TestSemantics( + id: 0, + children: [ + new TestSemantics( + id: 1, + ), + new TestSemantics( + id: 2, + label: 'label', + ), + ] + ) + )); + + semantics.dispose(); }); } diff --git a/packages/flutter/test/widget/semantics_7_test.dart b/packages/flutter/test/widget/semantics_7_test.dart index 202208c6c57..8011a712992 100644 --- a/packages/flutter/test/widget/semantics_7_test.dart +++ b/packages/flutter/test/widget/semantics_7_test.dart @@ -2,16 +2,17 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:ui' show SemanticsFlags; + import 'package:flutter/material.dart'; import 'package:flutter/widgets.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() { testWidgets('Semantics 7 - Merging', (WidgetTester tester) async { - TestSemanticsClient client = new TestSemanticsClient(tester.binding.pipelineOwner); + SemanticsTester semantics = new SemanticsTester(tester); String label; @@ -44,43 +45,26 @@ void main() { ] ) ); - expect(client.updates.length, equals(1)); - expect(client.updates[0].id, equals(0)); - 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('')); - 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(2)); - expect(client.updates[0].children[0].id, equals(1)); - expect(client.updates[0].children[0].actions, isEmpty); - expect(client.updates[0].children[0].flags.hasCheckedState, isTrue); - expect(client.updates[0].children[0].flags.isChecked, isTrue); - expect(client.updates[0].children[0].strings.label, equals(label)); - 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(); + + expect(semantics, hasSemantics( + new TestSemantics( + id: 0, + children: [ + new TestSemantics( + id: 1, + flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index, + label: label, + ), + // IDs 2 and 3 are used up by the nodes that get merged in + new TestSemantics( + id: 4, + flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index, + label: label, + ), + // IDs 5 and 6 are used up by the nodes that get merged in + ], + ) + )); label = '2'; await tester.pumpWidget( @@ -111,43 +95,27 @@ void main() { ] ) ); - expect(client.updates.length, equals(2)); - // The order of the nodes is undefined, so allow both orders. - mojom.SemanticsNode a, b; - if (client.updates[0].id == 1) { - a = client.updates[0]; - b = client.updates[1]; - } else { - a = client.updates[1]; - b = client.updates[0]; - } + expect(semantics, hasSemantics( + new TestSemantics( + id: 0, + children: [ + new TestSemantics( + id: 1, + flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index, + label: label, + ), + // IDs 2 and 3 are used up by the nodes that get merged in + new TestSemantics( + id: 4, + flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index, + label: label, + ), + // IDs 5 and 6 are used up by the nodes that get merged in + ], + ) + )); - expect(a.id, equals(1)); - expect(a.actions, isEmpty); - expect(a.flags.hasCheckedState, isTrue); - expect(a.flags.isChecked, isTrue); - expect(a.strings.label, equals(label)); - expect(a.geometry.transform, isNull); - 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(); - client.dispose(); + semantics.dispose(); }); } diff --git a/packages/flutter/test/widget/semantics_8_test.dart b/packages/flutter/test/widget/semantics_8_test.dart index 8cc882d02bb..8135ae69ae7 100644 --- a/packages/flutter/test/widget/semantics_8_test.dart +++ b/packages/flutter/test/widget/semantics_8_test.dart @@ -2,15 +2,17 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:ui' show SemanticsFlags; + import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; -import '../rendering/test_semantics_client.dart'; +import 'semantics_tester.dart'; void main() { testWidgets('Semantics 8 - Merging with reset', (WidgetTester tester) async { - TestSemanticsClient client = new TestSemanticsClient(tester.binding.pipelineOwner); + SemanticsTester semantics = new SemanticsTester(tester); await tester.pumpWidget( new MergeSemantics( @@ -32,19 +34,14 @@ void main() { ) ) ); - expect(client.updates.length, equals(1)); - expect(client.updates[0].id, equals(0)); - expect(client.updates[0].actions, isEmpty); - expect(client.updates[0].flags.hasCheckedState, isTrue); - expect(client.updates[0].flags.isChecked, isTrue); - expect(client.updates[0].strings.label, equals('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(); + + expect(semantics, hasSemantics( + new TestSemantics( + id: 0, + flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index, + label: 'label', + ) + )); // switch the order of the inner Semantics node to trigger a reset await tester.pumpWidget( @@ -67,19 +64,15 @@ void main() { ) ) ); - expect(client.updates.length, equals(1)); - expect(client.updates[0].id, equals(0)); - expect(client.updates[0].actions, isEmpty); - expect(client.updates[0].flags.hasCheckedState, isTrue); - expect(client.updates[0].flags.isChecked, isTrue); - expect(client.updates[0].strings.label, equals('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(); - client.dispose(); + + expect(semantics, hasSemantics( + new TestSemantics( + id: 0, + flags: SemanticsFlags.hasCheckedState.index | SemanticsFlags.isChecked.index, + label: 'label', + ) + )); + + semantics.dispose(); }); } diff --git a/packages/flutter/test/widget/semantics_test.dart b/packages/flutter/test/widget/semantics_test.dart index 47de1c20bcd..039f04e54d3 100644 --- a/packages/flutter/test/widget/semantics_test.dart +++ b/packages/flutter/test/widget/semantics_test.dart @@ -5,13 +5,17 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.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() { 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( new Container( @@ -22,37 +26,22 @@ void main() { ) ); - void checkUpdates(List updates) { - 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)); - } + expect(semantics, hasSemantics(expectedSemantics)); - checkUpdates(client.updates); - client.updates.clear(); - client.dispose(); + semantics.dispose(); + semantics = null; expect(tester.binding.hasScheduledFrame, isFalse); - client = new TestSemanticsClient(tester.binding.pipelineOwner); + semantics = new SemanticsTester(tester); expect(tester.binding.hasScheduledFrame, isTrue); await tester.pump(); - checkUpdates(client.updates); - client.updates.clear(); - client.dispose(); + expect(semantics, hasSemantics(expectedSemantics)); + semantics.dispose(); }); testWidgets('Detach and reattach assert', (WidgetTester tester) async { - TestSemanticsClient client = new TestSemanticsClient(tester.binding.pipelineOwner); + SemanticsTester semantics = new SemanticsTester(tester); GlobalKey key = new GlobalKey(); await tester.pumpWidget( @@ -69,11 +58,18 @@ void main() { ) ); - expect(client.updates.length, equals(1)); - expect(client.updates[0].strings.label, equals('test1')); - expect(client.updates[0].children.length, equals(1)); - expect(client.updates[0].children[0].strings.label, equals('test2a')); - client.updates.clear(); + expect(semantics, hasSemantics( + new TestSemantics( + id: 0, + label: 'test1', + children: [ + new TestSemantics( + id: 1, + label: 'test2a', + ) + ] + ) + )); await tester.pumpWidget( new Container( @@ -93,14 +89,25 @@ void main() { ) ); - expect(client.updates.length, equals(1)); - expect(client.updates[0].strings.label, equals('test1')); - expect(client.updates[0].children.length, equals(1)); - expect(client.updates[0].children[0].strings.label, equals('middle')); - expect(client.updates[0].children[0].children.length, equals(1)); - expect(client.updates[0].children[0].children[0].strings.label, equals('test2b')); - expect(client.updates[0].children[0].children[0].children.length, equals(0)); + expect(semantics, hasSemantics( + new TestSemantics( + id: 0, + label: 'test1', + children: [ + new TestSemantics( + id: 2, + label: 'middle', + children: [ + new TestSemantics( + id: 1, + label: 'test2b', + ) + ] + ) + ] + ) + )); - client.dispose(); + semantics.dispose(); }); } diff --git a/packages/flutter/test/widget/semantics_tester.dart b/packages/flutter/test/widget/semantics_tester.dart new file mode 100644 index 00000000000..0326d5e176e --- /dev/null +++ b/packages/flutter/test/widget/semantics_tester.dart @@ -0,0 +1,164 @@ +// 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 [], + }) : 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 children; + + SemanticsData _getSemanticsData() { + return new SemanticsData( + flags: flags, + actions: actions, + label: label, + rect: rect, + transform: transform + ); + } + + bool _matches(SemanticsNode node, Map 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 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 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 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);