There is a Vue3 composable function that I'm trying to test with Jest, but I get this error:
(node:79950) UnhandledPromiseRejectionWarning: TypeError: Converting circular structure to JSON
--> starting at object with constructor 'ComputedRefImpl'
| property 'effect' -> object with constructor 'ReactiveEffect'
--- property 'computed' closes the circle
at stringify (<anonymous>)
at writeChannelMessage (internal/child_process/serialization.js:127:20)
at process.target._send (internal/child_process.js:836:17)
at process.target.send (internal/child_process.js:734:19)
at reportSuccess (/Users/XXXXX/dev/XXXXX/packages/XXXXX/node_modules/jest-worker/build/workers/processChild.js:67:11)
(Use `node --trace-warnings ...` to show where the warning was created)
(node:79950) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag `--unhandled-rejections=strict` (see https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode). (rejection id: 2)
(node:79950) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
This function is used by most of the existing components. What's weird is when I delete all the component that use this function the tests goes through:
> jest
FAIL src/hooks/useClassNames.spec.ts
useClassNames
✕ render (4 ms)
● useClassNames › render
expect(received).toBe(expected) // Object.is equality
Expected: []
Received: {"__v_isReadonly": true, "__v_isRef": true, "_cacheable": true, "_dirty": true, "_setter": [Function setter], "dep": undefined, "effect": {"active": true, "computed": [Circular], "deps": [], "fn": [Function anonymous], "parent": undefined, "scheduler": [Function anonymous]}}
4 | it('render', async () => {
5 | const result = useClassNames([]);
> 6 | expect(result).toBe([]);
| ^
7 | });
8 | });
9 |
at Object.<anonymous> (src/hooks/useClassNames.spec.ts:6:20)
Composable function
import { computed, ComputedRef } from 'vue';
export type ClassNameOptions = {
base?: string;
modifier?: string;
condition?: boolean;
};
function useClassNames(options: ClassNameOptions[], addClassNames?: string): ComputedRef {
return computed(() => {
return [
...options.map((option: ClassNameOptions) => {
const { base = '', modifier = '', condition = true } = option;
return condition && `${base}${modifier ? '--' : ''}${modifier}`;
}),
addClassNames,
].filter(Boolean);
});
}
export default useClassNames;
Here is the test
import useClassNames from './useClassNames';
describe('useClassNames', () => {
it('render', async () => {
const result = useClassNames([]);
expect(result).toBe([]);
});
});
useClassNames is used by every component to generate class names like so
<template>
<button :class="rootClasses">
<slot />
</button>
</template>
<script lang="ts" setup>
import useClassNames from '@/hooks/useClassNames';
export type ButtonVariant = 'primary' | 'secondary';
export type ButtonProps = {
variant?: ButtonVariant;
addClassNames?: string;
};
const props = defineProps<ButtonProps>();
const rootClasses = useClassNames(
[
{
base: 'button',
},
{
base: 'button',
modifier: props.variant,
condition: Boolean(props.variant),
},
],
props.addClassNames,
);
</script>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
name: 'Button',
});
</script>
Installed dependencies
{
"version": "0.1.0",
"private": false,
"scripts": {
"test-with-coverage": "COLLECT_TEST_COVERAGE=TRUE jest",
"test": "jest"
},
"engines": {
"node": ">=14.0.0"
},
"dependencies": {
"vue": "^3.2.37"
},
"devDependencies": {
"@testing-library/dom": "^8.14.0",
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/user-event": "^14.2.1",
"@types/jest": "^27.0.2",
"@types/jest-axe": "^3.5.4",
"@types/node": "^16.11.6",
"@typescript-eslint/eslint-plugin": "^4.28.3",
"@typescript-eslint/parser": "^4.28.3",
"@vitejs/plugin-vue": "2.3.3",
"@vue/test-utils": "^2.0.0",
"@vue/vue3-jest": "26.0.1",
"babel-jest": "^26.6.3",
"eslint": "^7.31.0",
"eslint-plugin-jest": "^24.3.6",
"eslint-plugin-prettier": "^3.4.0",
"eslint-plugin-vue": "^9.1.1",
"jest": "~26.6.3",
"jest-axe": "^6.0.0",
"prettier": "2.1.2",
"sass": "^1.52.2",
"ts-jest": "~26.3.0",
"typescript": "4.0.8",
"vite": "^2.9.10",
"vue-tsc": "0.37.0"
}
}
Jest configuration
const collectCoverage = process.env.COLLECT_TEST_COVERAGE === 'TRUE';
module.exports = {
// Test in browser like environment.
testEnvironment: 'jsdom',
moduleDirectories: ['node_modules', 'src'],
moduleFileExtensions: ['vue', 'ts', 'js'],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
},
transform: {
'^.+\\.vue$': '@vue/vue3-jest',
'^.+\\.js$': 'babel-jest',
'^.+\\.ts$': 'ts-jest',
},
setupFilesAfterEnv: ['<rootDir>/jest.setup.ts'],
collectCoverage,
collectCoverageFrom: ['src/{components,hooks}/**/*.{js,ts,vue}'],
coverageReporters: ['html', 'text-summary'],
coverageDirectory: '<rootDir>/coverage',
coverageThreshold: {
global: {
statements: 80,
branches: 80,
functions: 80,
lines: 80,
},
},
};