vitest-dev / vitest-dev/vitest

inline snapshots have double indent

Open
#2,339 2 comments 1 reaction 0 assignees View on GitHub

Nobody has claimed this yet.

pending triage
Dominant language
TypeScript
Stars
17.1k
Forks
2k
Avg merge
1d 21h
Merged PRs (30d)
92

Description

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

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the linked StackBlitz reproduction, then inspect packages/vitest/src/integrations/snapshot/port/inlineSnapshot.ts, especially stripSnapshotIndentation and prepareSnapString. Compare the serializer and inline-snapshot paths described in the issue; done means string snapshots no longer acquire the reported double indentation without breaking the shown snapshot behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
testing
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
42/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.