vitest icon indicating copy to clipboard operation
vitest copied to clipboard

inline snapshots have double indent

Open milahu opened this issue 3 years ago • 2 comments

Describe the bug

snapshot strings have double indent in line 2 and following

      a
            b
            c

i want "string snapshots" like in #856 but this bug-feature produces ugly snapshots

Reproduction

https://stackblitz.com/edit/vitest-dev-vitest-kfybto?file=test/basic.test.ts

demo.test.js

import { assert, describe, expect, it } from 'vitest';

// dont escape string snapshots
const stringSnapshotSerializer = {
  serialize(val) {
    return val
  },
  test(val) {
    return (typeof val == "string")
  },
}

describe('suite name', () => {

  // expected

  it('string snapshot expected', () => {
    expect.addSnapshotSerializer(stringSnapshotSerializer)
    expect(`
      a
      b
      c
    `).toMatchInlineSnapshot(`
      a
      b
      c
    `);
  });

  // actual: default serializer

  it('snapshot', () => {
    // value indent: 6 spaces
    // sshot indent: 12 spaces
    expect(`
      a
      b
      c
    `).toMatchInlineSnapshot(`
      "
            a
            b
            c
          "
    `);
  });

  // actual: string serializer

  it('string snapshot actual', () => {
    expect.addSnapshotSerializer(stringSnapshotSerializer)
    // value indent: 6 spaces
    // sshot indent: 12 spaces
    expect(`
      a
      b
      c
    `).toMatchInlineSnapshot(`
      a
            b
            c
    `);
  });

  {
    it('string snapshot actual', () => {
      expect.addSnapshotSerializer(stringSnapshotSerializer)
      // value indent: 8 spaces
      // sshot indent: 16 spaces
      expect(`
        a
        b
        c
      `).toMatchInlineSnapshot(`
        a
                b
                c
      `);
    });
  }

});

Workaround

add a prefix to the string

// dont escape string snapshots
const stringSnapshotSerializer = {
  serialize(val) {
    //return val
    return "string:" + val
  },
  test(val) {
    return (typeof val == "string")
  },
}

