_I'm not sure whether this should be a bug report or a feature proposal..._
Unlike webpack
, which correctly processes SASS files that are imported sans suffix:
import Styles from './styles' // file: styles.sass
Jest does not. When I attempted to set up a moduleNameMapper
with a custom proxy object (an enhanced version of identity-obj-proxy
), I configured it thusly:
moduleNameMapper: {
'\\.sass$': '<rootDir>/test/enhanced-obj-proxy.js',
},
But my suffixless imports fail:
Details:
/workplace/website/src/components/ContentStripe/styles.sass:1
({"Object.<anonymous>":function(module,exports,require,__dirname,__filename,global,jest){.root
^
SyntaxError: Unexpected token .
4 | import { Grid } from 'mui'
5 |
> 6 | import Styles from './styles'
| ^
Only when I change my import to use a suffix (import Styles from './styles.sass'
) does the moduleNameMapper
actually work. Aside from violating POLA, this means that our components must be tied to a particular stylesheet implementation, and the imports for stylesheets are thus different from all of our other imports...and the only reason is to to satisfy the behavior of a test framework.
In #4488, @cpojer closed a similar feature request saying that one should set up a custom resolver
to alleviate the problem, but provided no further guidance. After spending several hours poking around in Jest, and littering the source with logging statements, this advice appears to not only be unreasonable (given the complexity of the default resolver), but also a red herring. As I see it, this solution only makes sense if the problem were that the default resolver is unable to figure out what to do with ./sass
as an import path. But that is clearly not the case:
Error: Resolved './styles' to '/workplace/website/src/components/ContentStripe/styles.sass'
at defaultResolver (/workplace/website/node_modules/jest-resolve/build/default_resolver.js:64:8)
at Function.findNodeModule (/workplace/website/node_modules/jest-resolve/build/index.js:111:14)
at resolveNodeModule (/workplace/website/node_modules/jest-resolve/build/index.js:173:16)
at Resolver.resolveModuleFromDirIfExists (/workplace/website/node_modules/jest-resolve/build/index.js:184:16)
at Resolver.resolveModule (/workplace/website/node_modules/jest-resolve/build/index.js:213:25)
at Resolver._getVirtualMockPath (/workplace/website/node_modules/jest-resolve/build/index.js:344:16)
at Resolver._getAbsolutePath (/workplace/website/node_modules/jest-resolve/build/index.js:330:14)
at Resolver.getModuleID (/workplace/website/node_modules/jest-resolve/build/index.js:307:31)
at Runtime._shouldMock (/workplace/website/node_modules/jest-runtime/build/index.js:771:37)
at Runtime.requireModuleOrMock (/workplace/website/node_modules/jest-runtime/build/index.js:465:14)
As you can see, the default resolver has no problem whatsoever in turning ./styles
into .../styles.sass
.
Later, Resolver._resolveStubModuleName()
, is invoked with the context file path (from
) and the raw string from the import
statement:
Resolver: _resolveStubModuleName(/workplace/website/src/components/ContentStripe/index.jsx, ./styles)
And it is in this method where the moduleNameMapper
finally comes into play:
Resolver: moduleNameMapper: [ { moduleName:
'/workplace/website/test/enhanced-obj-proxy.js',
regex: { /\.sass$/ [lastIndex]: 0 } },
[length]: 1 ]
Resolver: Error
at Resolver._resolveStubModuleName (/workplace/website/node_modules/jest-resolve/build/index.js:363:10)
at Resolver.getMockModule (/workplace/website/node_modules/jest-resolve/build/index.js:274:31)
at Runtime.requireModule (/workplace/website/node_modules/jest-runtime/build/index.js:334:40)
at Runtime.requireModuleOrMock (/workplace/website/node_modules/jest-runtime/build/index.js:468:19)
at Object.<anonymous> (/workplace/website/src/components/ContentStripe/index.jsx:6:1)
at Runtime._execModule (/workplace/website/node_modules/jest-runtime/build/index.js:699:13)
at Runtime.requireModule (/workplace/website/node_modules/jest-runtime/build/index.js:381:14)
at Runtime.requireModuleOrMock (/workplace/website/node_modules/jest-runtime/build/index.js:468:19)
at Object.<anonymous> (/workplace/website/src/components/ContentStripe/index.test.jsx:7:1)
at Runtime._execModule (/workplace/website/node_modules/jest-runtime/build/index.js:699:13)
Test
// file: foo.test.js
// Change this to './foostyles.css' and it'll work
import Styles from './foostyles'
describe('Styles', () => {
it('works', () => {
expect(Styles).toBeDefined()
})
})
Stylesheet
/* file: foostyles.css */
.root {
color: black;
}
jest.config.js
{
// ...snip...
moduleNameMapper: {
'\\.css$': 'identity-obj-proxy',
},
}
moduleNameMapper
patterns should be applied to the resolved filepath, not the raw file path found in the source code:
import Styles './style'
npx envinfo --preset jest
Paste the results here:
npx: installed 1 in 2.142s
System:
OS: macOS 10.14
CPU: x64 Intel(R) Core(TM) i7-7567U CPU @ 3.50GHz
Binaries:
Node: 10.8.0 - ~/.nvm/versions/node/v10.8.0/bin/node
npm: 6.4.1 - ~/.nvm/versions/node/v10.8.0/bin/npm
https://jestjs.io/docs/en/configuration#modulefileextensions-array-string
@thymikee, I'm not sure how that would be relevant as I've already demonstrated that the resolver has no problems resolving .sass
files. Besides, that's already part of my config:
$ npm test -- --showConfig | grep -A5 moduleFileExtensions
"moduleFileExtensions": [
"js",
"jsx",
"sass",
"svg"
],
Here's the full config for reference:
{
"configs": [
{
"automock": false,
"browser": false,
"cache": true,
"cacheDirectory": "/var/folders/75/mk6rfnd52xg_mbfwpw2g9s5cwpk8v1/T/jest_fxgjfl",
"clearMocks": false,
"coveragePathIgnorePatterns": [
"/workplace/website/src/index.jsx$"
],
"detectLeaks": false,
"detectOpenHandles": false,
"errorOnDeprecated": false,
"filter": null,
"forceCoverageMatch": [],
"globals": {},
"haste": {
"providesModuleNodeModules": []
},
"moduleDirectories": [
"node_modules"
],
"moduleFileExtensions": [
"js",
"jsx",
"sass",
"svg"
],
"moduleNameMapper": [
[
"\\.sass$",
"/workplace/website/test/enhanced-obj-proxy.js"
]
],
"modulePathIgnorePatterns": [],
"name": "46ec5fecf0fa2b69c57c1e81f71ec1b1",
"prettierPath": null,
"resetMocks": false,
"resetModules": false,
"resolver": null,
"restoreMocks": false,
"rootDir": "/workplace/website",
"roots": [
"/workplace/website"
],
"runner": "jest-runner",
"setupFiles": [
"/workplace/website/node_modules/regenerator-runtime/runtime.js"
],
"setupTestFrameworkScriptFile": "/workplace/website/test/setup-tests.js",
"skipFilter": false,
"snapshotSerializers": [],
"testEnvironment": "/workplace/website/node_modules/jest-environment-JSDOM/build/index.js",
"testEnvironmentOptions": {},
"testLocationInResults": false,
"testMatch": [
"**/src/**/*.test.js?(x)"
],
"testPathIgnorePatterns": [
"/node_modules/"
],
"testRegex": "",
"testRunner": "/workplace/website/node_modules/jest-jasmine2/build/index.js",
"testURL": "http://localhost/",
"timers": "real",
"transform": [
[
"^.+\\.jsx?$",
"/workplace/website/node_modules/babel-jest/build/index.js"
]
],
"transformIgnorePatterns": [
"/node_modules/"
],
"watchPathIgnorePatterns": []
}
],
"globalConfig": {
"bail": false,
"changedFilesWithAncestor": false,
"collectCoverage": false,
"collectCoverageFrom": [
"src/**/*.js?(x)"
],
"coverageDirectory": "/workplace/website/test/coverage",
"coverageReporters": [
"json",
"text",
"lcov",
"clover"
],
"coverageThreshold": null,
"detectLeaks": false,
"detectOpenHandles": false,
"errorOnDeprecated": false,
"expand": false,
"filter": null,
"globalSetup": null,
"globalTeardown": null,
"listTests": false,
"maxWorkers": 3,
"noStackTrace": false,
"nonFlagArgs": [],
"notify": false,
"notifyMode": "always",
"passWithNoTests": false,
"projects": null,
"rootDir": "/workplace/website",
"runTestsByPath": false,
"skipFilter": false,
"testFailureExitCode": 1,
"testPathPattern": "",
"testResultsProcessor": null,
"updateSnapshot": "new",
"useStderr": false,
"verbose": null,
"watch": false,
"watchman": true
},
"version": "23.6.0"
}
By the way, to add insult to injury, the way that transform
works is different than moduleNameMapper
--it will match on the resolved filename. This makes me believe more strongly that this is a bug, not a feature request.
any news? Got similar error with this package:
13:54 $ yarn test
yarn run v1.12.3
$ jest --config=./config/jest/jest.config.js
FAIL src/components/App.spec.jsx
● Test suite failed to run
/home/foton/workspace/self_study/material_ui/node_modules/typeface-roboto/index.css:2
@font-face {
^
SyntaxError: Invalid or unexpected token
1 | import React from 'react';
2 | import { hot } from 'react-hot-loader';
> 3 | import 'typeface-roboto';
| ^
4 |
5 |
6 | const App = () => (
at ScriptTransformer._transformAndBuildScript (node_modules/jest-runtime/build/script_transformer.js:403:17)
at Object.<anonymous> (src/components/App.jsx:3:1)
at Object.<anonymous> (src/components/App.spec.jsx:3:1)
Test Suites: 1 failed, 1 total
Tests: 0 total
Snapshots: 0 total
Time: 1.666s
Ran all test suites.
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
jest.config.js:
const path = require('path');
const dirs = require('../dirs');
const fileMock = path.resolve(__dirname, 'fileMock.js');
const cssMock = path.resolve(__dirname, 'cssMock.js');
module.exports = {
rootDir: dirs.ROOT,
testMatch: [
'**/*.spec.(js|jsx)',
],
setupTestFrameworkScriptFile: 'jest-enzyme',
testEnvironment: 'enzyme',
moduleNameMapper: {
'.(gif|png|jpe?g|svg|woff|woff2|eot|ttf|otf)$': fileMock,
'.(css|less)$': cssMock,
},
moduleFileExtensions: [
'js',
'jsx',
'css',
],
};
package.json:
{
"scripts": {
"build": "webpack --config config/webpack.prod.js",
"start": "webpack-dev-server --config config/webpack.dev.js",
"test": "jest --config=./config/jest/jest.config.js",
"lint": "eslint --no-inline-config --ext .jsx,.js .",
"fix": "eslint --fix --ext .jsx,.js ."
},
"dependencies": {
"@material-ui/core": "^3.5.1",
"@material-ui/icons": "^3.0.1",
"dotenv": "^6.1.0",
"prop-types": "^15.6.2",
"react": "^16.6.0",
"react-dom": "^16.6.0",
"react-hot-loader": "^4.3.12",
"typeface-roboto": "^0.0.54"
},
"devDependencies": {
"@babel/core": "^7.1.5",
"@babel/preset-env": "^7.1.0",
"@babel/preset-react": "^7.0.0",
"autoprefixer": "^9.3.1",
"babel-core": "^7.0.0-bridge",
"babel-eslint": "^10.0.1",
"babel-jest": "^23.6.0",
"babel-loader": "^8.0.4",
"clean-webpack-plugin": "^0.1.19",
"css-loader": "^1.0.1",
"cssnano": "^4.1.7",
"enzyme": "^3.7.0",
"enzyme-adapter-react-16": "^1.7.0",
"error-overlay-webpack-plugin": "^0.1.5",
"eslint": "^5.9.0",
"eslint-config-airbnb": "^17.1.0",
"eslint-loader": "^2.1.1",
"eslint-plugin-import": "^2.14.0",
"eslint-plugin-jest": "^22.0.0",
"eslint-plugin-jsx-a11y": "^6.1.2",
"eslint-plugin-react": "^7.11.1",
"file-loader": "^2.0.0",
"friendly-errors-webpack-plugin": "^1.7.0",
"html-webpack-plugin": "^3.2.0",
"html-webpack-template": "^6.2.0",
"image-webpack-loader": "^4.5.0",
"jest": "^23.6.0",
"jest-environment-enzyme": "^7.0.1",
"jest-enzyme": "^7.0.1",
"mini-css-extract-plugin": "^0.4.4",
"postcss-loader": "^3.0.0",
"regenerator-runtime": "^0.12.1",
"style-loader": "^0.23.1",
"webpack": "^4.25.0",
"webpack-bundle-analyzer": "^3.0.3",
"webpack-cli": "^3.1.2",
"webpack-dev-server": "^3.1.10",
"webpack-merge": "^4.1.4",
"webpack-notifier": "^1.7.0",
"webpackbar": "^2.6.3"
},
"license": "MIT"
}
fileMock.js:
module.exports = 'test-file-stub';
cssMock.js:
module.exports = {};
Sorry, but I think this is not related issue. Jest exclude node_modules from testing and can't use my mocks :/
@fotonmoton I hit this issue with the typeface library today and was able to get it working by specifying the path to the css file.
import 'typeface-montserrat/index.css'
Seems moduleNameMapper will unsurprisingly only catch modules that have been imported with .css
in the path
@gi-alec, yeah sounds right, how can module resolver decide what to do with module if it doesn't know what module type it is? :) Thanks for solution.
You saved my day @gi-alec ❤️
Ran into the same issue today, any update on how to deal with this problem? :(
After playing around a little bit more, I figured out that what we need to change a little bit the way we were defining the moduleWrapperMapper
config. It should look like this instead.
import Styles from './styles' // file: styles.sass
moduleNameMapper: {
"^./styles": "identity-obj-proxy",
},
And problem solved. cc @dankreft
@nopito, that suggestion works for some, but not all codebases. I ran into the exact same issue as described above, except one of my node_modules
’ dependencies imports a javascript file named styles
:
// node_modules/colors/lib/colors.js
var ansiStyles = colors.styles = require('./styles');
Although obviously an edge case, the robust solution is still to let moduleNameMapper
match on the resolved file name.
@dankreft
I just has a similar issue with a project that imports html files in typescript. This was my solution:
https://github.com/nickvorie/jest-string-transform
Not sure if this will help you any.
I use identity-obj-proxy
package for handling style files, but as mentioned above, the usual setting of '.+\\.(css|styl|less|sass|scss)$': 'identity-obj-proxy'
only covers imports that have their file extension explicitly stated.
I think most font packages on NPM start with typeface-
. So I use this in my moduleNameMapper
: '^typeface-(.*)': 'identity-obj-proxy'
.
@RemLampa Thx a lot, that helped me testing my gatsby app using material ui. I import 'typeface-roboto' there in the mui-theme.
I ran into this when adding @material-ui
to our project. @material-ui
has files named style.js
and I was mocking files named style.less
- both matching the "^./style$"
regex in "moduleNameMapper"
.
By mapping the style.js
files to identity-obj-proxy
, material-ui would fail to build.
As a workaround I took advantage of the "transform"
option to resolve modules against the full file path; and wrote a transform to stub files with identity-obj-proxy
:
+ package.json
...
"jest": {
"testPathIgnorePatterns": [
"<rootDir>/tests/transforms/*"
],
"moduleFileExtensions": [
"js",
"jsx",
"less"
],
"transform": {
"^.+\\.js$": "babel-jest",
"\\.(css|less)$": "<rootDir>/tests/transforms/identity-obj-proxy.js"
}
}
+ tests/transforms/identity-obj-proxy.js
const fs = require('fs');
// Read the identity-obj-proxy as text. NOTE This only works because the entire module is inside one file
const identityObjProxySrc = fs.readFileSync(require.resolve('identity-obj-proxy'), 'utf-8');
/**
* The jest config option "moduleNameMapper" does not resolve to absolute paths, which
* can cause conflicts with similar file names. In this case, "style.less" and "style.js" are
* both mocked with the identity-obj-proxy. This causes compiling of @material-ui to fail.
* Instead use the "transforms" option to accomplish stubbing CSS/LESS files with identityObjProxy
*/
module.exports = {
process() {
return identityObjProxySrc;
}
};
Not pretty, but it worked for me.
I truly believe that JEST should give us the way to map modules by matching the entire resolved paths and not only the module names specified in the specific import expressions. Otherwise it's pretty hard to use this option reliably. Also, all other tools (like e.g. webpack) does work on full resolved paths. It would get JEST more consistent with the ecosystem (and less surprising) if it would do the same.
Most helpful comment
@fotonmoton I hit this issue with the typeface library today and was able to get it working by specifying the path to the css file.
import 'typeface-montserrat/index.css'
Seems moduleNameMapper will unsurprisingly only catch modules that have been imported with
.css
in the path