From 7ece746b29edfe385fedb24ebe1d2fd9c9ec87e3 Mon Sep 17 00:00:00 2001
From: Mark Pedrotti <pedrottimark@gmail.com>
Date: Mon, 4 Nov 2019 14:13:10 -0500
Subject: [PATCH] jest-snapshot: Improve report when the matcher has properties
 (#9104)

* jest-snapshot: Improve report when the matcher has properties

* Fix context in new matcher error test

* Update CHANGELOG.md

* Add test for received null when the matcher has properties
---
 CHANGELOG.md                                  |   1 +
 e2e/__tests__/toMatchSnapshot.test.ts         |   4 +-
 packages/jest-snapshot/src/State.ts           |   3 +-
 .../__snapshots__/printSnapshot.test.ts.snap  | 123 ++++++++++++------
 .../src/__tests__/printSnapshot.test.ts       |  87 +++++++++----
 .../jest-snapshot/src/__tests__/utils.test.ts |   2 +-
 packages/jest-snapshot/src/index.ts           |  32 +++--
 packages/jest-snapshot/src/printSnapshot.ts   |  45 ++++++-
 packages/jest-snapshot/src/utils.ts           |  20 ++-
 9 files changed, 232 insertions(+), 85 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 975de612d35c..5865b17d2e3c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -23,6 +23,7 @@
 - `[jest-matcher-utils]` Add `BigInt` support to `ensureNumbers` `ensureActualIsNumber`, `ensureExpectedIsNumber` ([#8382](https://github.com/facebook/jest/pull/8382))
 - `[jest-runner]` Warn if a worker had to be force exited ([#8206](https://github.com/facebook/jest/pull/8206))
 - `[jest-snapshot]` Display change counts in annotation lines ([#8982](https://github.com/facebook/jest/pull/8982))
+- `[jest-snapshot]` [**BREAKING**] Improve report when the matcher has properties ([#9104](https://github.com/facebook/jest/pull/9104))
 - `[@jest/test-result]` Create method to create empty `TestResult` ([#8867](https://github.com/facebook/jest/pull/8867))
 - `[jest-worker]` [**BREAKING**] Return a promise from `end()`, resolving with the information whether workers exited gracefully ([#8206](https://github.com/facebook/jest/pull/8206))
 - `[jest-reporters]` Transform file paths into hyperlinks ([#8980](https://github.com/facebook/jest/pull/8980))
diff --git a/e2e/__tests__/toMatchSnapshot.test.ts b/e2e/__tests__/toMatchSnapshot.test.ts
index 59336c948bce..2c5c1dce8bdc 100644
--- a/e2e/__tests__/toMatchSnapshot.test.ts
+++ b/e2e/__tests__/toMatchSnapshot.test.ts
@@ -255,7 +255,7 @@ test('handles property matchers with hint', () => {
     expect(stderr).toMatch(
       'Snapshot name: `handles property matchers with hint: descriptive hint 1`',
     );
-    expect(stderr).toMatch('Expected properties:');
+    expect(stderr).toMatch('Expected properties');
     expect(stderr).toMatch('Snapshots:   1 failed, 1 total');
     expect(exitCode).toBe(1);
   }
@@ -287,7 +287,7 @@ test('handles property matchers with deep properties', () => {
     expect(stderr).toMatch(
       'Snapshot name: `handles property matchers with deep properties 1`',
     );
-    expect(stderr).toMatch('Expected properties:');
+    expect(stderr).toMatch('Expected properties');
     expect(stderr).toMatch('Snapshots:   1 failed, 1 total');
     expect(exitCode).toBe(1);
   }
diff --git a/packages/jest-snapshot/src/State.ts b/packages/jest-snapshot/src/State.ts
index ea0e886d9a0f..af3e659ab54f 100644
--- a/packages/jest-snapshot/src/State.ts
+++ b/packages/jest-snapshot/src/State.ts
@@ -10,6 +10,7 @@ import {Config} from '@jest/types';
 
 import {getStackTraceLines, getTopFrame} from 'jest-message-util';
 import {
+  addExtraLineBreaks,
   getSnapshotData,
   keyToTestName,
   removeExtraLineBreaks,
@@ -198,7 +199,7 @@ export default class SnapshotState {
       this._uncheckedKeys.delete(key);
     }
 
-    const receivedSerialized = serialize(received);
+    const receivedSerialized = addExtraLineBreaks(serialize(received));
     const expected = isInline ? inlineSnapshot : this._snapshotData[key];
     const pass = expected === receivedSerialized;
     const hasSnapshot = expected !== undefined;
diff --git a/packages/jest-snapshot/src/__tests__/__snapshots__/printSnapshot.test.ts.snap b/packages/jest-snapshot/src/__tests__/__snapshots__/printSnapshot.test.ts.snap
index 9d9621f48717..b32622fee2b7 100644
--- a/packages/jest-snapshot/src/__tests__/__snapshots__/printSnapshot.test.ts.snap
+++ b/packages/jest-snapshot/src/__tests__/__snapshots__/printSnapshot.test.ts.snap
@@ -38,7 +38,7 @@ exports[`matcher error toMatchSnapshot Expected properties must be an object (no
 <b>Matcher error</>: Expected <g>properties</> must be an object
 
 Expected properties has type:  function
-Expected properties has value: <g>[Function properties]</>
+Expected properties has value: <g>[Function]</>
 `;
 
 exports[`matcher error toMatchSnapshot Expected properties must be an object (null) with hint 1`] = `
@@ -67,6 +67,23 @@ Snapshot state must be initialized
 Snapshot state has value: undefined
 `;
 
+exports[`matcher error toMatchSnapshot received value must be an object (non-null) 1`] = `
+<d>expect(</><r>received</><d>).</>toMatchSnapshot<d>(</><g>properties</><d>)</>
+
+<b>Matcher error</>: <r>received</> value must be an object when the matcher has <g>properties</>
+
+Received has type:  string
+Received has value: <r>"string"</>
+`;
+
+exports[`matcher error toMatchSnapshot received value must be an object (null) 1`] = `
+<d>expect(</><r>received</><d>).</>toMatchSnapshot<d>(</><g>properties</><d>)</>
+
+<b>Matcher error</>: <r>received</> value must be an object when the matcher has <g>properties</>
+
+Received has value: <r>null</>
+`;
+
 exports[`matcher error toThrowErrorMatchingInlineSnapshot Inline snapshot must be a string 1`] = `
 <d>expect(</><r>received</><d>).</>toThrowErrorMatchingInlineSnapshot<d>(</>snapshot<d>)</>
 
@@ -110,8 +127,15 @@ exports[`pass false toMatchInlineSnapshot with properties equals false with snap
 
 Snapshot name: \`with properties 1\`
 
-Expected properties: <g>{"id": "abcdef"}</>
-Received value:      <r>{"id": "abcdefg", "text": "Increase code coverage", "type": "ADD_ITEM"}</>
+<g>- Expected properties  - 1</>
+<r>+ Received value       + 3</>
+
+<d>  Object {</>
+<g>-   "id": "abcdef",</>
+<r>+   "id": "abcdefg",</>
+<r>+   "text": "Increase code coverage",</>
+<r>+   "type": "ADD_ITEM",</>
+<d>  }</>
 `;
 
 exports[`pass false toMatchInlineSnapshot with properties equals false without snapshot 1`] = `
@@ -119,8 +143,15 @@ exports[`pass false toMatchInlineSnapshot with properties equals false without s
 
 Snapshot name: \`with properties 1\`
 
-Expected properties: <g>{"id": "abcdef"}</>
-Received value:      <r>{"id": "abcdefg", "text": "Increase code coverage", "type": "ADD_ITEM"}</>
+<g>- Expected properties  - 1</>
+<r>+ Received value       + 3</>
+
+<d>  Object {</>
+<g>-   "id": "abcdef",</>
+<r>+   "id": "abcdefg",</>
+<r>+   "text": "Increase code coverage",</>
+<r>+   "type": "ADD_ITEM",</>
+<d>  }</>
 `;
 
 exports[`pass false toMatchInlineSnapshot with properties equals true 1`] = `
@@ -165,13 +196,29 @@ This is likely because this test is run in a continuous integration (CI) environ
 Received: <r>"Write me if you can!"</>
 `;
 
-exports[`pass false toMatchSnapshot with properties equals false 1`] = `
+exports[`pass false toMatchSnapshot with properties equals false isLineDiffable false 1`] = `
+<d>expect(</><r>received</><d>).</>toMatchSnapshot<d>(</><g>properties</><d>)</>
+
+Snapshot name: \`with properties 1\`
+
+Expected properties: <g>{"name": "Error"}</>
+Received value:      <r>[RangeError: Invalid array length]</>
+`;
+
+exports[`pass false toMatchSnapshot with properties equals false isLineDiffable true 1`] = `
 <d>expect(</><r>received</><d>).</>toMatchSnapshot<d>(</><g>properties</><d>)</>
 
 Snapshot name: \`with properties 1\`
 
-Expected properties: <g>{"id": "abcdef"}</>
-Received value:      <r>{"id": "abcdefg", "text": "Increase code coverage", "type": "ADD_ITEM"}</>
+<g>- Expected properties  - 1</>
+<r>+ Received value       + 3</>
+
+<d>  Object {</>
+<g>-   "id": "abcdef",</>
+<r>+   "id": "abcdefg",</>
+<r>+   "text": "Increase code coverage",</>
+<r>+   "type": "ADD_ITEM",</>
+<d>  }</>
 `;
 
 exports[`pass false toMatchSnapshot with properties equals true 1`] = `
@@ -199,17 +246,17 @@ Snapshot: <g>"inline snapshot"</>
 Received: <r>"received"</>
 `;
 
-exports[`printDiffOrStringified backtick single line expected and received 1`] = `
+exports[`printSnapshotAndReceived backtick single line expected and received 1`] = `
 Snapshot: <g>"var foo = \`backtick\`;"</>
 Received: <r>"var foo = <i>tag</i>\`backtick\`;"</>
 `;
 
-exports[`printDiffOrStringified empty string expected and received single line 1`] = `
+exports[`printSnapshotAndReceived empty string expected and received single line 1`] = `
 Snapshot: <g>""</>
 Received: <r>"single line string"</>
 `;
 
-exports[`printDiffOrStringified empty string received and expected multi line 1`] = `
+exports[`printSnapshotAndReceived empty string received and expected multi line 1`] = `
 <g>- Snapshot  - 3</>
 <r>+ Received  + 0</>
 
@@ -218,7 +265,7 @@ exports[`printDiffOrStringified empty string received and expected multi line 1`
 <g>- string</>
 `;
 
-exports[`printDiffOrStringified escape backslash in multi line string 1`] = `
+exports[`printSnapshotAndReceived escape backslash in multi line string 1`] = `
 <g>- Snapshot  - 1</>
 <r>+ Received  + 2</>
 
@@ -227,22 +274,22 @@ exports[`printDiffOrStringified escape backslash in multi line string 1`] = `
 <r>+ <i>B</i>ack \\ slash</>
 `;
 
-exports[`printDiffOrStringified escape backslash in single line string 1`] = `
+exports[`printSnapshotAndReceived escape backslash in single line string 1`] = `
 Snapshot: <g>"<i>f</i>orward / slash and back \\\\ slash"</>
 Received: <r>"<i>F</i>orward / slash and back \\\\ slash"</>
 `;
 
-exports[`printDiffOrStringified escape double quote marks in string 1`] = `
+exports[`printSnapshotAndReceived escape double quote marks in string 1`] = `
 Snapshot: <g>"What does \\"<i>oo</i>bleck\\" mean?"</>
 Received: <r>"What does \\"<i>ew</i>bleck\\" mean?"</>
 `;
 
-exports[`printDiffOrStringified escape regexp 1`] = `
+exports[`printSnapshotAndReceived escape regexp 1`] = `
 Snapshot: <g>/\\\\\\\\\\("\\)/g</>
 Received: <r>/\\\\\\\\\\("\\)/</>
 `;
 
-exports[`printDiffOrStringified expand false 1`] = `
+exports[`printSnapshotAndReceived expand false 1`] = `
 <g>- Snapshot  - 1</>
 <r>+ Received  + 3</>
 
@@ -259,7 +306,7 @@ exports[`printDiffOrStringified expand false 1`] = `
 <d>  ↵</>
 `;
 
-exports[`printDiffOrStringified expand true 1`] = `
+exports[`printSnapshotAndReceived expand true 1`] = `
 <g>- Snapshot  - 1</>
 <r>+ Received  + 3</>
 
@@ -286,7 +333,7 @@ exports[`printDiffOrStringified expand true 1`] = `
 <d>  ↵</>
 `;
 
-exports[`printDiffOrStringified fallback to line diff 1`] = `
+exports[`printSnapshotAndReceived fallback to line diff 1`] = `
 <g>- Snapshot  - 1</>
 <r>+ Received  + 8</>
 
@@ -306,7 +353,7 @@ exports[`printDiffOrStringified fallback to line diff 1`] = `
 <r>+ ================================================================================</>
 `;
 
-exports[`printDiffOrStringified has no common after clean up chaff array 1`] = `
+exports[`printSnapshotAndReceived has no common after clean up chaff array 1`] = `
 <g>- Snapshot  - 2</>
 <r>+ Received  + 2</>
 
@@ -318,44 +365,44 @@ exports[`printDiffOrStringified has no common after clean up chaff array 1`] = `
 <d>  ]</>
 `;
 
-exports[`printDiffOrStringified has no common after clean up chaff string single line 1`] = `
+exports[`printSnapshotAndReceived has no common after clean up chaff string single line 1`] = `
 Snapshot: <g>"delete"</>
 Received: <r>"insert"</>
 `;
 
-exports[`printDiffOrStringified isLineDiffable false asymmetric matcher 1`] = `
+exports[`printSnapshotAndReceived isLineDiffable false asymmetric matcher 1`] = `
 Snapshot: <g>null</>
 Received: <r>Object {</>
 <r>  "asymmetricMatch": [Function],</>
 <r>}</>
 `;
 
-exports[`printDiffOrStringified isLineDiffable false boolean 1`] = `
+exports[`printSnapshotAndReceived isLineDiffable false boolean 1`] = `
 Snapshot: <g>true</>
 Received: <r>false</>
 `;
 
-exports[`printDiffOrStringified isLineDiffable false date 1`] = `
+exports[`printSnapshotAndReceived isLineDiffable false date 1`] = `
 Snapshot: <g>2019-09-19T00:00:00.000Z</>
 Received: <r>2019-09-20T00:00:00.000Z</>
 `;
 
-exports[`printDiffOrStringified isLineDiffable false error 1`] = `
+exports[`printSnapshotAndReceived isLineDiffable false error 1`] = `
 Snapshot: <g>[Error: Cannot spread fragment "NameAndAppearances" within itself.]</>
 Received: <r>[Error: Cannot spread fragment "NameAndAppearancesAndFriends" within itself.]</>
 `;
 
-exports[`printDiffOrStringified isLineDiffable false function 1`] = `
+exports[`printSnapshotAndReceived isLineDiffable false function 1`] = `
 Snapshot: <g>undefined</>
 Received: <r>[Function]</>
 `;
 
-exports[`printDiffOrStringified isLineDiffable false number 1`] = `
+exports[`printSnapshotAndReceived isLineDiffable false number 1`] = `
 Snapshot: <g>-0</>
 Received: <r>NaN</>
 `;
 
-exports[`printDiffOrStringified isLineDiffable true array 1`] = `
+exports[`printSnapshotAndReceived isLineDiffable true array 1`] = `
 <g>- Snapshot  - 0</>
 <r>+ Received  + 2</>
 
@@ -373,7 +420,7 @@ exports[`printDiffOrStringified isLineDiffable true array 1`] = `
 <d>  ]</>
 `;
 
-exports[`printDiffOrStringified isLineDiffable true object 1`] = `
+exports[`printSnapshotAndReceived isLineDiffable true object 1`] = `
 <g>- Snapshot  - 2</>
 <r>+ Received  + 3</>
 
@@ -389,7 +436,7 @@ exports[`printDiffOrStringified isLineDiffable true object 1`] = `
 <d>  }</>
 `;
 
-exports[`printDiffOrStringified isLineDiffable true single line expected and multi line received 1`] = `
+exports[`printSnapshotAndReceived isLineDiffable true single line expected and multi line received 1`] = `
 <g>- Snapshot  - 1</>
 <r>+ Received  + 3</>
 
@@ -399,7 +446,7 @@ exports[`printDiffOrStringified isLineDiffable true single line expected and mul
 <r>+ ]</>
 `;
 
-exports[`printDiffOrStringified isLineDiffable true single line expected and received 1`] = `
+exports[`printSnapshotAndReceived isLineDiffable true single line expected and received 1`] = `
 <g>- Snapshot  - 1</>
 <r>+ Received  + 1</>
 
@@ -407,7 +454,7 @@ exports[`printDiffOrStringified isLineDiffable true single line expected and rec
 <r>+ Object {}</>
 `;
 
-exports[`printDiffOrStringified multi line small change in one line and other is unchanged 1`] = `
+exports[`printSnapshotAndReceived multi line small change in one line and other is unchanged 1`] = `
 <g>- Snapshot  - 1</>
 <r>+ Received  + 1</>
 
@@ -416,7 +463,7 @@ exports[`printDiffOrStringified multi line small change in one line and other is
 <d>  Must be one of: 'Home'</>
 `;
 
-exports[`printDiffOrStringified multi line small changes 1`] = `
+exports[`printSnapshotAndReceived multi line small changes 1`] = `
 <g>- Snapshot  - 7</>
 <r>+ Received  + 7</>
 
@@ -437,12 +484,12 @@ exports[`printDiffOrStringified multi line small changes 1`] = `
 <r>+     at Object.doesNotThrow (__tests__/assertionError.test.js:7<i>0</i>:10)</>
 `;
 
-exports[`printDiffOrStringified single line large changes 1`] = `
+exports[`printSnapshotAndReceived single line large changes 1`] = `
 Snapshot: <g>"<i>A</i>rray length<i> must be a finite positive integer</i>"</>
 Received: <r>"<i>Invalid a</i>rray length"</>
 `;
 
-exports[`printDiffOrStringified without serialize backtick single line expected and multi line received 1`] = `
+exports[`printSnapshotAndReceived without serialize backtick single line expected and multi line received 1`] = `
 <g>- Snapshot  - 1</>
 <r>+ Received  + 2</>
 
@@ -451,7 +498,7 @@ exports[`printDiffOrStringified without serialize backtick single line expected
 <r>+ tick\`;</>
 `;
 
-exports[`printDiffOrStringified without serialize backtick single line expected and received 1`] = `
+exports[`printSnapshotAndReceived without serialize backtick single line expected and received 1`] = `
 <g>- Snapshot  - 1</>
 <r>+ Received  + 1</>
 
@@ -459,7 +506,7 @@ exports[`printDiffOrStringified without serialize backtick single line expected
 <r>+ var foo = \`back<i>\${x}</i>tick\`;</>
 `;
 
-exports[`printDiffOrStringified without serialize has no common after clean up chaff multi line 1`] = `
+exports[`printSnapshotAndReceived without serialize has no common after clean up chaff multi line 1`] = `
 <g>- Snapshot  - 2</>
 <r>+ Received  + 2</>
 
@@ -469,7 +516,7 @@ exports[`printDiffOrStringified without serialize has no common after clean up c
 <r>+ 2</>
 `;
 
-exports[`printDiffOrStringified without serialize has no common after clean up chaff single line 1`] = `
+exports[`printSnapshotAndReceived without serialize has no common after clean up chaff single line 1`] = `
 <g>- Snapshot  - 1</>
 <r>+ Received  + 1</>
 
@@ -477,7 +524,7 @@ exports[`printDiffOrStringified without serialize has no common after clean up c
 <r>+ insert</>
 `;
 
-exports[`printDiffOrStringified without serialize prettier/pull/5590 1`] = `
+exports[`printSnapshotAndReceived without serialize prettier/pull/5590 1`] = `
 <g>- Snapshot  - 1</>
 <r>+ Received  + 1</>
 
diff --git a/packages/jest-snapshot/src/__tests__/printSnapshot.test.ts b/packages/jest-snapshot/src/__tests__/printSnapshot.test.ts
index 7f4898a756bd..6df0bab18d65 100644
--- a/packages/jest-snapshot/src/__tests__/printSnapshot.test.ts
+++ b/packages/jest-snapshot/src/__tests__/printSnapshot.test.ts
@@ -11,8 +11,8 @@ import chalk from 'chalk';
 import format = require('pretty-format');
 
 import jestSnapshot = require('../index');
-import {printDiffOrStringified} from '../printSnapshot';
-import {stringify} from '../utils';
+import {printSnapshotAndReceived} from '../printSnapshot';
+import {serialize} from '../utils';
 
 const convertAnsi = (val: string): string =>
   val.replace(ansiRegex(), match => {
@@ -168,6 +168,28 @@ describe('matcher error', () => {
       }).toThrowErrorMatchingSnapshot();
     });
 
+    describe('received value must be an object', () => {
+      const context = {
+        currentTestName: '',
+        isNot: false,
+        promise: '',
+        snapshotState: {},
+      };
+      const properties = {};
+
+      test('(non-null)', () => {
+        expect(() => {
+          toMatchSnapshot.call(context, 'string', properties);
+        }).toThrowErrorMatchingSnapshot();
+      });
+
+      test('(null)', () => {
+        expect(() => {
+          toMatchSnapshot.call(context, null, properties);
+        }).toThrowErrorMatchingSnapshot();
+      });
+    });
+
     // Future test: Snapshot hint must be a string
 
     test('Snapshot state must be initialized', () => {
@@ -433,7 +455,7 @@ describe('pass false', () => {
       const properties = {id};
       const type = 'ADD_ITEM';
 
-      test('equals false', () => {
+      describe('equals false', () => {
         const context = {
           currentTestName: 'with properties',
           equals: () => false,
@@ -447,19 +469,32 @@ describe('pass false', () => {
             subsetEquality: () => {},
           },
         };
-        const received = {
-          id: 'abcdefg',
-          text: 'Increase code coverage',
-          type,
-        };
 
-        const {message, pass} = toMatchSnapshot.call(
-          context,
-          received,
-          properties,
-        );
-        expect(pass).toBe(false);
-        expect(message()).toMatchSnapshot();
+        test('isLineDiffable false', () => {
+          const {message, pass} = toMatchSnapshot.call(
+            context,
+            new RangeError('Invalid array length'),
+            {name: 'Error'},
+          );
+          expect(pass).toBe(false);
+          expect(message()).toMatchSnapshot();
+        });
+
+        test('isLineDiffable true', () => {
+          const received = {
+            id: 'abcdefg',
+            text: 'Increase code coverage',
+            type,
+          };
+
+          const {message, pass} = toMatchSnapshot.call(
+            context,
+            received,
+            properties,
+          );
+          expect(pass).toBe(false);
+          expect(message()).toMatchSnapshot();
+        });
       });
 
       test('equals true', () => {
@@ -564,16 +599,16 @@ describe('pass true', () => {
   });
 });
 
-describe('printDiffOrStringified', () => {
+describe('printSnapshotAndReceived', () => {
   // Simulate default serialization.
   const testWithStringify = (
     expected: unknown,
     received: unknown,
     expand: boolean,
   ): string =>
-    printDiffOrStringified(
-      stringify(expected),
-      stringify(received),
+    printSnapshotAndReceived(
+      serialize(expected),
+      serialize(received),
       received,
       expand,
     );
@@ -583,7 +618,7 @@ describe('printDiffOrStringified', () => {
     expected: string,
     received: string,
     expand: boolean,
-  ): string => printDiffOrStringified(expected, received, received, expand);
+  ): string => printSnapshotAndReceived(expected, received, received, expand);
 
   describe('backtick', () => {
     test('single line expected and received', () => {
@@ -747,7 +782,7 @@ describe('printDiffOrStringified', () => {
 
       test('both are less', () => {
         const less2 = 'multi\nline';
-        const difference = printDiffOrStringified(less2, less, less, true);
+        const difference = printSnapshotAndReceived(less2, less, less, true);
 
         expect(difference).toMatch('- multi');
         expect(difference).toMatch('- line');
@@ -756,7 +791,7 @@ describe('printDiffOrStringified', () => {
       });
 
       test('expected is more', () => {
-        const difference = printDiffOrStringified(more, less, less, true);
+        const difference = printSnapshotAndReceived(more, less, less, true);
 
         expect(difference).toMatch('- multi line');
         expect(difference).toMatch('+ single line');
@@ -764,7 +799,7 @@ describe('printDiffOrStringified', () => {
       });
 
       test('received is more', () => {
-        const difference = printDiffOrStringified(less, more, more, true);
+        const difference = printSnapshotAndReceived(less, more, more, true);
 
         expect(difference).toMatch('- single line');
         expect(difference).toMatch('+ multi line');
@@ -782,7 +817,7 @@ describe('printDiffOrStringified', () => {
 
       test('both are less', () => {
         const lessQuoted2 = '"0 numbers"';
-        const stringified = printDiffOrStringified(
+        const stringified = printSnapshotAndReceived(
           lessQuoted2,
           lessQuoted,
           less,
@@ -795,7 +830,7 @@ describe('printDiffOrStringified', () => {
       });
 
       test('expected is more', () => {
-        const stringified = printDiffOrStringified(
+        const stringified = printSnapshotAndReceived(
           moreQuoted,
           lessQuoted,
           less,
@@ -809,7 +844,7 @@ describe('printDiffOrStringified', () => {
       });
 
       test('received is more', () => {
-        const stringified = printDiffOrStringified(
+        const stringified = printSnapshotAndReceived(
           lessQuoted,
           moreQuoted,
           more,
diff --git a/packages/jest-snapshot/src/__tests__/utils.test.ts b/packages/jest-snapshot/src/__tests__/utils.test.ts
index f3db3a0ac438..f2c12fb2b1d9 100644
--- a/packages/jest-snapshot/src/__tests__/utils.test.ts
+++ b/packages/jest-snapshot/src/__tests__/utils.test.ts
@@ -191,7 +191,7 @@ test('serialize handles \\r\\n', () => {
   const data = '<div>\r\n</div>';
   const serializedData = serialize(data);
 
-  expect(serializedData).toBe('\n"<div>\n</div>"\n');
+  expect(serializedData).toBe('"<div>\n</div>"');
 });
 
 describe('ExtraLineBreaks', () => {
diff --git a/packages/jest-snapshot/src/index.ts b/packages/jest-snapshot/src/index.ts
index b83d1b1bd93b..33b5c9176429 100644
--- a/packages/jest-snapshot/src/index.ts
+++ b/packages/jest-snapshot/src/index.ts
@@ -16,8 +16,6 @@ import {
   RECEIVED_COLOR,
   matcherErrorMessage,
   matcherHint,
-  printExpected,
-  printReceived,
   printWithType,
   stringify,
 } from 'jest-matcher-utils';
@@ -34,7 +32,10 @@ import {
   SNAPSHOT_ARG,
   matcherHintFromConfig,
   noColor,
-  printDiffOrStringified,
+  printExpected,
+  printPropertiesAndReceived,
+  printReceived,
+  printSnapshotAndReceived,
 } from './printSnapshot';
 import {Context, MatchSnapshotConfig} from './types';
 import * as utils from './utils';
@@ -254,7 +255,7 @@ const toMatchInlineSnapshot = function(
         matcherErrorMessage(
           matcherHint(matcherName, undefined, PROPERTIES_ARG, options),
           `Inline snapshot must be a string`,
-          printWithType('Inline snapshot', inlineSnapshot, stringify),
+          printWithType('Inline snapshot', inlineSnapshot, utils.serialize),
         ),
       );
     }
@@ -301,6 +302,8 @@ const _toMatchSnapshot = (config: MatchSnapshotConfig) => {
 
   if (snapshotState == null) {
     // Because the state is the problem, this is not a matcher error.
+    // Call generic stringify from jest-matcher-utils package
+    // because uninitialized snapshot state does not need snapshot serializers.
     throw new Error(
       matcherHintFromConfig(config, false) +
         '\n\n' +
@@ -316,6 +319,20 @@ const _toMatchSnapshot = (config: MatchSnapshotConfig) => {
       : currentTestName || ''; // future BREAKING change: || hint
 
   if (typeof properties === 'object') {
+    if (typeof received !== 'object' || received === null) {
+      throw new Error(
+        matcherErrorMessage(
+          matcherHintFromConfig(config, false),
+          `${RECEIVED_COLOR(
+            'received',
+          )} value must be an object when the matcher has ${EXPECTED_COLOR(
+            'properties',
+          )}`,
+          printWithType('Received', received, printReceived),
+        ),
+      );
+    }
+
     const propertyPass = context.equals(received, properties, [
       context.utils.iterableEquality,
       context.utils.subsetEquality,
@@ -331,8 +348,7 @@ const _toMatchSnapshot = (config: MatchSnapshotConfig) => {
         '\n\n' +
         printSnapshotName(currentTestName, hint, count) +
         '\n\n' +
-        `Expected properties: ${printExpected(properties)}\n` +
-        `Received value:      ${printReceived(received)}`;
+        printPropertiesAndReceived(properties, received, snapshotState.expand);
 
       return {
         message,
@@ -376,7 +392,7 @@ const _toMatchSnapshot = (config: MatchSnapshotConfig) => {
           '\n\n' +
           printSnapshotName(currentTestName, hint, count) +
           '\n\n' +
-          printDiffOrStringified(
+          printSnapshotAndReceived(
             expected,
             actual,
             received,
@@ -437,7 +453,7 @@ const toThrowErrorMatchingInlineSnapshot = function(
       matcherErrorMessage(
         matcherHint(matcherName, undefined, SNAPSHOT_ARG, options),
         `Inline snapshot must be a string`,
-        printWithType('Inline snapshot', inlineSnapshot, stringify),
+        printWithType('Inline snapshot', inlineSnapshot, utils.serialize),
       ),
     );
   }
diff --git a/packages/jest-snapshot/src/printSnapshot.ts b/packages/jest-snapshot/src/printSnapshot.ts
index bc7836ed043c..40de416e60a8 100644
--- a/packages/jest-snapshot/src/printSnapshot.ts
+++ b/packages/jest-snapshot/src/printSnapshot.ts
@@ -28,7 +28,7 @@ import {
 } from 'jest-matcher-utils';
 import prettyFormat = require('pretty-format');
 import {MatchSnapshotConfig} from './types';
-import {unstringifyString} from './utils';
+import {deserializeString, minify, serialize} from './utils';
 
 export const noColor = (string: string) => string;
 
@@ -132,9 +132,48 @@ const isLineDiffable = (received: any): boolean => {
   return true;
 };
 
+export const printExpected = (val: unknown) => EXPECTED_COLOR(minify(val));
+export const printReceived = (val: unknown) => RECEIVED_COLOR(minify(val));
+
+export const printPropertiesAndReceived = (
+  properties: object,
+  received: object,
+  expand: boolean, // CLI options: true if `--expand` or false if `--no-expand`
+): string => {
+  const aAnnotation = 'Expected properties';
+  const bAnnotation = 'Received value';
+
+  if (isLineDiffable(properties) && isLineDiffable(received)) {
+    return diffLinesUnified(
+      splitLines0(serialize(properties)),
+      splitLines0(serialize(received)),
+      {
+        aAnnotation,
+        aColor: EXPECTED_COLOR,
+        bAnnotation,
+        bColor: RECEIVED_COLOR,
+        changeLineTrailingSpaceColor: chalk.bgYellow,
+        commonLineTrailingSpaceColor: chalk.bgYellow,
+        emptyFirstOrLastLinePlaceholder: '↵', // U+21B5
+        expand,
+        includeChangeCounts: true,
+      },
+    );
+  }
+
+  const printLabel = getLabelPrinter(aAnnotation, bAnnotation);
+  return (
+    printLabel(aAnnotation) +
+    printExpected(properties) +
+    '\n' +
+    printLabel(bAnnotation) +
+    printReceived(received)
+  );
+};
+
 const MAX_DIFF_STRING_LENGTH = 20000;
 
-export const printDiffOrStringified = (
+export const printSnapshotAndReceived = (
   a: string, // snapshot without extra line breaks
   b: string, // received serialized but without extra line breaks
   received: unknown,
@@ -193,7 +232,7 @@ export const printDiffOrStringified = (
       }
 
       // Else either string is multiline, so display as unquoted strings.
-      a = unstringifyString(a); //  hypothetical unserialized expected string
+      a = deserializeString(a); //  hypothetical expected string
       b = received; // not serialized
     }
     // Else expected had custom serialization or was not a string
diff --git a/packages/jest-snapshot/src/utils.ts b/packages/jest-snapshot/src/utils.ts
index eb97da5c3edc..af0d059476e1 100644
--- a/packages/jest-snapshot/src/utils.ts
+++ b/packages/jest-snapshot/src/utils.ts
@@ -136,20 +136,28 @@ export const removeExtraLineBreaks = (string: string): string =>
     ? string.slice(1, -1)
     : string;
 
-export const serialize = (val: unknown): string =>
-  addExtraLineBreaks(stringify(val));
+const escapeRegex = true;
+const printFunctionName = false;
 
-export const stringify = (val: unknown): string =>
+export const serialize = (val: unknown): string =>
   normalizeNewlines(
     prettyFormat(val, {
-      escapeRegex: true,
+      escapeRegex,
       plugins: getSerializers(),
-      printFunctionName: false,
+      printFunctionName,
     }),
   );
 
+export const minify = (val: unknown): string =>
+  prettyFormat(val, {
+    escapeRegex,
+    min: true,
+    plugins: getSerializers(),
+    printFunctionName,
+  });
+
 // Remove double quote marks and unescape double quotes and backslashes.
-export const unstringifyString = (stringified: string): string =>
+export const deserializeString = (stringified: string): string =>
   stringified.slice(1, -1).replace(/\\("|\\)/g, '$1');
 
 export const escapeBacktickString = (str: string): string =>