describe('suite name', () => {
  it('string snapshot expected', () => {
    expect.addSnapshotSerializer(stringSnapshotSerializer)
    expect(`
      a
      b
      c
    `).toMatchInlineSnapshot(`
      string:
            a
            b
            c
    `);
  });

Fix

blame: stripSnapshotIndentation, prepareSnapString @ inlineSnapshot.ts

the "wrong first line" is caused by snap.trim()

the "double indent" is caused by lines.map((i) => i ? indentNext + i : "")

the serialized snapshot is also modified by addExtraLineBreaks(serialize(received and prepareExpected( and expect(actual.trim()).equals(expected ? expected.trim() : ""); which is not desired in my case

these could be made optional

// dont escape string snapshots
const stringSnapshotSerializer = {
  serialize(val) {
    return val
    //return "string:" + val
  },
  test(val) {
    return (typeof val == "string")
  },
  trim: false,
  indent: false,
  addExtraLineBreaks: false,
}

or add a new method toMatchStringSnapshot

patches/vitest+0.25.2.patch
diff --git a/node_modules/vitest/dist/chunk-runtime-chain.a0b441dc.js b/node_modules/vitest/dist/chunk-runtime-chain.a0b441dc.js
index 4323980..07ba202 100644
--- a/node_modules/vitest/dist/chunk-runtime-chain.a0b441dc.js
+++ b/node_modules/vitest/dist/chunk-runtime-chain.a0b441dc.js
@@ -1,3 +1,5 @@
+const debugSnaps = false;
+
 import util$1 from 'util';
 import { i as isObject, b as getCallLastIndex, s as slash, g as getWorkerState, c as getNames, d as assertTypes, e as getFullName, n as noop, f as isRunningInTest, h as isRunningInBenchmark } from './chunk-typecheck-constants.4891f22f.js';
 import * as chai$2 from 'chai';
@@ -502,10 +504,18 @@ const removeExtraLineBreaks = (string) => string.length > 2 && string.startsWith
 const escapeRegex = true;
 const printFunctionName = false;
 function serialize(val, indent = 2, formatOverrides = {}) {
+
+  debugSnaps && console.dir({
+    f: "serialize",
+    val,
+    plugins: getSerializers(),
+  })
+  
   return normalizeNewlines(
     format_1(val, {
       escapeRegex,
       indent,
+      // expect.addSnapshotSerializer
       plugins: getSerializers(),
       printFunctionName,
       ...formatOverrides
@@ -547,6 +557,8 @@ ${snapshots.join("\n\n")}
   ));
 }
 function prepareExpected(expected) {
+  // dont prepare
+  return expected
   function findStartIndent() {
     var _a, _b;
     const matchObject = /^( +)}\s+$/m.exec(expected || "");
@@ -607,6 +619,12 @@ async function saveInlineSnapshots(snapshots) {
     const code = await promises.readFile(file, "utf8");
     const s = new MagicString(code);
     for (const snap of snaps) {
+
+      debugSnaps && console.dir({
+        f: "saveInlineSnapshots",
+        snap,
+      })
+
       const index = posToNumber(code, snap);
       replaceInlineSnap(code, s, index, snap.snapshot);
     }
@@ -629,22 +647,52 @@ function replaceObjectSnap(code, s, index, newSnap) {
   return true;
 }
 function prepareSnapString(snap, source, index) {
+
   const lineIndex = numberToPos(source, index).line;
   const line = source.split(lineSplitRE)[lineIndex - 1];
   const indent = line.match(/^\s*/)[0] || "";
   const indentNext = indent.includes("	") ? `${indent}	` : `${indent}  `;
-  const lines = snap.trim().replace(/\\/g, "\\\\").split(/\n/g);
+  //const lines = snap.trim().replace(/\\/g, "\\\\").split(/\n/g);
+  // dont trim
+  const lines = snap.replace(/\\/g, "\\\\").split(/\n/g);
   const isOneline = lines.length <= 1;
   const quote = isOneline ? "'" : "`";
+
+  debugSnaps && console.dir({
+    f: "prepareSnapString",
+    snap,
+    line,
+    indent,
+    indentNext,
+    isOneline,
+    lines,
+    //source, index // test file source
+  })
+
+// add indentNext
+//${lines.map((i) => i ? indentNext + i : "").join("\n").replace(/`/g, "\\`").replace(/\${/g, "\\${")}
+// dont add indentNext
+
+// dont wrap
+return `${quote}${snap.replace(/`/g, "\\`").replace(/\${/g, "\\${")}${quote}`
+
+
   if (isOneline)
     return `'${lines.join("\n").replace(/'/g, "\\'")}'`;
   else
     return `${quote}
-${lines.map((i) => i ? indentNext + i : "").join("\n").replace(/`/g, "\\`").replace(/\${/g, "\\${")}
+${lines.join("\n").replace(/`/g, "\\`").replace(/\${/g, "\\${")}
 ${indent}${quote}`;
 }
+
 const startRegex = /(?:toMatchInlineSnapshot|toThrowErrorMatchingInlineSnapshot)\s*\(\s*(?:\/\*[\S\s]*\*\/\s*|\/\/.*\s+)*\s*[\w_$]*(['"`\)])/m;
 function replaceInlineSnap(code, s, index, newSnap) {
+
+  debugSnaps && console.dir({
+    f: "replaceInlineSnap",
+    newSnap,
+  })
+
   const startMatch = startRegex.exec(code.slice(index));
   if (!startMatch)
     return replaceObjectSnap(code, s, index, newSnap);
@@ -742,6 +790,12 @@ ${JSON.stringify(stacks)}`
         );
       }
       stack.column--;
+
+      debugSnaps && console.dir({
+        f: "SnapshotState._addSnapshot",
+        receivedSerialized,
+      })
+
       this._inlineSnapshots.push({
         snapshot: receivedSerialized,
         ...stack
@@ -770,8 +824,15 @@ ${JSON.stringify(stacks)}`
     if ((this._dirty || this._uncheckedKeys.size) && !isEmpty) {
       if (hasExternalSnapshots)
         await saveSnapshotFile(this._snapshotData, this.snapshotPath);
-      if (hasInlineSnapshots)
+      if (hasInlineSnapshots) {
+
+        debugSnaps && console.dir({
+          f: "SnapshotState.save",
+          _inlineSnapshots: this._inlineSnapshots,
+        })
+
         await saveInlineSnapshots(this._inlineSnapshots);
+      }
       status.saved = true;
     } else if (!hasExternalSnapshots && fs.existsSync(this.snapshotPath)) {
       if (this._updateSnapshot === "all")
@@ -807,7 +868,36 @@ ${JSON.stringify(stacks)}`
       key = testNameToKey(testName, count);
     if (!(isInline && this._snapshotData[key] !== void 0))
       this._uncheckedKeys.delete(key);
-    const receivedSerialized = addExtraLineBreaks(serialize(received, void 0, this._snapshotFormat));
+
+    debugSnaps && console.dir({
+      f: "sstate.match",
+      received,
+    })
+
+    /*
+    const serializer = getSerializers().find(s => s.test(received))
+    //const receivedSerialized = addExtraLineBreaks(serialize(received, void 0, this._snapshotFormat));
+    const receivedSerialized = serialize(received, void 0, this._snapshotFormat);
+    if (serializer?.addExtraLineBreaks != false) {
+      receivedSerialized = addExtraLineBreaks(receivedSerialized);
+    }
+    const expected = isInline ? inlineSnapshot : this._snapshotData[key];
+    //const expectedTrimmed = prepareExpected(expected);
+    let expectedTrimmed = expected;
+    if (serializer?.prepareExpected != false) {
+      expectedTrimmed = prepareExpected(expectedTrimmed);
+    }
+    //const pass = expectedTrimmed === prepareExpected(receivedSerialized);
+    let receivedSerializedTrimmed = receivedSerialized;
+    if (serializer?.prepareExpected != false) {
+      receivedSerializedTrimmed = prepareExpected(receivedSerializedTrimmed);
+    }
+    const pass = expectedTrimmed === receivedSerializedTrimmed;
+    */
+
+    //const receivedSerialized = addExtraLineBreaks(serialize(received, void 0, this._snapshotFormat));
+    const receivedSerialized = (serialize(received, void 0, this._snapshotFormat));
+
     const expected = isInline ? inlineSnapshot : this._snapshotData[key];
     const expectedTrimmed = prepareExpected(expected);
     const pass = expectedTrimmed === prepareExpected(receivedSerialized);
@@ -931,6 +1021,13 @@ class SnapshotClient {
       errorMessage
     } = options;
     let { received } = options;
+
+    debugSnaps && console.dir({
+      f: "SnapshotClient.assert",
+      received,
+      inlineSnapshot,
+    })
+
     if (!test)
       throw new Error("Snapshot cannot be used outside of test");
     if (typeof properties === "object") {
@@ -960,13 +1057,19 @@ class SnapshotClient {
       inlineSnapshot
     });
     if (!pass) {
+      debugSnaps && console.log(`SnapshotClient.assert: pass == false`)
       try {
-        expect(actual.trim()).equals(expected ? expected.trim() : "");
+        //expect(actual.trim()).equals(expected ? expected.trim() : "");
+        // dont trim
+        expect(actual).equals(expected ? expected : "");
       } catch (error2) {
         error2.message = errorMessage || `Snapshot \`${key || "unknown"}\` mismatched`;
         throw error2;
       }
     }
+    else {
+      debugSnaps && console.log(`SnapshotClient.assert: pass == true`)
+    }
   }
   async saveCurrent() {
     if (!this.snapshotState)
@@ -1040,8 +1143,16 @@ const SnapshotPlugin = (chai, utils) => {
         inlineSnapshot = properties;
         properties = void 0;
       }
+      /*
       if (inlineSnapshot)
         inlineSnapshot = stripSnapshotIndentation(inlineSnapshot);
+      */
+      debugSnaps && console.dir({
+        f: "toMatchInlineSnapshot",
+        expected,
+        inlineSnapshot,
+      })
+
       const errorMessage = utils.flag(this, "message");
       getSnapshotClient().assert({
         received: expected,

System Info

vitest: ^0.25.2 => 0.25.2 

Used Package Manager

pnpm

Validations

milahu avatar Nov 16 '22 10:11 milahu

You can disable this in the pretty format options with escapeString

// vitest.config.ts
test: {
  snapshotFormat: {
    escapeString: false,
  },
},

evad1n avatar May 31 '23 16:05 evad1n

no, i still get

 ❯ test/basic.test.ts (3)
   ❯ suite name (3)
     ✓ snapshot
     ✓ string snapshot actual
     × string snapshot expected

expected:

  • "string snapshot actual" should fail
  • "string snapshot expected" should pass

i tried both vite.config.ts and vitest.config.ts

milahu avatar May 31 '23 17:05 milahu