Skip to content

feat: show expected/received diffs for failed assertions - #202

Open
mrousavy wants to merge 1 commit into
callstackincubator:mainfrom
mrousavy:feat/assertion-diffs
Open

mrousavy wants to merge 1 commit into
callstackincubator:mainfrom
mrousavy:feat/assertion-diffs

Conversation

@mrousavy

Copy link
Copy Markdown
Contributor

What is this?

When an expect assertion fails, Harness prints the assertion message and the code frame, but no diff. Chai shortens objects in that message, so a failing toEqual can look like this:

expected { year: 2018, make: 'Tesla', …(5) } to deeply equal { year: 2018, make: 'Tesla', …(5) }

Both sides read the same, and finding the field that differs means logging the values and running the test on the device again. Vitest prints a - Expected / + Received diff under the message for the same assertion. Harness uses the same @vitest/expect, so it can print that diff as well. With this change the failure reads:

expected { year: 2018, make: 'Tesla', …(5) } to deeply equal { year: 2018, make: 'Tesla', …(5) }

  - Expected
  + Received
  @@ -1,7 +1,7 @@
    {
  -   "color": "blue",
  +   "color": "red",
      "electric": true,
      "make": "Tesla",
      "model": "Model 3",
      "owner": "Nitro",
      "seats": 5,

How does it work?

@vitest/expect doesn't build the diff when an assertion throws. In Vitest, the test runner builds it afterwards by passing the failed test's error to processError from @vitest/utils/error. Harness runs its own runner on the device and never made that call. Soft assertions were the one case with a diff, because @vitest/expect calls processError on them itself, but Harness dropped those diffs when it merged the soft failures into a single error.

The device runner now calls processError when a test fails and sends the diff in a new optional diff field on the serialized error. When Harness merges several failures into one error, as it does for expect.soft, each failure's diff goes right after its own message. The Jest reporter prints diff between the message and the code frame.

The diff is built on the device because that's where the actual and expected values live. @vitest/utils was already in the bundle through @vitest/expect, and the runtime now declares it directly at the same 4.0.16 version.

Why is this useful?

You can tell which value is wrong from the CLI output, without adding logs and rerunning on the device. This matters most for toEqual and toMatchObject on larger objects, where chai's message hides the difference. Plugins that read test errors get the diff through the same diff field.

The device runner now runs failed test errors through processError from
@vitest/utils/error, as Vitest's runner does, and sends the resulting
diff in a new optional `diff` field on SerializedError. Merged soft
assertion errors keep each failure's diff next to its message, and the
Jest reporter prints the diff between the message and the code frame.
@vercel

vercel Bot commented Oct 15, 2026

Copy link
Copy Markdown

@mrousavy is attempting to deploy a commit to the Callstack Team on Vercel.

A member of the Team first needs to authorize it.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant