Hone logo
Hone
Problems

Implementing toMatchSnapshot in Jest

This challenge asks you to build a simplified version of Jest's powerful toMatchSnapshot matcher. This matcher is invaluable for testing UI components, configuration objects, or any complex data structures, as it allows you to quickly verify that your output hasn't unexpectedly changed. You'll be creating a custom Jest matcher that captures the serialized output of a received value and compares it against a stored snapshot.

Problem Description

Your task is to implement a custom Jest matcher function named toMatchSnapshot. This matcher will:

  1. Receive a value: This is the actual output of your code under test.
  2. Serialize the value: Convert the received value into a string representation. For simplicity, you can use JSON.stringify.
  3. Manage snapshots:
    • If no snapshot exists for this test, create one using the serialized value and save it to a .snap file. The matcher should then pass.
    • If a snapshot does exist, compare the serialized received value with the content of the existing snapshot.
      • If they match, the matcher should pass.
      • If they don't match, the matcher should fail, reporting the differences.
  4. Handle updates: Provide a mechanism to update existing snapshots when intentional changes are made to the code. This is typically done via a command-line flag (e.g., --updateSnapshot or -u in Jest).

Key Requirements

  • Implement a function that can be registered as a custom Jest matcher.
  • The matcher should be named toMatchSnapshot.
  • It must correctly handle the initial creation of snapshots.
  • It must correctly compare received values against existing snapshots.
  • It must gracefully handle updates to snapshots when the appropriate flag is present.
  • For this challenge, assume you are working within a Jest environment and have access to Jest's testing utilities. You do not need to implement the full Jest runner, but rather the logic of a single custom matcher.

Expected Behavior

  • First run (no snapshot): The test passes, and a .snap file is created with the serialized output.
  • Subsequent runs (matching snapshot): The test passes.
  • Subsequent runs (mismatching snapshot): The test fails, and the output clearly indicates the differences between the received value and the snapshot.
  • Subsequent runs with update flag: The test passes, and the .snap file is updated with the new serialized output.

Edge Cases

  • Values that cannot be serialized by JSON.stringify (e.g., circular references) should be handled gracefully, perhaps by throwing an informative error or indicating they cannot be snapshotted. For simplicity, you can assume inputs are JSON-serializable for this exercise.
  • Handling of whitespace and formatting differences within the JSON itself. JSON.stringify provides some control (e.g., the space argument).

Examples

Example 1: Initial Snapshot Creation

Let's say you have a simple object and you run your custom matcher for the first time.

// Your test file (e.g., component.test.ts)
describe('MyComponent', () => {
  it('should render correctly', () => {
    const componentOutput = {
      type: 'div',
      props: { id: 'main' },
      children: ['Hello, world!'],
    };
    expect(componentOutput).toMatchSnapshot();
  });
});

Expected Outcome:

  • The test passes.
  • A file named component.test.snap is created in the same directory.
  • The component.test.snap file contains:
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`MyComponent should render correctly 1`] =
\`\`\`json
{
  "type": "div",
  "props": {
    "id": "main"
  },
  "children": [
    "Hello, world!"
  ]
}
\`\`\`
`;

(Note: The exact formatting might vary slightly based on JSON.stringify's space argument, but the structure and content should be equivalent).

Example 2: Snapshot Mismatch

Now, imagine you run the test again after changing the children property.

// Your test file (e.g., component.test.ts)
describe('MyComponent', () => {
  it('should render correctly', () => {
    const componentOutput = {
      type: 'div',
      props: { id: 'main' },
      children: ['Hello, updated world!'], // Changed here
    };
    expect(componentOutput).toMatchSnapshot();
  });
});

Expected Outcome:

  • The test fails.
  • The Jest output will show a diff highlighting the change in the children array:
- Expected
+ Received

@@ -6,7 +6,7 @@
   "props": {
     "id": "main"
   },
-  "children": [
-    "Hello, world!"
+  "children": [
+    "Hello, updated world!"
   ]
 }

Example 3: Updating a Snapshot

If you intentionally made the change in Example 2 and want to update the snapshot, you would run Jest with the update flag (e.g., npm test -- -u or jest -u).

Expected Outcome:

  • The test passes.
  • The component.test.snap file is updated to reflect the new children value.

Constraints

  • The input to toMatchSnapshot will be a JavaScript value that is JSON-serializable.
  • The snapshot files should be named according to Jest's convention (e.g., test-file-name.test.snap).
  • You should assume the existence of a mechanism to read from and write to these .snap files. For the purpose of this challenge, you can mock these file operations.
  • You need to simulate the behavior of Jest's snapshot update flag.

Notes

  • Think about how Jest associates snapshots with specific tests. The test name and the order of the matchers within a test are typically used.
  • Consider how you'll handle formatting differences in JSON.stringify. Using a consistent space argument (e.g., 2 for indentation) is good practice.
  • You'll need to simulate Jest's expect object and how custom matchers are added to it.
  • The core logic involves comparing strings. The difficulty lies in managing the state (the snapshot file) and integrating with the testing framework's expectations.
  • You will likely need to simulate the Jest testPath and currentTestName properties to correctly name and locate snapshot files.
Loading editor...
typescript