Jest cannot load svg file

Viewed 36541

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

11 Answers

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.

module.exports = {
    process() {
        return 'module.exports = {};';
    },
    getCacheKey() {
        // The output is always the same.
        return 'svgTransform';
    },
};

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

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"
}

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"
    }
  }
}

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",
  }

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.

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"
    }
}

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;
};

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();
});

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

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',
  }

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',

With default configuration you must write in jest.config.js

"transform": {
    "\\.[jt]sx?$": "babel-jest",
    "^.+\\.svg$": "jest-svg-transformer"
}
Related