A snapshot is a test that compares a result not with a hand-written expectation but with a saved reference. The idea is strong: describing a large output by hand is tedious, while a snapshot records it automatically and then guards it from changes. But that same strength has a dark side: a snapshot easily turns into a dump nobody reads, and then it checks not a contract but the mere fact that nothing changed.
The line runs by the size and meaningfulness of the reference. A snapshot of a component's whole DOM tree, hundreds of lines long, is useless: at the first markup change it goes red, the developer waves it off and updates it without looking. A small, focused snapshot - of a public DTO, a formatter's result, a short piece of markup - is, on the contrary, read in full, and its diff at review speaks to the point.
For such small references an inline snapshot is convenient. toMatchInlineSnapshot writes the expected value right into the test body, next to the call, rather than into a separate .snap file. The reference is visible in the same place where the check lives: there is no need to open a side file to see what is being asserted, and the diff in a pull request shows the contract change right in the test code.
Unstable fields are a separate concern. An id, a creation date, a random token change from run to run and, without handling, will make the snapshot forever red. For this there are property matchers: in the snapshot such a field is described not by value but by type - id as expect.any(String), createdAt as expect.any(Date). Then the reference fixes the shape and the stable part without clinging to what changes by definition.
The key thing is what the -u flag means. vitest -u does not fix the test and does not confirm the output's correctness: it simply accepts the current result as the new reference, whatever it is. Running -u reflexively on every red snapshot makes the check vanish: the snapshot starts to agree with any output, including a broken one, and stops protecting anything.
So updating a snapshot is a conscious review, not an automatism. A red snapshot poses a question: did I deliberately change the contract, or is this a regression? You answer it by looking at the diff, not by rewriting the reference blindly. An accepted diff means the developer read the change and confirmed the new output is correct; that is the whole value of the mechanism.
Hence the practice that keeps snapshots useful. Snapshot the small and meaningful, not everything at once; prefer inline so the reference is in plain sight; cover unstable fields with matchers; read every diff on update. A snapshot is a fixed contract of the output, and it is worth exactly as much as the attention with which it is re-read when it goes red.
The typical failure is a huge snapshot of the whole tree, updated without looking: it creates an illusion of coverage but catches not a single real regression, because any change is accepted automatically. The second failure is a snapshot with a live date or id without property matchers: it fails out of nowhere, trains the team to hit -u reflexively and thereby devalues every other snapshot in the project.
test('toPublicDto hides internal fields', () => {
const dto = toPublicDto(user)
expect(dto).toMatchInlineSnapshot(
{ id: expect.any(String) }, // unstable field - by type
`
{
"email": "a@shop.io",
"id": Any<String>,
"role": "customer",
}
`,
)
})