Repository: vuejs/vue
Pull request: feat(warn): make the warning messages more explicit (close #7764)

<!--
Please make sure to read the Pull Request Guidelines:
https://github.com/vuejs/vue/blob/dev/.github/CONTRIBUTING.md#pull-request-guidelines
-->

<!-- PULL REQUEST TEMPLATE -->
<!-- (Update "[ ]" to "[x]" to check a box) -->

**What kind of change does this PR introduce?** (check at least one)

- [x] Feature

**Does this PR introduce a breaking change?** (check one)

- [x] No

If yes, please describe the impact and migration path for existing applications:

**The PR fulfills these requirements:**

- [x] It's submitted to the `dev` branch for v2.x (or to a previous version branch), _not_ the `master` branch
- [x] When resolving a specific issue, it's referenced in the PR's title (e.g. `fix #xxx[,#xxx]`, where "xxx" is the issue number)
- [x] All tests are passing: https://github.com/vuejs/vue/blob/dev/.github/CONTRIBUTING.md#development-setup
- [x] New/updated tests are included

If adding a **new feature**, the PR's description includes:
- [x] A convincing reason for adding this feature (to avoid wasting your time, it's best to open a suggestion issue first and wait for approval before working on it)

**Other information:**

Here is my solution for #7764.
I'm sure it's far from perfect, so I'll be happy to see any suggestion or advice 👍 

Changed files:
filename: src/core/util/props.js
patch: @@ -127,11 +127,10 @@ function assertProp (
       valid = assertedType.valid
     }
   }
+
   if (!valid) {
     warn(
-      `Invalid prop: type check failed for prop "${name}".` +
-      ` Expected ${expectedTypes.map(capitalize).join(', ')}` +
-      `, got ${toRawType(value)}.`,
+      getInvalidTypeMessage(name, value, expectedTypes),
       vm
     )
     return
@@ -200,3 +199,43 @@ function getTypeIndex (type, expectedTypes): number {
   }
   return -1
 }
+
+function getInvalidTypeMessage (name, value, expectedTypes) {
+  let message = `Invalid prop: type check failed for prop "${name}".` +
+    ` Expected ${expectedTypes.map(capitalize).join(', ')}`
+  const expectedType = expectedTypes[0]
+  const receivedType = toRawType(value)
+  const expectedValue = styleValue(value, expectedType)
+  const receivedValue = styleValue(value, receivedType)
+  // check if we need to specify expected value
+  if (expectedTypes.length === 1 &&
+      isExplicable(expectedType) &&
+      !isBoolean(expectedType, receivedType)) {
+    message += ` with value ${expectedValue}`
+  }
+  message += `, got ${receivedType} `
+  // check if we need to specify received value
+  if (isExplicable(receivedType)) {
+    message += `with value ${receivedValue}.`
+  }
+  return message
+}
+
+function styleValue (value, type) {
+  if (type === 'String') {
+    return `"${value}"`
+  } else if (type === 'Number') {
+    return `${Number(value)}`
+  } else {
+    return `${value}`
+  }
+}
+
+function isExplicable (value) {
+  const explicitTypes = ['string', 'number', 'boolean']
+  return explicitTypes.some(elem => value.toLowerCase() === elem)
+}
+
+function isBoolean (...args) {
+  return args.some(elem => elem.toLowerCase() === 'boolean')
+}

filename: test/unit/features/options/props.spec.js
patch: @@ -171,42 +171,56 @@ describe('Options props', () => {
       makeInstance('hello', String)
       expect(console.error.calls.count()).toBe(0)
       makeInstance(123, String)
-      expect('Expected String').toHaveBeenWarned()
+      expect('Expected String with value "123", got Number with value 123').toHaveBeenWarned()
     })
 
     it('number', () => {
       makeInstance(123, Number)
       expect(console.error.calls.count()).toBe(0)
       makeInstance('123', Number)
-      expect('Expected Number').toHaveBeenWarned()
+      expect('Expected Number with value 123, got String with value "123"').toHaveBeenWarned()
+    })
+
+    it('number & boolean', () => {
+      makeInstance(123, Number)
+      expect(console.error.calls.count()).toBe(0)
+      makeInstance(false, Number)
+      expect('Expected Number, got Boolean with value false').toHaveBeenWarned()
+    })
+
+    it('string & boolean', () => {
+      makeInstance('hello', String)
+      expect(console.error.calls.count()).toBe(0)
+      makeInstance(true, String)
+      expect('Expected String, got Boolean with value true').toHaveBeenWarned()
     })
 
     it('boolean', () => {
       makeInstance(true, Boolean)
       expect(console.error.calls.count()).toBe(0)
       makeInstance('123', Boolean)
-      expect('Expected Boolean').toHaveBeenWarned()
+      expect('Expected Boolean, got String with value "123"').toHaveBeenWarned()
     })
 
     it('function', () => {
       makeInstance(() => {}, Function)
       expect(console.error.calls.count()).toBe(0)
       makeInstance(123, Function)
-      expect('Expected Function').toHaveBeenWarned()
+      expect('Expected Function, got Number with value 123').toHaveBeenWarned()
     })
 
     it('object', () => {
       makeInstance({}, Object)
       expect(console.error.calls.count()).toBe(0)
       makeInstance([], Object)
-      expect('Expected Object').toHaveBeenWarned()
+      expect('Expected Object, got Array').toHaveBeenWarned()
     })
 
     it('array', () => {
       makeInstance([], Array)
       expect(console.error.calls.count()).toBe(0)
       makeInstance({}, Array)
-      expect('Expected Array').toHaveBeenWarned()
+      expect('Expected Array, got Object').toHaveBeenWarned()
     })
 
     it('primitive wrapper objects', () => {
@@ -225,7 +239,7 @@ describe('Options props', () => {
         makeInstance(Symbol('foo'), Symbol)
         expect(console.error.calls.count()).toBe(0)
         makeInstance({}, Symbol)
-        expect('Expected Symbol').toHaveBeenWarned()
+        expect('Expected Symbol, got Object').toHaveBeenWarned()
       })
     }
 
@@ -257,7 +271,7 @@ describe('Options props', () => {
       makeInstance(123, Number, v => v === 234)
       expect('custom validator check failed').toHaveBeenWarned()
       makeInstance(123, String, v => v === 123)
-      expect('Expected String').toHaveBeenWarned()
+      expect('Expected String with value "123", got Number with value 123').toHaveBeenWarned()
     })
 
     it('multiple types + custom validator', () => {