relay-test-utils
Version:
Utilities for testing Relay applications.
664 lines (524 loc) • 25.5 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.
*
* @emails oncall+relay
*
* @format
*/
// flowlint ambiguous-object-type:error
;
var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault");
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/defineProperty"));
var _objectSpread3 = _interopRequireDefault(require("@babel/runtime/helpers/objectSpread2"));
var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime/helpers/toConsumableArray"));
var invariant = require("fbjs/lib/invariant");
var _require = require('relay-runtime'),
TYPENAME_KEY = _require.TYPENAME_KEY,
RelayConcreteNode = _require.RelayConcreteNode,
getModuleComponentKey = _require.getModuleComponentKey,
getModuleOperationKey = _require.getModuleOperationKey;
var CLIENT_EXTENSION = RelayConcreteNode.CLIENT_EXTENSION,
CONDITION = RelayConcreteNode.CONDITION,
CONNECTION = RelayConcreteNode.CONNECTION,
DEFER = RelayConcreteNode.DEFER,
FLIGHT_FIELD = RelayConcreteNode.FLIGHT_FIELD,
INLINE_FRAGMENT = RelayConcreteNode.INLINE_FRAGMENT,
LINKED_FIELD = RelayConcreteNode.LINKED_FIELD,
LINKED_HANDLE = RelayConcreteNode.LINKED_HANDLE,
MODULE_IMPORT = RelayConcreteNode.MODULE_IMPORT,
SCALAR_FIELD = RelayConcreteNode.SCALAR_FIELD,
SCALAR_HANDLE = RelayConcreteNode.SCALAR_HANDLE,
STREAM = RelayConcreteNode.STREAM,
TYPE_DISCRIMINATOR = RelayConcreteNode.TYPE_DISCRIMINATOR;
function createIdGenerator() {
var id = 0;
return function () {
return ++id;
};
}
var DEFAULT_MOCK_RESOLVERS = {
ID: function ID(context, generateId) {
return "<".concat(context.parentType != null && context.parentType !== DEFAULT_MOCK_TYPENAME ? context.parentType + '-' : '', "mock-id-").concat(generateId(), ">");
},
Boolean: function Boolean() {
return false;
},
Int: function Int() {
return 42;
},
Float: function Float() {
return 4.2;
}
};
var DEFAULT_MOCK_TYPENAME = '__MockObject';
/**
* Basic value resolver
*/
function valueResolver(generateId, mockResolvers, typeName, context) {
var plural = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : false;
var defaultValue = arguments.length > 5 ? arguments[5] : undefined;
var generateValue = function generateValue(possibleDefaultValue) {
var mockValue;
var mockResolver = typeName != null && mockResolvers != null ? mockResolvers[typeName] : null;
if (mockResolver != null) {
mockValue = mockResolver(context, generateId);
}
if (mockValue === undefined) {
var _ref, _context$alias;
mockValue = possibleDefaultValue !== null && possibleDefaultValue !== void 0 ? possibleDefaultValue : typeName === 'ID' ? DEFAULT_MOCK_RESOLVERS.ID(context, generateId) : "<mock-value-for-field-\"".concat((_ref = (_context$alias = context.alias) !== null && _context$alias !== void 0 ? _context$alias : context.name) !== null && _ref !== void 0 ? _ref : 'undefined', "\">");
}
return mockValue;
};
return plural === true ? generateMockList(Array.isArray(defaultValue) ? defaultValue : Array(1).fill(), generateValue) : generateValue(defaultValue);
}
function createValueResolver(mockResolvers) {
var generateId = createIdGenerator();
return function () {
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
return valueResolver.apply(void 0, [generateId, mockResolvers].concat(args));
};
}
function generateMockList(placeholderArray, generateListItem) {
return placeholderArray.map(function (possibleDefaultValue) {
return generateListItem(possibleDefaultValue);
});
}
var RelayMockPayloadGenerator = /*#__PURE__*/function () {
function RelayMockPayloadGenerator(options) {
var _options$mockResolver, _options$selectionMet;
this._variables = options.variables; // $FlowFixMe[cannot-spread-indexer]
// $FlowFixMe[cannot-spread-inexact]
this._mockResolvers = (0, _objectSpread3["default"])((0, _objectSpread3["default"])({}, DEFAULT_MOCK_RESOLVERS), (_options$mockResolver = options.mockResolvers) !== null && _options$mockResolver !== void 0 ? _options$mockResolver : {});
this._selectionMetadata = (_options$selectionMet = options.selectionMetadata) !== null && _options$selectionMet !== void 0 ? _options$selectionMet : {};
this._resolveValue = createValueResolver(this._mockResolvers);
}
var _proto = RelayMockPayloadGenerator.prototype;
_proto.generate = function generate(selections, operationType) {
var defaultValues = this._getDefaultValuesForObject(operationType, null, null, [], // path
{});
return this._traverse({
selections: selections,
typeName: operationType,
isAbstractType: false,
name: null,
alias: null,
args: null
}, [], // path
null, // prevData
defaultValues);
};
_proto._traverse = function _traverse(traversable, path, prevData, defaultValues) {
var selections = traversable.selections,
typeName = traversable.typeName,
isAbstractType = traversable.isAbstractType;
return this._traverseSelections(selections, typeName, isAbstractType, path, prevData, defaultValues);
}
/**
* Generate mock values for selection of fields
*/
;
_proto._traverseSelections = function _traverseSelections(selections, typeName, isAbstractType, path, prevData, defaultValues) {
var _this = this;
var mockData = prevData !== null && prevData !== void 0 ? prevData : {};
selections.forEach(function (selection) {
switch (selection.kind) {
case SCALAR_FIELD:
{
mockData = _this._mockScalar(selection, typeName, path, mockData, defaultValues);
break;
}
// $FlowFixMe[incompatible-type]
case CONNECTION:
{
mockData = _this._traverseSelections([selection.edges, selection.pageInfo], typeName, isAbstractType, path, prevData, defaultValues);
break;
}
case LINKED_FIELD:
{
mockData = _this._mockLink(selection, path, mockData, defaultValues);
break;
}
case CONDITION:
var conditionValue = _this._getVariableValue(selection.condition);
if (conditionValue === selection.passingValue) {
mockData = _this._traverseSelections(selection.selections, typeName, isAbstractType, path, mockData, defaultValues);
}
break;
case DEFER:
case STREAM:
{
mockData = _this._traverseSelections(selection.selections, typeName, isAbstractType, path, mockData, defaultValues);
break;
}
case INLINE_FRAGMENT:
{
var _abstractKey = selection.abstractKey;
if (_abstractKey != null) {
if (mockData != null) {
mockData[_abstractKey] = true;
}
mockData = _this._traverseSelections(selection.selections, typeName, isAbstractType, path, mockData, defaultValues);
break;
} // If it's the first time we're trying to handle fragment spread
// on this selection, we will generate data for this type.
// Next fragment spread on this selection will be added only if the
// types are matching
if (mockData != null && (mockData[TYPENAME_KEY] == null || mockData[TYPENAME_KEY] === DEFAULT_MOCK_TYPENAME)) {
var _defaultValues$TYPENA;
mockData[TYPENAME_KEY] = (_defaultValues$TYPENA = defaultValues === null || defaultValues === void 0 ? void 0 : defaultValues[TYPENAME_KEY]) !== null && _defaultValues$TYPENA !== void 0 ? _defaultValues$TYPENA : selection.type;
} // Now, we need to make sure that we don't select abstract type
// for inline fragments
if (isAbstractType === true && mockData != null && mockData[TYPENAME_KEY] === typeName) {
mockData[TYPENAME_KEY] = selection.type;
}
if (mockData != null && mockData[TYPENAME_KEY] === selection.type) {
// This will get default values for current selection type
var defaults = _this._getDefaultValuesForObject(selection.type, path[path.length - 1], null, path); // Also, if the selection has an abstract type
// we may have mock resolvers for it
var defaultsForAbstractType = typeName !== selection.type ? _this._getDefaultValuesForObject(typeName, path[path.length - 1], null, path) : defaults; // Now let's select which defaults we're going to use
// for the selections
var defaultValuesForSelection = defaults; // First, defaults for
// concrete type of the selection
if (defaultValuesForSelection === undefined) {
// Second, defaults for abstract type of the selection
defaultValuesForSelection = defaultsForAbstractType;
} // And last, values from the parent mock resolver
if (defaultValuesForSelection === undefined) {
defaultValuesForSelection = defaultValues;
} // Now, if the default value for the type is explicit null,
// we may skip traversing child selection
if (defaultValuesForSelection === null) {
mockData = null;
break;
}
mockData = _this._traverseSelections(selection.selections, selection.type, isAbstractType, path, mockData, defaultValuesForSelection);
if (mockData[TYPENAME_KEY] != null) {
mockData[TYPENAME_KEY] = selection.type;
} // Make sure we're using id form the default values, an
// ID may be referenced in the same selection as InlineFragment
if (mockData.id != null && defaults != null && defaults.id != null) {
mockData.id = defaults.id;
}
}
break;
}
case MODULE_IMPORT:
// Explicit `null` of `defaultValues` handled in the INLINE_FRAGMENT
if (defaultValues != null) {
var _objectSpread2;
if (defaultValues.__typename !== typeName) {
break;
} // In order to mock 3d payloads, we need to receive an object with
// the type `NormalizationSplitOperation` from mock resolvers.
// In this case, we can traverse into its selection
// and generated payloads for it.
var operation = defaultValues.__module_operation; // Basic sanity checks of the provided default value.
// It should look like NormalizationSplitOperation
!(typeof operation === 'object' && operation !== null && operation.kind === 'SplitOperation' && Array.isArray(operation.selections) && typeof operation.name === 'string') ? process.env.NODE_ENV !== "production" ? invariant(false, 'RelayMockPayloadGenerator(): Unexpected default value for ' + 'a field `__module_operation` in the mock resolver for ' + '@module dependency. Provided value is "%s" and we\'re ' + 'expecting an object of a type `NormalizationSplitOperation`. ' + 'Please adjust mock resolver for the type "%s". ' + 'Typically it should require a file "%s$normalization.graphql".', JSON.stringify(operation), typeName, selection.fragmentName) : invariant(false) : void 0;
var splitOperation = operation;
var documentName = selection.documentName;
if (mockData == null) {
mockData = {};
} // $FlowFixMe[cannot-spread-indexer]
mockData = (0, _objectSpread3["default"])((0, _objectSpread3["default"])({}, mockData), {}, (_objectSpread2 = {}, (0, _defineProperty2["default"])(_objectSpread2, TYPENAME_KEY, typeName), (0, _defineProperty2["default"])(_objectSpread2, getModuleOperationKey(documentName), operation.name), (0, _defineProperty2["default"])(_objectSpread2, getModuleComponentKey(documentName), defaultValues.__module_component), _objectSpread2), _this._traverseSelections(splitOperation.selections, typeName, false, path, null, null));
}
break;
case CLIENT_EXTENSION:
// We do not expect to receive data for the client extensions
// from the server. MockPayloadGenerator should not generate it too.
break;
case TYPE_DISCRIMINATOR:
var abstractKey = selection.abstractKey;
if (mockData != null) {
mockData[abstractKey] = true;
}
break;
case SCALAR_HANDLE:
case LINKED_HANDLE:
break;
case FLIGHT_FIELD:
throw new Error('Flight fields are not yet supported.');
default:
selection;
!false ? process.env.NODE_ENV !== "production" ? invariant(false, 'RelayMockPayloadGenerator(): Unexpected AST kind `%s`.', selection.kind) : invariant(false) : void 0;
}
});
return mockData;
}
/**
* Generate default enum value
* @private
*/
;
_proto._getCorrectDefaultEnum = function _getCorrectDefaultEnum(enumValues, value, path, applicationName) {
if (value === undefined) {
return value;
}
if (value === null) {
// null is a valid enum value
return value;
}
var valueToValidate = Array.isArray(value) ? value.map(function (v) {
return String(v).toUpperCase();
}) : [String(value).toUpperCase()];
var enumValuesNormalized = enumValues.map(function (s) {
return s.toUpperCase();
}); // Let's validate the correctness of the provided enum value
// We will throw if value provided by mock resolvers is invalid
var correctValues = valueToValidate.filter(function (v) {
return enumValuesNormalized.includes(v);
});
if (correctValues.length !== valueToValidate.length) {
!false ? process.env.NODE_ENV !== "production" ? invariant(false, 'RelayMockPayloadGenerator: Invalid value "%s" provided for enum ' + 'field "%s" via MockResolver.' + 'Expected one of the following values: %s.', value, "".concat(path.join('.'), ".").concat(applicationName), enumValues.map(function (v) {
return "\"".concat(v, "\"");
}).join(', ')) : invariant(false) : void 0;
} // But missing case should be acceptable, we will just use
// a correct spelling from enumValues
var correctSpellingValues = valueToValidate.map(function (v) {
var correctSpellingEnumIndex = enumValuesNormalized.indexOf(String(v).toUpperCase());
return enumValues[correctSpellingEnumIndex];
});
return Array.isArray(value) ? correctSpellingValues : correctSpellingValues[0];
}
/**
* Generate mock value for a scalar field in the selection
*/
;
_proto._mockScalar = function _mockScalar(field, typeName, path, mockData, defaultValues) {
var _field$alias;
var data = mockData !== null && mockData !== void 0 ? mockData : {};
var applicationName = (_field$alias = field.alias) !== null && _field$alias !== void 0 ? _field$alias : field.name;
if (data.hasOwnProperty(applicationName) && field.name !== TYPENAME_KEY) {
return data;
}
var value; // For __typename fields we are going to return typeName
if (field.name === TYPENAME_KEY) {
value = typeName !== null && typeName !== void 0 ? typeName : DEFAULT_MOCK_TYPENAME;
}
var selectionPath = [].concat((0, _toConsumableArray2["default"])(path), [applicationName]);
var _this$_getScalarField = this._getScalarFieldTypeDetails(field, typeName, selectionPath),
type = _this$_getScalarField.type,
plural = _this$_getScalarField.plural,
enumValues = _this$_getScalarField.enumValues; // We may have an object with default values (generated in _mockLink(...))
// let's check if we have a possible default value there for our field
if (defaultValues != null && defaultValues.hasOwnProperty(applicationName)) {
value = defaultValues[applicationName];
if (enumValues != null) {
value = this._getCorrectDefaultEnum(enumValues, value, path, applicationName);
} // And if it's a plural field, we need to return an array
if (value !== undefined && plural && !Array.isArray(value)) {
value = [value];
}
} // If the value has not been generated yet (__id, __typename fields, or defaults)
// then we need to generate mock value for a scalar type
if (value === undefined) {
// Get basic type information: type of the field (Int, Float, String, etc..)
// And check if it's a plural type
var _defaultValue = enumValues != null ? enumValues[0] : undefined;
value = this._resolveValue( // If we don't have schema let's assume that fields with name (id, __id)
// have type ID
type, {
parentType: typeName,
name: field.name,
alias: field.alias,
path: selectionPath,
args: this._getFieldArgs(field)
}, plural, _defaultValue);
}
data[applicationName] = value;
return data;
}
/**
* Generate mock data for linked fields in the selection
*/
;
_proto._mockLink = function _mockLink(field, path, prevData, defaultValues) {
var _this2 = this;
var _field$alias2, _this$_selectionMetad, _field$concreteType;
var applicationName = (_field$alias2 = field.alias) !== null && _field$alias2 !== void 0 ? _field$alias2 : field.name;
var data = prevData !== null && prevData !== void 0 ? prevData : {};
var args = this._getFieldArgs(field); // Let's check if we have a custom mock resolver for the object type
// We will pass this data down to selection, so _mockScalar(...) can use
// values from `defaults`
var selectionPath = [].concat((0, _toConsumableArray2["default"])(path), [applicationName]);
var typeFromSelection = (_this$_selectionMetad = this._selectionMetadata[selectionPath.join('.')]) !== null && _this$_selectionMetad !== void 0 ? _this$_selectionMetad : {
type: DEFAULT_MOCK_TYPENAME
};
var defaults;
if (defaultValues != null && typeof defaultValues[applicationName] === 'object') {
defaults = defaultValues[applicationName];
} // In cases when we have explicit `null` in the defaults - let's return
// null for full branch
if (defaults === null) {
data[applicationName] = null;
return data;
} // If concrete type is null, let's try to get if from defaults,
// and fallback to default object type
var typeName = (_field$concreteType = field.concreteType) !== null && _field$concreteType !== void 0 ? _field$concreteType : defaults != null && typeof defaults[TYPENAME_KEY] === 'string' ? defaults[TYPENAME_KEY] : typeFromSelection.type; // Let's assume, that if the concrete type is null and selected type name is
// different from type information form selection, most likely this type
// information came from mock resolver __typename value and it was
// an intentional selection of the specific type
var isAbstractType = field.concreteType === null && typeName === typeFromSelection.type;
var generateDataForField = function generateDataForField(possibleDefaultValue) {
var _this$_getDefaultValu, _field$concreteType2;
var fieldDefaultValue = (_this$_getDefaultValu = _this2._getDefaultValuesForObject((_field$concreteType2 = field.concreteType) !== null && _field$concreteType2 !== void 0 ? _field$concreteType2 : typeFromSelection.type, field.name, field.alias, selectionPath, args)) !== null && _this$_getDefaultValu !== void 0 ? _this$_getDefaultValu : possibleDefaultValue;
if (fieldDefaultValue === null) {
return null;
}
return _this2._traverse({
selections: field.selections,
typeName: typeName,
isAbstractType: isAbstractType,
name: field.name,
alias: field.alias,
args: args
}, [].concat((0, _toConsumableArray2["default"])(path), [applicationName]), typeof data[applicationName] === 'object' ? // $FlowFixMe[incompatible-variance]
data[applicationName] : null, // $FlowFixMe[incompatible-call]
// $FlowFixMe[incompatible-variance]
fieldDefaultValue);
};
data[applicationName] = field.kind === 'LinkedField' && field.plural ? generateMockList(Array.isArray(defaults) ? defaults : Array(1).fill(), generateDataForField) : generateDataForField(defaults);
return data;
}
/**
* Get the value for a variable by name
*/
;
_proto._getVariableValue = function _getVariableValue(name) {
!this._variables.hasOwnProperty(name) ? process.env.NODE_ENV !== "production" ? invariant(false, 'RelayMockPayloadGenerator(): Undefined variable `%s`.', name) : invariant(false) : void 0;
return this._variables[name];
}
/**
* This method should call mock resolver for a specific type name
* and the result of this mock resolver will be passed as a default values for
* _mock*(...) methods
*/
;
_proto._getDefaultValuesForObject = function _getDefaultValuesForObject(typeName, fieldName, fieldAlias, path, args) {
var data;
if (typeName != null && this._mockResolvers[typeName] != null) {
data = this._resolveValue(typeName, {
parentType: null,
name: fieldName,
alias: fieldAlias,
args: args,
path: path
}, false);
}
if (typeof data === 'object') {
// $FlowFixMe[incompatible-variance]
return data;
}
}
/**
* Get object with variables for field
*/
;
_proto._getFieldArgs = function _getFieldArgs(field) {
var _this3 = this;
var args = {};
if (field.args != null) {
field.args.forEach(function (arg) {
args[arg.name] = _this3._getArgValue(arg);
});
}
return args;
};
_proto._getArgValue = function _getArgValue(arg) {
var _this4 = this;
switch (arg.kind) {
case 'Literal':
return arg.value;
case 'Variable':
return this._getVariableValue(arg.variableName);
case 'ObjectValue':
{
var value = {};
arg.fields.forEach(function (field) {
value[field.name] = _this4._getArgValue(field);
});
return value;
}
case 'ListValue':
{
var _value = [];
arg.items.forEach(function (item) {
_value.push(item != null ? _this4._getArgValue(item) : null);
});
return _value;
}
}
}
/**
* Helper function to get field type information (name of the type, plural)
*/
;
_proto._getScalarFieldTypeDetails = function _getScalarFieldTypeDetails(field, typeName, selectionPath) {
var _this$_selectionMetad2;
return (_this$_selectionMetad2 = this._selectionMetadata[selectionPath.join('.')]) !== null && _this$_selectionMetad2 !== void 0 ? _this$_selectionMetad2 : {
type: field.name === 'id' ? 'ID' : 'String',
plural: false,
enumValues: null,
nullable: false
};
};
return RelayMockPayloadGenerator;
}();
/**
* Generate mock data for NormalizationOperation selection
*/
function generateData(node, variables, mockResolvers, selectionMetadata) {
var mockGenerator = new RelayMockPayloadGenerator({
variables: variables,
mockResolvers: mockResolvers,
selectionMetadata: selectionMetadata
});
var operationType;
if (node.name.endsWith('Mutation')) {
operationType = 'Mutation';
} else if (node.name.endsWith('Subscription')) {
operationType = 'Subscription';
} else {
operationType = 'Query';
}
return mockGenerator.generate(node.selections, operationType);
}
/**
* Type refinement for selection metadata
*/
function getSelectionMetadataFromOperation(operation) {
var _operation$request$no;
var selectionTypeInfo = (_operation$request$no = operation.request.node.params.metadata) === null || _operation$request$no === void 0 ? void 0 : _operation$request$no.relayTestingSelectionTypeInfo;
if (selectionTypeInfo != null && !Array.isArray(selectionTypeInfo) && typeof selectionTypeInfo === 'object') {
var selectionMetadata = {};
Object.keys(selectionTypeInfo).forEach(function (path) {
var item = selectionTypeInfo[path];
if (item != null && !Array.isArray(item) && typeof item === 'object') {
if (typeof item.type === 'string' && typeof item.plural === 'boolean' && typeof item.nullable === 'boolean' && (item.enumValues === null || Array.isArray(item.enumValues))) {
selectionMetadata[path] = {
type: item.type,
plural: item.plural,
nullable: item.nullable,
enumValues: Array.isArray(item.enumValues) ? item.enumValues.map(String) : null
};
}
}
});
return selectionMetadata;
}
return null;
}
function generateDataForOperation(operation, mockResolvers) {
var data = generateData(operation.request.node.operation, operation.request.variables, mockResolvers !== null && mockResolvers !== void 0 ? mockResolvers : null, getSelectionMetadataFromOperation(operation));
return {
data: data
};
}
module.exports = {
generate: generateDataForOperation
};