42

In my app I use React and TypeScript. I tried to run jest tests I get following error:

C:\Users\e-KDKK\workspace\konrad\mikskarpety\src\images\icons\Sock.svg:1
({"Object.<anonymous>":function(module,exports,require,__dirname,__filename,global,jest){<svg xmlns="http://www.w3.org/2000/svg" width="18.725" height="23.947" viewBox="0 0 18.725 23.947">
                                                                                         ^

SyntaxError: Unexpected token <

  1 | import React, { FC } from 'react';
  2 | 
> 3 | import SockIcon from '../../images/icons/Sock.svg';
    | ^
  4 | 
  5 | export const Spinner: FC = () => {
  6 |   return (

  at ScriptTransformer._transformAndBuildScript (node_modules/@jest/transform/build/ScriptTransformer.js:537:17)
  at ScriptTransformer.transform (node_modules/@jest/transform/build/ScriptTransformer.js:579:25)
  at Object.<anonymous> (src/components/Spinner/Spinner.tsx:3:1)

This file is not even tested. I don't know why it tries to compile it. My jest.config.json file contains only coverage thresholds.

I read that jest sometimes needs additional transform section for specific files like SVG, but when I added to configuration

"transform": {
    "^.+\\.svg$": "jest-svg-transformer"
},

my error message changed only to:

C:\Users\e-KDKK\workspace\konrad\mikskarpety\test\utils\debounce.test.ts:1 ({"Object.":function(module,exports,require,__dirname,__filename,global,jest){import { getVersion } from 'jest'; ^

SyntaxError: Unexpected token {

  at ScriptTransformer._transformAndBuildScript (node_modules/@jest/transform/build/ScriptTransformer.js:537:17)
  at ScriptTransformer.transform (node_modules/@jest/transform/build/ScriptTransformer.js:579:25)

Which is even more confusing to me.

The code for the app you can find here: https://github.com/KonradKlimczak/mikskarpety

skyboyer
  • 22,209
  • 7
  • 57
  • 64
Konrad Klimczak
  • 1,474
  • 2
  • 22
  • 44

14 Answers14

60

jest doesn't know how to load other file extensions than js/jsx,you need to find the jest.config.js and add transformer for svg files.

"transform": {
   "^.+\\.tsx?$": "ts-jest",
   "^.+\\.svg$": "<rootDir>/svgTransform.js"
},

and create the svgTransform.js file (see Custom transformers) in your root directory with the following content:

// See https://stackoverflow.com/questions/58603201/jest-cannot-load-svg-file
module.exports = {
  process() {
    return {
      code: `module.exports = {};`,
    };
  },
  getCacheKey() {
    // The output is always the same.
    return "svgTransform";
  },
};

link: https://jestjs.io/docs/code-transformation#examples

Note this is the updated syntax for jest 28.

mikemaccana
  • 110,530
  • 99
  • 389
  • 494
Tom Pang
  • 669
  • 5
  • 4
  • 6
    This is a straight copy paste of this answer: https://stackoverflow.com/a/46810154/3550183 – Doug Aug 15 '22 at 21:18
  • 2
    This doesn't seem to work presently. Same error message – Ash Blue Oct 27 '22 at 20:01
  • 1
    I am using svg inside tsx file and it doesn't work since the above config is referring directly to svg only so my code is basically ```const Component = () => { return ()}```. any fix for such ? I get below error ```console.error Warning: React.createElement: type is invalid -- expected a string (for built-in components) or a class/function (for composite components) but got: undefined. You likely forgot to export your component from the file it's defined in, or you might have mixed up default and named imports.``` – Aman Sadhwani Jun 24 '23 at 15:11
  • I dont have the preprocessor.js file in react testing library, how does it look like? – mattsmith5 Jul 17 '23 at 23:28
31

Another easier option to solve this problem is jest-svg-transformer.

Just install it: npm i -D jest-svg-transformer or yarn add -D jest-svg-transformer

And add to the jest.config.js to the transform's section:

"transform": {
   ...
   "^.+\\.svg$": "jest-svg-transformer"
}
Aharon Ohayon
  • 1,171
  • 1
  • 17
  • 20
18

Another option (not specific to React) is to use jest-transform-stub since that will work for any non-JS file you want.

{
  "jest": {
    // ..
    "transform": {
      "^.+\\.js$": "babel-jest",
      ".+\\.(svg|css|styl|less|sass|scss|png|jpg|ttf|woff|woff2)$": "jest-transform-stub"
    }
  }
}
Günter Zöchbauer
  • 623,577
  • 216
  • 2,003
  • 1,567
Maurice
  • 1,223
  • 14
  • 21
  • 3
    The package actually does nothing but return an empty module.export. If you don't want to add a new dependency for this, I recommend Tom Pang's answer. – Florian Falk Sep 20 '21 at 13:10
17

While using jest-svg-transformer, for the setup I had it didn't work when placed under the transformer section. So I had to add it under moduleNameMapper and it worked.

yarn add jest-svg-transformer --dev
 moduleNameMapper: {
    "^.+\\.svg$": "jest-svg-transformer",
  }
whitetiger1399
  • 423
  • 3
  • 6
11

SVG + Jest = Pain

When it comes to testing components with images, be prepared for a bit of pain. Recently, I spent a whole day to understand how it works in Jest.

I understood the following:

  • we can transform FILES with specific extensions (not components)
  • we can transform all images as we like, but most often people transform images into the names of these images in order to get something like <img src="image.svg" /> in the test
  • jest don't understand non-js assets (without custom things)

Problem

Let's describe an example that will return an error if we don't add an svg transformer to jest.config.js. I will use svg-jest library (I will describe it later with more details)

import vectorUpImg from './assets/icons/vector-up.svg';

test('Renders svg as img src', () => {
  /* ... */
  render(<img src={vectorUpImg} />);
  /* ... */
});

The above example returns an error like:

({"Object.<anonymous>":function(module,exports,require,__dirname,__filename,global,jest){<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">

SyntaxError: Unexpected token '<'

>  7 | import vectorUpSvg from 'assets/icons/v2/plain/vector-up.svg';

OK. Let's add a custom transformer. I will describe it later with more details, just know that the transformer will transform (lol) our svg into a string like vector-up.svg and we'll get in the console:

 √ Renders svg as img src (55 ms)

  console.log
    <body>
      <div>
        <img
          src="vector-up.svg"
        />
      </div>
    </body>

Ok, test passed. But... What if we want to use svg as component?

import { ReactComponent as VectorUpComponent } from './assets/icons/vector-up.svg';

test('Renders svg as a ReactComponent. El by querySelector', () => {
  const { container } = render(<VectorUpComponent />);
  const svg = container.querySelector('svg');
  expect(svg).toBeInTheDocument();
});

The above example returns an error like:

Warning: React.jsx: type is invalid -- expected a string (for built-in components) 
or a class/function (for composite components) but got: undefined. 
You likely forgot to export your component from the file it's defined in, 
or you might have mixed up default and named imports.

We can change the transformer (not add another one because transformers are for file extensions, we can't add two different transformers for one ".svg" extension), So now the transformer will parse the incoming content and create something like an svg module, so in our test we get:

√ Renders svg as a ReactComponent. El by querySelector (48 ms)

  console.log
    <body>
      <div>
        <svg
          data-jest-file-name="vector-up.svg"
          data-jest-svg-name="vector-up"
          data-testid="vector-up"
        />
      </div>
    </body>

Coooool, nuh? No... If we now try to pass svg to img src, we will get this:

 console.log
    <body>
      <div>
        <img
          src="[object Object]"
        />
      </div>
    </body>

Awful... But today I found the solution. @ChromeQ made a fix for the svg-jest library and now everything works fine, but you need to add some config until the owner of the svg-jest updates the package or merges the PR by @ChromeQ or PR by me.

So, lets workaround with this...

My workaround

My jest.config.js:

 transform: {
    ...
    '^.+\\.svg$': '<rootDir>/src/tests/transformers/svg.js',
    ...
  },

Transformer src/tests/transformers/svg.js:

I added the changes made by @ChromeQ, did a little refactoring, and added a few docs - PR.


const path = require('path');

/**
 * This function build module.
 *
 * Changes by https://github.com/ChromeQ:
 * - module.exports.default = ${functionName};
 * + module.exports.__esModule = true;
 * + module.exports.default = '${pathname}';
 *
 * Object props is a more extensible solution when we have a lot of props
 * @param {Object} props
 * @param {string} props.functionName
 * @param {string} props.pathname
 * @param {string} props.filename
 *
 * @function {(functionName, pathname, filename) => string} buildModule
 * @param {string} props
 * @return {string} string module
 */
const buildModule = props => {

  const {
    filename,
    functionName,
    pathname,
  } = props;

  return `
      const React = require('react');
      const ${functionName} = (props) =>
      {
      return React.createElement('svg', {
        ...props,
        'data-jest-file-name': '${pathname}',
        'data-jest-svg-name': '${filename}',
        'data-testid': '${filename}'
      });
      }
      module.exports.__esModule = true;
      module.exports.default = '${pathname}';
      module.exports.ReactComponent = ${functionName};
  `;
};

/**
 * This function creates a function name
 * @function {(base) => string} createFunctionName
 * @param {string} base
 * @return {string} string module
 */
const createFunctionName = base => {
  const words = base.split(/\W+/);
  /* here I refactored the code a bit and replaced "substr" (Deprecated) with "substring" */
  return words.reduce((identifier, word) => identifier + word.substring(0, 1).toUpperCase() + word.substring(1), '');
};

/**
 * This function process incoming svg data
 * @function {(contents, filename) => string} processSvg
 * @param {string} contents - your svg. String like "<svg viewBox="..."><path d="..."/></svg>"
 * @param {string} filename - full path of your file
 * @return {string} string module
 */
const processSvg = (contents, filename) => {
  const parts = path.parse(filename);
  if (parts.ext.toLowerCase() === '.svg') {
    const functionName = createFunctionName(parts.name);
    return buildModule({
      filename: parts.name,
      functionName,
      pathname: parts.base,
    });
  }

  return contents;

  // for Jest28 it has to return an object with `code`
  // return { code: contents };
};

module.exports = { process: processSvg };


My test:

import React from 'react';
import { render, screen, } from '@testing-library/react';
import vectorUpImg from './assets/icons/vector-up.svg';
import { ReactComponent as VectorUpComponent } from './assets/icons/vector-up.svg';

test('Renders svg as img src', () => {
  const { container } = render(<img src={vectorUpImg} />);
  const img = container.querySelector('img');
  expect(img).toBeInTheDocument();
  screen.debug();
});

test('Renders svg as a ReactComponent', () => {
  const label = 'icon-vector-up';
  const { getByLabelText } = render(<VectorUpComponent aria-label={label} />);
  expect(getByLabelText(label)).toBeInTheDocument();
  screen.debug();
});

Console happily returns:

  √ Renders svg as img src (46 ms)
  √ Renders svg as a ReactComponent (7 ms)

  console.log
    <body>
      <div>
        <img
          src="vector-up.svg"
        />
      </div>
    </body>

      at logDOM (node_modules/@testing-library/dom/dist/pretty-dom.js:90:13)

  console.log
    <body>
      <div>
        <svg
          aria-label="icon-vector-up"
          data-jest-file-name="vector-up.svg"
          data-jest-svg-name="vector-up"
          data-testid="vector-up"
        />
      </div>
    </body>

      at logDOM (node_modules/@testing-library/dom/dist/pretty-dom.js:90:13)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total

One more thing:

This library (as described here) assigns some attributes:

  • data-jest-file-name: The name of the file (e.g. 'some-image.svg')
  • data-jest-svg-name: Only the name portion of the file (e.g. 'some-image')
  • data-testid: Same as data-jest-svg-name, but works with @testing-library/react getByTestId()

So if you want to find the rendered element in your test by attribute you have to pass in some other attributes. In my example above, I passed the aria-label attribute and then found my svg at that label. Or you can find it with data-testid, initialized by svg-jest depends on your filename. Or you can find it with querySelector, as in the example below:

test('renders svg as a ReactComponent', () => {
  const { container } = render(<VectorUpComponent />);
  const svg = container.querySelector('svg');
  expect(svg).toBeInTheDocument();
});
Bryan Tung
  • 13
  • 4
Garvae
  • 465
  • 4
  • 14
7

Importing *.svg from node_modules?

I had the same problem when importing *.svg files from an icon library in node_modules1. Using jest-transform-stub in the "transform" section of jest.config.js did not work for me, I had to use it in the "moduleNameMapper" section instead.

What helped for me was (in jest.config.js):

module.exports = {
  ...
  moduleNameMapper: {
    '^.+.(svg)$': 'jest-transform-stub',
  }
};

1 The reason for this can be found in jest-transform-stub's README.

Jest doesn't apply transforms to node_modules by default. You can solve this by using moduleNameMapper.

Thomas
  • 2,155
  • 16
  • 22
4

If you are still looking for a easy solution you can define your mock file as this

// svgMock.js
var createReactClass = require('create-react-class');

var Greeting = createReactClass({
  render: function() {
    return null;
  }
});
module.exports = Greeting;

Then in jest.config file do

{
    "moduleNameMapper": {
        "^.+\\.svg$": "<rootDir>/__mocks__/svgMock.js"
    }
}
Jonathan Guerrero
  • 341
  • 1
  • 4
  • 8
  • Thanks for this answer. Depending on your project/linter configuration, a functional component such as the following may work too: ``` // svgMock.js const MockSvgFile = (): JSX.Element => <>>; export default MockSvgFile; ``` – 96ethanh Jul 08 '21 at 21:35
  • doing module.exports = 'div' is simpler – Phap Dinh May 23 '22 at 21:46
4

For anyone who looking for a solution for Jest 28. This is an updated version of Tom Pang's answer

svgTransform.js

const path = require('path');

module.exports = {
    process(sourceText, sourcePath, options) {
        return {
            code: `module.exports = ${JSON.stringify(path.basename(sourcePath))};`,
        };
    },
    getCacheKey() {
        return 'svgTransform';
    }
};

jest.config.js

module.exports = {
    // ...
    transform: {
        "^.+\\.tsx?$": "ts-jest",
        "^.+\\.svg$": "<rootDir>/svgTransform.js",
    }
}

Ref. https://jestjs.io/docs/upgrading-to-jest28#transformer

vinboxx
  • 605
  • 11
  • 13
1

Unfortunately it looks like the various out of the box transformers are no longer maintained for modern Jest versions. I adapted one of them and simplified the code some. Hope this helps someone out there.

const fs = require('fs');
const path = require('path');
const crypto = require('crypto');

const base64 = (filename, data) => {
  const type = filename.endsWith('.svg') ? 'svg+xml' : path.extname(filename).slice(1) || 'png';
  return `data:image/${type};base64,${data.toString('base64')}`;
};

module.exports = {
  getCacheKey: (_fileData, filename) => crypto.createHash('md5').update(filename).digest('hex'),
  process: (sourceText, filename) => `module.exports = ${JSON.stringify(base64(filename, fs.readFileSync(filename)))};`,
};

Use this snippet to add it to the jest config transformers:

'^.+\\.(svg|png|jpg|jpeg|gif|webp)$': '<rootDir>/test/imageTransformer.js',
Bryan McLemore
  • 6,438
  • 1
  • 26
  • 30
1

What worked for me, even though a bit hacky with:
Jest 29
2023
React & Vite

was to do this:

// inside jest.config.json
 ...
 "moduleNameMapper": {
    "^.+\\.svg": "<rootDir>/tests/mocks/svgMock.jsx"
 }
 ...

The the mock file itself was a jsx file, with a div that just 'replaced' the svg files:

 // ./tests/mocks/svgMock.jsx

 import React from 'react';
 const SvgComponent = () => <div data-testid={'mock-svg'}></div>;
 export { SvgComponent };
 

The reason I used this setup was because I have one component that renders Icons and the export had to be with the exact name. Not pretty but worked for me.

Chen Peleg
  • 953
  • 10
  • 16
0

If you are using ts-jest, you can just add svg to the transform list, on the jest.config.js or jest.preset.js file:

transform: {
    '^.+\\.(ts|js|html|svg)$': 'ts-jest',
  }
Raphael Mello
  • 99
  • 2
  • 7
0

For those who initialize project using CRA with TS, and React version is 17 or higher, use jest to run the test failed.

  • you should add .babelrc in your rootDir as below:
// In order for the .babelrc file to take effect, you need to install babel-loader first.
{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": {
          "node": "current"
        }
      }
    ],
    [
      "@babel/preset-react",
      {
        // You don't need to import React after configured this
        "runtime": "automatic"
      }
    ],
    "@babel/typescript"
  ]
}
  • config jest in your package.json or jest.config.js, the config below is in package.json
  "jest": {
    "moduleNameMapper": {
      "^.\\.(css|less|scss)$": "identity-obj-proxy",
      "^@/(.*)$": "<rootDir>/src/$1"
    },
    "transform": {
      "\\.[jt]sx?$": "babel-jest",
      ".+\\.(svg|css|less|sass|scss|png|jpg|ttf|woff|woff2)$": "jest-transform-stub"
    },
    "setupFilesAfterEnv": [
      "<rootDir>/src/setupTests.js"
    ],
    "testEnvironment": "jsdom"
  }
  • In my case, I use jest + enzyme, this is my setupTests.js
import '@testing-library/jest-dom';
import { configure } from 'enzyme';
import Adapter from '@wojtekmaj/enzyme-adapter-react-17';

configure({ adapter: new Adapter() });
Leslie Wu
  • 84
  • 5
0

For the one coming with a higher version than jest@27

This is the new transformer you can use : jest-transformer-svg

Baldráni
  • 5,332
  • 7
  • 51
  • 79
0

This code resolved the same issue, you can also use only SVG in the below code

//../jest.config.js

moduleNameMapper: {
    '\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$': '<rootDir>/src/utils/file-mock.js',
    '^.+\\.(css|less|scss)$': 'babel-jest'
}


//../src/utils/file-mock.js
module.exports = 'stubbed-file';
  • Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Aug 26 '23 at 12:15