Skip to main content

Snapshot testing

Griffel generates atomic class names by hashing style declarations, so a component renders as:

<div class="static-class ___1t65jhk_nkb4zh0 fe3e8s9 frdkuqy"></div>

These hashes are an implementation detail. Changing an unrelated style, upgrading Griffel or reordering declarations can change them, and every snapshot that captured them has to be updated even though the rendered result is identical.

@griffel/jest-serializer is a snapshot serializer that removes the generated class names, so snapshots only contain markup you actually wrote:

<div class="static-class"></div>

Install

yarn add --dev @griffel/jest-serializer

Setup

Despite the name, the package works with both Jest and Vitest.

jest.config.js
module.exports = {
snapshotSerializers: ['@griffel/jest-serializer'],
};

Classes from makeStyles() and makeResetStyles() are both removed, any other class name is kept:

const useStyles = makeStyles({ root: { color: 'red', paddingLeft: '10px' } });
const useResetStyles = makeResetStyles({ marginLeft: '20px' });

function Component() {
const classes = useStyles();
const resetClassName = useResetStyles();

return <div data-testid="element" className={mergeClasses('static-class', resetClassName, classes.root)} />;
}
<div data-testid="element" class="static-class"></div>

Asserting styles

Because the serializer removes the class names, a snapshot no longer tells you which styles were applied. Assert the applied styles directly instead, with toHaveStyle() from @testing-library/jest-dom:

render(<Component />);

expect(screen.getByTestId('element')).toHaveStyle({
color: 'rgb(255, 0, 0)',
paddingLeft: '10px',
marginLeft: '20px',
});

This reads the computed styles from the document, so it covers everything Griffel applied, including shorthands, RTL flipping and overrides from mergeClasses().

Static class names are untouched by the serializer, so they can still be asserted:

expect(screen.getByTestId('element')).toHaveClass('static-class');
tip

Use snapshots for structure and toHaveStyle() for styling. A snapshot that contains generated class names will churn on every unrelated style change, while toHaveStyle() describes the intent of the test.

Slot names in snapshots

The serializer cannot print slot names (root, primary, ...) instead of the generated ones, because a class name does not identify a slot. Identical styles produce identical classes regardless of which makeStyles() call or slot they came from:

const useClassesA = makeStyles({ rootA: { color: 'red', paddingTop: '10px' } });
const useClassesB = makeStyles({ rootB: { color: 'red', paddingTop: '10px' } });

Both useClassesA().rootA and useClassesB().rootB return the same string, so there is nothing to map back to rootA or rootB.

If you still want slot names in snapshots, mock @griffel/react so that each slot returns its own name. This replaces Griffel's runtime, so no styles are applied and toHaveStyle() will not work:

jest.mock('@griffel/react', () => {
const actual = jest.requireActual('@griffel/react');

return {
...actual,
makeStyles: stylesBySlots => () =>
Object.fromEntries(Object.keys(stylesBySlots).map(slotName => [slotName, slotName])),
mergeClasses: (...classNames) => classNames.filter(Boolean).join(' '),
};
});
function Component(props) {
const classes = useStyles();

return <div className={mergeClasses(classes.root, props.primary && classes.primary)} />;
}
<div class="root primary"></div>

The mock can also be registered globally, see manual mocks.

note

If you want the generated class names in your snapshots, simply do not add the serializer.