QueryObserver test created

This commit is contained in:
Kingkor Roy Tirtho
2022-04-13 13:53:02 +06:00
parent 06e15c3096
commit 181c5f8616
7 changed files with 914 additions and 12 deletions
+13 -3
View File
@@ -120,7 +120,7 @@ class QueryFilters {
bool? exact; bool? exact;
bool? inactive; bool? inactive;
bool Function(Query query)? predicate; bool Function(Query query)? predicate;
bool? queryKey; QueryKey? queryKey;
bool? stale; bool? stale;
bool? fetching; bool? fetching;
@@ -162,7 +162,7 @@ class RefetchableQueryFilters<TPageData> extends QueryFilters
bool? exact, bool? exact,
bool? inactive, bool? inactive,
bool Function(Query query)? predicate, bool Function(Query query)? predicate,
bool? queryKey, QueryKey? queryKey,
bool? stale, bool? stale,
bool? fetching, bool? fetching,
this.refetchPage, this.refetchPage,
@@ -212,7 +212,7 @@ class InvalidateQueryFilters<TPageData>
bool? exact, bool? exact,
bool? inactive, bool? inactive,
bool Function(Query query)? predicate, bool Function(Query query)? predicate,
bool? queryKey, QueryKey? queryKey,
bool? stale, bool? stale,
bool? fetching, bool? fetching,
bool Function(TPageData lastPage, int index, List<TPageData> allPages)? bool Function(TPageData lastPage, int index, List<TPageData> allPages)?
@@ -452,6 +452,11 @@ class QueryObserverOptions<
QueryMeta? meta, QueryMeta? meta,
bool? structuralSharing, bool? structuralSharing,
bool? defaulted, bool? defaulted,
ShouldRetryFunction<TError>? retry,
RetryDelayFunction<TError>? retryDelay,
QueryBehavior<TQueryFnData, TError, TQueryData>? behavior,
GetPreviousPageParamFunction<TQueryFnData>? getPreviousPageParam,
GetNextPageParamFunction<TQueryFnData>? getNextPageParam,
}) : super( }) : super(
queryKey: queryKey, queryKey: queryKey,
queryKeyHashFn: queryKeyHashFn, queryKeyHashFn: queryKeyHashFn,
@@ -464,6 +469,11 @@ class QueryObserverOptions<
meta: meta, meta: meta,
structuralSharing: structuralSharing, structuralSharing: structuralSharing,
defaulted: defaulted, defaulted: defaulted,
behavior: behavior,
getNextPageParam: getNextPageParam,
getPreviousPageParam: getPreviousPageParam,
retry: retry,
retryDelay: retryDelay,
); );
QueryObserverOptions.fromJson(Map<String, dynamic> json) QueryObserverOptions.fromJson(Map<String, dynamic> json)
+6 -2
View File
@@ -1,4 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:collection';
import 'dart:convert';
import 'dart:math'; import 'dart:math';
import 'package:fl_query/src/core/models.dart'; import 'package:fl_query/src/core/models.dart';
@@ -280,7 +282,9 @@ class Query<TQueryFnData extends Map<String, dynamic>, TError,
data = prevData as TData; data = prevData as TData;
} else if (this.options.structuralSharing != false) { } else if (this.options.structuralSharing != false) {
// Structurally share data between prev and new data if needed // Structurally share data between prev and new data if needed
data = replaceEqualDeep(prevData, data); final merged =
Map<String, dynamic>.from(replaceEqualDeep(prevData, data));
data = merged as TData;
} }
// Set data and mark it as cached // Set data and mark it as cached
_dispatch(Action( _dispatch(Action(
@@ -360,7 +364,7 @@ class Query<TQueryFnData extends Map<String, dynamic>, TError,
// Use the options from the first observer with a query function if no function is found. // Use the options from the first observer with a query function if no function is found.
// This can happen when the query is hydrated or created with setQueryData. // This can happen when the query is hydrated or created with setQueryData.
if (this.options.queryFn == null) { if (this.options.queryFn == null) {
var observer = final observer =
_observers.firstWhereOrNull((x) => x.options.queryFn != null); _observers.firstWhereOrNull((x) => x.options.queryFn != null);
if (observer != null) { if (observer != null) {
_setOptions(QueryOptions<TQueryFnData, TError, TData>( _setOptions(QueryOptions<TQueryFnData, TError, TData>(
@@ -171,7 +171,7 @@ class QueryCache extends Subscribable<QueryCacheListener> {
void notify(QueryCacheNotifyEvent event) { void notify(QueryCacheNotifyEvent event) {
notifyManager.batch(() { notifyManager.batch(() {
for (var listener in listeners) { for (final listener in listeners) {
listener(event); listener(event);
} }
}); });
@@ -266,7 +266,7 @@ class QueryClient {
QueryFunction<TQueryFnData, dynamic>? queryFn, QueryFunction<TQueryFnData, dynamic>? queryFn,
FetchQueryOptions<TQueryFnData, TError, TData>? options, FetchQueryOptions<TQueryFnData, TError, TData>? options,
}) { }) {
var defaultedOptions = this.defaultQueryOptions( final defaultedOptions = this.defaultQueryOptions(
QueryObserverOptions<Map<String, dynamic>, dynamic, Map<String, dynamic>, QueryObserverOptions<Map<String, dynamic>, dynamic, Map<String, dynamic>,
TData>( TData>(
queryFn: queryFn, queryFn: queryFn,
@@ -285,7 +285,7 @@ class QueryClient {
); );
// returning 0 indicates turing off retry // returning 0 indicates turing off retry
defaultedOptions.retry ??= (_, __) => 0; defaultedOptions.retry ??= (_, __) => 0;
var query = _queryCache.build<Map<String, dynamic>, dynamic, TData>( final query = _queryCache.build<Map<String, dynamic>, dynamic, TData>(
this, defaultedOptions); this, defaultedOptions);
return query.isStaleByTime(defaultedOptions.staleTime) return query.isStaleByTime(defaultedOptions.staleTime)
? query.fetch(defaultedOptions) ? query.fetch(defaultedOptions)
@@ -313,7 +313,7 @@ class QueryClient {
TQueryData extends Map<String, dynamic>>( TQueryData extends Map<String, dynamic>>(
QueryObserverOptions<TQueryFnData, TError, TData, TQueryData>? options) { QueryObserverOptions<TQueryFnData, TError, TData, TQueryData>? options) {
if (options?.defaulted == true) return options!; if (options?.defaulted == true) return options!;
var defaultedOptions = final defaultedOptions =
QueryObserverOptions<TQueryFnData, TError, TData, TQueryData>.fromJson({ QueryObserverOptions<TQueryFnData, TError, TData, TQueryData>.fromJson({
...(_defaultOptions.queries?.toJson() ?? {}), ...(_defaultOptions.queries?.toJson() ?? {}),
...(getQueryDefaults(options?.queryKey)?.toJson() ?? {}), ...(getQueryDefaults(options?.queryKey)?.toJson() ?? {}),
@@ -94,6 +94,9 @@ class QueryObserver<
Timer? _refetchInterval; Timer? _refetchInterval;
Duration? _currentRefetchInterval; Duration? _currentRefetchInterval;
@protected
Timer? get refetchInterval => _refetchInterval;
QueryObserver( QueryObserver(
this._client, this._client,
QueryObserverOptions<TQueryFnData, TError, TData, TQueryData>? _options, QueryObserverOptions<TQueryFnData, TError, TData, TQueryData>? _options,
@@ -225,9 +228,9 @@ class QueryObserver<
throw UnimplementedError("COULD NOT IMPLEMENT DUE TO LANGUAGE LIMITATIONS"); throw UnimplementedError("COULD NOT IMPLEMENT DUE TO LANGUAGE LIMITATIONS");
} }
Future<QueryObserverResult<TData, TError>> getNextResult( Future<QueryObserverResult<TData, TError>> getNextResult([
bool? throwOnError, bool? throwOnError,
) { ]) {
final completer = Completer<QueryObserverResult<TData, TError>>(); final completer = Completer<QueryObserverResult<TData, TError>>();
var unsubscribe; var unsubscribe;
unsubscribe = subscribe((result) { unsubscribe = subscribe((result) {
@@ -445,7 +448,8 @@ class QueryObserver<
try { try {
data = options.select?.call(state.data); data = options.select?.call(state.data);
if (options.structuralSharing != false) { if (options.structuralSharing != false) {
data = replaceEqualDeep(prevResult?.data, data); data = Map<String, dynamic>.from(
replaceEqualDeep(prevResult?.data, data)) as TData;
} }
if (options.select != null && data != null) { if (options.select != null && data != null) {
_previousSelect = SelectQuery<TQueryData, TData>( _previousSelect = SelectQuery<TQueryData, TData>(
+43
View File
@@ -6,3 +6,46 @@ Uuid uuid = Uuid();
QueryKey queryKey() { QueryKey queryKey() {
return QueryKey("query_${uuid.v4()}"); return QueryKey("query_${uuid.v4()}");
} }
class SpyFn<T extends Function()> {
int _calls = 0;
late T _customFn;
int get calls => _calls;
T get customFn => _customFn;
SpyFn();
SpyFn.withFn(this._customFn);
fn([Function()? cb]) {
_calls++;
return () => cb?.call();
}
fn1([Function()? cb]) {
_calls++;
return (p0) => cb?.call();
}
fn2([Function()? cb]) {
_calls++;
return (p0, p1) => cb?.call();
}
fn3([Function()? cb]) {
_calls++;
return (p0, p1, p2) => cb?.call();
}
fn4([Function()? cb]) {
_calls++;
return (p0, p1, p2, p3) => cb?.call();
}
fn5([Function()? cb]) {
_calls++;
return (p0, p1, p2, p3, p4) => cb?.call();
}
}
Future<void> sleep(int ms) => Future.delayed(Duration(milliseconds: ms));
@@ -1,8 +1,14 @@
import 'dart:async';
import 'package:fl_query/src/core/core.dart'; import 'package:fl_query/src/core/core.dart';
import 'package:fl_query/src/core/query_observer.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
import '../../helpers/utils.dart'; import '../../helpers/utils.dart';
typedef QueryFn = FutureOr<Map<String, dynamic>> Function(
QueryFunctionContext<dynamic>);
void main() { void main() {
group('QueryObserver', () { group('QueryObserver', () {
late QueryClient queryClient; late QueryClient queryClient;
@@ -18,6 +24,7 @@ void main() {
test('should trigger a fetch when subscribed', () async { test('should trigger a fetch when subscribed', () async {
final key = queryKey(); final key = queryKey();
;
int calls = 0; int calls = 0;
queryFn(context) { queryFn(context) {
calls++; calls++;
@@ -64,5 +71,839 @@ void main() {
expect(results[3].data, {"data": 2}); expect(results[3].data, {"data": 2});
expect(results[3].status, QueryStatus.success); expect(results[3].status, QueryStatus.success);
}); });
test('should be able to fetch with a selector', () async {
final key = queryKey();
;
final observer = QueryObserver<Map<String, dynamic>, dynamic,
Map<String, dynamic>, Map<String, dynamic>>(
queryClient,
QueryObserverOptions<Map<String, dynamic>, dynamic,
Map<String, dynamic>, Map<String, dynamic>>(
queryKey: key,
queryFn: (_) => {"count": 1},
select: (data) => ({"myCount": data?["count"]}),
));
QueryObserverResult? observerResult;
final unsubscribe = observer.subscribe((result) {
observerResult = result;
});
await Future.delayed(Duration(milliseconds: 1));
unsubscribe();
expect(
observerResult?.data,
equals({"myCount": 1}),
);
});
test('should be able to fetch with a selector using the fetch method',
() async {
final key = queryKey();
;
final observer = QueryObserver<Map<String, dynamic>, dynamic,
Map<String, dynamic>, Map<String, dynamic>>(
queryClient,
QueryObserverOptions<Map<String, dynamic>, dynamic,
Map<String, dynamic>, Map<String, dynamic>>(
queryKey: key,
queryFn: (_) => {"count": 1},
select: (data) => ({"myCount": data?["count"]}),
));
final observerResult = await observer.refetch();
expect(observerResult?.data, equals({"myCount": 1}));
});
test('should run the selector again if the data changed', () async {
final key = queryKey();
;
int count = 0;
final observer = QueryObserver<Map<String, dynamic>, dynamic,
Map<String, dynamic>, Map<String, dynamic>>(
queryClient,
QueryObserverOptions<Map<String, dynamic>, dynamic,
Map<String, dynamic>, Map<String, dynamic>>(
queryKey: key,
queryFn: (_) => Map.from({"count": count}),
select: (data) {
count++;
return Map.from({"myCount": data?["count"]});
},
));
final observerResult1 = await observer.refetch();
final observerResult2 = await observer.refetch();
expect(count, 2);
expect(observerResult1?.data, equals({"myCount": 0}));
expect(observerResult2?.data, equals({"myCount": 1}));
});
test('should run the selector again if the selector changed', () async {
final key = queryKey();
;
int count = 0;
final List<QueryObserverResult> results = [];
final queryFn = (_) => ({"count": 1});
select1(data) {
count++;
return {"myCount": data?["count"]};
}
select2(_data) {
count++;
return {"myCount": 99};
}
final observer = new QueryObserver(
queryClient,
QueryObserverOptions(
queryKey: key,
queryFn: queryFn,
select: select1,
));
final unsubscribe = observer.subscribe((result) {
results.add(result);
});
await Future.delayed(Duration(milliseconds: 1));
observer.setOptions(QueryObserverOptions(
queryKey: key,
queryFn: queryFn,
select: select2,
));
await Future.delayed(Duration(milliseconds: 1));
await observer.refetch();
unsubscribe();
expect(count, 2);
expect(results.length, 5);
expect(results.first.status, QueryStatus.loading);
expect(results.first.isFetching, isTrue);
expect(results.first.data, isNull);
expect(results[1].status, QueryStatus.success);
expect(results[1].isFetching, false);
expect(results[1].data, {"myCount": 1});
expect(results[2].status, QueryStatus.success);
expect(results[2].isFetching, false);
expect(results[2].data, {"myCount": 99});
expect(results[3].status, QueryStatus.success);
expect(results[3].isFetching, true);
expect(results[3].data, {"myCount": 99});
expect(results.last.status, QueryStatus.success);
expect(results.last.isFetching, false);
expect(results.last.data, {"myCount": 99});
});
test(
'should not run the selector again if the data and selector did not change',
() async {
final key = queryKey();
;
int count = 0;
final List<QueryObserverResult> results = [];
final queryFn = (_) => {"count": 1};
select(data) {
count++;
return {"myCount": data["count"]};
}
final observer = new QueryObserver(
queryClient,
QueryObserverOptions(
queryKey: key,
queryFn: queryFn,
select: select,
));
final unsubscribe = observer.subscribe((result) {
results.add(result);
});
await Future.delayed(Duration(milliseconds: 1));
observer.setOptions(QueryObserverOptions(
queryKey: key,
queryFn: queryFn,
select: select,
));
await Future.delayed(Duration(milliseconds: 1));
await observer.refetch();
unsubscribe();
expect(count, 1);
expect(results.length, 4);
expect(results.first.status, QueryStatus.loading);
expect(results.first.isFetching, isTrue);
expect(results.first.data, isNull);
expect(results[1].status, QueryStatus.success);
expect(results[1].isFetching, false);
expect(results[1].data, {"myCount": 1});
expect(results[2].status, QueryStatus.success);
expect(results[2].isFetching, true);
expect(results[2].data, {"myCount": 1});
expect(results.last.status, QueryStatus.success);
expect(results.last.isFetching, false);
expect(results.last.data, {"myCount": 1});
});
test('should not run the selector again if the data did not change',
() async {
final key = queryKey();
;
int count = 0;
final observer = new QueryObserver<Map<String, dynamic>, dynamic,
Map<String, dynamic>, Map<String, dynamic>>(
queryClient,
QueryObserverOptions<Map<String, dynamic>, dynamic,
Map<String, dynamic>, Map<String, dynamic>>(
queryKey: key,
queryFn: (_) => {"count": count},
select: (data) {
count++;
return {"myCount": data?["count"]};
},
),
);
final observerResult1 = await observer.refetch();
final observerResult2 = await observer.refetch();
expect(count, 1);
expect(observerResult1?.data, equals({"myCount": 1}));
expect(observerResult2?.data, equals({"myCount": 1}));
});
test('should always run the selector again if selector throws an error',
() async {
final key = queryKey();
;
final List<QueryObserverResult> results = [];
select(data) {
throw new Exception('selector error');
}
queryFn(_) => ({"count": 1});
final observer = new QueryObserver<Map<String, dynamic>, dynamic,
Map<String, dynamic>, Map<String, dynamic>>(
queryClient,
QueryObserverOptions<Map<String, dynamic>, dynamic,
Map<String, dynamic>, Map<String, dynamic>>(
queryKey: key,
queryFn: queryFn,
select: select,
));
final unsubscribe = observer.subscribe((result) {
results.add(result);
});
await Future.delayed(Duration(milliseconds: 1));
await observer.refetch();
unsubscribe();
expect(results.length, 5);
expect(results.first.status, QueryStatus.loading);
expect(results.first.isFetching, isTrue);
expect(results.first.data, isNull);
expect(results[1].status, QueryStatus.error);
expect(results[1].isFetching, false);
expect(results[1].data, isNull);
expect(results[2].status, QueryStatus.error);
expect(results[2].isFetching, true);
expect(results[2].data, isNull);
expect(results[3].status, QueryStatus.error);
expect(results[3].isFetching, false);
expect(results[3].data, isNull);
expect(results.last.status, QueryStatus.error);
expect(results.last.isFetching, false);
expect(results.last.data, isNull);
});
test('should structurally share the selector', () async {
final key = queryKey();
;
int count = 0;
final observer = new QueryObserver(
queryClient,
QueryObserverOptions<Map<String, dynamic>, dynamic,
Map<String, dynamic>, Map<String, dynamic>>(
queryKey: key,
queryFn: (_) => {"count": ++count},
select: (data) {
return {"myCount": 1};
},
));
final observerResult1 = await observer.refetch();
final observerResult2 = await observer.refetch();
expect(count, 2);
expect(observerResult1?.data, isNotNull);
expect(observerResult1?.data, equals(observerResult2?.data));
});
test('should not trigger a fetch when subscribed and disabled', () async {
int count = 0;
final key = queryKey();
;
final observer = new QueryObserver(
queryClient,
QueryObserverOptions(
queryKey: key,
queryFn: (_) {
count++;
return {"data": null};
},
enabled: false,
));
final unsubscribe = observer.subscribe();
await Future.delayed(Duration(milliseconds: 1));
unsubscribe();
expect(count, 0);
});
test('should not trigger a fetch when not subscribed', () async {
int count = 0;
final key = queryKey();
;
new QueryObserver(
queryClient,
QueryObserverOptions(
queryKey: key,
queryFn: (_) {
count++;
return {"data": null};
},
));
await Future.delayed(Duration(milliseconds: 1));
expect(count, 0);
});
test('should be able to watch a query without defining a query function',
() async {
int count = 0;
int subscribeCount = 0;
final key = queryKey();
;
queryFn(_) {
count++;
return {"data": null};
}
final observer = QueryObserver<Map<String, dynamic>, dynamic,
Map<String, dynamic>, Map<String, dynamic>>(
queryClient,
QueryObserverOptions<Map<String, dynamic>, dynamic,
Map<String, dynamic>, Map<String, dynamic>>(
queryKey: key,
queryFn: queryFn,
enabled: false,
));
final unsubscribe = observer.subscribe((_) {
subscribeCount++;
});
await queryClient.fetchQuery(queryKey: key, queryFn: queryFn);
unsubscribe();
expect(count, 1);
expect(subscribeCount, 2);
});
test('should accept unresolved query config in update function', () async {
final key = queryKey();
;
final observer = new QueryObserver<Map<String, dynamic>, dynamic,
Map<String, dynamic>, Map<String, dynamic>>(
queryClient,
QueryObserverOptions<Map<String, dynamic>, dynamic,
Map<String, dynamic>, Map<String, dynamic>>(
queryKey: key,
enabled: false,
));
final List<QueryObserverResult> results = [];
final unsubscribe = observer.subscribe((x) {
results.add(x);
});
observer.setOptions(
QueryObserverOptions(
enabled: false,
staleTime: Duration(milliseconds: 10),
),
);
int count = 0;
queryFn(_) {
count++;
return {"data": null};
}
await queryClient.fetchQuery(queryKey: key, queryFn: queryFn);
await Future.delayed(Duration(milliseconds: 100));
unsubscribe();
expect(count, 1);
expect(results.length, 3);
expect(results[0].isStale, true);
expect(results[1].isStale, false);
expect(results[2].isStale, true);
});
test('should be able to handle multiple subscribers', () async {
final key = queryKey();
int count = 0;
final observer = new QueryObserver<Map<String, dynamic>, dynamic,
Map<String, dynamic>, Map<String, dynamic>>(
queryClient,
QueryObserverOptions<Map<String, dynamic>, dynamic,
Map<String, dynamic>, Map<String, dynamic>>(
queryKey: key,
enabled: false,
));
final List<QueryObserverResult> results1 = [];
final List<QueryObserverResult> results2 = [];
final unsubscribe1 = observer.subscribe((x) {
results1.add(x);
});
final unsubscribe2 = observer.subscribe((x) {
results2.add(x);
});
await queryClient
.fetchQuery<Map<String, dynamic>, dynamic, Map<String, dynamic>>(
queryKey: key,
queryFn: (_) {
count++;
return {"data": false};
},
);
await sleep(50);
unsubscribe1();
unsubscribe2();
expect(count, 1);
expect(results1.length, 2);
expect(results2.length, 2);
expect(results1[0].data?["data"], isNull);
expect(results1[1].data?["data"], isFalse);
expect(results2[0].data?["data"], isNull);
expect(results2[1].data?["data"], isFalse);
});
test('should be able to resolve a promise', () async {
final key = queryKey();
int count = 0;
final observer = new QueryObserver<Map<String, dynamic>, dynamic,
Map<String, dynamic>, Map<String, dynamic>>(
queryClient,
QueryObserverOptions<Map<String, dynamic>, dynamic,
Map<String, dynamic>, Map<String, dynamic>>(
queryKey: key,
enabled: false,
));
QueryObserverResult<Map<String, dynamic>, dynamic>? value;
observer.getNextResult().then((x) {
value = x;
});
queryClient
.prefetchQuery<Map<String, dynamic>, dynamic, Map<String, dynamic>>(
queryKey: key,
queryFn: (_) {
count++;
return {"data": "a data"};
});
await sleep(50);
expect(count, 1);
expect(value?.data?["data"], "a data");
});
test('should be able to resolve a promise with an error', () async {
final key = queryKey();
final observer = new QueryObserver<Map<String, dynamic>, dynamic,
Map<String, dynamic>, Map<String, dynamic>>(
queryClient,
QueryObserverOptions<Map<String, dynamic>, dynamic,
Map<String, dynamic>, Map<String, dynamic>>(
queryKey: key,
enabled: false,
));
var error;
observer.getNextResult(true).catchError((e) {
error = e;
});
queryClient
.prefetchQuery<Map<String, dynamic>, dynamic, Map<String, dynamic>>(
queryKey: key, queryFn: (_) => Future.error('reject'));
await sleep(50);
expect(error, 'reject');
});
test('should stop retry when unsubscribing', () async {
final key = queryKey();
int count = 0;
final observer = new QueryObserver<Map<String, dynamic>, dynamic,
Map<String, dynamic>, Map<String, dynamic>>(
queryClient,
QueryObserverOptions<Map<String, dynamic>, dynamic,
Map<String, dynamic>, Map<String, dynamic>>(
queryKey: key,
queryFn: (_) {
count++;
return Future.error({"data": 'reject'});
},
retry: (_, __) => 10,
retryDelay: (_, __) => 50,
),
);
final unsubscribe = observer.subscribe();
await sleep(70);
unsubscribe();
await sleep(200);
expect(count, 2);
});
test('should clear interval when unsubscribing to a refetchInterval query',
() async {
final key = queryKey();
final fetchData =
(_) => Future<Map<String, dynamic>>.error({'data': null});
final observer = new QueryObserver<Map<String, dynamic>, dynamic,
Map<String, dynamic>, Map<String, dynamic>>(
queryClient,
QueryObserverOptions<Map<String, dynamic>, dynamic,
Map<String, dynamic>, Map<String, dynamic>>(
queryKey: key,
queryFn: fetchData,
cacheTime: Duration.zero,
refetchInterval: (_, __) => Duration(milliseconds: 1),
));
final unsubscribe = observer.subscribe();
// @ts-expect-error
expect(observer.refetchInterval, isNull);
unsubscribe();
// @ts-expect-error
expect(observer.refetchInterval, isNotNull);
await sleep(10);
expect(queryClient.getQueryCache().find(key), isNotNull);
});
test(
'uses placeholderData as non-cache data when loading a query with no data',
() async {
final key = queryKey();
final observer = new QueryObserver<Map<String, dynamic>, dynamic,
Map<String, dynamic>, Map<String, dynamic>>(
queryClient,
QueryObserverOptions<Map<String, dynamic>, dynamic,
Map<String, dynamic>, Map<String, dynamic>>(
queryKey: key,
queryFn: (_) => {'data': 'data'},
placeholderData: {"data": 'placeholder'},
));
final result = observer.getCurrentResult();
expect(result?.status, QueryStatus.success);
final List<QueryObserverResult> results = [];
final unsubscribe = observer.subscribe((x) {
results.add(x);
});
await sleep(10);
unsubscribe();
expect(results.length, 2);
expect(results.first.status, QueryStatus.success);
expect(results.first.data, equals({"data": 'placeholder'}));
expect(results.last.status, QueryStatus.success);
expect(results.last.data, equals({'data': 'data'}));
});
test(
'the retryer should not throw an error when reject if the retrier is already resolved',
() async {
final key = queryKey();
int count = 0;
final observer = new QueryObserver<Map<String, dynamic>, dynamic,
Map<String, dynamic>, Map<String, dynamic>>(
queryClient,
QueryObserverOptions<Map<String, dynamic>, dynamic,
Map<String, dynamic>, Map<String, dynamic>>(
queryKey: key,
queryFn: (_) {
count++;
return Future.error({'reject': count});
},
retry: (_, __) => 1,
retryDelay: (_, __) => 20,
));
final unsubscribe = observer.subscribe();
// Simulate a race condition when an unsubscribe and a retry occur.
await sleep(20);
unsubscribe();
// A second reject is triggered for the retry
// but the retryer has already set isResolved to true
// so it does nothing and no error is thrown
// Should not log an error
queryClient.clear();
await sleep(40);
expect(true, true);
// expect(consoleMock).not.toHaveBeenNthCalledWith(1, 'reject 1')
// consoleMock.mockRestore()
});
test('getCurrentQuery should return the current query', () async {
final key = queryKey();
final observer = new QueryObserver<Map<String, dynamic>, dynamic,
Map<String, dynamic>, Map<String, dynamic>>(
queryClient,
QueryObserverOptions<Map<String, dynamic>, dynamic,
Map<String, dynamic>, Map<String, dynamic>>(
queryKey: key, queryFn: (_) => {'data': 'data'}));
expect(observer.getCurrentQuery().queryKey, key);
});
test('should throw an error if throwOnError option is true', () async {
final key = queryKey();
final observer = new QueryObserver<Map<String, dynamic>, dynamic,
Map<String, dynamic>, Map<String, dynamic>>(
queryClient,
QueryObserverOptions<Map<String, dynamic>, dynamic,
Map<String, dynamic>, Map<String, dynamic>>(
queryKey: key,
queryFn: (_) => Future.error({'error': 'error'}),
retry: (_, __) => 0,
));
var error = null;
try {
await observer.refetch(options: RefetchOptions(throwOnError: true));
} catch (err) {
error = err;
}
expect(error, equals({'error': 'error'}));
});
test(
'should not refetch in background if refetchIntervalInBackground is false',
() async {
final key = queryKey();
final spy = SpyFn();
// focusManager.setFocused(false)
final observer = new QueryObserver<Map<String, dynamic>, dynamic,
Map<String, dynamic>, Map<String, dynamic>>(
queryClient,
QueryObserverOptions<Map<String, dynamic>, dynamic,
Map<String, dynamic>, Map<String, dynamic>>(
queryKey: key,
queryFn: spy.fn1(),
refetchIntervalInBackground: false,
refetchInterval: (_, __) => Duration(milliseconds: 10),
));
final unsubscribe = observer.subscribe();
await sleep(30);
expect(spy.calls, 1);
// Clean-up
unsubscribe();
// focusManager.setFocused(true)
});
test(
'should not use replaceEqualDeep for select value when structuralSharing option is true',
() async {
final key = queryKey();
final data = {"value": 'data'};
final selectedData = {"value": 'data'};
final observer = new QueryObserver<Map<String, dynamic>, dynamic,
Map<String, dynamic>, Map<String, dynamic>>(
queryClient,
QueryObserverOptions<Map<String, dynamic>, dynamic,
Map<String, dynamic>, Map<String, dynamic>>(
queryKey: key,
queryFn: (_) => data,
select: (_) => data,
));
final unsubscribe = observer.subscribe();
await sleep(10);
expect(observer.getCurrentResult()?.data, data);
observer.setOptions(QueryObserverOptions(
queryKey: key,
queryFn: (_) => data,
structuralSharing: false,
select: (_) => selectedData,
));
await observer.refetch(filters: RefetchableQueryFilters(queryKey: key));
expect(observer.getCurrentResult()?.data, selectedData);
unsubscribe();
});
test('select function error using placeholderdata should log an error', () {
final key = queryKey();
QueryObserver<Map<String, dynamic>, dynamic, Map<String, dynamic>,
Map<String, dynamic>>(
queryClient,
QueryObserverOptions<Map<String, dynamic>, dynamic,
Map<String, dynamic>, Map<String, dynamic>>(
queryKey: key,
queryFn: (_) => {"data": 'data'},
placeholderData: {"data": 'placeholderdata'},
select: (_) {
throw new Exception('error');
},
));
expect(true, true);
// expect(consoleMock).toHaveBeenNthCalledWith(1, new Error('error'))
// consoleMock.mockRestore()
});
test(
'should not use replaceEqualDeep for select value when structuralSharing option is true and placeholderdata is defined',
() {
final key = queryKey();
final data = {"value": 'data'};
final selectedData1 = {"value": 'data'};
final selectedData2 = {"value": 'data'};
final placeholderData1 = {"value": 'data'};
final placeholderData2 = {"value": 'data'};
final observer = new QueryObserver<Map<String, dynamic>, dynamic,
Map<String, dynamic>, Map<String, dynamic>>(
queryClient,
QueryObserverOptions<Map<String, dynamic>, dynamic,
Map<String, dynamic>, Map<String, dynamic>>(
queryKey: key,
queryFn: (_) => data,
select: (_) => data,
));
observer.setOptions(QueryObserverOptions(
queryKey: key,
queryFn: (_) => data,
select: (_) {
return selectedData1;
},
placeholderData: placeholderData1,
));
observer.setOptions(QueryObserverOptions(
queryKey: key,
queryFn: (_) => data,
select: (_) {
return selectedData2;
},
placeholderData: placeholderData2,
structuralSharing: false,
));
expect(observer.getCurrentResult()?.data, equals(selectedData2));
});
test(
'should not use an undefined value returned by select as placeholderdata',
() {
final key = queryKey();
final data = {"value": 'data'};
final selectedData = {"value": 'data'};
final placeholderData1 = {"value": 'data'};
final placeholderData2 = {"value": 'data'};
final observer = new QueryObserver<Map<String, dynamic>, dynamic,
Map<String, dynamic>, Map<String, dynamic>>(
queryClient,
QueryObserverOptions<Map<String, dynamic>, dynamic,
Map<String, dynamic>, Map<String, dynamic>>(
queryKey: key,
queryFn: (_) => data,
select: (_) => data,
));
observer.setOptions(QueryObserverOptions<Map<String, dynamic>, dynamic,
Map<String, dynamic>, Map<String, dynamic>>(
queryKey: key,
queryFn: (_) => data,
select: (_) {
return selectedData;
},
placeholderData: placeholderData1,
));
expect(observer.getCurrentResult()?.isPlaceholderData, isTrue);
observer.setOptions(QueryObserverOptions<Map<String, dynamic>, dynamic,
Map<String, dynamic>, Map<String, dynamic>>(
queryKey: key,
queryFn: (_) => data,
select: (_) {
return {};
},
placeholderData: placeholderData2,
));
expect(observer.getCurrentResult()?.isPlaceholderData, isFalse);
});
test(
'updateResult should not notify cache listeners if cache option is false',
() async {
final key = queryKey();
final data1 = {"value": 'data 1'};
final data2 = {"value": 'data 2'};
await queryClient.prefetchQuery(queryKey: key, queryFn: (_) => data1);
final observer = new QueryObserver<Map<String, dynamic>, dynamic,
Map<String, dynamic>, Map<String, dynamic>>(
queryClient,
QueryObserverOptions<Map<String, dynamic>, dynamic,
Map<String, dynamic>, Map<String, dynamic>>(queryKey: key));
await queryClient.prefetchQuery(queryKey: key, queryFn: (_) => data2);
final spy = SpyFn();
final unsubscribe = queryClient.getQueryCache().subscribe(spy.fn1());
observer.updateResult(NotifyOptions(cache: false));
expect(spy.calls, 0);
unsubscribe();
});
test(
'should not notify observer when the stale timeout expires and the current result is stale',
() async {
final key = queryKey();
final queryFn = (_) => {'data': "data"};
await queryClient.prefetchQuery(queryKey: key, queryFn: queryFn);
final observer = new QueryObserver<Map<String, dynamic>, dynamic,
Map<String, dynamic>, Map<String, dynamic>>(
queryClient,
QueryObserverOptions<Map<String, dynamic>, dynamic,
Map<String, dynamic>, Map<String, dynamic>>(
queryKey: key,
queryFn: queryFn,
staleTime: Duration(milliseconds: 20),
));
final spy = SpyFn();
final unsubscribe = observer.subscribe(spy.fn1());
await queryClient.refetchQueries(queryKeys: key);
await sleep(10);
// Force isStale to true
// because no use case has been found to reproduce this condition
// @ts-ignore
// observer.getCurrentResult().isStale = true;
await sleep(30);
expect(spy.calls, 0);
unsubscribe();
});
}); });
} }