shallow-react-snapshot is a small utility library that brings Enzyme-style shallow snapshot testing to projects using React Testing Library. It inspects React's internal Fiber tree on an already-rendered DOM element and returns a jest-friendly shallow JSON object representing only the immediate children of the component under test — without rendering or mocking nested components.
Supports React 16–19 from a single build.
src/ # Library source
index.ts # Exported shallow() function
types.ts # TypeScript interfaces (Fiber, ReactTestObject, etc.)
reactSymbols.ts # React internal Symbol.for() constants (avoids react-is dep)
__tests__/
functional.test.tsx # Tests for functional components
class.test.tsx # Tests for class components
__snapshots__/ # Auto-generated Jest snapshots
dependencies/ # Isolated React installs per version
react-16/ # React 16 + @testing-library/react
react-17/
react-18/
react-19/
scripts/
lib/findDependency.mjs # Resolves per-version dep paths for jest moduleNameMapper
playground.sh # Starts browser playground
playground/ # Interactive JSX playground
dist/ # Build output (gitignored, published to npm)
function shallow(
rootElement: Element | null,
RootReactComponent: ReactComponent | string
): ReactTestChild | nullTakes an already-rendered DOM rootElement and the component constructor (or display name string) whose shallow output you want. Returns a ReactTestObject compatible with toMatchSnapshot().
Install dependencies (required before any other command):
npm ci| Task | Command |
|---|---|
| Build | npm run build |
| Build (watch) | npm run dev |
| Test (all React versions) | npm test |
| Test (single React version) | npm test -- --selectProjects react-16 |
| Lint | npm run lint |
| Lint + auto-fix | npm run lint:fix |
| Browser playground | npm run playground |
Tests run against four isolated React versions (16, 17, 18, 19) in parallel via Jest projects. Each project in dependencies/<react-version>/ has its own node_modules with a pinned React, react-dom, and @testing-library/react.
jest.config.mjs dynamically generates one Jest project per dependencies/ subdirectory and maps module imports to the correct version using moduleNameMapper.
Test files use toMatchSnapshot(). After any intentional output change, update snapshots:
npm test -- --updateSnapshotAll PRs must include tests for new behaviour.
This repo has an unusual snapshot setup: all four React version projects share the same snapshot files. This means every React version must produce identical output for the same test — if they don't, the second run will fail with snapshot mismatches even though the first run passed (because one version wrote the snapshot and another version disagrees with it).
The correct workflow after any output change:
npm test -- --updateSnapshot # re-generate snapshots from current output
npm test # second run verifies all versions agreeIf the second run fails with snapshot mismatches, it means React versions are producing different output — the implementation needs to be fixed to be consistent, not the snapshots.
Enforced by Biome:
- Spaces (not tabs), double quotes for JS strings.
recommendedlint rules; some rules relaxed inside__tests__/.dist/,playground/, andpackage.jsonexcluded from linting.
npm run lint # check
npm run lint:fix # auto-fix safe issuesAlways run lint before considering work done. Biome enforces a line length limit — long chained expressions (e.g. ?? chains) will need line breaks.
tsconfig.json— full config including tests (used by IDE and ts-jest).tsconfig.build.json— same but excludes__tests__/(used bynpm run build).- Compiler target:
es2016,module: commonjs,strict: true.
Releases are managed with Changesets.
Create a changeset file manually in .changeset/<descriptive-name>.md with the following format:
---
"shallow-react-snapshot": patch
---
Short user-facing description of the change. Focus on what users need to know, not implementation details. Use `patch` for bug fixes and new features that are backward-compatible, `minor` for new functionality, `major` for breaking changes.# In this repo
npm link
npm run dev
# In the consumer project
npm link shallow-react-snapshot- Fiber discovery —
getFirstNestedFiberOrInternalInstance()walks DOM children looking for__reactFiber$…(React 17+) or__reactInternalInstance$…(React 16) properties. - Component matching — climbs the
.return(parent) Fiber chain until.typematches the provided component reference or display name string. - State handling — traverses
.alternatelinked list to find the currently committedmemoizedProps, handling the stale-alternate edge case that caused the every-other-update bug fixed in v0.2.1. - React symbols —
src/reactSymbols.tsdeclares Fragment, Memo, ForwardRef, Portal, etc. viaSymbol.for()instead of importing fromreact-is, enabling cross-version compatibility. - React internals differ per version — when adding support for a new React type, always verify the actual object shape (keys,
$$typeof, back-references) across all four React versions usingnode -ewith each version'snode_modules. React 19 in particular restructured Context:ctx.Provider === ctx(the Provider IS the context object,$$typeof: react.context, no_context), andContext.Consumeruses a new$$typeof: react.consumer. React 16–18 use$$typeof: react.providerfor Provider and$$typeof: react.contextfor Consumer, both carrying a_contextback-reference to the parent context. ThedisplayNamealways lives on the context object itself (ctx.displayName), reachable astype._context.displayNamefrom Provider/Consumer in React 16–18, and astype.displayNamedirectly in React 19.
GitHub Actions runs on Node 18, 20, 22, and 24 for every push/PR to main:
npm cinpm run buildnpm testnpm run lint