Skip to content

Commit

Permalink
test: Write a generic jsonEquals check and deepToJson function
Browse files Browse the repository at this point in the history
  • Loading branch information
gnprice committed Aug 9, 2023
1 parent 15099f4 commit 365950a
Show file tree
Hide file tree
Showing 5 changed files with 69 additions and 21 deletions.
1 change: 1 addition & 0 deletions test/api/model/events_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'package:test/scaffolding.dart';
import 'package:zulip/api/model/events.dart';

import '../../example_data.dart' as eg;
import '../../stdlib_checks.dart';
import 'events_checks.dart';
import 'model_checks.dart';

Expand Down
20 changes: 0 additions & 20 deletions test/api/model/model_checks.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,6 @@ import 'package:checks/checks.dart';
import 'package:zulip/api/model/model.dart';

extension MessageChecks on Subject<Message> {
Subject<Map<String, dynamic>> get toJson => has((e) => e.toJson(), 'toJson');

void jsonEquals(Message expected) {
final expectedJson = expected.toJson();
expectedJson['reactions'] = it()..isA<List<Reaction>>().jsonEquals(expected.reactions);
toJson.deepEquals(expectedJson);
}

Subject<String> get content => has((e) => e.content, 'content');
Subject<bool> get isMeMessage => has((e) => e.isMeMessage, 'isMeMessage');
Subject<int?> get lastEditTimestamp => has((e) => e.lastEditTimestamp, 'lastEditTimestamp');
Expand All @@ -23,21 +15,9 @@ extension ReactionsChecks on Subject<List<Reaction>> {
void deepEquals(_) {
throw UnimplementedError('Tried to call [Subject<List<Reaction>>.deepEquals]. Use jsonEquals instead.');
}

void jsonEquals(List<Reaction> expected) {
// (cast, to bypass this extension's deepEquals implementation, which throws)
// ignore: unnecessary_cast
(this as Subject<List>).deepEquals(expected.map((r) => it()..isA<Reaction>().jsonEquals(r)));
}
}

extension ReactionChecks on Subject<Reaction> {
Subject<Map<String, dynamic>> get toJson => has((r) => r.toJson(), 'toJson');

void jsonEquals(Reaction expected) {
toJson.deepEquals(expected.toJson());
}

Subject<String> get emojiName => has((r) => r.emojiName, 'emojiName');
Subject<String> get emojiCode => has((r) => r.emojiCode, 'emojiCode');
Subject<ReactionType> get reactionType => has((r) => r.reactionType, 'reactionType');
Expand Down
1 change: 0 additions & 1 deletion test/api/route/messages_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import 'package:zulip/model/narrow.dart';
import '../../example_data.dart' as eg;
import '../../stdlib_checks.dart';
import '../fake_api.dart';
import '../model/model_checks.dart';
import 'route_checks.dart';

void main() {
Expand Down
1 change: 1 addition & 0 deletions test/model/message_list_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import '../api/model/model_checks.dart';
import '../example_data.dart' as eg;
import '../model/binding.dart';
import '../model/test_store.dart';
import '../stdlib_checks.dart';

const int userId = 1;

Expand Down
67 changes: 67 additions & 0 deletions test/stdlib_checks.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,73 @@ extension NullableMapChecks<K, V> on Subject<Map<K, V>?> {
}
}

/// Convert [object] to a pure JSON-like value.
///
/// The result is similar to `jsonDecode(jsonEncode(object))`, but without
/// passing through a serialized form.
///
/// All JSON atoms (numbers, booleans, null, and strings) are used directly.
/// All JSON containers (lists, and maps with string keys) are copied
/// as their elements are converted recursively.
/// For any other value, a dynamic call `.toJson()` is made and
/// should return either a JSON atom or a JSON container.
Object? deepToJson(Object? object) {
// Implementation is based on the recursion underlying [jsonEncode],
// at [_JsonStringifier.writeObject] in the stdlib's convert/json.dart .
// (We leave out the cycle-checking, for simplicity / out of laziness.)

var (result, success) = _deeplyConvertShallowJsonValue(object);
if (success) return result;

final Object? shallowlyConverted;
try {
shallowlyConverted = (object as dynamic).toJson();
} catch (e) {
throw JsonUnsupportedObjectError(object, cause: e);
}

(result, success) = _deeplyConvertShallowJsonValue(shallowlyConverted);
if (success) return result;
throw JsonUnsupportedObjectError(object);
}

(Object? result, bool success) _deeplyConvertShallowJsonValue(Object? object) {
final Object? result;
switch (object) {
case null || bool() || String() || num():
result = object;
case List():
result = object.map((x) => deepToJson(x)).toList();
case Map() when object.keys.every((k) => k is String):
result = object.map((k, v) => MapEntry(k, deepToJson(v)));
default:
return (null, false);
}
return (result, true);
}

extension JsonChecks on Subject<Object?> {
/// Expects that the value is deeply equal to [expected],
/// after calling [deepToJson] on both.
///
/// Deep equality is computed by [MapChecks.deepEquals]
/// or [IterableChecks.deepEquals].
void jsonEquals(Object? expected) {
final expectedJson = deepToJson(expected);
final actualJson = has((e) => deepToJson(e), 'deepToJson');
switch (expectedJson) {
case null || bool() || String() || num():
return actualJson.equals(expectedJson);
case List():
return actualJson.isA<List>().deepEquals(expectedJson);
case Map():
return actualJson.isA<Map>().deepEquals(expectedJson);
case _:
assert(false);
}
}
}

extension UriChecks on Subject<Uri> {
Subject<String> get asString => has((u) => u.toString(), 'toString'); // TODO(checks): what's a good convention for this?

Expand Down

0 comments on commit 365950a

Please sign in to comment.