relay-test-utils
Version:
Utilities for testing Relay applications.
421 lines (362 loc) • 16.3 kB
JavaScript
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*
*/
// flowlint ambiguous-object-type:error
;
/* global jest */
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
var _objectSpread2 = _interopRequireDefault(require("@babel/runtime/helpers/objectSpread2"));
var areEqual = require("fbjs/lib/areEqual");
var invariant = require("fbjs/lib/invariant");
var _require = require('relay-runtime'),
RecordSource = _require.RecordSource,
Store = _require.Store,
QueryResponseCache = _require.QueryResponseCache,
Observable = _require.Observable,
Environment = _require.Environment,
Network = _require.Network,
createOperationDescriptor = _require.createOperationDescriptor,
getRequest = _require.getRequest;
var MAX_SIZE = 10;
var MAX_TTL = 5 * 60 * 1000; // 5 min
function mockInstanceMethod(object, key) {
object[key] = jest.fn(object[key].bind(object));
}
function mockDisposableMethod(object, key) {
var fn = object[key].bind(object);
object[key] = jest.fn(function () {
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
var disposable = fn.apply(void 0, args);
var dispose = jest.fn(function () {
return disposable.dispose();
});
object[key].mock.dispose = dispose;
return {
dispose: dispose
};
});
var mockClear = object[key].mockClear.bind(object[key]);
object[key].mockClear = function () {
mockClear();
object[key].mock.dispose = null;
};
}
function mockObservableMethod(object, key) {
var fn = object[key].bind(object);
object[key] = jest.fn(function () {
for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
args[_key2] = arguments[_key2];
}
return fn.apply(void 0, args)["do"]({
start: function start(subscription) {
object[key].mock.subscriptions.push(subscription);
}
});
});
object[key].mock.subscriptions = [];
var mockClear = object[key].mockClear.bind(object[key]);
object[key].mockClear = function () {
mockClear();
object[key].mock.subscriptions = [];
};
}
/**
* Creates an instance of the `Environment` interface defined in
* RelayStoreTypes with a mocked network layer.
*
* Usage:
*
* ```
* const environment = RelayModernMockEnvironment.createMockEnvironment();
* ```
*
* Mock API:
*
* Helpers are available as `environment.mock.<helper>`:
*
* - `isLoading(query, variables): boolean`: Determine whether the given query
* is currently being loaded (not yet rejected/resolved).
* - `reject(query, error: Error): void`: Reject a query that has been fetched
* by the environment.
* - `resolve(query, payload: PayloadData): void`: Resolve a query that has been
* fetched by the environment.
* - `nextValue(...) - will add payload to the processing, but won't complete
* the request ()
* - getAllOperations() - every time there is an operation created by
* the Relay Component (query, mutation, subscription) this operation will be
* added to the internal list on the Mock Environment. This method will return
* an array of all pending operations in the order they occurred.
* - findOperation(findFn) - should find operation if findFn(...) return `true`
* for it. Otherwise, it will throw.
* - getMostRecentOperation(...) - should return the most recent operation
* generated by Relay Component.
* - resolveMostRecentOperation(...) - is accepting `GraphQLSingularResponse` or a
* callback function that will receive `operation` and should return
* `GraphQLSingularResponse`
* - rejectMostRecentOperation(...) - should reject the most recent operation
* with a specific error
*/
function createMockEnvironment(config) {
var _config$store, _global, _global$process, _global$process$env;
var store = (_config$store = config === null || config === void 0 ? void 0 : config.store) !== null && _config$store !== void 0 ? _config$store : new Store(new RecordSource());
var cache = new QueryResponseCache({
size: MAX_SIZE,
ttl: MAX_TTL
});
var pendingRequests = [];
var pendingOperations = [];
var queuePendingOperation = function queuePendingOperation(query, variables) {
var operationDescriptor = createOperationDescriptor(getRequest(query), variables);
pendingOperations = pendingOperations.concat([operationDescriptor]);
};
var resolversQueue = [];
var queueOperationResolver = function queueOperationResolver(resolver) {
resolversQueue = resolversQueue.concat([resolver]);
}; // Mock the network layer
var execute = function execute(request, variables, cacheConfig) {
var id = request.id,
text = request.text;
var cacheID = id !== null && id !== void 0 ? id : text;
var cachedPayload = null;
if (((cacheConfig === null || cacheConfig === void 0 ? void 0 : cacheConfig.force) == null || (cacheConfig === null || cacheConfig === void 0 ? void 0 : cacheConfig.force) === false) && cacheID != null) {
cachedPayload = cache.get(cacheID, variables);
}
if (cachedPayload !== null) {
return Observable.from(cachedPayload);
}
var currentOperation = pendingOperations.find(function (op) {
return op.request.node.params === request && areEqual(op.request.variables, variables);
}); // Handle network responses added by
if (currentOperation != null && resolversQueue.length > 0) {
var currentResolver = resolversQueue[0];
var result = currentResolver(currentOperation);
if (result != null) {
resolversQueue = resolversQueue.filter(function (res) {
return res !== currentResolver;
});
pendingOperations = pendingOperations.filter(function (op) {
return op !== currentOperation;
});
if (result instanceof Error) {
return Observable.create(function (sink) {
sink.error(result);
});
} else {
return Observable.from(result);
}
}
}
return Observable.create(function (sink) {
var nextRequest = {
request: request,
variables: variables,
cacheConfig: cacheConfig,
sink: sink
};
pendingRequests = pendingRequests.concat([nextRequest]);
return function () {
pendingRequests = pendingRequests.filter(function (pending) {
return !areEqual(pending, nextRequest);
});
pendingOperations = pendingOperations.filter(function (op) {
return op !== currentOperation;
});
};
});
};
function getConcreteRequest(input) {
if (input.kind === 'Request') {
var _request = input;
return _request;
} else {
var operationDescriptor = input;
!pendingOperations.includes(operationDescriptor) ? process.env.NODE_ENV !== "production" ? invariant(false, 'RelayModernMockEnvironment: Operation "%s" was not found in the list of pending operations', operationDescriptor.request.node.operation.name) : invariant(false) : void 0;
return operationDescriptor.request.node;
}
} // The same request may be made by multiple query renderers
function getRequests(input) {
var concreteRequest;
var operationDescriptor;
if (input.kind === 'Request') {
concreteRequest = input;
} else {
operationDescriptor = input;
concreteRequest = operationDescriptor.request.node;
}
var foundRequests = pendingRequests.filter(function (pending) {
if (!areEqual(pending.request, concreteRequest.params)) {
return false;
}
if (operationDescriptor) {
// If we handling `OperationDescriptor` we also need to check variables
// and return only pending request with equal variables
return areEqual(operationDescriptor.request.variables, pending.variables);
} else {
// In the case we received `ConcreteRequest` as input we will return
// all pending request, even if they have different variables
return true;
}
});
!foundRequests.length ? process.env.NODE_ENV !== "production" ? invariant(false, 'MockEnvironment: Cannot respond to request, it has not been requested yet.') : invariant(false) : void 0;
foundRequests.forEach(function (foundRequest) {
!foundRequest.sink ? process.env.NODE_ENV !== "production" ? invariant(false, 'MockEnvironment: Cannot respond to `%s`, it has not been requested yet.', concreteRequest.params.name) : invariant(false) : void 0;
});
return foundRequests;
}
function ensureValidPayload(payload) {
!(typeof payload === 'object' && payload !== null && payload.hasOwnProperty('data')) ? process.env.NODE_ENV !== "production" ? invariant(false, 'MockEnvironment(): Expected payload to be an object with a `data` key.') : invariant(false) : void 0;
return payload;
}
var cachePayload = function cachePayload(request, variables, payload) {
var _getConcreteRequest$p = getConcreteRequest(request).params,
id = _getConcreteRequest$p.id,
text = _getConcreteRequest$p.text;
var cacheID = id !== null && id !== void 0 ? id : text;
!(cacheID != null) ? process.env.NODE_ENV !== "production" ? invariant(false, 'CacheID should not be null') : invariant(false) : void 0;
cache.set(cacheID, variables, payload);
};
var clearCache = function clearCache() {
cache.clear();
}; // Helper to determine if a given query/variables pair is pending
var isLoading = function isLoading(request, variables, cacheConfig) {
return pendingRequests.some(function (pending) {
return areEqual(pending.request, getConcreteRequest(request).params) && areEqual(pending.variables, variables) && areEqual(pending.cacheConfig, cacheConfig !== null && cacheConfig !== void 0 ? cacheConfig : {});
});
}; // Helpers to reject or resolve the payload for an individual request.
var reject = function reject(request, error) {
var rejectError = typeof error === 'string' ? new Error(error) : error;
getRequests(request).forEach(function (foundRequest) {
var sink = foundRequest.sink;
!(sink !== null) ? process.env.NODE_ENV !== "production" ? invariant(false, 'Sink should be defined.') : invariant(false) : void 0;
sink.error(rejectError);
});
};
var nextValue = function nextValue(request, payload) {
getRequests(request).forEach(function (foundRequest) {
var sink = foundRequest.sink;
!(sink !== null) ? process.env.NODE_ENV !== "production" ? invariant(false, 'Sink should be defined.') : invariant(false) : void 0;
sink.next(ensureValidPayload(payload));
});
};
var complete = function complete(request) {
getRequests(request).forEach(function (foundRequest) {
var sink = foundRequest.sink;
!(sink !== null) ? process.env.NODE_ENV !== "production" ? invariant(false, 'Sink should be defined.') : invariant(false) : void 0;
sink.complete();
});
};
var resolve = function resolve(request, payload) {
getRequests(request).forEach(function (foundRequest) {
var sink = foundRequest.sink;
!(sink !== null) ? process.env.NODE_ENV !== "production" ? invariant(false, 'Sink should be defined.') : invariant(false) : void 0;
sink.next(ensureValidPayload(payload));
sink.complete();
});
};
var getMostRecentOperation = function getMostRecentOperation() {
var mostRecentOperation = pendingOperations[pendingOperations.length - 1];
!(mostRecentOperation != null) ? process.env.NODE_ENV !== "production" ? invariant(false, 'RelayModernMockEnvironment: There are no pending operations in the list') : invariant(false) : void 0;
return mostRecentOperation;
};
var findOperation = function findOperation(findFn) {
var pendingOperation = pendingOperations.find(findFn);
!(pendingOperation != null) ? process.env.NODE_ENV !== "production" ? invariant(false, 'RelayModernMockEnvironment: Operation was not found in the list of pending operations') : invariant(false) : void 0;
return pendingOperation;
}; // $FlowExpectedError[prop-missing]
var environment = new Environment((0, _objectSpread2["default"])({
configName: 'RelayModernMockEnvironment',
network: Network.create(execute, execute),
store: store
}, config));
var createExecuteProxy = function createExecuteProxy(env, fn) {
return function () {
for (var _len3 = arguments.length, argumentsList = new Array(_len3), _key3 = 0; _key3 < _len3; _key3++) {
argumentsList[_key3] = arguments[_key3];
}
var operation = argumentsList[0].operation;
pendingOperations = pendingOperations.concat([operation]);
return fn.apply(env, argumentsList);
};
}; // $FlowExpectedError[cannot-write]
environment.execute = createExecuteProxy(environment, environment.execute); // $FlowExpectedError[cannot-write]
environment.executeWithSource = createExecuteProxy(environment, environment.executeWithSource); // $FlowExpectedError[cannot-write]
environment.executeMutation = createExecuteProxy(environment, environment.executeMutation);
if (((_global = global) === null || _global === void 0 ? void 0 : (_global$process = _global.process) === null || _global$process === void 0 ? void 0 : (_global$process$env = _global$process.env) === null || _global$process$env === void 0 ? void 0 : _global$process$env.NODE_ENV) === 'test') {
// Mock all the functions with their original behavior
mockDisposableMethod(environment, 'applyUpdate');
mockInstanceMethod(environment, 'commitPayload');
mockInstanceMethod(environment, 'getStore');
mockInstanceMethod(environment, 'lookup');
mockInstanceMethod(environment, 'check');
mockDisposableMethod(environment, 'subscribe');
mockDisposableMethod(environment, 'retain');
mockObservableMethod(environment, 'execute');
mockObservableMethod(environment, 'executeWithSource');
mockObservableMethod(environment, 'executeMutation');
mockInstanceMethod(store, 'getSource');
mockInstanceMethod(store, 'lookup');
mockInstanceMethod(store, 'notify');
mockInstanceMethod(store, 'publish');
mockDisposableMethod(store, 'retain');
mockDisposableMethod(store, 'subscribe');
}
var mock = {
cachePayload: cachePayload,
clearCache: clearCache,
isLoading: isLoading,
reject: reject,
resolve: resolve,
nextValue: nextValue,
complete: complete,
getMostRecentOperation: getMostRecentOperation,
resolveMostRecentOperation: function resolveMostRecentOperation(payload) {
var operation = getMostRecentOperation();
var data = typeof payload === 'function' ? payload(operation) : payload;
return resolve(operation, data);
},
rejectMostRecentOperation: function rejectMostRecentOperation(error) {
var operation = getMostRecentOperation();
var rejector = typeof error === 'function' ? error(operation) : error;
return reject(operation, rejector);
},
findOperation: findOperation,
queuePendingOperation: queuePendingOperation,
getAllOperations: function getAllOperations() {
return pendingOperations;
},
queueOperationResolver: queueOperationResolver
}; // $FlowExpectedError[cannot-write]
environment.mock = mock; // $FlowExpectedError[cannot-write]
environment.mockClear = function () {
environment.applyUpdate.mockClear();
environment.commitPayload.mockClear();
environment.getStore.mockClear();
environment.lookup.mockClear();
environment.check.mockClear();
environment.subscribe.mockClear();
environment.retain.mockClear();
environment.execute.mockClear();
environment.executeMutation.mockClear();
store.getSource.mockClear();
store.lookup.mockClear();
store.notify.mockClear();
store.publish.mockClear();
store.retain.mockClear();
store.subscribe.mockClear();
cache.clear();
pendingOperations = [];
pendingRequests = [];
};
return environment;
}
module.exports = {
createMockEnvironment: createMockEnvironment
};