Repository: jquense/yup
Pull request #292 (2018-08-19 13:03:15)

Add function test names to email and url
I noticed that `string.email()` and `string.url()` don't have associated test function names and appear as `undefined` in the `tests` array of `SchemaDescription`. 

This just makes sure that a proper name is sent through the `matches` function.

jquense: why close this?
aytee17: Honestly, I thought I did something wrong (this is my first PR changing code) so I closed it out of embarrassment.
dontub: `matches` must have a name itself if none is provided, probably just "matches". Without name at least the async validation is (correctly) rejected, but without errors. Until this is fixed I have to overwrite the `matches` method.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index cd5de62..d6aa2e5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -34,6 +34,10 @@
 
 ## v0.26.0 - Fri, 20 Jul 2018 15:39:03 GMT
 
+### BREAKING CHANGES
+
+* locale `number` config properties `less` and `more` are now `lessThan` and `moreThan`
+
 ## v0.25.1 - Wed, 16 May 2018 23:59:14 GMT
 
 ## v0.25.0 - Tue, 15 May 2018 21:43:54 GMT
diff --git a/src/mixed.js b/src/mixed.js
index d2bb2bf..cb8818c 100644
--- a/src/mixed.js
+++ b/src/mixed.js
@@ -24,9 +24,7 @@ class RefSet {
     Ref.isRef(value) ? this.refs.set(value.key, value) : this.list.add(value);
   }
   delete(value) {
-    Ref.isRef(value)
-      ? this.refs.delete(value.key, value)
-      : this.list.delete(value);
+    Ref.isRef(value) ? this.refs.delete(value.key) : this.list.delete(value);
   }
   has(value, resolve) {
     if (this.list.has(value)) return true;
@@ -38,6 +36,20 @@ class RefSet {
 
     return false;
   }
+  clone() {
+    const next = new RefSet();
+    next.list = new Set(this.list);
+    next.refs = new Map(this.refs);
+    return next;
+  }
+  merge(newItems, removeItems) {
+    const next = this.clone();
+    newItems.list.forEach(value => next.add(value));
+    newItems.refs.forEach(value => next.add(value));
+    removeItems.list.forEach(value => next.delete(value));
+    removeItems.refs.forEach(value => next.delete(value));
+    return next;
+  }
 }
 
 export default function SchemaType(options = {}) {
@@ -112,12 +124,23 @@ const proto = (SchemaType.prototype = {
 
     var next = prependDeep(schema.clone(), this);
 
-    // new undefined default is overriden by old non-undefined one, revert
+    // new undefined default is overridden by old non-undefined one, revert
     if (has(schema, '_default')) next._default = schema._default;
 
     next.tests = this.tests;
     next._exclusive = this._exclusive;
 
+    // manually merge the blacklist/whitelist (the other `schema` takes
+    // precedence in case of conflicts)
+    next._whitelist = this._whitelist.merge(
+      schema._whitelist,
+      schema._blacklist,
+    );
+    next._blacklist = this._blacklist.merge(
+      schema._blacklist,
+      schema._whitelist,
+    );
+
     // manually add the new tests to ensure
     // the deduping logic is consistent
     next.withMutation(next => {
diff --git a/src/number.js b/src/number.js
index be65452..b179e6a 100644
--- a/src/number.js
+++ b/src/number.js
@@ -5,8 +5,6 @@ import isAbsent from './util/isAbsent';
 
 let isNaN = value => value != +value;
 
-let isInteger = val => isAbsent(val) || val === (val | 0);
-
 export default function NumberSchema() {
   if (!(this instanceof NumberSchema)) return new NumberSchema();
 
@@ -14,8 +12,8 @@ export default function NumberSchema() {
 
   this.withMutation(() => {
     this.transform(function(value) {
-      let parsed = value
-      
+      let parsed = value;
+
       if (typeof parsed === 'string') {
         parsed = parsed.replace(/\s/g, '');
         if (parsed === '') return NaN;
@@ -94,7 +92,11 @@ inherits(NumberSchema, MixedSchema, {
   },
 
   integer(message = locale.integer) {
-    return this.test({ name: 'integer', message, test: isInteger });
+    return this.test({
+      name: 'integer',
+      message,
+      test: val => isAbsent(val) || Number.isInteger(val),
+    });
   },
 
   truncate() {
diff --git a/src/string.js b/src/string.js
index 01bb428..05a26e9 100644
--- a/src/string.js
+++ b/src/string.js
@@ -16,7 +16,7 @@ export default function StringSchema() {
   MixedSchema.call(this, { type: 'string' });
 
   this.withMutation(() => {
-    this.transform(function(value) {
+    this.transform(function (value) {
       if (this.isType(value)) return value;
       return value != null && value.toString ? value.toString() : value;
     });
@@ -73,25 +73,32 @@ inherits(StringSchema, MixedSchema, {
   matches(regex, options) {
     let excludeEmptyString = false;
     let message;
+    let name;
 
     if (options) {
-      if (options.message || options.hasOwnProperty('excludeEmptyString')) {
-        ({ excludeEmptyString, message } = options);
+      if (
+        options.message ||
+        options.hasOwnProperty('excludeEmptyString') ||
+        options.name
+      ) {
+        ({ excludeEmptyString, message, name } = options);
       } else message = options;
     }
 
     return this.test({
+      name: name || 'matches',
       message: message || locale.matches,
       params: { regex },
       test: value =>
         isAbsent(value) ||
         (value === '' && excludeEmptyString) ||
-        regex.test(value),
+        value.search(regex) !== -1,
     });
   },
 
   email(message = locale.email) {
     return this.matches(rEmail, {
+      name: 'email',
       message,
       excludeEmptyString: true,
     });
@@ -99,6 +106,7 @@ inherits(StringSchema, MixedSchema, {
 
   url(message = locale.url) {
     return this.matches(rUrl, {
+      name: 'url',
       message,
       excludeEmptyString: true,
     });


Test patch:
diff --git a/test/mixed.js b/test/mixed.js
index 3fadcc5..3f744a1 100644
--- a/test/mixed.js
+++ b/test/mixed.js
@@ -670,6 +670,47 @@ describe('Mixed Types ', () => {
     );
   });
 
+  it('concat should preserve oneOf', async function() {
+    let inst = string()
+      .oneOf(['a'])
+      .concat(string().default('hi'));
+
+    await inst.isValid('a').should.become(true);
+  });
+
+  it('gives whitelist precedence to second in concat', async function() {
+    let inst = string()
+      .oneOf(['a', 'b', 'c'])
+      .concat(string().notOneOf(['b']));
+
+    await inst.isValid('a').should.become(true);
+    await inst.isValid('b').should.become(false);
+    await inst.isValid('c').should.become(true);
+  });
+
+  it('gives blacklist precedence to second in concat', async function() {
+    let inst = string()
+      .notOneOf(['a', 'b', 'c'])
+      .concat(string().oneOf(['b', 'c']));
+
+    await inst.isValid('a').should.become(false);
+    await inst.isValid('b').should.become(true);
+    await inst.isValid('c').should.become(true);
+  });
+
+  it('concats whitelist with refs', async function() {
+    let inst = object({
+      x: string().required(),
+      y: string()
+        .oneOf([ref('$x'), 'b', 'c'])
+        .concat(string().notOneOf(['c', ref('$x')])),
+    });
+
+    await inst.isValid({ x: 'a', y: 'a' }).should.become(false);
+    await inst.isValid({ x: 'a', y: 'b' }).should.become(true);
+    await inst.isValid({ x: 'a', y: 'c' }).should.become(false);
+  });
+
   it('defaults should be validated but not transformed', function() {
     let inst = string()
       .trim()
diff --git a/test/number.js b/test/number.js
index 4d9144b..651873d 100644
--- a/test/number.js
+++ b/test/number.js
@@ -203,8 +203,8 @@ describe('Number types', function() {
     var schema = number().integer();
 
     TestHelpers.validateAll(schema, {
-      valid: [4, -5222],
-      invalid: [10.53, 0.1 * 0.2, -34512535.626, 3.12312e51, new Date()],
+      valid: [4, -5222, 3.12312e51],
+      invalid: [10.53, 0.1 * 0.2, -34512535.626, new Date()],
     });
 
     it('should return default message', () => {
diff --git a/test/string.js b/test/string.js
index d99a144..55ebf8e 100644
--- a/test/string.js
+++ b/test/string.js
@@ -149,6 +149,21 @@ describe('String types', () => {
     ]);
   });
 
+  it('should check MATCHES correctly with global and sticky flags', function() {
+    var v = string().matches(/hi/gy);
+
+    return Promise.all([
+      v
+        .isValid('hi')
+        .should.eventually()
+        .equal(true),
+      v
+        .isValid('hi')
+        .should.eventually()
+        .equal(true),
+    ]);
+  });
+
   it('MATCHES should include empty strings', () => {
     let v = string().matches(/(hi|bye)/);