Skip to content

Commit 9c8a283

Browse files
gnpricechrisbobbe
authored andcommitted
content test: Compare parse trees via Diagnosticable
This makes for significantly cleaner and more informative output when a test fails (resolving all the TODO-comments), by taking more control of how the comparison is done. At the same time, it makes for less boilerplate and fewer places to update when adding more features in the ContentNode parse trees. Effectively it deduplicates some of the boilerplate, by reusing for comparing the trees the metadata that we're already prepared to generate for printing them out when a comparison fails.
1 parent 8aa1e52 commit 9c8a283

File tree

2 files changed

+57
-102
lines changed

2 files changed

+57
-102
lines changed

lib/model/content.dart

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,16 @@ import 'package:html/parser.dart';
1717
/// * Override [debugDescribeChildren] and/or [debugFillProperties]
1818
/// to report all the data attached to the node, for debugging.
1919
/// See docs: https://api.flutter.dev/flutter/foundation/Diagnosticable/debugFillProperties.html
20+
/// We also rely on these for comparing actual to expected in tests.
2021
///
21-
/// When modifying subclasses:
22-
/// * Always check the following places to see if they need a matching update:
23-
/// * [==] and [hashCode], if overridden.
24-
/// * [debugFillProperties] and/or [debugDescribeChildren], if present.
25-
/// * `equalsNode` in test/model/content_checks.dart .
22+
/// When modifying subclasses, always check the following places
23+
/// to see if they need a matching update:
24+
/// * [==] and [hashCode], if overridden.
25+
/// * [debugFillProperties] and/or [debugDescribeChildren].
26+
///
27+
/// In particular, a newly-added field typically must be added in
28+
/// [debugFillProperties]. Otherwise tests will not examine the new field,
29+
/// and will not spot discrepancies there.
2630
@immutable
2731
sealed class ContentNode extends DiagnosticableTree {
2832
const ContentNode({this.debugHtmlNode});
@@ -42,9 +46,6 @@ sealed class ContentNode extends DiagnosticableTree {
4246

4347
@override
4448
String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) {
45-
// TODO(checks): Better integrate package:checks with Diagnosticable, for
46-
// better interaction in output indentation.
47-
// (See also comment at equalsNode, with related improvements.)
4849
String? result;
4950
assert(() {
5051
result = toStringDeep(minLevel: minLevel);

test/model/content_checks.dart

Lines changed: 48 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1,108 +1,62 @@
1-
import 'package:checks/checks.dart';
1+
import 'package:checks/context.dart';
2+
import 'package:flutter/foundation.dart';
23
import 'package:zulip/model/content.dart';
34

45
extension ContentNodeChecks on Subject<ContentNode> {
56
void equalsNode(ContentNode expected) {
6-
// TODO: Make equalsNode output clearer on failure, applying Diagnosticable.
7-
// In particular (a) show the top-level expected node in one piece
8-
// (as well as the actual); (a') ideally, suppress on the "expected" side
9-
// the various predicates below, which should be redundant with just
10-
// the expected node; (b) show expected for the specific `equals` leaf.
11-
// See also comment on [ContentNode.toString].
12-
if (expected is ZulipContent) {
13-
isA<ZulipContent>()
14-
.nodes.equalsNodes(expected.nodes);
15-
} else if (expected is UnimplementedBlockContentNode) {
16-
isA<UnimplementedBlockContentNode>()
17-
.debugHtmlText.equals(expected.debugHtmlText);
18-
} else if (expected is ParagraphNode) {
19-
isA<ParagraphNode>()
20-
..wasImplicit.equals(expected.wasImplicit)
21-
..nodes.equalsNodes(expected.nodes);
22-
} else if (expected is HeadingNode) {
23-
isA<HeadingNode>()
24-
..level.equals(expected.level)
25-
..nodes.equalsNodes(expected.nodes);
26-
} else if (expected is ListNode) {
27-
isA<ListNode>()
28-
..style.equals(expected.style)
29-
..items.deepEquals(expected.items.map(
30-
(item) => it()..isA<List<BlockContentNode>>().equalsNodes(item)));
31-
} else if (expected is QuotationNode) {
32-
isA<QuotationNode>()
33-
.nodes.equalsNodes(expected.nodes);
34-
} else if (expected is UnimplementedInlineContentNode) {
35-
isA<UnimplementedInlineContentNode>()
36-
.debugHtmlText.equals(expected.debugHtmlText);
37-
} else if (expected is StrongNode) {
38-
isA<StrongNode>()
39-
.nodes.equalsNodes(expected.nodes);
40-
} else if (expected is EmphasisNode) {
41-
isA<EmphasisNode>()
42-
.nodes.equalsNodes(expected.nodes);
43-
} else if (expected is InlineCodeNode) {
44-
isA<InlineCodeNode>()
45-
.nodes.equalsNodes(expected.nodes);
46-
} else if (expected is LinkNode) {
47-
isA<LinkNode>()
48-
.nodes.equalsNodes(expected.nodes);
49-
} else if (expected is UserMentionNode) {
50-
isA<UserMentionNode>()
51-
.nodes.equalsNodes(expected.nodes);
52-
} else {
53-
// The remaining node types have structural `==`. Use that.
54-
equals(expected);
55-
}
7+
return context.expect(() => prefixFirst('equals ', literal(expected)), (actual) {
8+
final which = _compareDiagnosticsNodes(
9+
actual.toDiagnosticsNode(), expected.toDiagnosticsNode());
10+
return which == null ? null : Rejection(which: [
11+
'differs in that it:',
12+
...indent(which),
13+
]);
14+
});
5615
}
57-
58-
Subject<String> get debugHtmlText => has((n) => n.debugHtmlText, 'debugHtmlText');
5916
}
6017

61-
extension ZulipContentChecks on Subject<ZulipContent> {
62-
Subject<List<BlockContentNode>> get nodes => has((n) => n.nodes, 'nodes');
63-
}
18+
Iterable<String>? _compareDiagnosticsNodes(DiagnosticsNode actual, DiagnosticsNode expected) {
19+
assert(actual is DiagnosticableTreeNode && expected is DiagnosticableTreeNode);
6420

65-
extension BlockContentNodeListChecks on Subject<List<BlockContentNode>> {
66-
void equalsNodes(List<BlockContentNode> expected) {
67-
deepEquals(expected.map(
68-
(e) => it()..isA<BlockContentNode>().equalsNode(e)));
69-
// A shame we need the dynamic `isA` there. This
70-
// version hits a runtime type error:
71-
// .nodes.deepEquals(expected.nodes.map(
72-
// (e) => it<BlockContentNode>()..equalsNode(e)));
73-
// and with `it()` with no type argument, it doesn't type-check.
74-
// TODO(checks): report that as API feedback on deepEquals
21+
if (actual.value.runtimeType != expected.value.runtimeType) {
22+
return [
23+
'has type ${actual.value.runtimeType}',
24+
'expected: ${expected.value.runtimeType}',
25+
];
7526
}
76-
}
77-
78-
extension BlockInlineContainerNodeChecks on Subject<BlockInlineContainerNode> {
79-
Subject<List<InlineContentNode>> get nodes => has((n) => n.nodes, 'nodes');
80-
}
81-
82-
extension ParagraphNodeChecks on Subject<ParagraphNode> {
83-
Subject<bool> get wasImplicit => has((n) => n.wasImplicit, 'wasImplicit');
84-
}
85-
86-
extension HeadingNodeChecks on Subject<HeadingNode> {
87-
Subject<HeadingLevel> get level => has((n) => n.level, 'level');
88-
}
89-
90-
extension ListNodeChecks on Subject<ListNode> {
91-
Subject<ListStyle> get style => has((n) => n.style, 'style');
92-
Subject<List<List<BlockContentNode>>> get items => has((n) => n.items, 'items');
93-
}
9427

95-
extension QuotationNodeChecks on Subject<QuotationNode> {
96-
Subject<List<BlockContentNode>> get nodes => has((n) => n.nodes, 'nodes');
97-
}
28+
final actualProperties = actual.getProperties();
29+
final expectedProperties = expected.getProperties();
30+
assert(actualProperties.length == expectedProperties.length);
31+
for (int i = 0; i < actualProperties.length; i++) {
32+
assert(actualProperties[i].name == expectedProperties[i].name);
33+
if (actualProperties[i].value != expectedProperties[i].value) {
34+
return [
35+
'has ${actualProperties[i].name} that:',
36+
...indent(prefixFirst('is ', literal(actualProperties[i].value))),
37+
...indent(prefixFirst('expected: ', literal(expectedProperties[i].value)))
38+
];
39+
}
40+
}
9841

99-
extension InlineContentNodeListChecks on Subject<List<InlineContentNode>> {
100-
void equalsNodes(List<InlineContentNode> expected) {
101-
deepEquals(expected.map(
102-
(e) => it()..isA<InlineContentNode>().equalsNode(e)));
42+
final actualChildren = actual.getChildren();
43+
final expectedChildren = expected.getChildren();
44+
if (actualChildren.length != expectedChildren.length) {
45+
return [
46+
'has ${actualChildren.length} children',
47+
'expected: ${expectedChildren.length} children',
48+
];
49+
}
50+
for (int i = 0; i < actualChildren.length; i++) {
51+
final failure = _compareDiagnosticsNodes(actualChildren[i], expectedChildren[i]);
52+
if (failure != null) {
53+
final diagnosticable = actualChildren[i].value as Diagnosticable;
54+
return [
55+
'has child $i (${diagnosticable.toStringShort()}) that:',
56+
...indent(failure),
57+
];
58+
}
10359
}
104-
}
10560

106-
extension InlineContainerNodeChecks on Subject<InlineContainerNode> {
107-
Subject<List<InlineContentNode>> get nodes => has((n) => n.nodes, 'nodes');
61+
return null;
10862
}

0 commit comments

Comments
 (0)