diff --git a/__tests__/PropTypesDevelopmentReact15.js b/__tests__/PropTypesDevelopmentReact15.js index 7616bdd..5df432d 100644 --- a/__tests__/PropTypesDevelopmentReact15.js +++ b/__tests__/PropTypesDevelopmentReact15.js @@ -239,6 +239,7 @@ describe('PropTypesDevelopmentReact15', () => { typeCheckPass(PropTypes.object, {}); typeCheckPass(PropTypes.object, new Date()); typeCheckPass(PropTypes.object, /please/); + typeCheckPass(PropTypes.set, new Set()); typeCheckPass(PropTypes.symbol, Symbol()); }); @@ -283,6 +284,12 @@ describe('PropTypesDevelopmentReact15', () => { expectWarningInDevelopment(PropTypes.string.isRequired, 'foo'); expectWarningInDevelopment(PropTypes.string.isRequired, null); expectWarningInDevelopment(PropTypes.string.isRequired, undefined); + expectWarningInDevelopment(PropTypes.set, 0); + expectWarningInDevelopment(PropTypes.set, new Set()); + expectWarningInDevelopment(PropTypes.set.isRequired, 0); + expectWarningInDevelopment(PropTypes.set.isRequired, new Set()); + expectWarningInDevelopment(PropTypes.set.isRequired, null); + expectWarningInDevelopment(PropTypes.set.isRequired, undefined); expectWarningInDevelopment(PropTypes.symbol, 0); expectWarningInDevelopment(PropTypes.symbol, Symbol('Foo')); expectWarningInDevelopment(PropTypes.symbol.isRequired, 0); @@ -1181,6 +1188,119 @@ describe('PropTypesDevelopmentReact15', () => { }); }); + describe('SetOf Type', () => { + it('should fail for invalid argument', () => { + typeCheckFail( + PropTypes.setOf({foo: PropTypes.string}), + {foo: 'bar'}, + 'Property `testProp` of component `testComponent` has invalid PropType notation inside setOf.', + ); + }); + + it('should support the setOf propTypes', () => { + typeCheckPass(PropTypes.setOf(PropTypes.number), new Set([1, 2, 3])); + typeCheckPass(PropTypes.setOf(PropTypes.string), new Set(['a', 'b', 'c'])); + typeCheckPass(PropTypes.setOf(PropTypes.oneOf(['a', 'b'])), new Set(['a', 'b'])); + typeCheckPass(PropTypes.setOf(PropTypes.symbol), new Set([Symbol(), Symbol()])); + }); + + it('should support setOf with complex types', () => { + typeCheckPass( + PropTypes.setOf(PropTypes.shape({a: PropTypes.number.isRequired})), + new Set([{a: 1}, {a: 2}]), + ); + + function Thing() {} + typeCheckPass(PropTypes.setOf(PropTypes.instanceOf(Thing)), new Set([ + new Thing(), + new Thing(), + ])); + }); + + it('should warn with invalid items in the set', () => { + typeCheckFail( + PropTypes.setOf(PropTypes.number), + new Set([1, 2, 'b']), + 'Invalid value inside testProp of type `string` supplied to ' + + '`testComponent`, expected `number`.', + ); + }); + + it('should warn with invalid complex types', () => { + function Thing() {} + var name = Thing.name || '<>'; + + typeCheckFail( + PropTypes.setOf(PropTypes.instanceOf(Thing)), + new Set([new Thing(), 'xyz']), + 'Invalid value inside testProp of type `String` supplied to ' + + '`testComponent`, expected instance of `' + + name + + '`.', + ); + }); + + it('should warn when passed something other than a set', () => { + typeCheckFail( + PropTypes.setOf(PropTypes.number), + {'0': 'maybe-set', length: 1}, + 'Invalid prop `testProp` of type `object` supplied to ' + + '`testComponent`, expected a set.', + ); + typeCheckFail( + PropTypes.setOf(PropTypes.number), + 123, + 'Invalid prop `testProp` of type `number` supplied to ' + + '`testComponent`, expected a set.', + ); + typeCheckFail( + PropTypes.setOf(PropTypes.number), + 'string', + 'Invalid prop `testProp` of type `string` supplied to ' + + '`testComponent`, expected a set.', + ); + }); + + it('should not warn when passing an empty set', () => { + typeCheckPass(PropTypes.setOf(PropTypes.number), new Set()); + }); + + it('should be implicitly optional and not warn without values', () => { + typeCheckPass(PropTypes.setOf(PropTypes.number), null); + typeCheckPass(PropTypes.setOf(PropTypes.number), undefined); + }); + + it('should warn for missing required values', () => { + typeCheckFailRequiredValues( + PropTypes.setOf(PropTypes.number).isRequired, + ); + }); + + it('should warn if called manually in development', () => { + spyOn(console, 'error'); + expectWarningInDevelopment(PropTypes.setOf({foo: PropTypes.string}), { + foo: 'bar', + }); + expectWarningInDevelopment(PropTypes.setOf(PropTypes.number), new Set([ + 1, + 2, + 'b', + ])); + expectWarningInDevelopment(PropTypes.setOf(PropTypes.number), { + '0': 'maybe-array', + length: 1, + }); + expectWarningInDevelopment( + PropTypes.setOf(PropTypes.number).isRequired, + null, + ); + expectWarningInDevelopment( + PropTypes.setOf(PropTypes.number).isRequired, + undefined, + ); + }); + }); + describe('Symbol Type', () => { it('should warn for non-symbol', () => { typeCheckFail( diff --git a/__tests__/PropTypesDevelopmentStandalone-test.js b/__tests__/PropTypesDevelopmentStandalone-test.js index 1736d17..1a43345 100644 --- a/__tests__/PropTypesDevelopmentStandalone-test.js +++ b/__tests__/PropTypesDevelopmentStandalone-test.js @@ -235,6 +235,7 @@ describe('PropTypesDevelopmentStandalone', () => { typeCheckPass(PropTypes.object, {}); typeCheckPass(PropTypes.object, new Date()); typeCheckPass(PropTypes.object, /please/); + typeCheckPass(PropTypes.set, new Set()); typeCheckPass(PropTypes.symbol, Symbol()); }); @@ -279,6 +280,12 @@ describe('PropTypesDevelopmentStandalone', () => { expectThrowsInDevelopment(PropTypes.string.isRequired, 'foo'); expectThrowsInDevelopment(PropTypes.string.isRequired, null); expectThrowsInDevelopment(PropTypes.string.isRequired, undefined); + expectThrowsInDevelopment(PropTypes.set, 0); + expectThrowsInDevelopment(PropTypes.set, new Set()); + expectThrowsInDevelopment(PropTypes.set.isRequired, 0); + expectThrowsInDevelopment(PropTypes.set.isRequired, new Set()); + expectThrowsInDevelopment(PropTypes.set.isRequired, null); + expectThrowsInDevelopment(PropTypes.set.isRequired, undefined); expectThrowsInDevelopment(PropTypes.symbol, 0); expectThrowsInDevelopment(PropTypes.symbol, Symbol('Foo')); expectThrowsInDevelopment(PropTypes.symbol.isRequired, 0); @@ -1070,6 +1077,120 @@ describe('PropTypesDevelopmentStandalone', () => { }); }); + describe('SetOf Type', () => { + it('should fail for invalid argument', () => { + typeCheckFail( + PropTypes.setOf({foo: PropTypes.string}), + {foo: 'bar'}, + 'Property `testProp` of component `testComponent` has invalid PropType notation inside setOf.', + ); + }); + + it('should support the setOf propTypes', () => { + typeCheckPass(PropTypes.setOf(PropTypes.number), new Set([1, 2, 3])); + typeCheckPass(PropTypes.setOf(PropTypes.string), new Set(['a', 'b', 'c'])); + typeCheckPass(PropTypes.setOf(PropTypes.oneOf(['a', 'b'])), new Set(['a', 'b'])); + typeCheckPass(PropTypes.setOf(PropTypes.symbol), new Set([Symbol(), Symbol()])); + }); + + it('should support setOf with complex types', () => { + typeCheckPass( + PropTypes.setOf(PropTypes.shape({a: PropTypes.number.isRequired})), + new Set([{a: 1}, {a: 2}]), + ); + + function Thing() {} + typeCheckPass(PropTypes.setOf(PropTypes.instanceOf(Thing)), new Set([ + new Thing(), + new Thing(), + ])); + }); + + it('should warn with invalid items in the set', () => { + typeCheckFail( + PropTypes.setOf(PropTypes.number), + new Set([1, 2, 'b']), + 'Invalid value inside testProp of type `string` supplied to ' + + '`testComponent`, expected `number`.', + ); + }); + + it('should warn with invalid complex types', () => { + function Thing() {} + var name = Thing.name || '<>'; + + typeCheckFail( + PropTypes.setOf(PropTypes.instanceOf(Thing)), + new Set([new Thing(), 'xyz']), + 'Invalid value inside testProp of type `String` supplied to ' + + '`testComponent`, expected instance of `' + + name + + '`.', + ); + }); + + it('should warn when passed something other than a set', () => { + typeCheckFail( + PropTypes.setOf(PropTypes.number), + {'0': 'maybe-set', length: 1}, + 'Invalid prop `testProp` of type `object` supplied to ' + + '`testComponent`, expected a set.', + ); + typeCheckFail( + PropTypes.setOf(PropTypes.number), + 123, + 'Invalid prop `testProp` of type `number` supplied to ' + + '`testComponent`, expected a set.', + ); + typeCheckFail( + PropTypes.setOf(PropTypes.number), + 'string', + 'Invalid prop `testProp` of type `string` supplied to ' + + '`testComponent`, expected a set.', + ); + }); + + it('should not warn when passing an empty set', () => { + typeCheckPass(PropTypes.setOf(PropTypes.number), new Set()); + }); + + it('should be implicitly optional and not warn without values', () => { + typeCheckPass(PropTypes.setOf(PropTypes.number), null); + typeCheckPass(PropTypes.setOf(PropTypes.number), undefined); + }); + + it('should warn for missing required values', () => { + typeCheckFailRequiredValues( + PropTypes.setOf(PropTypes.number).isRequired, + ); + }); + + it('should warn if called manually in development', () => { + spyOn(console, 'error'); + expectThrowsInDevelopment(PropTypes.setOf({foo: PropTypes.string}), { + foo: 'bar', + }); + expectThrowsInDevelopment(PropTypes.setOf(PropTypes.number), new Set([ + 1, + 2, + 'b', + ])); + expectThrowsInDevelopment(PropTypes.setOf(PropTypes.number), { + '0': 'maybe-array', + length: 1, + }); + expectThrowsInDevelopment( + PropTypes.setOf(PropTypes.number).isRequired, + null, + ); + expectThrowsInDevelopment( + PropTypes.setOf(PropTypes.number).isRequired, + undefined, + ); + }); + }); + + describe('Symbol Type', () => { it('should warn for non-symbol', () => { typeCheckFail( diff --git a/factoryWithThrowingShims.js b/factoryWithThrowingShims.js index 2b3c924..00cf99a 100644 --- a/factoryWithThrowingShims.js +++ b/factoryWithThrowingShims.js @@ -48,6 +48,8 @@ module.exports = function() { oneOf: getShim, oneOfType: getShim, shape: getShim, + set: shim, + setOf: getShim, exact: getShim }; diff --git a/factoryWithTypeCheckers.js b/factoryWithTypeCheckers.js index 7c962b1..53c529d 100644 --- a/factoryWithTypeCheckers.js +++ b/factoryWithTypeCheckers.js @@ -110,6 +110,8 @@ module.exports = function(isValidElement, throwOnDirectAccess) { oneOf: createEnumTypeChecker, oneOfType: createUnionTypeChecker, shape: createShapeTypeChecker, + set: createSetTypeChecker(), + setOf: createSetOfTypeChecker, exact: createStrictShapeTypeChecker, }; @@ -379,6 +381,52 @@ module.exports = function(isValidElement, throwOnDirectAccess) { return createChainableTypeChecker(validate); } + function createSetTypeChecker(expectedType) { + function validate(props, propName, componentName, location, propFullName, secret) { + var propValue = props[propName]; + if (!(propValue instanceof Set)) { + var propType = getPropType(propValue); + + return new PropTypeError('Invalid ' + location + ' `' + propFullName + '` of type ' + ('`' + propType + '` supplied to `' + componentName + '`, expected a set.')); + } + return null; + } + return createChainableTypeChecker(validate); + } + + function createSetOfTypeChecker(typeChecker) { + function validate(props, propName, componentName, location, propFullName) { + if (typeof typeChecker !== 'function') { + return new PropTypeError('Property `' + propFullName + '` of component `' + componentName + '` has invalid PropType notation inside setOf.'); + } + var propValue = props[propName]; + if (!(propValue instanceof Set)) { + var propType = getPropType(propValue); + return new PropTypeError('Invalid ' + location + ' `' + propFullName + '` of type ' + ('`' + propType + '` supplied to `' + componentName + '`, expected a set.')); + } + // Check the types of the values inside the set using forEach(), which has broader support than values(). + var insideValidateResult = null; + var insideValidateContainer = {}; + var insideLocation = 'inside ' + propFullName; + propValue.forEach(function checkValue(value) { + if (null !== insideValidateResult) { + return; + } + insideValidateContainer.value = value; + var error = typeChecker(insideValidateContainer, 'value', componentName, 'value', insideLocation, ReactPropTypesSecret); + if (error instanceof Error) { + // If the error contains the inside location ("inside someSet") in quotes, remove those quotes. + if (error instanceof PropTypeError && error.message.includes('`' + insideLocation + '`')) { + error = new PropTypeError(error.message.replace('`' + insideLocation + '`', insideLocation)); + } + insideValidateResult = error; + } + }); + return insideValidateResult; + } + return createChainableTypeChecker(validate); + } + function createStrictShapeTypeChecker(shapeTypes) { function validate(props, propName, componentName, location, propFullName) { var propValue = props[propName]; @@ -505,6 +553,8 @@ module.exports = function(isValidElement, throwOnDirectAccess) { return 'date'; } else if (propValue instanceof RegExp) { return 'regexp'; + } else if (propValue instanceof Set) { + return 'set'; } } return propType; @@ -521,6 +571,7 @@ module.exports = function(isValidElement, throwOnDirectAccess) { case 'boolean': case 'date': case 'regexp': + case 'set': return 'a ' + type; default: return type;