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 {
}
@override
bool operator==(Object other) {
bool operator ==(Object other) {
if (other.runtimeType != runtimeType)
return false;
ThemeData otherData = other;
......
......@@ -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<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
/// directly. This is the glue that binds the framework to the Flutter engine.
///
......
......@@ -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<RenderObject> _nodesNeedingLayout = <RenderObject>[];
/// 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<RenderObject> _nodesNeedingSemantics = <RenderObject>[];
......@@ -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;
......
......@@ -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<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;
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<mojom.SemanticsNode> 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<SemanticsNode> _dirtyNodes = new Set<SemanticsNode>();
final Map<int, SemanticsNode> _nodes = <int, SemanticsNode>{};
final Set<SemanticsNode> _detachedNodes = new Set<SemanticsNode>();
final List<SemanticsListener> _listeners = <SemanticsListener>[];
/// 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<mojom.SemanticsNode> updatedNodes = <mojom.SemanticsNode>[];
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<SemanticsListener>.from(_listeners))
listener(updatedNodes);
_dirtyNodes.clear();
ui.window.updateSemantics(builder.build());
notifyListeners();
}
SemanticsActionHandler _getSemanticsActionHandlerForId(int id, SemanticsAction action) {
......
......@@ -73,7 +73,7 @@ class MediaQueryData {
}
@override
bool operator==(Object other) {
bool operator ==(Object other) {
if (other.runtimeType != runtimeType)
return false;
MediaQueryData typedOther = other;
......
......@@ -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<SemanticsDebugger> {
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<mojom.SemanticsNode> nodes) {
void _didUpdateSemantics() {
generation += 1;
notifyListeners();
}
......
......@@ -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();
});
}
......@@ -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();
});
}
// 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 @@
// 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(<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'));
expect(client.updates[0].children[0].children.length, equals(0));
client.updates.clear();
client.dispose();
expect(semantics, hasSemantics(
new TestSemantics(
id: 0,
children: <TestSemantics>[
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();
});
}
......@@ -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: <Widget>[
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: <Widget>[
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(<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();
expect(semantics, hasSemantics(
new TestSemantics(
id: 0,
children: <TestSemantics>[
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: <Widget>[
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(<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();
expect(semantics, hasSemantics(
new TestSemantics(
id: 0,
children: <TestSemantics>[
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();
});
}
......@@ -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(<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();
expect(semantics, hasSemantics(
new TestSemantics(
id: 0,
children: <TestSemantics>[
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(<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();
expect(semantics, hasSemantics(
new TestSemantics(
id: 0,
children: <TestSemantics>[
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();
});
}
......@@ -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();
});
}
......@@ -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: <TestSemantics>[
new TestSemantics(
id: 1,
label: 'L1',
),
new TestSemantics(
id: 2,
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
// / \ 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: <TestSemantics>[
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();
});
}
......@@ -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: <TestSemantics>[
new TestSemantics(
id: 1,
),
new TestSemantics(
id: 2,
label: 'label',
),
]
)
));
semantics.dispose();
});
}
......@@ -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: <TestSemantics>[
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(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));
expect(semantics, hasSemantics(
new TestSemantics(
id: 0,
children: <TestSemantics>[
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
],
)
));
client.updates.clear();
client.dispose();
semantics.dispose();
});
}
......@@ -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();
});
}
......@@ -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<mojom.SemanticsNode> 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: <TestSemantics>[
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: <TestSemantics>[
new TestSemantics(
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