TypeError: Converting circular structure to JSON when testing Vue3 composable function in jest

Viewed 122

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,
    },
  },
};
0 Answers
Related