vitest-dev / vitest

Next generation testing framework powered by Vite.
https://vitest.dev
MIT License
13.05k stars 1.17k forks source link

inline snapshots have double indent #2339

Open milahu opened 1 year ago

milahu commented 1 year ago

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 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

evad1n commented 1 year ago

You can disable this in the pretty format options with escapeString

// vitest.config.ts
test: {
  snapshotFormat: {
    escapeString: false,
  },
},
milahu commented 1 year ago

no, i still get

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

expected:

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