Skip to content

Commit 3cba058

Browse files
committed
test: Write a generic jsonEquals check and deepToJson function
1 parent c5500b1 commit 3cba058

File tree

4 files changed

+68
-21
lines changed

4 files changed

+68
-21
lines changed

test/api/model/events_test.dart

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import 'package:test/scaffolding.dart';
33
import 'package:zulip/api/model/events.dart';
44

55
import '../../example_data.dart' as eg;
6+
import '../../stdlib_checks.dart';
67
import 'events_checks.dart';
78
import 'model_checks.dart';
89

test/api/model/model_checks.dart

-20
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,6 @@ import 'package:checks/checks.dart';
22
import 'package:zulip/api/model/model.dart';
33

44
extension MessageChecks on Subject<Message> {
5-
Subject<Map<String, dynamic>> get toJson => has((e) => e.toJson(), 'toJson');
6-
7-
void jsonEquals(Message expected) {
8-
final expectedJson = expected.toJson();
9-
expectedJson['reactions'] = it()..isA<List<Reaction>>().jsonEquals(expected.reactions);
10-
toJson.deepEquals(expectedJson);
11-
}
12-
135
Subject<String> get content => has((e) => e.content, 'content');
146
Subject<bool> get isMeMessage => has((e) => e.isMeMessage, 'isMeMessage');
157
Subject<int?> get lastEditTimestamp => has((e) => e.lastEditTimestamp, 'lastEditTimestamp');
@@ -23,21 +15,9 @@ extension ReactionsChecks on Subject<List<Reaction>> {
2315
void deepEquals(_) {
2416
throw UnimplementedError('Tried to call [Subject<List<Reaction>>.deepEquals]. Use jsonEquals instead.');
2517
}
26-
27-
void jsonEquals(List<Reaction> expected) {
28-
// (cast, to bypass this extension's deepEquals implementation, which throws)
29-
// ignore: unnecessary_cast
30-
(this as Subject<List>).deepEquals(expected.map((r) => it()..isA<Reaction>().jsonEquals(r)));
31-
}
3218
}
3319

3420
extension ReactionChecks on Subject<Reaction> {
35-
Subject<Map<String, dynamic>> get toJson => has((r) => r.toJson(), 'toJson');
36-
37-
void jsonEquals(Reaction expected) {
38-
toJson.deepEquals(expected.toJson());
39-
}
40-
4121
Subject<String> get emojiName => has((r) => r.emojiName, 'emojiName');
4222
Subject<String> get emojiCode => has((r) => r.emojiCode, 'emojiCode');
4323
Subject<ReactionType> get reactionType => has((r) => r.reactionType, 'reactionType');

test/api/route/messages_test.dart

-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import 'package:zulip/model/narrow.dart';
1111
import '../../example_data.dart' as eg;
1212
import '../../stdlib_checks.dart';
1313
import '../fake_api.dart';
14-
import '../model/model_checks.dart';
1514
import 'route_checks.dart';
1615

1716
void main() {

test/stdlib_checks.dart

+67
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,73 @@ extension NullableMapChecks<K, V> on Subject<Map<K, V>?> {
1919
}
2020
}
2121

22+
/// Convert [object] to a pure JSON-like value.
23+
///
24+
/// The result is similar to `jsonDecode(jsonEncode(object))`, but without
25+
/// passing through a serialized form.
26+
///
27+
/// All JSON atoms (numbers, booleans, null, and strings) are used directly.
28+
/// All JSON containers (lists, and maps with string keys) are copied
29+
/// as their elements are converted recursively.
30+
/// For any other value, a dynamic call `.toJson()` is made and
31+
/// should return either a JSON atom or a JSON container.
32+
Object? deepToJson(Object? object) {
33+
// Implementation is based on the recursion underlying [jsonEncode],
34+
// at [_JsonStringifier.writeObject] in the stdlib's convert/json.dart .
35+
// (We leave out the cycle-checking, for simplicity / out of laziness.)
36+
37+
var (result, success) = _deeplyConvertShallowJsonValue(object);
38+
if (success) return result;
39+
40+
final Object? shallowlyConverted;
41+
try {
42+
shallowlyConverted = (object as dynamic).toJson();
43+
} catch (e) {
44+
throw JsonUnsupportedObjectError(object, cause: e);
45+
}
46+
47+
(result, success) = _deeplyConvertShallowJsonValue(shallowlyConverted);
48+
if (success) return result;
49+
throw JsonUnsupportedObjectError(object);
50+
}
51+
52+
(Object? result, bool success) _deeplyConvertShallowJsonValue(Object? object) {
53+
final Object? result;
54+
switch (object) {
55+
case null || bool() || String() || num():
56+
result = object;
57+
case List():
58+
result = object.map((x) => deepToJson(x)).toList();
59+
case Map() when object.keys.every((k) => k is String):
60+
result = object.map((k, v) => MapEntry(k, deepToJson(v)));
61+
default:
62+
return (null, false);
63+
}
64+
return (result, true);
65+
}
66+
67+
extension JsonChecks on Subject<Object?> {
68+
/// Expects that the value is deeply equal to [expected],
69+
/// after calling [deepToJson] on both.
70+
///
71+
/// Deep equality is computed by [MapChecks.deepEquals]
72+
/// or [IterableChecks.deepEquals].
73+
void jsonEquals(Object? expected) {
74+
final expectedJson = deepToJson(expected);
75+
final actualJson = has((e) => deepToJson(e), 'deepToJson');
76+
switch (expectedJson) {
77+
case null || bool() || String() || num():
78+
return actualJson.equals(expectedJson);
79+
case List():
80+
return actualJson.isA<List>().deepEquals(expectedJson);
81+
case Map():
82+
return actualJson.isA<Map>().deepEquals(expectedJson);
83+
case _:
84+
assert(false);
85+
}
86+
}
87+
}
88+
2289
extension UriChecks on Subject<Uri> {
2390
Subject<String> get asString => has((u) => u.toString(), 'toString'); // TODO(checks): what's a good convention for this?
2491

0 commit comments

Comments
 (0)