Skip to content

Testing Components

A component’s sz prop is compiled by the bundler plugin. A test runner that does not run the plugin renders the component with sz still on it as a live prop and no className, so an assertion like expect(el).toHaveClass('p-4') reads nothing the browser would see — and a value that compiles to nothing is invisible to the suite.

Nothing to set up. Vitest resolves modules through Vite, so the plugin in vite.config.ts transforms every component a test imports, exactly as it does for the app. The rendered element carries the same className the browser gets, and the same diagnostics reach the terminal.

Jest cannot host a bundler plugin, so @csszyx/unplugin/jest compiles sz in a transformer. It does one thing — replaces sz with the className the build would emit — and hands back TSX. Jest applies one transformer per file, so chain it in front of the one that compiles TSX:

csszyx-jest-transformer.cjs
const csszyx = require('@csszyx/unplugin/jest').createTransformer();
const babel = require('babel-jest').default.createTransformer();
module.exports = {
process(sourceText, sourcePath, options) {
const { code } = csszyx.process(sourceText, sourcePath);
return babel.process(code, sourcePath, options);
},
getCacheKey(sourceText, sourcePath, options) {
return (
csszyx.getCacheKey(sourceText, sourcePath, options) +
babel.getCacheKey(sourceText, sourcePath, options)
);
},
};

Then point Jest at it:

jest.config.js
module.exports = {
transform: {
'\\.[jt]sx?$': '<rootDir>/csszyx-jest-transformer.cjs',
},
};

Both cache keys are combined on purpose: Jest caches transformer output under that key, and csszyx’s half changes when the build’s answer for a file changes — which a change in another module can do without touching the file itself.

Two sources answer, in order.

  1. The build’s transform cache, .csszyx/cache/transform. It holds the output the bundler produced, which is the only output that resolves an sz object or an szv factory imported from another module — those come from the plugin’s project-wide scan, and a compiler handed one file cannot see them. An entry is used only when the file’s contents match what the build saw, and only when this csszyx version wrote it without variable mangling. Run your build (or dev server) before the suite when a component’s styles live in another module.
  2. A per-file compile, for everything else. This is the complete answer for an inline sz and for one built from a const in the same file. A shape it cannot resolve keeps the runtime path, exactly as the plugin would: the @csszyx/runtime helper is imported and the classes are computed when the component renders.

Either way, a dead key or value is printed to the console with the file it was found in — the same lines the build prints, minus the usage nudges that only matter to a bundler.

require('@csszyx/unplugin/jest').createTransformer({
// Where the plugin wrote its transform cache. Default: `.csszyx/cache/transform`
// under the directory Jest runs from; set it when `build.cacheDir` is configured.
cacheRoot: '.csszyx/cache/transform',
// Which files carry `sz`. Anything else is handed back untouched.
extensions: ['.tsx', '.jsx', '.ts', '.js', '.mts', '.mjs'],
});