UNPKG

relay-test-utils

Version:

Utilities for testing Relay applications.

421 lines (362 loc) • 16.3 kB
/** * 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 'use strict'; /* 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 };