cleared junks
This commit is contained in:
@@ -97,13 +97,6 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.8.1"
|
version: "1.8.1"
|
||||||
rxdart:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: rxdart
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "0.27.3"
|
|
||||||
sky_engine:
|
sky_engine:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description: flutter
|
description: flutter
|
||||||
@@ -116,13 +109,6 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.8.2"
|
version: "1.8.2"
|
||||||
stream_channel:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: stream_channel
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "2.1.0"
|
|
||||||
string_scanner:
|
string_scanner:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -158,13 +144,6 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.1"
|
version: "2.1.1"
|
||||||
web_socket_channel:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: web_socket_channel
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "2.1.0"
|
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=2.15.1 <3.0.0"
|
dart: ">=2.15.1 <3.0.0"
|
||||||
flutter: ">=1.17.0"
|
flutter: ">=1.17.0"
|
||||||
|
|||||||
@@ -12,20 +12,9 @@ dependencies:
|
|||||||
sdk: flutter
|
sdk: flutter
|
||||||
meta: ^1.3.0
|
meta: ^1.3.0
|
||||||
path: ^1.8.0
|
path: ^1.8.0
|
||||||
# gql: ^0.13.0
|
|
||||||
# gql_exec: 0.3.0
|
|
||||||
# gql_link: 0.4.0
|
|
||||||
# gql_http_link: 0.4.0
|
|
||||||
# gql_transform_link: ^0.2.0
|
|
||||||
# gql_error_link: ^0.2.0
|
|
||||||
# gql_dedupe_link: ^2.0.0
|
|
||||||
# normalize: ^0.6.0
|
|
||||||
hive: ^2.0.0
|
hive: ^2.0.0
|
||||||
http: ^0.13.0
|
http: ^0.13.0
|
||||||
collection: ^1.15.0
|
collection: ^1.15.0
|
||||||
web_socket_channel: ^2.0.0
|
|
||||||
stream_channel: ^2.1.0
|
|
||||||
rxdart: ^0.27.1
|
|
||||||
uuid: ^3.0.1
|
uuid: ^3.0.1
|
||||||
internet_connection_checker: ^0.0.1+3
|
internet_connection_checker: ^0.0.1+3
|
||||||
|
|
||||||
|
|||||||
@@ -1,171 +0,0 @@
|
|||||||
import 'package:gql/language.dart';
|
|
||||||
import 'package:test/test.dart';
|
|
||||||
import 'package:mockito/mockito.dart';
|
|
||||||
|
|
||||||
import 'package:fl_query/fl_query.dart';
|
|
||||||
|
|
||||||
import './helpers.dart';
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
const String readRepositories = r'''{
|
|
||||||
viewer {
|
|
||||||
repositories(last: 42) {
|
|
||||||
nodes {
|
|
||||||
__typename
|
|
||||||
id
|
|
||||||
name
|
|
||||||
viewerHasStarred
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
''';
|
|
||||||
|
|
||||||
const String addStar = r'''mutation {
|
|
||||||
action: addStar(input: {starrableId: "some_repo"}) {
|
|
||||||
starrable {
|
|
||||||
viewerHasStarred
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
''';
|
|
||||||
|
|
||||||
late MockLink link;
|
|
||||||
late GraphQLClient graphQLClientClient;
|
|
||||||
|
|
||||||
group('simple json', () {
|
|
||||||
setUp(() {
|
|
||||||
link = MockLink();
|
|
||||||
|
|
||||||
graphQLClientClient = GraphQLClient(
|
|
||||||
cache: getTestCache(),
|
|
||||||
link: link,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
group('query', () {
|
|
||||||
test('successful query', () async {
|
|
||||||
final WatchQueryOptions _options = WatchQueryOptions(
|
|
||||||
document: parseString(readRepositories),
|
|
||||||
variables: <String, dynamic>{},
|
|
||||||
);
|
|
||||||
|
|
||||||
when(
|
|
||||||
link.request(any),
|
|
||||||
).thenAnswer(
|
|
||||||
(_) => Stream.fromIterable(
|
|
||||||
[
|
|
||||||
Response(
|
|
||||||
data: <String, dynamic>{
|
|
||||||
'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 QueryResult r = await graphQLClientClient.query(_options);
|
|
||||||
|
|
||||||
verify(
|
|
||||||
link.request(
|
|
||||||
Request(
|
|
||||||
operation: Operation(
|
|
||||||
document: parseString(readRepositories),
|
|
||||||
operationName: null,
|
|
||||||
),
|
|
||||||
variables: <String, dynamic>{},
|
|
||||||
context: Context(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(r.exception, 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', () {
|
|
||||||
// String responseBody =
|
|
||||||
// '{\"message\":\"Bad credentials\",\"documentation_url\":\"https://developer.github.com/v4\"}';
|
|
||||||
// int responseCode = 401;
|
|
||||||
// });
|
|
||||||
// test('partially success query with some errors', {});
|
|
||||||
});
|
|
||||||
group('mutation', () {
|
|
||||||
test('successful mutation', () async {
|
|
||||||
final MutationOptions _options = MutationOptions(
|
|
||||||
document: parseString(addStar),
|
|
||||||
);
|
|
||||||
|
|
||||||
when(
|
|
||||||
link.request(any),
|
|
||||||
).thenAnswer(
|
|
||||||
(_) => Stream.fromIterable(
|
|
||||||
[
|
|
||||||
Response(
|
|
||||||
data: <String, dynamic>{
|
|
||||||
'action': {
|
|
||||||
'starrable': {
|
|
||||||
'viewerHasStarred': true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
final QueryResult response = await graphQLClientClient.mutate(_options);
|
|
||||||
|
|
||||||
verify(
|
|
||||||
link.request(
|
|
||||||
Request(
|
|
||||||
operation: Operation(
|
|
||||||
document: parseString(addStar),
|
|
||||||
),
|
|
||||||
variables: {},
|
|
||||||
context: Context(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(response.exception, isNull);
|
|
||||||
expect(response.data, isNotNull);
|
|
||||||
final bool? viewerHasStarred =
|
|
||||||
response.data!['action']['starrable']['viewerHasStarred'] as bool?;
|
|
||||||
expect(viewerHasStarred, true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
-362
@@ -1,362 +0,0 @@
|
|||||||
import 'package:gql_exec/gql_exec.dart';
|
|
||||||
import 'package:gql/language.dart';
|
|
||||||
import 'package:fl_query/fl_query.dart' show Fragment;
|
|
||||||
import 'package:fl_query/src/utilities/helpers.dart';
|
|
||||||
import 'package:http/http.dart' as http;
|
|
||||||
import 'package:http_parser/http_parser.dart';
|
|
||||||
|
|
||||||
const String rawOperationKey = 'rawOperationKey';
|
|
||||||
|
|
||||||
class TestCase {
|
|
||||||
TestCase({
|
|
||||||
required this.data,
|
|
||||||
required String operation,
|
|
||||||
Map<String, dynamic> variables = const <String, dynamic>{},
|
|
||||||
this.normalizedEntities,
|
|
||||||
}) : request = Request(
|
|
||||||
operation: Operation(document: parseString(operation)),
|
|
||||||
variables: variables,
|
|
||||||
context: Context(),
|
|
||||||
);
|
|
||||||
|
|
||||||
Request request;
|
|
||||||
|
|
||||||
/// data to write to cache
|
|
||||||
Map<String, dynamic> data;
|
|
||||||
|
|
||||||
/// entities to inspect the store for, if any
|
|
||||||
List<Map<String, dynamic>>? normalizedEntities;
|
|
||||||
}
|
|
||||||
|
|
||||||
final basicTest = TestCase(
|
|
||||||
operation: r'''{
|
|
||||||
a {
|
|
||||||
__typename
|
|
||||||
id
|
|
||||||
# union
|
|
||||||
list {
|
|
||||||
__typename
|
|
||||||
value
|
|
||||||
... on Item { id }
|
|
||||||
}
|
|
||||||
b {
|
|
||||||
__typename
|
|
||||||
id
|
|
||||||
c {
|
|
||||||
__typename
|
|
||||||
id,
|
|
||||||
cField
|
|
||||||
}
|
|
||||||
bField { field }
|
|
||||||
},
|
|
||||||
d {
|
|
||||||
id,
|
|
||||||
dField {field}
|
|
||||||
}
|
|
||||||
aField { field }
|
|
||||||
}
|
|
||||||
}''',
|
|
||||||
data: {
|
|
||||||
'a': {
|
|
||||||
'__typename': 'A',
|
|
||||||
'id': 1,
|
|
||||||
'list': [
|
|
||||||
{'__typename': 'Num', 'value': 1},
|
|
||||||
{'__typename': 'Num', 'value': 2},
|
|
||||||
{'__typename': 'Num', 'value': 3},
|
|
||||||
{'__typename': 'Item', 'id': 4, 'value': 4}
|
|
||||||
],
|
|
||||||
'b': {
|
|
||||||
'__typename': 'B',
|
|
||||||
'id': 5,
|
|
||||||
'c': {
|
|
||||||
'__typename': 'C',
|
|
||||||
'id': 6,
|
|
||||||
'cField': 'value',
|
|
||||||
},
|
|
||||||
'bField': {'field': true}
|
|
||||||
},
|
|
||||||
'd': {
|
|
||||||
'id': 9,
|
|
||||||
'dField': {'field': true}
|
|
||||||
},
|
|
||||||
'aField': {'field': false}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
/// https://github.com/gql-dart/gql/blob/master/links/gql_http_link/test/multipart_upload_test.dart
|
|
||||||
final fileVarsTest = TestCase(
|
|
||||||
data: {
|
|
||||||
"multipleUpload": [
|
|
||||||
{
|
|
||||||
"id": "r1odc4PAz",
|
|
||||||
"filename": "sample_upload.jpg",
|
|
||||||
"mimetype": "image/jpeg",
|
|
||||||
"path": "./uploads/r1odc4PAz-sample_upload.jpg"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "5Ea18qlMur",
|
|
||||||
"filename": "sample_upload.txt",
|
|
||||||
"mimetype": "text/plain",
|
|
||||||
"path": "./uploads/5Ea18qlMur-sample_upload.txt"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
},
|
|
||||||
operation: r"""
|
|
||||||
mutation($files: [Upload!]!) {
|
|
||||||
multipleUpload(files: $files) {
|
|
||||||
id
|
|
||||||
filename
|
|
||||||
mimetype
|
|
||||||
path
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""",
|
|
||||||
variables: {
|
|
||||||
'files': [
|
|
||||||
http.MultipartFile.fromBytes(
|
|
||||||
"",
|
|
||||||
[0, 1, 254, 255],
|
|
||||||
filename: "sample_upload.jpg",
|
|
||||||
contentType: MediaType("image", "jpeg"),
|
|
||||||
),
|
|
||||||
http.MultipartFile.fromString(
|
|
||||||
"",
|
|
||||||
"just plain text",
|
|
||||||
filename: "sample_upload.txt",
|
|
||||||
contentType: MediaType("text", "plain"),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
final originalCValue = <String, dynamic>{
|
|
||||||
'__typename': 'C',
|
|
||||||
'id': 6,
|
|
||||||
'cField': 'value',
|
|
||||||
};
|
|
||||||
final originalCFragment = Fragment(
|
|
||||||
document: parseString(
|
|
||||||
r'''
|
|
||||||
fragment partialC on C {
|
|
||||||
__typename
|
|
||||||
id
|
|
||||||
cField
|
|
||||||
}
|
|
||||||
''',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
final updatedCFragment = Fragment(
|
|
||||||
document: parseString(
|
|
||||||
r'''
|
|
||||||
fragment partialC on C {
|
|
||||||
__typename
|
|
||||||
id
|
|
||||||
new
|
|
||||||
cField
|
|
||||||
}
|
|
||||||
''',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
final updatedCValue = <String, dynamic>{
|
|
||||||
'__typename': 'C',
|
|
||||||
'id': 6,
|
|
||||||
'new': 'field',
|
|
||||||
'cField': 'changed value',
|
|
||||||
};
|
|
||||||
|
|
||||||
final Map? updatedCBasicTestData = deeplyMergeLeft([
|
|
||||||
basicTest.data,
|
|
||||||
{
|
|
||||||
'a': {
|
|
||||||
'b': {
|
|
||||||
'c': {
|
|
||||||
'__typename': 'C',
|
|
||||||
'id': 6,
|
|
||||||
'cField': 'changed value',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
final basicTestSubsetAValue = TestCase(
|
|
||||||
operation: r'''{
|
|
||||||
a {
|
|
||||||
__typename
|
|
||||||
id
|
|
||||||
list {
|
|
||||||
__typename
|
|
||||||
value
|
|
||||||
... on Item { id }
|
|
||||||
}
|
|
||||||
d { id }
|
|
||||||
}
|
|
||||||
}''',
|
|
||||||
data: {
|
|
||||||
'a': {
|
|
||||||
'__typename': 'A',
|
|
||||||
'id': 1,
|
|
||||||
'list': [
|
|
||||||
{'__typename': 'Num', 'value': 5},
|
|
||||||
{'__typename': 'Num', 'value': 6},
|
|
||||||
{'__typename': 'Num', 'value': 7},
|
|
||||||
{
|
|
||||||
'__typename': 'Item',
|
|
||||||
'id': 8,
|
|
||||||
'value': 8,
|
|
||||||
}
|
|
||||||
],
|
|
||||||
'd': {
|
|
||||||
'id': 10,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
getUpdatedSubsetOperationData({withUpdatedC = false}) => {
|
|
||||||
'a': {
|
|
||||||
'__typename': 'A',
|
|
||||||
'id': 1,
|
|
||||||
'list': basicTestSubsetAValue.data['a']['list'],
|
|
||||||
'b': {
|
|
||||||
'__typename': 'B',
|
|
||||||
'id': 5,
|
|
||||||
'c': {
|
|
||||||
'__typename': 'C',
|
|
||||||
'id': 6,
|
|
||||||
'cField': '${withUpdatedC ? "changed " : ""}value',
|
|
||||||
},
|
|
||||||
'bField': {'field': true}
|
|
||||||
},
|
|
||||||
'd': {
|
|
||||||
'id': 10,
|
|
||||||
'dField': {'field': true}
|
|
||||||
},
|
|
||||||
'aField': {'field': false}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
final cyclicalTest = TestCase(operation: r'''{
|
|
||||||
a {
|
|
||||||
__typename
|
|
||||||
id
|
|
||||||
b {
|
|
||||||
__typename
|
|
||||||
id
|
|
||||||
as {
|
|
||||||
__typename
|
|
||||||
id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}''', data: {
|
|
||||||
'a': {
|
|
||||||
'__typename': 'A',
|
|
||||||
'id': 1,
|
|
||||||
'b': {
|
|
||||||
'__typename': 'B',
|
|
||||||
'id': 5,
|
|
||||||
'as': [
|
|
||||||
{
|
|
||||||
'__typename': 'A',
|
|
||||||
'id': 1,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}, normalizedEntities: [
|
|
||||||
{
|
|
||||||
'__typename': 'A',
|
|
||||||
'id': 1,
|
|
||||||
'b': {r"$ref": 'B:5'}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'__typename': 'B',
|
|
||||||
'id': 5,
|
|
||||||
'as': [
|
|
||||||
{r"$ref": 'A:1'}
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
Map<String, dynamic> get cyclicalObjOperationData {
|
|
||||||
Map<String, dynamic> a;
|
|
||||||
Map<String, dynamic> b;
|
|
||||||
a = {
|
|
||||||
'__typename': 'A',
|
|
||||||
'id': 1,
|
|
||||||
};
|
|
||||||
b = {
|
|
||||||
'__typename': 'B',
|
|
||||||
'id': 5,
|
|
||||||
'as': [a]
|
|
||||||
};
|
|
||||||
a['b'] = b;
|
|
||||||
return {'a': a};
|
|
||||||
}
|
|
||||||
|
|
||||||
final typelessTest = TestCase(
|
|
||||||
operation: r'''{
|
|
||||||
a {
|
|
||||||
# union
|
|
||||||
list {
|
|
||||||
#__typename
|
|
||||||
value
|
|
||||||
#... on Item { id }
|
|
||||||
}
|
|
||||||
b {
|
|
||||||
id
|
|
||||||
c {
|
|
||||||
id,
|
|
||||||
cField
|
|
||||||
}
|
|
||||||
bField { field }
|
|
||||||
},
|
|
||||||
d {
|
|
||||||
id,
|
|
||||||
dField {field}
|
|
||||||
}
|
|
||||||
aField { field }
|
|
||||||
}
|
|
||||||
}''',
|
|
||||||
data: {
|
|
||||||
'a': {
|
|
||||||
'list': [
|
|
||||||
{
|
|
||||||
//'__typename': 'Num',
|
|
||||||
'value': 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
//'__typename': 'Num',
|
|
||||||
'value': 2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
//'__typename': 'Num',
|
|
||||||
'value': 3,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
//'__typename': 'Item',
|
|
||||||
//'id': 4,
|
|
||||||
'value': 4,
|
|
||||||
}
|
|
||||||
],
|
|
||||||
'b': {
|
|
||||||
'id': 5,
|
|
||||||
'c': {
|
|
||||||
'id': 6,
|
|
||||||
'cField': 'value',
|
|
||||||
},
|
|
||||||
'bField': {'field': true}
|
|
||||||
},
|
|
||||||
'd': {
|
|
||||||
'id': 9,
|
|
||||||
'dField': {'field': true}
|
|
||||||
},
|
|
||||||
'aField': {'field': false}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
@@ -1,313 +0,0 @@
|
|||||||
import 'package:fl_query/src/cache/_normalizing_data_proxy.dart';
|
|
||||||
import 'package:normalize/normalize.dart' show PartialDataException;
|
|
||||||
import 'package:test/test.dart';
|
|
||||||
|
|
||||||
import 'package:fl_query/src/cache/cache.dart';
|
|
||||||
|
|
||||||
import '../helpers.dart';
|
|
||||||
import './cache_data.dart';
|
|
||||||
|
|
||||||
typedef CacheTransaction = JSONDataProxy Function(JSONDataProxy proxy);
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
if (debuggingUnexpectedTestFailures) {
|
|
||||||
print(
|
|
||||||
'DEBUGGING UNEXPECTED TEST FAILURES: $debuggingUnexpectedTestFailures.\n'
|
|
||||||
'RUNNING TESTS WITH returnPartialData SET TO TRUE.\n',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
group('Normalizes writes', () {
|
|
||||||
late QueryCache cache;
|
|
||||||
setUp(() {
|
|
||||||
cache = getTestCache();
|
|
||||||
});
|
|
||||||
test('.writeQuery .readQuery round trip', () {
|
|
||||||
cache.writeQuery(basicTest.request, data: basicTest.data);
|
|
||||||
expect(
|
|
||||||
cache.readQuery(basicTest.request),
|
|
||||||
equals(basicTest.data),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('typeless .writeQuery .readQuery round trip', () {
|
|
||||||
cache.writeQuery(typelessTest.request, data: typelessTest.data);
|
|
||||||
expect(
|
|
||||||
cache.readQuery(typelessTest.request),
|
|
||||||
equals(typelessTest.data),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('typeless custom dataIdFromObject', () {
|
|
||||||
cache.writeQuery(typelessTest.request, data: typelessTest.data);
|
|
||||||
expect(
|
|
||||||
cache.readQuery(typelessTest.request),
|
|
||||||
equals(typelessTest.data),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('.writeQuery should fail on missing fields', () {
|
|
||||||
expect(
|
|
||||||
() => cache.writeQuery(basicTest.request, data: <String, dynamic>{
|
|
||||||
...basicTest.data,
|
|
||||||
'a': <String, dynamic>{
|
|
||||||
...basicTest.data['a'],
|
|
||||||
'b': <String, dynamic>{
|
|
||||||
'id': 5,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
throwsA(isA<PartialDataException>().having(
|
|
||||||
(e) => e.path,
|
|
||||||
'An accurate path to the first missing subfield',
|
|
||||||
['a', 'b', '__typename'],
|
|
||||||
)),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('updating nested normalized fragment changes top level operation', () {
|
|
||||||
cache.writeQuery(basicTest.request, data: basicTest.data);
|
|
||||||
final idFields = {
|
|
||||||
'__typename': updatedCValue['__typename'],
|
|
||||||
'id': updatedCValue['id'],
|
|
||||||
};
|
|
||||||
cache.writeFragment(
|
|
||||||
updatedCFragment.asRequest(
|
|
||||||
idFields: idFields,
|
|
||||||
),
|
|
||||||
data: updatedCValue,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
cache.readQuery(basicTest.request),
|
|
||||||
equals(updatedCBasicTestData),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
cache.readFragment(
|
|
||||||
updatedCFragment.asRequest(
|
|
||||||
idFields: idFields,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
updatedCValue,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('updating subset query only partially overrides superset query', () {
|
|
||||||
cache.writeQuery(basicTest.request, data: basicTest.data);
|
|
||||||
|
|
||||||
cache.writeQuery(
|
|
||||||
basicTestSubsetAValue.request,
|
|
||||||
data: basicTestSubsetAValue.data,
|
|
||||||
);
|
|
||||||
expect(
|
|
||||||
cache.readQuery(basicTest.request),
|
|
||||||
equals(getUpdatedSubsetOperationData()),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
group('Handles cyclical references', () {
|
|
||||||
final QueryCache cache = getTestCache();
|
|
||||||
test('lazily reads cyclical references', () {
|
|
||||||
cache.writeQuery(cyclicalTest.request, data: cyclicalTest.data);
|
|
||||||
for (final normalized in cyclicalTest.normalizedEntities!) {
|
|
||||||
final dataId = "${normalized['__typename']}:${normalized['id']}";
|
|
||||||
expect(cache.readNormalized(dataId), equals(normalized));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
group('Handles Object/pointer self-references/cycles', () {
|
|
||||||
final QueryCache cache = getTestCache();
|
|
||||||
test('correctly reads cyclical references', () {
|
|
||||||
cyclicalTest.data = cyclicalObjOperationData;
|
|
||||||
cache.writeQuery(cyclicalTest.request, data: cyclicalTest.data);
|
|
||||||
for (final normalized in cyclicalTest.normalizedEntities!) {
|
|
||||||
final dataId = "${normalized['__typename']}:${normalized['id']}";
|
|
||||||
expect(cache.readNormalized(dataId), equals(normalized));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
group(
|
|
||||||
'.recordOptimisticTransaction',
|
|
||||||
() {
|
|
||||||
late QueryCache cache;
|
|
||||||
|
|
||||||
setUp(() {
|
|
||||||
cache = getTestCache();
|
|
||||||
});
|
|
||||||
|
|
||||||
test(
|
|
||||||
'OptimisticCache.readQuery and .readFragment pass through',
|
|
||||||
() {
|
|
||||||
cache.writeQuery(basicTest.request, data: basicTest.data);
|
|
||||||
cache.broadcastRequested = false;
|
|
||||||
cache.recordOptimisticTransaction(
|
|
||||||
(proxy) {
|
|
||||||
expect(
|
|
||||||
proxy.readQuery(basicTest.request),
|
|
||||||
equals(basicTest.data),
|
|
||||||
);
|
|
||||||
|
|
||||||
final idFields = {
|
|
||||||
'__typename': originalCValue['__typename'],
|
|
||||||
'id': originalCValue['id'],
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(
|
|
||||||
proxy.readFragment(originalCFragment.asRequest(
|
|
||||||
idFields: idFields,
|
|
||||||
)),
|
|
||||||
originalCValue,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
(proxy as NormalizingDataProxy).broadcastRequested,
|
|
||||||
isFalse,
|
|
||||||
);
|
|
||||||
|
|
||||||
return proxy;
|
|
||||||
},
|
|
||||||
'1',
|
|
||||||
);
|
|
||||||
|
|
||||||
// no edits
|
|
||||||
expect(cache.broadcastRequested, isFalse);
|
|
||||||
expect(cache.optimisticPatches.first.id, equals('1'));
|
|
||||||
expect(cache.optimisticPatches.first.data, equals({}));
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
test(
|
|
||||||
'.writeQuery, .readQuery(optimistic: true) round trip',
|
|
||||||
() {
|
|
||||||
cache.recordOptimisticTransaction(
|
|
||||||
(proxy) => proxy
|
|
||||||
..writeQuery(
|
|
||||||
basicTest.request,
|
|
||||||
data: basicTest.data,
|
|
||||||
),
|
|
||||||
'1',
|
|
||||||
);
|
|
||||||
expect(
|
|
||||||
cache.readQuery(basicTest.request, optimistic: true),
|
|
||||||
equals(basicTest.data),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
recordCFragmentUpdate(QueryCache cache) =>
|
|
||||||
cache.recordOptimisticTransaction(
|
|
||||||
(proxy) => proxy
|
|
||||||
..writeFragment(
|
|
||||||
updatedCFragment.asRequest(idFields: {
|
|
||||||
'__typename': updatedCValue['__typename'],
|
|
||||||
'id': updatedCValue['id'],
|
|
||||||
}),
|
|
||||||
data: updatedCValue,
|
|
||||||
),
|
|
||||||
'2',
|
|
||||||
);
|
|
||||||
|
|
||||||
test(
|
|
||||||
'updating nested normalized fragment changes top level operation',
|
|
||||||
() {
|
|
||||||
cache.writeQuery(basicTest.request, data: basicTest.data);
|
|
||||||
recordCFragmentUpdate(cache);
|
|
||||||
expect(
|
|
||||||
cache.readQuery(basicTest.request),
|
|
||||||
equals(updatedCBasicTestData),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
recordBasicSubsetData(QueryCache cache) =>
|
|
||||||
cache.recordOptimisticTransaction(
|
|
||||||
(proxy) => proxy
|
|
||||||
..writeQuery(
|
|
||||||
basicTestSubsetAValue.request,
|
|
||||||
data: basicTestSubsetAValue.data,
|
|
||||||
),
|
|
||||||
'3',
|
|
||||||
);
|
|
||||||
test(
|
|
||||||
'updating subset query partially overrides superset query',
|
|
||||||
() {
|
|
||||||
cache.writeQuery(basicTest.request, data: basicTest.data);
|
|
||||||
recordCFragmentUpdate(cache);
|
|
||||||
recordBasicSubsetData(cache);
|
|
||||||
expect(
|
|
||||||
cache.readQuery(basicTest.request, optimistic: true),
|
|
||||||
equals(getUpdatedSubsetOperationData(withUpdatedC: true)),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
test(
|
|
||||||
'.removeOptimisticPatch results in data from lower layers on readQuery',
|
|
||||||
() {
|
|
||||||
cache.writeQuery(basicTest.request, data: basicTest.data);
|
|
||||||
recordCFragmentUpdate(cache);
|
|
||||||
recordBasicSubsetData(cache);
|
|
||||||
cache.removeOptimisticPatch('2');
|
|
||||||
cache.removeOptimisticPatch('3');
|
|
||||||
expect(
|
|
||||||
cache.readQuery(basicTest.request, optimistic: true),
|
|
||||||
equals(basicTest.data),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
group('Handles MultipartFile variables', () {
|
|
||||||
late QueryCache cache;
|
|
||||||
setUp(() {
|
|
||||||
cache = getTestCache();
|
|
||||||
});
|
|
||||||
test('.writeQuery .readQuery round trip', () {
|
|
||||||
cache.writeQuery(fileVarsTest.request, data: fileVarsTest.data);
|
|
||||||
expect(
|
|
||||||
cache.readQuery(fileVarsTest.request),
|
|
||||||
equals(fileVarsTest.data),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
group('custom dataIdFromObject', () {
|
|
||||||
/// Uses a `/` instead of the default `:`
|
|
||||||
String? customDataIdFromObject(Object object) {
|
|
||||||
if (object is Map<String, Object> &&
|
|
||||||
object.containsKey('__typename') &&
|
|
||||||
object.containsKey('id'))
|
|
||||||
return "${object['__typename']}/${object['id']}";
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
late QueryCache cache;
|
|
||||||
setUp(() {
|
|
||||||
cache = QueryCache(
|
|
||||||
dataIdFromObject: customDataIdFromObject,
|
|
||||||
partialDataPolicy: PartialDataCachePolicy.reject,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('.writeQuery .readQuery round trip', () {
|
|
||||||
cache.writeQuery(basicTest.request, data: basicTest.data);
|
|
||||||
expect(
|
|
||||||
cache.readQuery(basicTest.request),
|
|
||||||
equals(basicTest.data),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('typeless .writeQuery .readQuery round trip', () {
|
|
||||||
cache.writeQuery(typelessTest.request, data: typelessTest.data);
|
|
||||||
expect(
|
|
||||||
cache.readQuery(typelessTest.request),
|
|
||||||
equals(typelessTest.data),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
-79
@@ -1,79 +0,0 @@
|
|||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:fl_query/fl_query.dart';
|
|
||||||
import 'package:fl_query/src/utilities/helpers.dart';
|
|
||||||
import 'package:test/test.dart';
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
group('InMemoryStore', () {
|
|
||||||
final data = {
|
|
||||||
'id': {'key': 'value'},
|
|
||||||
'id2': {'otherKey': false}
|
|
||||||
};
|
|
||||||
test('basic methods', () {
|
|
||||||
final store = InMemoryStore();
|
|
||||||
store.put('id', data['id']);
|
|
||||||
expect(store.get('id'), equals(data['id']));
|
|
||||||
|
|
||||||
store.delete('id');
|
|
||||||
expect(store.data, equals({}));
|
|
||||||
});
|
|
||||||
test('bulk methods', () {
|
|
||||||
final store = InMemoryStore();
|
|
||||||
|
|
||||||
store.putAll(data);
|
|
||||||
|
|
||||||
expect(store.data, equals(data));
|
|
||||||
expect(store.toMap(), equals(data));
|
|
||||||
|
|
||||||
store.reset();
|
|
||||||
|
|
||||||
expect(data['id'], notNull); // no mutations
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
group('HiveStore', () {
|
|
||||||
final data = {
|
|
||||||
'id': {'key': 'value'},
|
|
||||||
'id2': {'otherKey': false}
|
|
||||||
};
|
|
||||||
final path = './test/cache/test_hive_boxes/';
|
|
||||||
test('basic methods', () async {
|
|
||||||
final store =
|
|
||||||
await HiveStore.open(boxName: 'basic', path: path + 'basic');
|
|
||||||
store.put('id', data['id']);
|
|
||||||
expect(store.get('id'), equals(data['id']));
|
|
||||||
|
|
||||||
store.delete('id');
|
|
||||||
expect(store.toMap(), equals({}));
|
|
||||||
|
|
||||||
await store.box.deleteFromDisk();
|
|
||||||
});
|
|
||||||
test('bulk methods', () async {
|
|
||||||
final store = await HiveStore.open(boxName: 'bulk', path: path + 'bulk');
|
|
||||||
|
|
||||||
store.putAll(data);
|
|
||||||
expect(store.toMap(), equals(data));
|
|
||||||
|
|
||||||
await store.reset();
|
|
||||||
expect(store.toMap(), equals({}));
|
|
||||||
|
|
||||||
expect(data['id'], notNull); // no mutations
|
|
||||||
|
|
||||||
await store.box.deleteFromDisk();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('box rereferencing', () async {
|
|
||||||
final store = await HiveStore.open(path: path);
|
|
||||||
store.putAll(data);
|
|
||||||
|
|
||||||
expect(HiveStore().toMap(), equals(data));
|
|
||||||
|
|
||||||
await store.box.deleteFromDisk();
|
|
||||||
});
|
|
||||||
|
|
||||||
tearDownAll(() async {
|
|
||||||
await Directory(path).delete(recursive: true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
// initially auto-generated by test_coverage,
|
|
||||||
// but that project is unmaintained https://github.com/pulyaevskiy/test-coverage/issues/40
|
|
||||||
|
|
||||||
import 'anonymous_operations_test.dart' as anonymous_operations_test;
|
|
||||||
import 'cache/graphql_cache_test.dart' as cache_graphql_cache_test;
|
|
||||||
import 'cache/store_test.dart' as cache_store_test;
|
|
||||||
import 'fetch_policy_test.dart' as fetch_policy_test;
|
|
||||||
import 'graphql_client_test.dart' as graphql_client_test;
|
|
||||||
import 'query_options_test.dart' as query_options_test;
|
|
||||||
import 'websocket_test.dart' as websocket_test;
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
query_options_test.main();
|
|
||||||
cache_store_test.main();
|
|
||||||
cache_graphql_cache_test.main();
|
|
||||||
fetch_policy_test.main();
|
|
||||||
anonymous_operations_test.main();
|
|
||||||
websocket_test.main();
|
|
||||||
graphql_client_test.main();
|
|
||||||
}
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
import 'package:test/test.dart';
|
|
||||||
import 'package:mockito/mockito.dart';
|
|
||||||
|
|
||||||
import 'package:fl_query/fl_query.dart';
|
|
||||||
import 'package:gql/language.dart';
|
|
||||||
|
|
||||||
import './helpers.dart';
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
const String readRepositories = r'''
|
|
||||||
query ReadRepositories($nRepositories: Int!) {
|
|
||||||
viewer {
|
|
||||||
repositories(last: $nRepositories) {
|
|
||||||
nodes {
|
|
||||||
__typename
|
|
||||||
id
|
|
||||||
name
|
|
||||||
viewerHasStarred
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
''';
|
|
||||||
readRepositoryData({withTypenames = true, withIds = true}) {
|
|
||||||
return {
|
|
||||||
'viewer': {
|
|
||||||
'repositories': {
|
|
||||||
'nodes': [
|
|
||||||
{
|
|
||||||
if (withIds) 'id': 'MDEwOlJlcG9zaXRvcnkyNDgzOTQ3NA==',
|
|
||||||
'name': 'pq',
|
|
||||||
'viewerHasStarred': false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
if (withIds) 'id': 'MDEwOlJlcG9zaXRvcnkzMjkyNDQ0Mw==',
|
|
||||||
'name': 'go-evercookie',
|
|
||||||
'viewerHasStarred': false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
if (withIds) 'id': 'MDEwOlJlcG9zaXRvcnkzNTA0NjgyNA==',
|
|
||||||
'name': 'watchbot',
|
|
||||||
'viewerHasStarred': false
|
|
||||||
},
|
|
||||||
]
|
|
||||||
.map((map) =>
|
|
||||||
withTypenames ? {'__typename': 'Repository', ...map} : map)
|
|
||||||
.toList(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
late MockLink link;
|
|
||||||
late GraphQLClient client;
|
|
||||||
|
|
||||||
group('FetchPolicy', () {
|
|
||||||
setUp(() {
|
|
||||||
link = MockLink();
|
|
||||||
|
|
||||||
client = GraphQLClient(
|
|
||||||
cache: getTestCache(),
|
|
||||||
link: link,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
group('query', () {
|
|
||||||
// TODO cacheFirst code path: Return result from cache. Only fetch from network if cached result is not available.
|
|
||||||
// TODO cacheAndNetwork code path: Return result from cache first (if it exists), then return network result once it's available.
|
|
||||||
// TODO cacheOnly code path: Return result from cache if available, fail otherwise.
|
|
||||||
// TODO noCache code path: Return result from network, fail if network call doesn't succeed, don't save to cache.
|
|
||||||
// TODO networkOnly code path: Return result from network, fail if network call doesn't succeed, save to cache.
|
|
||||||
test('switch to cacheOnly returns cached data', () async {
|
|
||||||
final _options = QueryOptions(
|
|
||||||
fetchPolicy: FetchPolicy.cacheAndNetwork,
|
|
||||||
document: parseString(readRepositories),
|
|
||||||
variables: <String, dynamic>{
|
|
||||||
'nRepositories': 42,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
final repoData = readRepositoryData(withTypenames: true);
|
|
||||||
|
|
||||||
when(
|
|
||||||
link.request(any),
|
|
||||||
).thenAnswer(
|
|
||||||
(_) => Stream.fromIterable([
|
|
||||||
Response(data: repoData),
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
|
|
||||||
final QueryResult r = await client.query(_options);
|
|
||||||
|
|
||||||
verify(
|
|
||||||
link.request(
|
|
||||||
Request(
|
|
||||||
operation: Operation(
|
|
||||||
document: parseString(readRepositories),
|
|
||||||
//operationName: 'ReadRepositories',
|
|
||||||
),
|
|
||||||
variables: <String, dynamic>{
|
|
||||||
'nRepositories': 42,
|
|
||||||
},
|
|
||||||
context: Context(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(r.exception, isNull);
|
|
||||||
expect(r.data, equals(repoData));
|
|
||||||
|
|
||||||
final QueryResult cacheResult = await client.query(QueryOptions(
|
|
||||||
fetchPolicy: FetchPolicy.cacheOnly,
|
|
||||||
document: parseString(readRepositories),
|
|
||||||
variables: <String, dynamic>{
|
|
||||||
'nRepositories': 42,
|
|
||||||
},
|
|
||||||
));
|
|
||||||
expect(cacheResult.exception, isNull);
|
|
||||||
expect(cacheResult.data, equals(repoData));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,888 +0,0 @@
|
|||||||
import 'package:fl_query/src/core/result_parser.dart';
|
|
||||||
import 'package:test/test.dart';
|
|
||||||
import 'package:mockito/mockito.dart';
|
|
||||||
|
|
||||||
import 'package:fl_query/fl_query.dart';
|
|
||||||
import 'package:gql/language.dart';
|
|
||||||
|
|
||||||
import './helpers.dart';
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
const String readSingle = r'''
|
|
||||||
query ReadSingle($id: ID!) {
|
|
||||||
single(id: $id) {
|
|
||||||
id,
|
|
||||||
__typename,
|
|
||||||
name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
''';
|
|
||||||
|
|
||||||
const String writeSingle = r'''
|
|
||||||
mutation WriteSingle($id: ID!, $name: String!) {
|
|
||||||
updateSingle(id: $id, name: $name) {
|
|
||||||
id,
|
|
||||||
__typename,
|
|
||||||
name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
''';
|
|
||||||
|
|
||||||
const String readRepositories = r'''
|
|
||||||
query ReadRepositories($nRepositories: Int!) {
|
|
||||||
viewer {
|
|
||||||
repositories(last: $nRepositories) {
|
|
||||||
nodes {
|
|
||||||
__typename
|
|
||||||
id
|
|
||||||
name
|
|
||||||
viewerHasStarred
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
''';
|
|
||||||
Map<String, dynamic> readRepositoryData({
|
|
||||||
bool withTypenames = true,
|
|
||||||
bool withIds = true,
|
|
||||||
bool viewerHasStarred = false,
|
|
||||||
}) {
|
|
||||||
return {
|
|
||||||
'viewer': {
|
|
||||||
'repositories': {
|
|
||||||
'nodes': [
|
|
||||||
{
|
|
||||||
if (withIds) 'id': 'MDEwOlJlcG9zaXRvcnkyNDgzOTQ3NA==',
|
|
||||||
'name': 'pq',
|
|
||||||
'viewerHasStarred': viewerHasStarred
|
|
||||||
},
|
|
||||||
{
|
|
||||||
if (withIds) 'id': 'MDEwOlJlcG9zaXRvcnkzMjkyNDQ0Mw==',
|
|
||||||
'name': 'go-evercookie',
|
|
||||||
'viewerHasStarred': viewerHasStarred
|
|
||||||
},
|
|
||||||
{
|
|
||||||
if (withIds) 'id': 'MDEwOlJlcG9zaXRvcnkzNTA0NjgyNA==',
|
|
||||||
'name': 'watchbot',
|
|
||||||
'viewerHasStarred': viewerHasStarred
|
|
||||||
},
|
|
||||||
]
|
|
||||||
.map((map) =>
|
|
||||||
withTypenames ? {'__typename': 'Repository', ...map} : map)
|
|
||||||
.toList(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const String addStar = r'''
|
|
||||||
mutation AddStar($starrableId: ID!) {
|
|
||||||
action: addStar(input: {starrableId: $starrableId}) {
|
|
||||||
starrable {
|
|
||||||
viewerHasStarred
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
''';
|
|
||||||
|
|
||||||
late MockLink link;
|
|
||||||
late GraphQLClient client;
|
|
||||||
|
|
||||||
group('simple json', () {
|
|
||||||
setUp(() {
|
|
||||||
link = MockLink();
|
|
||||||
|
|
||||||
client = GraphQLClient(
|
|
||||||
cache: getTestCache(),
|
|
||||||
link: link,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
group('query', () {
|
|
||||||
test('successful response', () async {
|
|
||||||
final _options = QueryOptions(
|
|
||||||
document: parseString(readRepositories),
|
|
||||||
variables: <String, dynamic>{
|
|
||||||
'nRepositories': 42,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
final repoData = readRepositoryData(withTypenames: true);
|
|
||||||
|
|
||||||
when(
|
|
||||||
link.request(any),
|
|
||||||
).thenAnswer(
|
|
||||||
(_) => Stream.fromIterable([
|
|
||||||
Response(
|
|
||||||
data: repoData,
|
|
||||||
context: Context().withEntry(
|
|
||||||
HttpLinkResponseContext(
|
|
||||||
statusCode: 200,
|
|
||||||
headers: {'foo': 'bar'},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
|
|
||||||
final QueryResult r = await client.query(_options);
|
|
||||||
|
|
||||||
verify(
|
|
||||||
link.request(
|
|
||||||
Request(
|
|
||||||
operation: Operation(
|
|
||||||
document: parseString(readRepositories),
|
|
||||||
//operationName: 'ReadRepositories',
|
|
||||||
),
|
|
||||||
variables: <String, dynamic>{
|
|
||||||
'nRepositories': 42,
|
|
||||||
},
|
|
||||||
context: Context(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(r.exception, isNull);
|
|
||||||
expect(r.data, equals(repoData));
|
|
||||||
|
|
||||||
expect(
|
|
||||||
r.context.entry<HttpLinkResponseContext>()!.statusCode,
|
|
||||||
equals(200),
|
|
||||||
);
|
|
||||||
expect(
|
|
||||||
r.context.entry<HttpLinkResponseContext>()!.headers['foo'],
|
|
||||||
equals('bar'),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
test('successful response with parser', () async {
|
|
||||||
final ResultParserFn<List<String>> parserFn = (data) {
|
|
||||||
return data['viewer']['repositories']['nodes']
|
|
||||||
.map<String>((node) => node['name'] as String)
|
|
||||||
.toList();
|
|
||||||
};
|
|
||||||
final _options = QueryOptions(
|
|
||||||
document: parseString(readRepositories),
|
|
||||||
variables: <String, dynamic>{
|
|
||||||
'nRepositories': 42,
|
|
||||||
},
|
|
||||||
parserFn: parserFn,
|
|
||||||
);
|
|
||||||
final repoData = readRepositoryData(withTypenames: true);
|
|
||||||
|
|
||||||
when(
|
|
||||||
link.request(any),
|
|
||||||
).thenAnswer(
|
|
||||||
(_) => Stream.fromIterable([
|
|
||||||
Response(
|
|
||||||
data: repoData,
|
|
||||||
context: Context().withEntry(
|
|
||||||
HttpLinkResponseContext(
|
|
||||||
statusCode: 200,
|
|
||||||
headers: {'foo': 'bar'},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
|
|
||||||
final QueryResult<List<String>> r = await client.query(_options);
|
|
||||||
|
|
||||||
verify(
|
|
||||||
link.request(
|
|
||||||
Request(
|
|
||||||
operation: Operation(
|
|
||||||
document: parseString(readRepositories),
|
|
||||||
//operationName: 'ReadRepositories',
|
|
||||||
),
|
|
||||||
variables: <String, dynamic>{
|
|
||||||
'nRepositories': 42,
|
|
||||||
},
|
|
||||||
context: Context(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(r.exception, isNull);
|
|
||||||
expect(r.data, equals(repoData));
|
|
||||||
|
|
||||||
List<String>? parsedData = r.parsedData;
|
|
||||||
expect(
|
|
||||||
parsedData,
|
|
||||||
equals([
|
|
||||||
'pq',
|
|
||||||
'go-evercookie',
|
|
||||||
'watchbot',
|
|
||||||
]));
|
|
||||||
|
|
||||||
expect(
|
|
||||||
r.context.entry<HttpLinkResponseContext>()!.statusCode,
|
|
||||||
equals(200),
|
|
||||||
);
|
|
||||||
expect(
|
|
||||||
r.context.entry<HttpLinkResponseContext>()!.headers['foo'],
|
|
||||||
equals('bar'),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('successful response without normalization', () async {
|
|
||||||
final readUnidentifiedRepositories = parseString(r'''
|
|
||||||
query ReadRepositories($nRepositories: Int!) {
|
|
||||||
viewer {
|
|
||||||
repositories(last: $nRepositories) {
|
|
||||||
nodes {
|
|
||||||
name
|
|
||||||
viewerHasStarred
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
''');
|
|
||||||
final repoData = readRepositoryData(
|
|
||||||
withTypenames: false,
|
|
||||||
withIds: false,
|
|
||||||
);
|
|
||||||
|
|
||||||
final _options = QueryOptions(
|
|
||||||
document: readUnidentifiedRepositories,
|
|
||||||
variables: {'nRepositories': 42},
|
|
||||||
);
|
|
||||||
|
|
||||||
when(
|
|
||||||
link.request(any),
|
|
||||||
).thenAnswer(
|
|
||||||
(_) => Stream.fromIterable([
|
|
||||||
Response(data: repoData),
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
|
|
||||||
final QueryResult r = await client.query(_options);
|
|
||||||
|
|
||||||
verify(link.request(_options.asRequest));
|
|
||||||
expect(r.data, equals(repoData));
|
|
||||||
});
|
|
||||||
test('correct consecutive responses', () async {
|
|
||||||
final _options = QueryOptions(
|
|
||||||
fetchPolicy: FetchPolicy.networkOnly,
|
|
||||||
document: parseString(readRepositories),
|
|
||||||
variables: <String, dynamic>{
|
|
||||||
'nRepositories': 42,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
final firstData =
|
|
||||||
readRepositoryData(withTypenames: true, viewerHasStarred: false);
|
|
||||||
final secondData =
|
|
||||||
readRepositoryData(withTypenames: true, viewerHasStarred: true);
|
|
||||||
|
|
||||||
final resp = (d) => Stream.fromIterable([
|
|
||||||
Response(
|
|
||||||
data: d,
|
|
||||||
context: Context().withEntry(
|
|
||||||
HttpLinkResponseContext(
|
|
||||||
statusCode: 200,
|
|
||||||
headers: {'foo': 'bar'},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
]);
|
|
||||||
|
|
||||||
when(link.request(any)).thenAnswer((_) => resp(firstData));
|
|
||||||
QueryResult r = await client.query(_options);
|
|
||||||
expect(r.exception, isNull);
|
|
||||||
expect(r.data, equals(firstData));
|
|
||||||
|
|
||||||
when(link.request(any)).thenAnswer((_) => resp(secondData));
|
|
||||||
r = await client.query(_options);
|
|
||||||
expect(r.exception, isNull);
|
|
||||||
expect(r.data, equals(secondData));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('malformed server response', () async {
|
|
||||||
final _options = QueryOptions(
|
|
||||||
document: parseString(readRepositories),
|
|
||||||
variables: {'nRepositories': 42},
|
|
||||||
);
|
|
||||||
final malformedRepoData = {
|
|
||||||
'viewer': {
|
|
||||||
// maybe the server doesn't validate response structures properly,
|
|
||||||
// or a user generates a response on the client, etc
|
|
||||||
'repos': readRepositoryData()['viewer']!['repositories']
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
when(
|
|
||||||
link.request(any),
|
|
||||||
).thenAnswer(
|
|
||||||
(_) => Stream.fromIterable([
|
|
||||||
Response(data: malformedRepoData),
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
|
|
||||||
final QueryResult r = await client.query(_options);
|
|
||||||
|
|
||||||
expect(r.data, equals(malformedRepoData),
|
|
||||||
reason: 'Malformed data should be passed along with errors');
|
|
||||||
|
|
||||||
throwsA(isA<PartialDataException>().having(
|
|
||||||
(e) => e.path,
|
|
||||||
'An accurate path to the first missing subfield',
|
|
||||||
['a', 'b', '__typename'],
|
|
||||||
));
|
|
||||||
});
|
|
||||||
|
|
||||||
test('failed query because of an exception with null string', () async {
|
|
||||||
final e = Exception();
|
|
||||||
|
|
||||||
when(
|
|
||||||
link.request(any),
|
|
||||||
).thenAnswer(
|
|
||||||
(_) => Stream.fromFuture(Future.error(e)),
|
|
||||||
);
|
|
||||||
|
|
||||||
final QueryResult r = await client.query(
|
|
||||||
WatchQueryOptions(
|
|
||||||
document: parseString(readRepositories),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
r.exception!.exception!.originalException,
|
|
||||||
e,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('failed query because of an exception with empty string', () async {
|
|
||||||
final e = Exception('');
|
|
||||||
|
|
||||||
when(
|
|
||||||
link.request(any),
|
|
||||||
).thenAnswer(
|
|
||||||
(_) => Stream.fromFuture(Future.error(e)),
|
|
||||||
);
|
|
||||||
|
|
||||||
final QueryResult r = await client.query(
|
|
||||||
WatchQueryOptions(
|
|
||||||
document: parseString(readRepositories),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
r.exception!.exception!.originalException,
|
|
||||||
e,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
// test('failed query because of because of error response', {});
|
|
||||||
// test('failed query because of because of invalid response', () {
|
|
||||||
// String responseBody =
|
|
||||||
// '{\"message\":\"Bad credentials\",\"documentation_url\":\"https://developer.github.com/v4\"}';
|
|
||||||
// int responseCode = 401;
|
|
||||||
// });
|
|
||||||
// test('partially success query with some errors', {});
|
|
||||||
});
|
|
||||||
group('mutation', () {
|
|
||||||
test('query stream notified', () async {
|
|
||||||
final initialQueryResponse = Response(
|
|
||||||
data: <String, dynamic>{
|
|
||||||
'single': {
|
|
||||||
'id': '1',
|
|
||||||
'__typename': 'Single',
|
|
||||||
'name': 'initialQueryName',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
when(
|
|
||||||
link.request(any),
|
|
||||||
).thenAnswer(
|
|
||||||
(_) => Stream.fromIterable(
|
|
||||||
[initialQueryResponse],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
final ObservableQuery observable = client.watchQuery(
|
|
||||||
WatchQueryOptions(
|
|
||||||
document: parseString(readSingle),
|
|
||||||
eagerlyFetchResults: true,
|
|
||||||
variables: {'id': '1'},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
observable.stream,
|
|
||||||
emitsInOrder(
|
|
||||||
[
|
|
||||||
// we have no optimistic result
|
|
||||||
isA<QueryResult>().having(
|
|
||||||
(result) => result.isLoading,
|
|
||||||
'loading result',
|
|
||||||
true,
|
|
||||||
),
|
|
||||||
isA<QueryResult>().having(
|
|
||||||
(result) => result.data!['single']['name'],
|
|
||||||
'initial query result',
|
|
||||||
'initialQueryName',
|
|
||||||
),
|
|
||||||
isA<QueryResult>().having(
|
|
||||||
(result) => result.data!['single']['name'],
|
|
||||||
'result caused by mutation',
|
|
||||||
'newNameFromMutation',
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
final mutationResponseWithNewName = Response(
|
|
||||||
data: <String, dynamic>{
|
|
||||||
'updateSingle': {
|
|
||||||
'id': '1',
|
|
||||||
'__typename': 'Single',
|
|
||||||
'name': 'newNameFromMutation',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
when(
|
|
||||||
link.request(any),
|
|
||||||
).thenAnswer(
|
|
||||||
(_) => Stream.fromIterable(
|
|
||||||
[mutationResponseWithNewName],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
final variables = {'id': '1', 'name': 'newNameFromMutation'};
|
|
||||||
|
|
||||||
final QueryResult response = await client.mutate(MutationOptions(
|
|
||||||
document: parseString(writeSingle), variables: variables));
|
|
||||||
|
|
||||||
expect(response.data!['updateSingle']['name'], variables['name']);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('successful mutation', () async {
|
|
||||||
final MutationOptions _options = MutationOptions(
|
|
||||||
document: parseString(addStar),
|
|
||||||
);
|
|
||||||
|
|
||||||
when(
|
|
||||||
link.request(any),
|
|
||||||
).thenAnswer(
|
|
||||||
(_) => Stream.fromIterable(
|
|
||||||
[
|
|
||||||
Response(
|
|
||||||
data: <String, dynamic>{
|
|
||||||
'action': {
|
|
||||||
'starrable': {
|
|
||||||
'viewerHasStarred': true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
final QueryResult response = await client.mutate(_options);
|
|
||||||
|
|
||||||
verify(
|
|
||||||
link.request(
|
|
||||||
Request(
|
|
||||||
operation: Operation(
|
|
||||||
document: parseString(addStar),
|
|
||||||
//operationName: 'AddStar',
|
|
||||||
),
|
|
||||||
variables: <String, dynamic>{},
|
|
||||||
context: Context(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(response.exception, isNull);
|
|
||||||
expect(response.data, isNotNull);
|
|
||||||
final bool? viewerHasStarred =
|
|
||||||
response.data!['action']['starrable']['viewerHasStarred'] as bool?;
|
|
||||||
expect(viewerHasStarred, true);
|
|
||||||
});
|
|
||||||
test('successful mutation with parser', () async {
|
|
||||||
final ResultParserFn<bool> resultParser =
|
|
||||||
(data) => data['action']['starrable']['viewerHasStarred'] as bool;
|
|
||||||
final MutationOptions _options = MutationOptions(
|
|
||||||
document: parseString(addStar),
|
|
||||||
parserFn: resultParser,
|
|
||||||
);
|
|
||||||
|
|
||||||
when(
|
|
||||||
link.request(any),
|
|
||||||
).thenAnswer(
|
|
||||||
(_) => Stream.fromIterable(
|
|
||||||
[
|
|
||||||
Response(
|
|
||||||
data: <String, dynamic>{
|
|
||||||
'action': {
|
|
||||||
'starrable': {
|
|
||||||
'viewerHasStarred': true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
final QueryResult response = await client.mutate(_options);
|
|
||||||
|
|
||||||
verify(
|
|
||||||
link.request(
|
|
||||||
Request(
|
|
||||||
operation: Operation(
|
|
||||||
document: parseString(addStar),
|
|
||||||
//operationName: 'AddStar',
|
|
||||||
),
|
|
||||||
variables: <String, dynamic>{},
|
|
||||||
context: Context(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
final bool parsedResult = response.parsedData;
|
|
||||||
expect(parsedResult, isTrue);
|
|
||||||
expect(response.exception, isNull);
|
|
||||||
expect(response.data, isNotNull);
|
|
||||||
final bool? viewerHasStarred =
|
|
||||||
response.data!['action']['starrable']['viewerHasStarred'] as bool?;
|
|
||||||
expect(viewerHasStarred, true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('successful mutation through watchQuery', () async {
|
|
||||||
final _options = MutationOptions(
|
|
||||||
document: parseString(addStar),
|
|
||||||
variables: {},
|
|
||||||
);
|
|
||||||
|
|
||||||
when(
|
|
||||||
link.request(any),
|
|
||||||
).thenAnswer(
|
|
||||||
(_) => Stream.fromIterable(
|
|
||||||
[
|
|
||||||
Response(
|
|
||||||
data: <String, dynamic>{
|
|
||||||
'action': {
|
|
||||||
'starrable': {
|
|
||||||
'viewerHasStarred': true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
final observableQuery = client.watchQuery(WatchQueryOptions(
|
|
||||||
document: _options.document,
|
|
||||||
variables: _options.variables,
|
|
||||||
fetchResults: false,
|
|
||||||
));
|
|
||||||
|
|
||||||
final result = await observableQuery.fetchResults().networkResult!;
|
|
||||||
|
|
||||||
verify(
|
|
||||||
link.request(
|
|
||||||
Request(
|
|
||||||
operation: Operation(
|
|
||||||
document: parseString(addStar),
|
|
||||||
//operationName: 'AddStar',
|
|
||||||
),
|
|
||||||
variables: <String, dynamic>{},
|
|
||||||
context: Context(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(result.hasException, isFalse);
|
|
||||||
expect(result.data, isNotNull);
|
|
||||||
final bool? viewerHasStarred =
|
|
||||||
result.data!['action']['starrable']['viewerHasStarred'] as bool?;
|
|
||||||
expect(viewerHasStarred, true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
group('subscription', () {
|
|
||||||
test('results', () async {
|
|
||||||
final responses = [
|
|
||||||
{
|
|
||||||
'id': '1',
|
|
||||||
'name': 'first',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'id': '2',
|
|
||||||
'name': 'second',
|
|
||||||
},
|
|
||||||
].map((item) => Response(
|
|
||||||
data: <String, dynamic>{
|
|
||||||
'item': {
|
|
||||||
'__typename': 'Item',
|
|
||||||
...item,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
));
|
|
||||||
when(
|
|
||||||
link.request(any),
|
|
||||||
).thenAnswer(
|
|
||||||
(_) => Stream.fromIterable(responses),
|
|
||||||
);
|
|
||||||
|
|
||||||
final stream = client.subscribe(
|
|
||||||
SubscriptionOptions(
|
|
||||||
document: parseString(
|
|
||||||
r'''
|
|
||||||
subscription {
|
|
||||||
item {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
''',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
stream,
|
|
||||||
emitsInOrder(
|
|
||||||
[
|
|
||||||
isA<QueryResult>().having(
|
|
||||||
(result) => result.data!['item']['name'],
|
|
||||||
'first subscription item',
|
|
||||||
'first',
|
|
||||||
),
|
|
||||||
isA<QueryResult>().having(
|
|
||||||
(result) => result.data!['item']['name'],
|
|
||||||
'second subscription item',
|
|
||||||
'second',
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
test('parses results', () async {
|
|
||||||
final responses = [
|
|
||||||
{
|
|
||||||
'id': '1',
|
|
||||||
'name': 'first',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'id': '2',
|
|
||||||
'name': 'second',
|
|
||||||
},
|
|
||||||
].map((item) => Response(
|
|
||||||
data: <String, dynamic>{
|
|
||||||
'item': {
|
|
||||||
'__typename': 'Item',
|
|
||||||
...item,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
));
|
|
||||||
when(
|
|
||||||
link.request(any),
|
|
||||||
).thenAnswer(
|
|
||||||
(_) => Stream.fromIterable(responses),
|
|
||||||
);
|
|
||||||
|
|
||||||
final ResultParserFn<String> parserFn =
|
|
||||||
(data) => data['item']['name'] as String;
|
|
||||||
;
|
|
||||||
|
|
||||||
final stream = client.subscribe(
|
|
||||||
SubscriptionOptions(
|
|
||||||
parserFn: parserFn,
|
|
||||||
document: parseString(
|
|
||||||
r'''
|
|
||||||
subscription {
|
|
||||||
item {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
''',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
stream,
|
|
||||||
emitsInOrder(['first', 'second']
|
|
||||||
.map((e) => isA<QueryResult<String>>().having((result) {
|
|
||||||
final String? parsed = result.parsedData;
|
|
||||||
return parsed;
|
|
||||||
}, "Parsed item", e))),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('wraps stream exceptions', () async {
|
|
||||||
final ex = ServerException(
|
|
||||||
parsedResponse: null,
|
|
||||||
originalException: Error(),
|
|
||||||
);
|
|
||||||
|
|
||||||
when(
|
|
||||||
link.request(any),
|
|
||||||
).thenAnswer(
|
|
||||||
(_) => Stream.error(ex),
|
|
||||||
);
|
|
||||||
|
|
||||||
final stream = client.subscribe(
|
|
||||||
SubscriptionOptions(
|
|
||||||
document: parseString(
|
|
||||||
r'''
|
|
||||||
subscription {
|
|
||||||
item {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
''',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
stream,
|
|
||||||
emitsInOrder(
|
|
||||||
[
|
|
||||||
isA<QueryResult>().having(
|
|
||||||
(result) => result.exception!.exception,
|
|
||||||
'wrapped exception',
|
|
||||||
ex,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
test('wraps all exceptions from outside of stream', () async {
|
|
||||||
final err = Error();
|
|
||||||
|
|
||||||
when(
|
|
||||||
link.request(any),
|
|
||||||
).thenThrow(err);
|
|
||||||
|
|
||||||
final stream = client.subscribe(
|
|
||||||
SubscriptionOptions(
|
|
||||||
document: parseString(
|
|
||||||
r'''
|
|
||||||
subscription {
|
|
||||||
item {
|
|
||||||
id
|
|
||||||
name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
''',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
stream,
|
|
||||||
emitsInOrder(
|
|
||||||
[
|
|
||||||
isA<QueryResult>().having(
|
|
||||||
(result) => result.exception!.exception!.originalException,
|
|
||||||
'wrapped exception',
|
|
||||||
err,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
group('direct cache access', () {
|
|
||||||
setUp(() {
|
|
||||||
link = MockLink();
|
|
||||||
|
|
||||||
client = GraphQLClient(
|
|
||||||
cache: getTestCache(),
|
|
||||||
link: link,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('all methods with exposition', () {
|
|
||||||
/// entity identifiers for normalization
|
|
||||||
final idFields = {'__typename': 'MyType', 'id': 1};
|
|
||||||
|
|
||||||
/// The direct cache API uses `gql_link` Requests directly
|
|
||||||
/// These can also be obtained via `options.asRequest` from any `Options` object,
|
|
||||||
/// or via `Operation(document: gql(...)).asRequest()`
|
|
||||||
final queryRequest = Request(
|
|
||||||
operation: Operation(
|
|
||||||
document: gql(
|
|
||||||
r'''{
|
|
||||||
someField {
|
|
||||||
id,
|
|
||||||
myField
|
|
||||||
}
|
|
||||||
}''',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
final queryData = {
|
|
||||||
'__typename': 'Query',
|
|
||||||
'someField': {
|
|
||||||
...idFields,
|
|
||||||
'myField': 'originalValue',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
/// `broadcast: true` (the default) would rebroadcast cache updates to all safe instances of `ObservableQuery`
|
|
||||||
/// **NOTE**: only `GraphQLClient` can immediately call for a query rebroadcast. if you request a rebroadcast directly
|
|
||||||
/// from the cache, it still has to wait for the client to check in on it
|
|
||||||
client.writeQuery(queryRequest, data: queryData, broadcast: false);
|
|
||||||
|
|
||||||
/// `optimistic: true` (the default) integrates optimistic data
|
|
||||||
/// written to the cache into your read.
|
|
||||||
expect(
|
|
||||||
client.readQuery(queryRequest, optimistic: false), equals(queryData));
|
|
||||||
|
|
||||||
/// While fragments are never executed themselves, we provide a `gql_link`-like API for consistency.
|
|
||||||
/// These can also be obtained via `Fragment(document: gql(...)).asRequest()`.
|
|
||||||
final fragmentRequest = FragmentRequest(
|
|
||||||
fragment: Fragment(
|
|
||||||
document: gql(
|
|
||||||
r'''
|
|
||||||
fragment mySmallSubset on MyType {
|
|
||||||
myField,
|
|
||||||
someNewField
|
|
||||||
}
|
|
||||||
''',
|
|
||||||
),
|
|
||||||
),
|
|
||||||
idFields: idFields,
|
|
||||||
);
|
|
||||||
|
|
||||||
/// We've specified `idFields` and are only editing a subset of the data
|
|
||||||
final fragmentData = {
|
|
||||||
'myField': 'updatedValue',
|
|
||||||
'someNewField': [
|
|
||||||
{'newData': false}
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
/// We didn't disable `broadcast`, so all instances of `ObservableQuery` will be notified of any changes
|
|
||||||
client.writeFragment(fragmentRequest, data: fragmentData);
|
|
||||||
|
|
||||||
/// __typename is automatically included in all reads
|
|
||||||
expect(
|
|
||||||
client.readFragment(fragmentRequest),
|
|
||||||
equals({
|
|
||||||
'__typename': 'MyType',
|
|
||||||
...fragmentData,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
final updatedQueryData = {
|
|
||||||
'__typename': 'Query',
|
|
||||||
'someField': {
|
|
||||||
...idFields,
|
|
||||||
'myField': 'updatedValue',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
/// `myField` is updated, but we don't have `someNewField`, as expected.
|
|
||||||
expect(client.readQuery(queryRequest), equals(updatedQueryData));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
|
|
||||||
import 'package:fl_query/fl_query.dart';
|
|
||||||
|
|
||||||
import 'package:mockito/mockito.dart';
|
|
||||||
|
|
||||||
class MockLink extends Mock implements Link {
|
|
||||||
@override
|
|
||||||
Stream<Response> request(Request? request, [NextLink? forward]) =>
|
|
||||||
super.noSuchMethod(
|
|
||||||
Invocation.method(#request, [request, forward]),
|
|
||||||
returnValue: Stream.fromIterable(
|
|
||||||
<Response>[],
|
|
||||||
),
|
|
||||||
) as Stream<Response>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const debuggingUnexpectedTestFailures = false;
|
|
||||||
|
|
||||||
overridePrint(testFn(List<String> log)) => () {
|
|
||||||
final log = <String>[];
|
|
||||||
final spec = ZoneSpecification(print: (_, __, ___, String msg) {
|
|
||||||
log.add(msg);
|
|
||||||
});
|
|
||||||
return Zone.current.fork(specification: spec).run(() => testFn(log));
|
|
||||||
};
|
|
||||||
|
|
||||||
class TestCache extends QueryCache {
|
|
||||||
bool get returnPartialData => debuggingUnexpectedTestFailures;
|
|
||||||
|
|
||||||
get partialDataPolicy => PartialDataCachePolicy.reject;
|
|
||||||
}
|
|
||||||
|
|
||||||
QueryCache getTestCache() => TestCache();
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
/// Web Socket echo server
|
|
||||||
/// to run the test and cover the web socket test
|
|
||||||
///
|
|
||||||
/// author: https://github.com/vincenzopalazzo
|
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
const String forceDisconnectCommand = '___force_disconnect___';
|
|
||||||
|
|
||||||
/// Main function to create and run the echo server over the web socket.
|
|
||||||
Future<String> runWebSocketServer(
|
|
||||||
{String host = "127.0.0.1", int port = 5600}) async {
|
|
||||||
HttpServer server = await HttpServer.bind(host, port);
|
|
||||||
server.transform(WebSocketTransformer()).listen(onWebSocketData);
|
|
||||||
return "ws://$host:$port";
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handle event received on server.
|
|
||||||
void onWebSocketData(WebSocket client) {
|
|
||||||
client.listen((data) async {
|
|
||||||
if (data != null && data.toString().contains(forceDisconnectCommand)) {
|
|
||||||
client.close(WebSocketStatus.normalClosure, 'shutting down');
|
|
||||||
} else {
|
|
||||||
client.add(data);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
import 'package:gql/ast.dart';
|
|
||||||
import 'package:gql/language.dart';
|
|
||||||
import 'package:fl_query/fl_query.dart';
|
|
||||||
import 'package:test/test.dart';
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
group('query options', () {
|
|
||||||
group('type getters', () {
|
|
||||||
test('on QueryOptions', () {
|
|
||||||
final options = QueryOptions(
|
|
||||||
document: parseString('query { bar }'),
|
|
||||||
);
|
|
||||||
expect(options.type, equals(OperationType.query));
|
|
||||||
expect(options.isQuery, equals(true));
|
|
||||||
});
|
|
||||||
test('on MutationOptions', () {
|
|
||||||
final options = MutationOptions(
|
|
||||||
document: parseString('mutation { bar }'),
|
|
||||||
);
|
|
||||||
expect(options.type, equals(OperationType.mutation));
|
|
||||||
expect(options.isMutation, equals(true));
|
|
||||||
});
|
|
||||||
test('on SubscriptionOptions', () {
|
|
||||||
final options = SubscriptionOptions(
|
|
||||||
document: parseString('subscription { bar }'),
|
|
||||||
);
|
|
||||||
expect(options.type, equals(OperationType.subscription));
|
|
||||||
expect(options.isSubscription, equals(true));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
group('gql integration', () {
|
|
||||||
test('Options.asRequest', () {
|
|
||||||
final options = QueryOptions(
|
|
||||||
document: parseString('query { bar }'),
|
|
||||||
variables: {
|
|
||||||
'foo': {
|
|
||||||
'biz': 'bar',
|
|
||||||
'bam': [1]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
context: Context.fromList([
|
|
||||||
HttpLinkHeaders(headers: {'my': 'header'})
|
|
||||||
]));
|
|
||||||
final req = options.asRequest;
|
|
||||||
expect(options.document, equals(req.operation.document));
|
|
||||||
expect(options.variables, equals(req.variables));
|
|
||||||
expect(options.context, equals(req.context));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Binary file not shown.
Binary file not shown.
@@ -1,70 +0,0 @@
|
|||||||
import 'dart:collection';
|
|
||||||
|
|
||||||
import 'package:test/test.dart';
|
|
||||||
|
|
||||||
import 'package:fl_query/src/utilities/helpers.dart';
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
group('deeplyMergeLeft', () {
|
|
||||||
test('shallow', () {
|
|
||||||
expect(
|
|
||||||
deeplyMergeLeft([
|
|
||||||
{'keyA': 'a1'},
|
|
||||||
{'keyA': 'a2', 'keyB': 'b2'},
|
|
||||||
{'keyB': 'b3'}
|
|
||||||
]),
|
|
||||||
equals({'keyA': 'a2', 'keyB': 'b3'}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('deep', () {
|
|
||||||
expect(
|
|
||||||
deeplyMergeLeft([
|
|
||||||
<String, dynamic>{
|
|
||||||
'keyA': 'a1',
|
|
||||||
'keyB': {
|
|
||||||
'keyC': {'keyD': 'd1'}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
<String, dynamic>{
|
|
||||||
'keyA': 'a2',
|
|
||||||
'keyB': {
|
|
||||||
'keyC': {'keyD': 'd2'}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
equals({
|
|
||||||
'keyA': 'a2',
|
|
||||||
'keyB': {
|
|
||||||
'keyC': {'keyD': 'd2'}
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('deep hashmaps are merged', () {
|
|
||||||
expect(
|
|
||||||
deeplyMergeLeft([
|
|
||||||
HashMap<String, dynamic>.from({
|
|
||||||
'keyA': 'a1',
|
|
||||||
'keyB': {
|
|
||||||
'keyC': HashMap.from({'keyD': 'd1'})
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
'keyA': 'a2',
|
|
||||||
'keyB': {
|
|
||||||
'keyC': HashMap.from({'keyD': 'd2'})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
]),
|
|
||||||
equals({
|
|
||||||
'keyA': 'a2',
|
|
||||||
'keyB': {
|
|
||||||
'keyC': {'keyD': 'd2'}
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,431 +0,0 @@
|
|||||||
import 'dart:async';
|
|
||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:test/test.dart';
|
|
||||||
import 'dart:convert';
|
|
||||||
import 'dart:typed_data';
|
|
||||||
|
|
||||||
import 'package:gql/language.dart';
|
|
||||||
import 'package:fl_query/fl_query.dart';
|
|
||||||
|
|
||||||
import './helpers.dart';
|
|
||||||
import './mock_server/ws_echo_server.dart';
|
|
||||||
import 'mock_server/ws_echo_server.dart';
|
|
||||||
|
|
||||||
SocketClient getTestClient(
|
|
||||||
{required String wsUrl,
|
|
||||||
StreamController? controller,
|
|
||||||
bool autoReconnect = true,
|
|
||||||
Map<String, dynamic>? customHeaders,
|
|
||||||
Duration delayBetweenReconnectionAttempts =
|
|
||||||
const Duration(milliseconds: 1)}) =>
|
|
||||||
SocketClient(
|
|
||||||
wsUrl,
|
|
||||||
config: SocketClientConfig(
|
|
||||||
autoReconnect: autoReconnect,
|
|
||||||
headers: customHeaders,
|
|
||||||
delayBetweenReconnectionAttempts: delayBetweenReconnectionAttempts,
|
|
||||||
),
|
|
||||||
randomBytesForUuid: Uint8List.fromList(
|
|
||||||
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
Future<void> main() async {
|
|
||||||
String wsUrl = await runWebSocketServer();
|
|
||||||
group('InitOperation', () {
|
|
||||||
test('null payload', () {
|
|
||||||
// ignore: deprecated_member_use_from_same_package
|
|
||||||
final operation = InitOperation(null);
|
|
||||||
expect(operation.toJson(), {'type': 'connection_init'});
|
|
||||||
});
|
|
||||||
test('simple payload', () {
|
|
||||||
// ignore: deprecated_member_use_from_same_package
|
|
||||||
final operation = InitOperation(42);
|
|
||||||
expect(operation.toJson(), {'type': 'connection_init', 'payload': 42});
|
|
||||||
});
|
|
||||||
test('complex payload', () {
|
|
||||||
// ignore: deprecated_member_use_from_same_package
|
|
||||||
final operation = InitOperation({
|
|
||||||
'value': 42,
|
|
||||||
'nested': {
|
|
||||||
'number': [3, 7],
|
|
||||||
'string': ['foo', 'bar']
|
|
||||||
}
|
|
||||||
});
|
|
||||||
expect(operation.toJson(), {
|
|
||||||
'type': 'connection_init',
|
|
||||||
'payload': {
|
|
||||||
'value': 42,
|
|
||||||
'nested': {
|
|
||||||
'number': [3, 7],
|
|
||||||
'string': ['foo', 'bar']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
group('SocketClient without payload', () {
|
|
||||||
late SocketClient socketClient;
|
|
||||||
StreamController controller;
|
|
||||||
final expectedMessage = r'{'
|
|
||||||
r'"type":"start","id":"01020304-0506-4708-890a-0b0c0d0e0f10",'
|
|
||||||
r'"payload":{"operationName":null,"variables":{},"query":"subscription {\n \n}"}'
|
|
||||||
r'}';
|
|
||||||
setUp(overridePrint((log) {
|
|
||||||
controller = StreamController(sync: true);
|
|
||||||
socketClient = getTestClient(controller: controller, wsUrl: wsUrl);
|
|
||||||
}));
|
|
||||||
tearDown(overridePrint(
|
|
||||||
(log) => socketClient.dispose(),
|
|
||||||
));
|
|
||||||
test('connection', () async {
|
|
||||||
await expectLater(
|
|
||||||
socketClient.connectionState.asBroadcastStream(),
|
|
||||||
emitsInOrder(
|
|
||||||
[
|
|
||||||
SocketConnectionState.connecting,
|
|
||||||
SocketConnectionState.connected,
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
test('disconnect via dispose', () async {
|
|
||||||
// First wait for connection to complete
|
|
||||||
await expectLater(
|
|
||||||
socketClient.connectionState.asBroadcastStream(),
|
|
||||||
emitsInOrder(
|
|
||||||
[
|
|
||||||
SocketConnectionState.connecting,
|
|
||||||
SocketConnectionState.connected,
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
// We need to begin waiting on the connectionState
|
|
||||||
// before we issue the command to disconnect; otherwise
|
|
||||||
// it can reconnect so fast that it will be reconnected
|
|
||||||
// by the time that the expectLater check is initiated.
|
|
||||||
await overridePrint((_) async {
|
|
||||||
Timer(const Duration(milliseconds: 20), () async {
|
|
||||||
await socketClient.dispose();
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
// The connectionState BehaviorController emits the current state
|
|
||||||
// to any new listener, so we expect it to start in the connected
|
|
||||||
// state and transition to notConnected because of dispose.
|
|
||||||
await expectLater(
|
|
||||||
socketClient.connectionState,
|
|
||||||
emitsInOrder([
|
|
||||||
SocketConnectionState.connected,
|
|
||||||
SocketConnectionState.notConnected,
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Have to wait for socket close to be fully processed after we reach
|
|
||||||
// the notConnected state, including updating channel with close code.
|
|
||||||
await Future.delayed(const Duration(milliseconds: 20));
|
|
||||||
|
|
||||||
// The websocket should be in a fully closed state at this point,
|
|
||||||
// we should have a confirmed close code in the channel.
|
|
||||||
expect(socketClient.socketChannel, isNotNull);
|
|
||||||
expect(socketClient.socketChannel!.closeCode, isNotNull);
|
|
||||||
});
|
|
||||||
test('subscription data', () async {
|
|
||||||
final payload = Request(
|
|
||||||
operation: Operation(document: parseString('subscription {}')),
|
|
||||||
);
|
|
||||||
final waitForConnection = true;
|
|
||||||
final subscriptionDataStream =
|
|
||||||
socketClient.subscribe(payload, waitForConnection);
|
|
||||||
await socketClient.connectionState
|
|
||||||
.where((state) => state == SocketConnectionState.connected)
|
|
||||||
.first;
|
|
||||||
|
|
||||||
// ignore: unawaited_futures
|
|
||||||
socketClient.socketChannel!.stream
|
|
||||||
.where((message) => message == expectedMessage)
|
|
||||||
.first
|
|
||||||
.then((_) {
|
|
||||||
socketClient.socketChannel!.sink.add(jsonEncode({
|
|
||||||
'type': 'data',
|
|
||||||
'id': '01020304-0506-4708-890a-0b0c0d0e0f10',
|
|
||||||
'payload': {
|
|
||||||
'data': {'foo': 'bar'},
|
|
||||||
'errors': [
|
|
||||||
{'message': 'error and data can coexist'}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
await expectLater(
|
|
||||||
subscriptionDataStream,
|
|
||||||
emits(
|
|
||||||
// todo should ids be included in response context? probably '01020304-0506-4708-890a-0b0c0d0e0f10'
|
|
||||||
Response(
|
|
||||||
data: {'foo': 'bar'},
|
|
||||||
errors: [
|
|
||||||
GraphQLError(message: 'error and data can coexist'),
|
|
||||||
],
|
|
||||||
context: Context().withEntry(ResponseExtensions(null)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
test('resubscribe', () async {
|
|
||||||
final payload = Request(
|
|
||||||
operation: Operation(document: gql('subscription {}')),
|
|
||||||
);
|
|
||||||
final waitForConnection = true;
|
|
||||||
final subscriptionDataStream =
|
|
||||||
socketClient.subscribe(payload, waitForConnection);
|
|
||||||
|
|
||||||
await expectLater(
|
|
||||||
socketClient.connectionState,
|
|
||||||
emitsInOrder([
|
|
||||||
SocketConnectionState.connecting,
|
|
||||||
SocketConnectionState.connected,
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
|
|
||||||
await overridePrint((_) async {
|
|
||||||
socketClient.onConnectionLost();
|
|
||||||
})();
|
|
||||||
|
|
||||||
await expectLater(
|
|
||||||
socketClient.connectionState,
|
|
||||||
emitsInOrder([
|
|
||||||
SocketConnectionState.notConnected,
|
|
||||||
SocketConnectionState.connecting,
|
|
||||||
SocketConnectionState.connected,
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
|
|
||||||
// ignore: unawaited_futures
|
|
||||||
socketClient.socketChannel!.stream
|
|
||||||
.where((message) => message == expectedMessage)
|
|
||||||
.first
|
|
||||||
.then((_) {
|
|
||||||
socketClient.socketChannel!.sink.add(jsonEncode({
|
|
||||||
'type': 'data',
|
|
||||||
'id': '01020304-0506-4708-890a-0b0c0d0e0f10',
|
|
||||||
'payload': {
|
|
||||||
'data': {'foo': 'bar'},
|
|
||||||
'errors': [
|
|
||||||
{'message': 'error and data can coexist'}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
await expectLater(
|
|
||||||
subscriptionDataStream,
|
|
||||||
emits(
|
|
||||||
// todo should ids be included in response context? probably '01020304-0506-4708-890a-0b0c0d0e0f10'
|
|
||||||
Response(
|
|
||||||
data: {'foo': 'bar'},
|
|
||||||
errors: [
|
|
||||||
GraphQLError(message: 'error and data can coexist'),
|
|
||||||
],
|
|
||||||
context: Context().withEntry(ResponseExtensions(null)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
test('resubscribe after server disconnect', () async {
|
|
||||||
final payload = Request(
|
|
||||||
operation: Operation(document: gql('subscription {}')),
|
|
||||||
);
|
|
||||||
final waitForConnection = true;
|
|
||||||
final subscriptionDataStream =
|
|
||||||
socketClient.subscribe(payload, waitForConnection);
|
|
||||||
|
|
||||||
await expectLater(
|
|
||||||
socketClient.connectionState,
|
|
||||||
emitsInOrder([
|
|
||||||
SocketConnectionState.connecting,
|
|
||||||
SocketConnectionState.connected,
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
|
|
||||||
// We need to begin waiting on the connectionState
|
|
||||||
// before we issue the command to disconnect; otherwise
|
|
||||||
// it can reconnect so fast that it will be reconnected
|
|
||||||
// by the time that the expectLater check is initiated.
|
|
||||||
Timer(const Duration(milliseconds: 20), () async {
|
|
||||||
socketClient.socketChannel!.sink.add(forceDisconnectCommand);
|
|
||||||
});
|
|
||||||
// The connectionState BehaviorController emits the current state
|
|
||||||
// to any new listener, so we expect it to start in the connected
|
|
||||||
// state, transition to notConnected, and then reconnect after that.
|
|
||||||
await expectLater(
|
|
||||||
socketClient.connectionState,
|
|
||||||
emitsInOrder([
|
|
||||||
SocketConnectionState.connected,
|
|
||||||
SocketConnectionState.notConnected,
|
|
||||||
SocketConnectionState.connecting,
|
|
||||||
SocketConnectionState.connected,
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
|
|
||||||
// ignore: unawaited_futures
|
|
||||||
socketClient.socketChannel!.stream
|
|
||||||
.where((message) => message == expectedMessage)
|
|
||||||
.first
|
|
||||||
.then((_) {
|
|
||||||
socketClient.socketChannel!.sink.add(jsonEncode({
|
|
||||||
'type': 'data',
|
|
||||||
'id': '01020304-0506-4708-890a-0b0c0d0e0f10',
|
|
||||||
'payload': {
|
|
||||||
'data': {'foo': 'bar'},
|
|
||||||
'errors': [
|
|
||||||
{'message': 'error and data can coexist'}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
await expectLater(
|
|
||||||
subscriptionDataStream,
|
|
||||||
emits(
|
|
||||||
// todo should ids be included in response context? probably '01020304-0506-4708-890a-0b0c0d0e0f10'
|
|
||||||
Response(
|
|
||||||
data: {'foo': 'bar'},
|
|
||||||
errors: [
|
|
||||||
GraphQLError(message: 'error and data can coexist'),
|
|
||||||
],
|
|
||||||
context: Context().withEntry(ResponseExtensions(null)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}, tags: "integration");
|
|
||||||
|
|
||||||
group('SocketClient without autoReconnect', () {
|
|
||||||
late SocketClient socketClient;
|
|
||||||
StreamController controller;
|
|
||||||
setUp(overridePrint((log) {
|
|
||||||
controller = StreamController(sync: true);
|
|
||||||
socketClient = getTestClient(
|
|
||||||
controller: controller, wsUrl: wsUrl, autoReconnect: false);
|
|
||||||
}));
|
|
||||||
tearDown(overridePrint(
|
|
||||||
(log) => socketClient.dispose(),
|
|
||||||
));
|
|
||||||
test('server disconnect', () async {
|
|
||||||
final payload = Request(
|
|
||||||
operation: Operation(document: gql('subscription {}')),
|
|
||||||
);
|
|
||||||
final waitForConnection = true;
|
|
||||||
socketClient.subscribe(payload, waitForConnection);
|
|
||||||
|
|
||||||
await expectLater(
|
|
||||||
socketClient.connectionState,
|
|
||||||
emitsInOrder([
|
|
||||||
SocketConnectionState.connecting,
|
|
||||||
SocketConnectionState.connected,
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
|
|
||||||
Timer(const Duration(milliseconds: 20), () async {
|
|
||||||
socketClient.socketChannel!.sink.add(forceDisconnectCommand);
|
|
||||||
});
|
|
||||||
// Same strategy as elsewhere, start expecting the state on the
|
|
||||||
// stream before the disconnect actually happens...
|
|
||||||
await expectLater(
|
|
||||||
socketClient.connectionState,
|
|
||||||
emitsInOrder([
|
|
||||||
SocketConnectionState.connected,
|
|
||||||
SocketConnectionState.notConnected,
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(
|
|
||||||
socketClient.socketChannel!.closeCode, WebSocketStatus.normalClosure);
|
|
||||||
});
|
|
||||||
}, tags: "integration");
|
|
||||||
|
|
||||||
group('SocketClient with const payload', () {
|
|
||||||
late SocketClient socketClient;
|
|
||||||
const initPayload = {'token': 'mytoken'};
|
|
||||||
|
|
||||||
setUp(overridePrint((log) {
|
|
||||||
socketClient = SocketClient(
|
|
||||||
wsUrl,
|
|
||||||
config: SocketClientConfig(initialPayload: () => initPayload),
|
|
||||||
);
|
|
||||||
}));
|
|
||||||
|
|
||||||
tearDown(overridePrint(
|
|
||||||
(log) => expectLater(
|
|
||||||
socketClient.dispose().timeout(Duration(seconds: 1)),
|
|
||||||
completion(null),
|
|
||||||
),
|
|
||||||
));
|
|
||||||
|
|
||||||
test('connection', () async {
|
|
||||||
await socketClient.connectionState
|
|
||||||
.where((state) => state == SocketConnectionState.connected)
|
|
||||||
.first;
|
|
||||||
|
|
||||||
await expectLater(
|
|
||||||
socketClient.socketChannel!.stream.map((s) {
|
|
||||||
return jsonDecode(s)['payload'];
|
|
||||||
}),
|
|
||||||
emits(initPayload));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
group('SocketClient with future payload', () {
|
|
||||||
late SocketClient socketClient;
|
|
||||||
const initPayload = {'token': 'mytoken'};
|
|
||||||
|
|
||||||
setUp(overridePrint((log) {
|
|
||||||
socketClient = SocketClient(
|
|
||||||
wsUrl,
|
|
||||||
config: SocketClientConfig(
|
|
||||||
initialPayload: () async {
|
|
||||||
await Future.delayed(Duration(seconds: 3));
|
|
||||||
return initPayload;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}));
|
|
||||||
|
|
||||||
tearDown(overridePrint((log) async {
|
|
||||||
await socketClient.dispose();
|
|
||||||
}));
|
|
||||||
|
|
||||||
test('connection', () async {
|
|
||||||
await socketClient.connectionState
|
|
||||||
.where((state) => state == SocketConnectionState.connected)
|
|
||||||
.first;
|
|
||||||
|
|
||||||
await expectLater(
|
|
||||||
socketClient.socketChannel!.stream.map((s) {
|
|
||||||
return jsonDecode(s)['payload'];
|
|
||||||
}),
|
|
||||||
emits(initPayload),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
/*
|
|
||||||
FIXME: Testing the correct header in the request
|
|
||||||
group('SocketClient with custom headers with const payload', () {
|
|
||||||
const customHeaders = {'myHeader': 'myHeader'};
|
|
||||||
|
|
||||||
setUp(overridePrint((log) {
|
|
||||||
socketClient = getTestClient(wsUrl: wsUrl, customHeaders: customHeaders);
|
|
||||||
}));
|
|
||||||
|
|
||||||
test('check header', () async {
|
|
||||||
await socketClient.connectionState
|
|
||||||
.where((state) => state == SocketConnectionState.notConnected)
|
|
||||||
.first;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
*/
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user