refactor unit test and get test coverage
ADD
.cirrus.yml
+container:
+ image: cirrusci/flutter:latest
+
+test_task:
+ # pub_cache:
+ # folder: $HOME/.pub-cache
+ # fingerprint_script: cat pubspec.lock
+ # populate_script: pub get
+ pub_script: flutter packages get # later when we seperate graphql_client and graphql_flutter, we can change this
+ # format_script: dartfmt **/*.dart -n --set-exit-if-changed
+ # analyze_script: dartanalyzer --fatal-infos .
+ # publishable_script: pub publish --dry-run
+ environment:
+ CODECOV_TOKEN: ENCRYPTED[fa57a1dda8bca6ad45ef10043f632b68b7824b60c634415f741a847788c1a44ae9374155e3af3346fb9b6d72bac0e1d3]
+ test_script:
+ # - pub global activate coverage
+ # - dart --enable-vm-service=8111 --pause-isolates-on-exit test/* &
+ # - pub global run coverage:collect_coverage --port=8111 --out=coverage.json --wait-paused --resume-isolates
+ # - pub global run coverage:format_coverage --lcov --in=coverage.json --out=lcov.info --packages=.packages --report-on=lib
+ - flutter test --coverage
+ - bash <(curl -s https://codecov.io/bash)

MODIFY
.gitignore
.gitignore
@@ -20,3 +20,7 @@ ios/Runner/GeneratedPluginRegistrant.*
example/lib/local.dart
+
+# Coverage
+coverage.json
+coverage/*

ADD
example/lib/bloc.dart
+import 'package:rxdart/subjects.dart';
+import 'package:graphql_flutter/graphql_flutter.dart';
+
+import './mutations/mutations.dart' as mutations;
+import './queries/readRepositories.dart' as queries;
+
+
+const String YOUR_PERSONAL_ACCESS_TOKEN =
+ '';
+
+class Repo {
+ const Repo({this.id, this.name, this.viewerHasStarred});
+ final String id;
+ final String name;
+ final bool viewerHasStarred;
+}
+
+class Bloc {
+ Bloc() {
+ _queryRepo();
+ _updateNumberOfRepo.listen((int n) async => _queryRepo(nRepositories: n));
+ _toggleStarSubject.listen((Repo t) async {
+ _toggleStarLoadingSubject.add(true);
+ // @todo handle error
+ final r = await _mutateToggleStar(t);
+
+ _repoSubject.add(_repoSubject.value.map((Repo e) {
+ if (e.id != t.id) {
+ return e;
+ }
+ return Repo(
+ id: t.id, name: t.name, viewerHasStarred: !t.viewerHasStarred);
+ }).toList());
+ _toggleStarLoadingSubject.add(false);
+ });
+ }
+
+ final BehaviorSubject<List<Repo>> _repoSubject =
+ BehaviorSubject<List<Repo>>();
+ Stream<List<Repo>> get repoStream => _repoSubject.stream;
+
+ final ReplaySubject<Repo> _toggleStarSubject = ReplaySubject<Repo>();
+ Sink<Repo> get toggleStarSink => _toggleStarSubject;
+
+ final BehaviorSubject<bool> _toggleStarLoadingSubject =
+ BehaviorSubject<bool>();
+ Stream<bool> get toggleStarLoadingStream => _toggleStarLoadingSubject.stream;
+
+ final BehaviorSubject<int> _updateNumberOfRepo = BehaviorSubject<int>();
+
+ Sink<int> get updateNumberOfRepoSink => _updateNumberOfRepo;
+
+ static final HttpLink _httpLink = HttpLink(
+ uri: 'https://api.github.com/graphql',
+ );
+
+ static final AuthLink _authLink = AuthLink(
+ getToken: () async => 'Bearer $YOUR_PERSONAL_ACCESS_TOKEN',
+ );
+
+ static final Link _link = _authLink.concat(_httpLink);
+
+ static final GraphQLClient _client = GraphQLClient(
+ cache: NormalizedInMemoryCache(
+ dataIdFromObject: typenameDataIdFromObject,
+ ),
+ link: _link,
+ );
+
+ Future<void> _mutateToggleStar(Repo repo) async {
+ final MutationOptions _options = MutationOptions(
+ document:
+ repo.viewerHasStarred ? mutations.removeStar : mutations.addStar,
+ variables: <String, String>{
+ 'starrableId': repo.id,
+ },
+// fetchPolicy: widget.options.fetchPolicy,
+// errorPolicy: widget.options.errorPolicy,
+ );
+
+ final QueryResult result = await _client.mutate(_options);
+ return result;
+ }
+
+ Future<void> _queryRepo({int nRepositories = 50}) async {
+ // null is loading
+ _repoSubject.add(null);
+// FetchPolicy fetchPolicy = widget.options.fetchPolicy;
+//
+// if (fetchPolicy == FetchPolicy.cacheFirst) {
+// fetchPolicy = FetchPolicy.cacheAndNetwork;
+// }
+ final WatchQueryOptions _options = WatchQueryOptions(
+ document: queries.readRepositories,
+ variables: <String, dynamic>{
+ 'nRepositories': nRepositories,
+ },
+// fetchPolicy: fetchPolicy,
+// errorPolicy: widget.options.errorPolicy,
+ pollInterval: 4,
+ fetchResults: true,
+// context: widget.options.context,
+ );
+
+ final QueryResult result = await _client.query(_options);
+
+ if (result.hasErrors) {
+ _repoSubject.addError(result.errors);
+ return;
+ }
+
+ // result.data can be either a [List<dynamic>] or a [Map<String, dynamic>]
+ final List<dynamic> repositories =
+ result.data['viewer']['repositories']['nodes'] as List<dynamic>;
+
+ _repoSubject.add(repositories
+ .map((dynamic e) => Repo(
+ id: e['id'] as String,
+ name: e['name'] as String,
+ viewerHasStarred: e['viewerHasStarred'] as bool,
+ ))
+ .toList());
+ }
+}
+
+/**
+ *
+ *
+ key: Key(starred.toString()),
+ options: MutationOptions(
+ document: starred ? mutations.removeStar : mutations.addStar,
+
+ builder: (RunMutation toggleStar, QueryResult result) {
+
+
+ toggleStar(<String, dynamic>{
+ 'starrableId': widget.repository['id'],
+ });
+ ),
+
+
+
+ update: (Cache cache, QueryResult result) {
+ if (result.hasErrors) {
+ print(result.errors);
+ } else {
+ final Map<String, Object> updated =
+ Map<String, Object>.from(widget.repository)
+ ..addAll(extractRepositoryData(result.data as Map<String, Object>));
+
+ cache.write(typenameDataIdFromObject(updated), updated);
+ }
+ },
+ onCompleted: (QueryResult result) {
+ showDialog<AlertDialog>(
+ context: context,
+ builder: (BuildContext context) {
+ return AlertDialog(
+ title: Text(
+ extractRepositoryData(result.data as Map<String, Object>)['viewerHasStarred'] as bool
+ ? 'Thanks for your star!'
+ : 'Sorry you changed your mind!',
+ ),
+ actions: <Widget>[
+ SimpleDialogOption(
+ child: const Text('Dismiss'),
+ onPressed: () {
+ Navigator.of(context).pop();
+ },
+ )
+ ],
+ );
+ },
+ );
+ setState(() {
+ loading = false;
+ });
+ },
+ */

MODIFY
example/lib/main.dart
example/lib/main.dart
import 'package:flutter/material.dart';
-import 'package:graphql_flutter/graphql_flutter.dart';
-import './mutations/mutations.dart' as mutations;
-import './queries/readRepositories.dart' as queries;
-
-const String YOUR_PERSONAL_ACCESS_TOKEN = '<YOUR_PERSONAL_ACCESS_TOKEN>';
+import 'bloc.dart' show Bloc, Repo;
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
- final HttpLink httpLink = HttpLink(
- uri: 'https://api.github.com/graphql',
- );
-
- final AuthLink authLink = AuthLink(
- getToken: () async => 'Bearer $YOUR_PERSONAL_ACCESS_TOKEN',
- );
-
- final Link link = authLink.concat(httpLink);
-
- final ValueNotifier<GraphQLClient> client = ValueNotifier<GraphQLClient>(
- GraphQLClient(
- cache: NormalizedInMemoryCache(
- dataIdFromObject: typenameDataIdFromObject,
- ),
- link: link,
- ),
- );
-
- return GraphQLProvider(
- client: client,
- child: CacheProvider(
- child: MaterialApp(
+ return MaterialApp(
title: 'GraphQL Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
- home: const MyHomePage(title: 'GraphQL Flutter Home Page'),
- ),
- ),
+ home: MyHomePage(title: 'GraphQL Flutter Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
- const MyHomePage({
+ MyHomePage({
Key key,
this.title,
- }) : super(key: key);
+ }) : bloc = Bloc(),
+ super(key: key);
final String title;
+ final Bloc bloc;
@override
- _MyHomePageState createState() => _MyHomePageState();
+ _MyHomePageState createState() => _MyHomePageState(bloc);
}
class _MyHomePageState extends State<MyHomePage> {
- int nRepositories = 50;
-
- void changeQuery(String number) {
- setState(() {
- nRepositories = int.parse(number) ?? 50;
- });
- }
+ _MyHomePageState(this.bloc);
+ final Bloc bloc;
@override
Widget build(BuildContext context) {
@@ -83,36 +52,31 @@ class _MyHomePageState extends State<MyHomePage> {
labelText: 'Number of repositories (default 50)',
),
keyboardType: TextInputType.number,
- onSubmitted: changeQuery,
+ onChanged: (String n) =>
+ bloc.updateNumberOfRepoSink.add(int.parse(n)),
),
- Query(
- options: QueryOptions(
- document: queries.readRepositories,
- variables: <String, dynamic>{
- 'nRepositories': nRepositories,
- },
- pollInterval: 4,
- ),
- builder: (QueryResult result) {
- if (result.loading) {
+ StreamBuilder<List<Repo>>(
+ stream: bloc.repoStream,
+ builder: (BuildContext context,
+ AsyncSnapshot<List<Repo>> snapshot) {
+ if (snapshot.hasError) {
+ return Text('\nErrors: \n ' +
+ (snapshot.error as List<dynamic>).join(',\n '));
+ }
+ if (snapshot.data == null) {
return const Center(
child: CircularProgressIndicator(),
);
}
- if (result.hasErrors) {
- return Text('\nErrors: \n ' + result.errors.join(',\n '));
- }
-
- // result.data can be either a [List<dynamic>] or a [Map<String, dynamic>]
- final List<dynamic> repositories =
- result.data['viewer']['repositories']['nodes'] as List<dynamic>;
+ final List<Repo> repositories = snapshot.data;
return Expanded(
child: ListView.builder(
itemCount: repositories.length,
itemBuilder: (BuildContext context, int index) =>
- StarrableRepository(repository: repositories[index] as Map<String, Object>),
+ StarrableRepository(
+ repository: repositories[index], bloc: bloc),
),
);
},
@@ -124,22 +88,15 @@ class _MyHomePageState extends State<MyHomePage> {
}
}
-class StarrableRepository extends StatefulWidget {
+class StarrableRepository extends StatelessWidget {
const StarrableRepository({
Key key,
@required this.repository,
+ @required this.bloc,
}) : super(key: key);
- final Map<String, Object> repository;
-
- @override
- StarrableRepositoryState createState() {
- return StarrableRepositoryState();
- }
-}
-
-class StarrableRepositoryState extends State<StarrableRepository> {
- bool loading = false;
+ final Bloc bloc;
+ final Repo repository;
Map<String, Object> extractRepositoryData(Map<String, Object> data) {
final Map<String, Object> action = data['action'] as Map<String, Object>;
@@ -151,74 +108,28 @@ class StarrableRepositoryState extends State<StarrableRepository> {
return action['starrable'] as Map<String, Object>;
}
- bool get viewerHasStarred => widget.repository['viewerHasStarred'] as bool;
+ bool get viewerHasStarred => repository.viewerHasStarred;
@override
Widget build(BuildContext context) {
- final bool starred = loading ? !viewerHasStarred : viewerHasStarred;
-
- return Mutation(
- key: Key(starred.toString()),
- options: MutationOptions(
- document: starred ? mutations.removeStar : mutations.addStar,
- ),
- builder: (RunMutation toggleStar, QueryResult result) {
+ return StreamBuilder<bool>(
+ stream: bloc.toggleStarLoadingStream,
+ initialData: false,
+ builder: (BuildContext context, AsyncSnapshot<bool> result) {
+ final bool loading = result.data;
return ListTile(
- leading: starred
+ leading: viewerHasStarred
? const Icon(
Icons.star,
color: Colors.amber,
)
: const Icon(Icons.star_border),
trailing: loading ? const CircularProgressIndicator() : null,
- title: Text(widget.repository['name'] as String),
+ title: Text(repository.name),
onTap: () {
- // optimistic ui updates are not implemented yet,
- // so we track loading manually
- setState(() {
- loading = true;
- });
- toggleStar(<String, dynamic>{
- 'starrableId': widget.repository['id'],
- });
- },
- );
- },
- update: (Cache cache, QueryResult result) {
- if (result.hasErrors) {
- print(result.errors);
- } else {
- final Map<String, Object> updated =
- Map<String, Object>.from(widget.repository)
- ..addAll(extractRepositoryData(result.data as Map<String, Object>));
-
- cache.write(typenameDataIdFromObject(updated), updated);
- }
- },
- onCompleted: (QueryResult result) {
- showDialog<AlertDialog>(
- context: context,
- builder: (BuildContext context) {
- return AlertDialog(
- title: Text(
- extractRepositoryData(result.data as Map<String, Object>)['viewerHasStarred'] as bool
- ? 'Thanks for your star!'
- : 'Sorry you changed your mind!',
- ),
- actions: <Widget>[
- SimpleDialogOption(
- child: const Text('Dismiss'),
- onPressed: () {
- Navigator.of(context).pop();
- },
- )
- ],
- );
+ bloc.toggleStarSink.add(repository);
},
);
- setState(() {
- loading = false;
- });
},
);
}

MODIFY
example/pubspec.yaml
example/pubspec.yaml
@@ -5,6 +5,7 @@ dependencies:
flutter:
sdk: flutter
cupertino_icons: ^0.1.2
+ rxdart: 0.21.0
graphql_flutter:
path: ..

MODIFY
lib/src/link/http/link_http.dart
lib/src/link/http/link_http.dart
import 'dart:async';
import 'dart:convert';
+import 'dart:io';
import 'package:meta/meta.dart';
import 'package:http/http.dart';
@@ -15,7 +16,9 @@ class HttpLink extends Link {
HttpLink({
@required String uri,
bool includeExtensions,
- Client fetch,
+
+ /// pass on customized httpClient, especially handy for mocking and testing
+ Client httpClient,
Map<String, String> headers,
Map<String, dynamic> credentials,
Map<String, dynamic> fetchOptions,
@@ -24,7 +27,7 @@ class HttpLink extends Link {
Operation operation, [
NextLink forward,
]) {
- final Client fetcher = fetch ?? Client();
+ final Client fetcher = httpClient ?? Client();
final HttpConfig linkConfig = HttpConfig(
http: HttpQueryOptions(
@@ -59,26 +62,26 @@ class HttpLink extends Link {
);
final Map<String, dynamic> options = httpOptionsAndBody.options;
- final Map<String, String> httpHeaders = options['headers'] as Map<String, String>;
+ final Map<String, String> httpHeaders =
+ options['headers'] as Map<String, String>;
StreamController<FetchResult> controller;
Future<void> onListen() async {
- Response response;
+ StreamedResponse response;
try {
- // TODO: support multiple http methods
- response = await fetcher.post(
- uri,
- headers: httpHeaders,
- body: httpOptionsAndBody.body,
- );
- operation.setContext(<String, Response>{
+ Request r = Request('post', Uri.parse(uri))
+ ..headers.addAll(httpHeaders)
+ ;
+ r.body = httpOptionsAndBody.body as String;
+ response = await fetcher.send(r);
+
+ operation.setContext(<String, StreamedResponse>{
'response': response,
});
-
- final FetchResult parsedResponse = _parseResponse(response);
+ final FetchResult parsedResponse = await _parseResponse(response);
controller.add(parsedResponse);
} catch (error) {
@@ -184,25 +187,31 @@ HttpOptionsAndBody _selectHttpOptionsAndBody(
return HttpOptionsAndBody(
options: options,
- body: json.encode(body),
+ body: encodeBody(body),
);
}
-FetchResult _parseResponse(Response response) {
+dynamic encodeBody(dynamic body) {
+ final encodedBody = json.encode(body, toEncodable: (dynamic object) {
+ if(object is File){
+ return null;
+ }
+ return object.toJson();
+ });
+ return encodedBody;
+}
+
+Future<FetchResult> _parseResponse(StreamedResponse response) async {
final int statusCode = response.statusCode;
final String reasonPhrase = response.reasonPhrase;
- /* TODO this discards a lot of useful info in development */
- if (statusCode < 200 || statusCode >= 400) {
- throw ClientException(
- 'Network Error: $statusCode $reasonPhrase',
- );
- }
-
+ try {
final Encoding encoding = _determineEncodingFromResponse(response);
- final String decodedBody = encoding.decode(response.bodyBytes);
+ // @todo limit bodyBytes
+ final String decodedBody = encoding.decode(await response.stream.toBytes());
- final Map<String, dynamic> jsonResponse = json.decode(decodedBody) as Map<String, dynamic>;
+ final Map<String, dynamic> jsonResponse =
+ json.decode(decodedBody) as Map<String, dynamic>;
final FetchResult fetchResult = FetchResult();
if (jsonResponse['errors'] != null) {
@@ -214,6 +223,15 @@ FetchResult _parseResponse(Response response) {
}
return fetchResult;
+ } catch (e) {
+ // @todo get body as well;
+ // final String decodedBody = encoding.decode(response.bodyBytes);
+ if (statusCode < 200 || statusCode >= 400) {
+ throw ClientException(
+ 'Network Error: $statusCode $reasonPhrase',
+ );
+ }
+ }
}
/// Returns the charset encoding for the given response.
@@ -221,7 +239,7 @@ FetchResult _parseResponse(Response response) {
/// The default fallback encoding is set to UTF-8 according to the IETF RFC4627 standard
/// which specifies the application/json media type:
/// "JSON text SHALL be encoded in Unicode. The default encoding is UTF-8."
-Encoding _determineEncodingFromResponse(Response response,
+Encoding _determineEncodingFromResponse(BaseResponse response,
[Encoding fallback = utf8]) {
final String contentType = response.headers['content-type'];

MODIFY
pubspec.yaml
pubspec.yaml
@@ -18,9 +18,10 @@ dependencies:
dev_dependencies:
perfectionist: ^2.0.0
+ mockito: ^4.0.0
flutter_test:
sdk: flutter
test: ^1.3.0
environment:
- sdk: ">=2.2.0 <3.0.0"
+ sdk: ">=2.0.0 <3.0.0"

MODIFY
test/graphql_flutter_test.dart
test/graphql_flutter_test.dart
-// import 'package:test/test.dart';
+import 'dart:convert';
+import 'dart:io' show File, Platform;
-// import 'package:graphql_flutter/graphql_flutter.dart';
+import 'package:test/test.dart';
+import 'package:mockito/mockito.dart';
+import 'package:http/http.dart' as http;
+import 'package:graphql_flutter/graphql_flutter.dart';
+
+class MockHttpClient extends Mock implements http.Client {}
void main() {
- // TODO: write tests
+ const String readRepositories = r'''
+ query ReadRepositories($nRepositories: Int!) {
+ viewer {
+ repositories(last: $nRepositories) {
+ nodes {
+ __typename
+ id
+ name
+ viewerHasStarred
+ }
+ }
+ }
+ }
+''';
+
+ const String addStar = r'''
+ mutation AddStar($starrableId: ID!) {
+ action: addStar(input: {starrableId: $starrableId}) {
+ starrable {
+ viewerHasStarred
+ }
+ }
+ }
+''';
+
+ HttpLink httpLink;
+ AuthLink authLink;
+ Link link;
+ GraphQLClient graphQLClientClient;
+ MockHttpClient mockHttpClient;
+
+ setUp(() {
+ mockHttpClient = MockHttpClient();
+
+ httpLink = HttpLink(
+ uri: 'https://api.github.com/graphql', httpClient: mockHttpClient);
+
+ authLink = AuthLink(
+ getToken: () async => 'Bearer my-special-bearer-token',
+ );
+
+ link = authLink.concat(httpLink);
+
+ graphQLClientClient = GraphQLClient(
+ cache: NormalizedInMemoryCache(
+ dataIdFromObject: typenameDataIdFromObject,
+ ),
+ link: link,
+ );
+ });
+ group('query', () {
+ test('successful query', () async {
+ final WatchQueryOptions _options = WatchQueryOptions(
+ document: readRepositories,
+ variables: <String, dynamic>{
+ 'nRepositories': 42,
+ },
+ );
+ when(
+ mockHttpClient.send(any),
+ ).thenAnswer((Invocation a) async {
+ const String body = '''
+{
+ "data": {
+ "viewer": {
+ "repositories": {
+ "nodes": [
+ {
+ "__typename": "Repository",
+ "id": "MDEwOlJlcG9zaXRvcnkyNDgzOTQ3NA==",
+ "name": "pq",
+ "viewerHasStarred": false
+ },
+ {
+ "__typename": "Repository",
+ "id": "MDEwOlJlcG9zaXRvcnkzMjkyNDQ0Mw==",
+ "name": "go-evercookie",
+ "viewerHasStarred": false
+ },
+ {
+ "__typename": "Repository",
+ "id": "MDEwOlJlcG9zaXRvcnkzNTA0NjgyNA==",
+ "name": "watchbot",
+ "viewerHasStarred": false
+ }
+ ]
+ }
+ }
+ }
+}
+ ''';
+
+ final List<int> bytes = utf8.encode(body);
+ final Stream<List<int>> stream =
+ Stream<List<int>>.fromIterable([bytes]);
+
+ final http.StreamedResponse r = http.StreamedResponse(stream, 200);
+
+ return r;
+ });
+ final QueryResult r = await graphQLClientClient.query(_options);
+
+ final http.Request capt = verify(mockHttpClient.send(captureAny))
+ .captured
+ .first as http.Request;
+ expect(capt.method, 'post');
+ expect(capt.url.toString(), 'https://api.github.com/graphql');
+ expect(
+ capt.headers,
+ <String, String>{
+ 'accept': '*/*',
+ 'content-type': 'application/json; charset=utf-8',
+ 'Authorization': 'Bearer my-special-bearer-token',
+ },
+ );
+ expect(await capt.finalize().bytesToString(), r'{"operationName":"ReadRepositories","variables":{"nRepositories":42},"query":" query ReadRepositories($nRepositories: Int!) {\n viewer {\n repositories(last: $nRepositories) {\n nodes {\n __typename\n id\n name\n viewerHasStarred\n }\n }\n }\n }\n"}');
+
+ expect(r.errors, isNull);
+ expect(r.data, isNotNull);
+ final List<Map<String, dynamic>> nodes =
+ (r.data['viewer']['repositories']['nodes'] as List<dynamic>)
+ .cast<Map<String, dynamic>>();
+ expect(nodes, hasLength(3));
+ expect(nodes[0]['id'], 'MDEwOlJlcG9zaXRvcnkyNDgzOTQ3NA==');
+ expect(nodes[1]['name'], 'go-evercookie');
+ expect(nodes[2]['viewerHasStarred'], false);
+ return;
+ });
+// test('failed query because of network', {});
+// test('failed query because of because of error response', {});
+// test('failed query because of because of invalid response', {});
+ });
+ group('mutation', () {
+ test('successful mutation', () async {
+ final MutationOptions _options = MutationOptions(
+ document: addStar,
+ variables: <String, dynamic>{
+ 'nRepositories': 38,
+ },
+ );
+ when(
+ mockHttpClient.send(any)
+ ).thenAnswer((Invocation a) async {
+ const String body =
+ '{"data":{"action":{"starrable":{"viewerHasStarred":true}}}}';
+
+ final List<int> bytes = utf8.encode(body);
+ final Stream<List<int>> stream =
+ Stream<List<int>>.fromIterable(<List<int>>[bytes]);
+
+ final http.StreamedResponse r = http.StreamedResponse(stream, 200);
+ return r;
+ });
+
+ final QueryResult r = await graphQLClientClient.mutate(_options);
+
+ final http.Request capt = verify(mockHttpClient.send(captureAny))
+ .captured
+ .first as http.Request;
+ expect(capt.method, 'post');
+ expect(capt.url.toString(), 'https://api.github.com/graphql');
+ expect(
+ capt.headers,
+ <String, String>{
+ 'accept': '*/*',
+ 'content-type': 'application/json; charset=utf-8',
+ 'Authorization': 'Bearer my-special-bearer-token',
+ },
+ );
+ expect(await capt.finalize().bytesToString(), r'{"operationName":"AddStar","variables":{"nRepositories":38},"query":" mutation AddStar($starrableId: ID!) {\n action: addStar(input: {starrableId: $starrableId}) {\n starrable {\n viewerHasStarred\n }\n }\n }\n"}');
+
+ expect(r.errors, isNull);
+ expect(r.data, isNotNull);
+ final bool viewerHasStarred =
+ r.data['action']['starrable']['viewerHasStarred'] as bool;
+ expect(viewerHasStarred, true);
+ });
+ });
+ group('upload', () {
+ const String uploadMutation = r'''
+ mutation($files: [Upload!]!) {
+ multipleUpload(files: $files) {
+ id
+ filename
+ mimetype
+ path
+ }
+ }
+
+ ''';
+// test('upload success', () async {
+// final List<File> files = <String>['sample_upload.bin', 'sample_upload.img']
+// .map((String fileName) => join(
+// dirname(Platform.script.path),
+// 'test',
+// ))
+// .map((String filePath) => File(filePath))
+// .toList();
+//
+// final MutationOptions _options = MutationOptions(
+// document: uploadMutation,
+// variables: <String, dynamic>{
+// 'files': files,
+// },
+// );
+//
+//
+// final QueryResult r = await graphQLClientClient.mutate(_options);
+// expect(r.errors, isNull);
+// expect(r.data, isNotNull);
+// final bool viewerHasStarred =
+// r.data['action']['starrable']['viewerHasStarred'] as bool;
+// expect(viewerHasStarred, true);
+// });
+ });
}

refactor unit test and get test coverage