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

Fix attaching a dirty, detached SemanticsNode (#4855)

Previously we triggered an assert.

Fixes #4850
parent c9248cd4
...@@ -362,7 +362,6 @@ class SemanticsNode extends AbstractNode { ...@@ -362,7 +362,6 @@ class SemanticsNode extends AbstractNode {
return; return;
_dirty = true; _dirty = true;
if (attached) { if (attached) {
assert(!owner._dirtyNodes.contains(this));
assert(!owner._detachedNodes.contains(this)); assert(!owner._detachedNodes.contains(this));
owner._dirtyNodes.add(this); owner._dirtyNodes.add(this);
} }
...@@ -478,7 +477,7 @@ class SemanticsOwner { ...@@ -478,7 +477,7 @@ class SemanticsOwner {
final VoidCallback _onLastListenerRemoved; final VoidCallback _onLastListenerRemoved;
final List<SemanticsNode> _dirtyNodes = <SemanticsNode>[]; final Set<SemanticsNode> _dirtyNodes = new Set<SemanticsNode>();
final Map<int, SemanticsNode> _nodes = <int, SemanticsNode>{}; final Map<int, SemanticsNode> _nodes = <int, SemanticsNode>{};
final Set<SemanticsNode> _detachedNodes = new Set<SemanticsNode>(); final Set<SemanticsNode> _detachedNodes = new Set<SemanticsNode>();
...@@ -526,42 +525,45 @@ class SemanticsOwner { ...@@ -526,42 +525,45 @@ class SemanticsOwner {
_detachedNodes.clear(); _detachedNodes.clear();
if (_dirtyNodes.isEmpty) if (_dirtyNodes.isEmpty)
return; return;
_dirtyNodes.sort((SemanticsNode a, SemanticsNode b) => a.depth - b.depth); List<SemanticsNode> visitedNodes = <SemanticsNode>[];
for (int index = 0; index < _dirtyNodes.length; index += 1) { while (_dirtyNodes.isNotEmpty) {
// we mutate the list as we walk it here, which is why we use an index instead of an iterator List<SemanticsNode> localDirtyNodes = _dirtyNodes.toList();
SemanticsNode node = _dirtyNodes[index]; _dirtyNodes.clear();
assert(node._dirty); localDirtyNodes.sort((SemanticsNode a, SemanticsNode b) => a.depth - b.depth);
assert(node.parent == null || !node.parent._shouldMergeAllDescendantsIntoThisNode || node._inheritedMergeAllDescendantsIntoThisNode); visitedNodes.addAll(localDirtyNodes);
if (node._shouldMergeAllDescendantsIntoThisNode) { for (SemanticsNode node in localDirtyNodes) {
assert(node.mergeAllDescendantsIntoThisNode || node.parent != null); assert(node._dirty);
if (node.mergeAllDescendantsIntoThisNode || assert(node.parent == null || !node.parent._shouldMergeAllDescendantsIntoThisNode || node._inheritedMergeAllDescendantsIntoThisNode);
node.parent != null && node.parent._shouldMergeAllDescendantsIntoThisNode) { if (node._shouldMergeAllDescendantsIntoThisNode) {
// if we're merged into our parent, make sure our parent is added to the list assert(node.mergeAllDescendantsIntoThisNode || node.parent != null);
if (node.parent != null && node.parent._shouldMergeAllDescendantsIntoThisNode) if (node.mergeAllDescendantsIntoThisNode ||
node.parent._markDirty(); // this can add the node to the dirty list node.parent != null && node.parent._shouldMergeAllDescendantsIntoThisNode) {
// make sure all the descendants are also marked, so that if one gets marked dirty later we know to walk up then too // if we're merged into our parent, make sure our parent is added to the list
if (node._children != null) { if (node.parent != null && node.parent._shouldMergeAllDescendantsIntoThisNode)
for (SemanticsNode child in node._children) node.parent._markDirty(); // this can add the node to the dirty list
child._inheritedMergeAllDescendantsIntoThisNode = true; // this can add the node to the dirty list // make sure all the descendants are also marked, so that if one gets marked dirty later we know to walk up then too
} if (node._children != null) {
} else { for (SemanticsNode child in node._children)
// we previously were being merged but aren't any more child._inheritedMergeAllDescendantsIntoThisNode = true; // this can add the node to the dirty list
// update our bits and all our descendants' }
assert(node._inheritedMergeAllDescendantsIntoThisNode); } else {
assert(!node.mergeAllDescendantsIntoThisNode); // we previously were being merged but aren't any more
assert(node.parent == null || !node.parent._shouldMergeAllDescendantsIntoThisNode); // update our bits and all our descendants'
node._inheritedMergeAllDescendantsIntoThisNode = false; assert(node._inheritedMergeAllDescendantsIntoThisNode);
if (node._children != null) { assert(!node.mergeAllDescendantsIntoThisNode);
for (SemanticsNode child in node._children) assert(node.parent == null || !node.parent._shouldMergeAllDescendantsIntoThisNode);
child._inheritedMergeAllDescendantsIntoThisNode = false; // this can add the node to the dirty list node._inheritedMergeAllDescendantsIntoThisNode = false;
if (node._children != null) {
for (SemanticsNode child in node._children)
child._inheritedMergeAllDescendantsIntoThisNode = false; // this can add the node to the dirty list
}
} }
} }
} }
assert(_dirtyNodes[index] == node); // make sure nothing went in front of us in the list
} }
_dirtyNodes.sort((SemanticsNode a, SemanticsNode b) => a.depth - b.depth); visitedNodes.sort((SemanticsNode a, SemanticsNode b) => a.depth - b.depth);
List<mojom.SemanticsNode> updatedNodes = <mojom.SemanticsNode>[]; List<mojom.SemanticsNode> updatedNodes = <mojom.SemanticsNode>[];
for (SemanticsNode node in _dirtyNodes) { for (SemanticsNode node in visitedNodes) {
assert(node.parent?._dirty != true); // could be null (no parent) or false (not dirty) assert(node.parent?._dirty != true); // could be null (no parent) or false (not dirty)
// The _serialize() method marks the node as not dirty, and // The _serialize() method marks the node as not dirty, and
// recurses through the tree to do a deep serialization of all // recurses through the tree to do a deep serialization of all
......
...@@ -50,4 +50,57 @@ void main() { ...@@ -50,4 +50,57 @@ void main() {
client.updates.clear(); client.updates.clear();
client.dispose(); client.dispose();
}); });
testWidgets('Detach and reattach assert', (WidgetTester tester) async {
TestSemanticsClient client = new TestSemanticsClient(tester.binding.pipelineOwner);
GlobalKey key = new GlobalKey();
await tester.pumpWidget(
new Container(
child: new Semantics(
label: 'test1',
child: new Semantics(
key: key,
container: true,
label: 'test2a',
child: new Container()
)
)
)
);
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();
await tester.pumpWidget(
new Container(
child: new Semantics(
label: 'test1',
child: new Semantics(
container: true,
label: 'middle',
child: new Semantics(
key: key,
container: true,
label: 'test2b',
child: new Container()
)
)
)
)
);
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));
client.dispose();
});
} }
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