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
- NPM
yarn add --dev @griffel/jest-serializer
npm install --save-dev @griffel/jest-serializer
Setup
Despite the name, the package works with both Jest and Vitest.
- Jest
- Vitest
module.exports = {
snapshotSerializers: ['@griffel/jest-serializer'],
};
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
snapshotSerializers: ['@griffel/jest-serializer'],
},
});
Alternatively, register it from a setup file:
import { expect } from 'vitest';
import { print, test } from '@griffel/jest-serializer';
expect.addSnapshotSerializer({ print, test });
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');
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
- Vitest
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(' '),
};
});
vi.mock('@griffel/react', async importActual => {
const actual = await importActual<typeof import('@griffel/react')>();
return {
...actual,
makeStyles: (stylesBySlots: Record<string, unknown>) => () =>
Object.fromEntries(Object.keys(stylesBySlots).map(slotName => [slotName, slotName])),
mergeClasses: (...classNames: unknown[]) => 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.
If you want the generated class names in your snapshots, simply do not add the serializer.