diff --git a/pubspec.lock b/pubspec.lock index 9bebf58582..65b78c2b16 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1113,7 +1113,7 @@ packages: source: hosted version: "1.25.8" test_api: - dependency: transitive + dependency: "direct dev" description: name: test_api sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" diff --git a/pubspec.yaml b/pubspec.yaml index e7aaee6e85..8f3a78bcfc 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -105,6 +105,7 @@ dev_dependencies: plugin_platform_interface: ^2.1.8 stack_trace: ^1.11.1 test: ^1.23.1 + test_api: ^0.7.3 video_player_platform_interface: ^6.2.2 # Keep list sorted when adding dependencies; it helps prevent merge conflicts. diff --git a/test/api/core_test.dart b/test/api/core_test.dart index 7f433bcb45..b3a40fdb0e 100644 --- a/test/api/core_test.dart +++ b/test/api/core_test.dart @@ -11,6 +11,7 @@ import 'package:zulip/model/localizations.dart'; import '../model/binding.dart'; import '../stdlib_checks.dart'; +import '../test_async.dart'; import 'exception_checks.dart'; import 'fake_api.dart'; import '../example_data.dart' as eg; @@ -19,8 +20,8 @@ void main() { TestZulipBinding.ensureInitialized(); test('ApiConnection.get', () async { - Future checkRequest(Map? params, String expectedRelativeUrl) { - return FakeApiConnection.with_(account: eg.selfAccount, (connection) async { + void checkRequest(Map? params, String expectedRelativeUrl) { + finish(FakeApiConnection.with_(account: eg.selfAccount, (connection) async { connection.prepare(json: {}); await connection.get(kExampleRouteName, (json) => json, 'example/route', params); check(connection.lastRequest!).isA() @@ -31,7 +32,7 @@ void main() { ...kFallbackUserAgentHeader, }) ..body.equals(''); - }); + })); } checkRequest(null, '/api/v1/example/route'); @@ -50,8 +51,8 @@ void main() { }); test('ApiConnection.post', () async { - Future checkRequest(Map? params, String expectedBody, {bool expectContentType = true}) { - return FakeApiConnection.with_(account: eg.selfAccount, (connection) async { + void checkRequest(Map? params, String expectedBody, {bool expectContentType = true}) { + finish(FakeApiConnection.with_(account: eg.selfAccount, (connection) async { connection.prepare(json: {}); await connection.post(kExampleRouteName, (json) => json, 'example/route', params); check(connection.lastRequest!).isA() @@ -64,7 +65,7 @@ void main() { 'content-type': 'application/x-www-form-urlencoded; charset=utf-8', }) ..body.equals(expectedBody); - }); + })); } checkRequest(null, '', expectContentType: false); @@ -81,9 +82,9 @@ void main() { }); test('ApiConnection.postFileFromStream', () async { - Future checkRequest(List> content, int length, + void checkRequest(List> content, int length, {String? filename, String? contentType, bool isContentTypeInvalid = false}) { - return FakeApiConnection.with_(account: eg.selfAccount, (connection) async { + finish(FakeApiConnection.with_(account: eg.selfAccount, (connection) async { connection.prepare(json: {}); await connection.postFileFromStream( kExampleRouteName, (json) => json, 'example/route', @@ -108,7 +109,7 @@ void main() { ..has>>((f) => f.finalize().toBytes(), 'contents') .completes((it) => it.deepEquals(content.expand((l) => l))) ); - }); + })); } checkRequest([], 0, filename: null); @@ -126,8 +127,8 @@ void main() { }); test('ApiConnection.delete', () async { - Future checkRequest(Map? params, String expectedBody, {bool expectContentType = true}) { - return FakeApiConnection.with_(account: eg.selfAccount, (connection) async { + void checkRequest(Map? params, String expectedBody, {bool expectContentType = true}) { + finish(FakeApiConnection.with_(account: eg.selfAccount, (connection) async { connection.prepare(json: {}); await connection.delete(kExampleRouteName, (json) => json, 'example/route', params); check(connection.lastRequest!).isA() @@ -140,7 +141,7 @@ void main() { 'content-type': 'application/x-www-form-urlencoded; charset=utf-8', }) ..body.equals(expectedBody); - }); + })); } checkRequest(null, '', expectContentType: false); @@ -166,13 +167,13 @@ void main() { }); test('API network errors', () async { - Future checkRequest( + void checkRequest( T exception, Condition condition) { - return check(tryRequest(exception: exception)) + finish(check(tryRequest(exception: exception)) .throws((it) => it ..routeName.equals(kExampleRouteName) ..cause.equals(exception) - ..which(condition)); + ..which(condition))); } final zulipLocalizations = GlobalLocalizations.zulipLocalizations; @@ -219,14 +220,14 @@ void main() { }); test('API 4xx errors, malformed', () async { - Future checkMalformed({ - int httpStatus = 400, Map? json, String? body}) async { + void checkMalformed({ + int httpStatus = 400, Map? json, String? body}) { assert((json == null) != (body == null)); - await check(tryRequest(httpStatus: httpStatus, json: json, body: body)) + finish(check(tryRequest(httpStatus: httpStatus, json: json, body: body)) .throws((it) => it ..routeName.equals(kExampleRouteName) ..httpStatus.equals(httpStatus) - ..data.deepEquals(json)); + ..data.deepEquals(json))); } await check( diff --git a/test/test_async.dart b/test/test_async.dart new file mode 100644 index 0000000000..df1255c9e2 --- /dev/null +++ b/test/test_async.dart @@ -0,0 +1,18 @@ +import 'package:test_api/hooks.dart'; + +/// Ensure the test runner will wait for the given future to complete +/// before considering the current test complete. +/// +/// Consider using this function, instead of `await`, when a test invokes +/// a check which is asynchronous and has no interaction with other tasks +/// the test will do later. +/// +/// Use `await`, instead of this function, when it matters what order the +/// rest of the test's logic runs in relative to the asynchronous work +/// represented by the given future. In particular, when calling a function +/// that performs setup for later logic in the test, the returned future +/// should always be awaited. +void finish(Future future) { + final outstandingWork = TestHandle.current.markPending(); + future.whenComplete(outstandingWork.complete); +}