Search code examples
typescriptnext.jsjestjsconfig

Nextjs, Jest: Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: object


I am trying to setup unit tests on my Nextjs project. I have followed the documentation on official Nextjs docs for setup.

The problem I get seems to be related either with config itself or the @headlessui/react library and the way it is imported.

When I try to run test I get the below error message:

Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: object. Check the render method of Listbox.

The component works just fine at runtime and build.

Here is the configuration that I have for my project.

tsconfig.json

{
  "compilerOptions": {
    "target": "ES6",
    "lib": ["dom", "dom.iterable", "esnext"],
    "baseUrl": "./",
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "exactOptionalPropertyTypes": true,
    "noImplicitReturns": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "forceConsistentCasingInFileNames": true,
    "removeComments": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
  "exclude": ["node_modules"]
}

jest.config.mjs

import nextJest from 'next/jest.js';

const createJestConfig = nextJest({
  dir: './',
});

/** @type {import('jest').Config} */
const config = {
  setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
  testEnvironment: 'jest-environment-jsdom',
  moduleDirectories: ['node_modules', '<rootDir>/'],
  testMatch: ['**/*.(test|spec).(js|jsx|ts|tsx)'],
  coveragePathIgnorePatterns: ['/node_modules/'],
};

export default createJestConfig(config);

next.config.js

/** @type {import('next').NextConfig} */
const nextConfig = {
  images: {
    remotePatterns: [{ protocol: 'https', hostname: '**.pixabay.com' }],
  },
  reactStrictMode: true,
  webpack(config) {
    config.module.rules.push({
      test: /\.svg$/,
      use: ['@svgr/webpack'],
    });
    return config;
  },
};

module.exports = nextConfig;

test file in __tests__/components

import { render, screen } from '@testing-library/react';
import { ListType } from 'components/calculator/builder/utils/types/list';
import List from 'components/ui/fields/list/list';

jest.mock('components/calculator/context/hooks', () => ({
 // ... mock custom hook.
}));

const mockList: ListType = {
// ... mock data
};

const renderComponent = () => {
  render(<List list={mockList} />);
};

describe('List component', () => {
  test('check if the correct list options are displayed', () => {
    renderComponent();
    expect(screen.getAllByRole('option')).toHaveLength(mockList.options.length);
    expect(screen.getByText(mockList.description)).toBeInTheDocument();
    expect(screen.getByText(mockList.placeholder)).toBeInTheDocument();
  });
});

snippet of the actual component tested

import { Listbox } from '@headlessui/react'; // <= the actual Listbox import from node_modules
// some other inner imports

interface ListProps {
  list: ListType;
}

export default function List({ list }: ListProps) {
return (
    <Listbox>
       // ...implementation is irrelevant
    </Listbox>)
}

package.json

{
  "name": "cw-client",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint",
    "typescript": "tsc",
    "test": "jest",
    "test:ci": "jest --ci"
  },
  "engines": {
    "node": ">=19.3.0"
  },
  "dependencies": {
    "@dnd-kit/core": "^6.0.7",
    "@dnd-kit/modifiers": "^6.0.1",
    "@dnd-kit/sortable": "^7.0.2",
    "@dnd-kit/utilities": "^3.2.1",
    "@headlessui/react": "^1.7.7",
    "@next/font": "13.1.1",
    "lodash.debounce": "^4.0.8",
    "mathjs": "^11.6.0",
    "next": "13.1.1",
    "react": "18.2.0",
    "react-dom": "18.2.0",
    "react-number-format": "^5.1.4"
  },
  "devDependencies": {
    "@next/eslint-plugin-next": "^13.1.1",
    "@svgr/webpack": "^6.5.1",
    "@tailwindcss/forms": "^0.5.3",
    "@testing-library/jest-dom": "^5.16.5",
    "@testing-library/react": "^14.0.0",
    "@types/lodash.debounce": "^4.0.7",
    "@types/node": "18.11.17",
    "@types/react": "18.0.26",
    "@types/react-dom": "18.0.10",
    "@typescript-eslint/eslint-plugin": "^5.47.0",
    "@typescript-eslint/parser": "^5.47.0",
    "autoprefixer": "^10.4.13",
    "eslint": "8.30.0",
    "eslint-config-next": "13.1.1",
    "eslint-config-prettier": "^8.5.0",
    "eslint-plugin-import": "^2.26.0",
    "eslint-plugin-prettier": "^4.2.1",
    "eslint-plugin-react": "^7.31.11",
    "jest": "^29.5.0",
    "jest-environment-jsdom": "^29.5.0",
    "postcss": "^8.4.20",
    "prettier": "2.8.1",
    "prettier-plugin-tailwindcss": "^0.2.1",
    "tailwindcss": "^3.3.1",
    "typescript": "4.9.4"
  }
}

I have checked the resouces and most of the time they are related to two problems.

  1. Using default export but importing named module. This is not my case as you can see based on a preview but also the fact that the component works at runtime.
  2. Not using a function to return actual JSX (const component = </>). This is not also my case but also the error is slightly different as it says that it git the object rather than undefined.

So none of these have helped in my case. Also I have seen this could be the issue when using path aliases which you would then add to the path in tsconfig.json and amend in jest.config.json. But since I am not using path aliases, I dont think its relevant.

Interestingly enough, I tried to use repo from Nextjs boilerplate with test and it worked. But my application got quite big already to start fresh.

Some of the other options that I have tried is:

  1. Importing react at the top.
  2. Moving the export default to the bottom.

None of these helped.


Solution

  • So, I have managed to find the root cause.

    The issue was not in the import or aliases as initially thought. The problem was that I was using @svgr/webpack to convert .svg files into the react components.

    These, however, are not JSX components, which were throwing an error in tests as Jest did not know how to resolve svg imports.

    For anyone having a similar problem, here is the fix.

    1. Create a mock svg that you want to use. In my case, I have created folder __mocks__/svg.tsx with the below code.
    
        import React, { SVGProps } from 'react';
    
        const SvgrMock = React.forwardRef<SVGSVGElement, SVGProps<SVGSVGElement>>(
          (props, ref) => <svg ref={ref} {...props} />
        );
    
        export const ReactComponent = SvgrMock;
        export default SvgrMock;
    
    
    1. Add the config to jest.config.mjs
      moduleNameMapper: {
        '^.+\\.(svg)$': '<rootDir>/__mocks__/svg.tsx',
      },
    

    This config resolves imports to the mock svg if there are any in components for jest so you can test the functionality.

    I hope it helps anyone running into a similar problem.