Jest/React - How to use global object in unit tests? - javascript

I use CommonJS modules with require() except React, which is global:
// I don't want require React in every module:
// var React = require("react");
var MyComponent = React.createClass({ // React is global here
});
When running a unit test on MyComponent, Jest can't find React. Is there a way to tell Jest to insert a global React object? (I use npm and gulp-browserify.)

Works for me with the following settings.
// package.json
"jest": {
"scriptPreprocessor": "preprocessor.js",
"unmockedModulePathPatterns": [
"react"
],
"setupEnvScriptFile": "before_test.js"
}
// preprocessor.js
var ReactTools = require('react-tools');
module.exports = {
process: function(src) {
return ReactTools.transform(src);
}
};
// before_test.js
React = require("react"); // Global React object

Related

`window` is not being exposed to Jest

I have a test that imports a component that in turn imports a helper file that uses the window object to pull out a query string parameter. I get the following error about window:
FAIL src/js/components/__tests__/Controls.test.jsx
● Test suite failed to run
ReferenceError: window is not defined
Controls.jsx:
import { Unwrapped as Controls } from '../Controls'
describe('<MyInterestsControls />', () => {
it('should render the component with the fixture data', () => {
const component = shallow(
<UnwrappedMyInterestControls
dashboardData={dashboardData}
loadingFlags={{ controls: false }}
/>
)
expect(component).toMatchSnapshot()
})
})
Controls.jsx imports ./helpers/services.js which contains the following:
import * as queryString from 'query-string'
const flag = queryString.parse(window.location.search).flag || 'off'
^^^^^^ this seems to be the problem
I have attempted to import jsdom:
import { JSDOM } from 'jsdom'
And implemented the solution presented here at the top of my test file:
const { JSDOM } = require('jsdom');
const jsdom = new JSDOM('<!doctype html><html><body></body></html>');
const { window } = jsdom;
function copyProps(src, target) {
const props = Object.getOwnPropertyNames(src)
.filter(prop => typeof target[prop] === 'undefined')
.map(prop => Object.getOwnPropertyDescriptor(src, prop));
Object.defineProperties(target, props);
}
global.window = window;
global.document = window.document;
global.navigator = {
userAgent: 'node.js',
};
copyProps(window, global);
However, I still get the error and it seems JSDOM's window object isn't exposed to the test.
How can I properly expose global objects like window or document to a Jest test?
Relevant package.json
"scripts": {
"test:watch": "NODE_ENV=test jest --watch"
},
...
"devDependencies": {
...
"jest": "^20.0.4",
"jest-mock": "^21.2.0",
"jsdom": "^11.0.0",
...
},
...
"jest": {
"verbose": true,
"collectCoverageFrom": [
"src/js/helpers/preparePayload.js",
"src/js/components-ni",
"!**/node_modules/**",
"!**/dist/**"
],
"coverageThreshold": {
"global": {
"statements": 50,
"branches": 50,
"functions": 50,
"lines": 75
}
},
"testEnvironment": "jest-environment-node"
}
As mentioned by #RiZKiT in the comment below, since Jest v27.0 the default test environment has changed from "jsdom" to "node".
Your problem relies on the configuration.
In the moment you set:
"testEnvironment": "jest-environment-node"
You are changing the default configuration from Jest which is browser-like to jest-environment-node (Node.js-like) meaning that your test will be run under a Node.js environment
To solve it either you set your testEnvironment to jsdom
Or you remove the testEnvironment from your configuration, so it will take the default value in yourpackage.json:
...
"jest": {
"verbose": true,
"collectCoverageFrom": [
"src/js/helpers/preparePayload.js",
"src/js/components-ni",
"!**/node_modules/**",
"!**/dist/**"
],
"coverageThreshold": {
"global": {
"statements": 50,
"branches": 50,
"functions": 50,
"lines": 75
}
}
}
This is what they say in the documentation
testEnvironment [string] # Default: "jsdom"
The test environment that
will be used for testing. The default environment in Jest is a
browser-like environment through jsdom. If you are building a node
service, you can use the node option to use a node-like environment
instead.
## Do you need the `node` environment?
As I could see, your tests are meant to be run under a browser-like environment.
If you ever need an explicit Node.js environment, better you isolate that case using #jest-environment:
/**
* #jest-environment node
*/
test('use node in this test file', () => {
expect(true).not.toBeNull();
});
Or the other way around, if you are meant to run the tests under a Node.js environment:
/**
* #jest-environment jsdom
*/
test('use jsdom in this test file', () => {
const element = document.createElement('div');
expect(element).not.toBeNull();
});
## Conclusion
With this you can avoid importing jsdom manually and setting global variables. jsdom will mock the DOM implementation automatically.
If you need to change the environment for your tests, use the notation #jest-environment.
You could try doing
global.window = new jsdom.JSDOM().window;
global.document = window.document;
See Expose jsdom to global environment #2460
It seems like one of the contributors declared that he is not planning to expose jsdom to global under the Jest environment.
However, you could use Object.defineProperty(window, 'location', {value: '…'} API to approach it, like the developer from Facebook do. In your case it could be like:
Object.defineProperty(window, 'location', {
value: {
search: ...
},
})
Here you can find examples of how to do this:
DOM Testing React Applications with Jest
For example:
import {jsdom} from 'jsdom';
const documentHTML = '<!doctype html><html><body><div id="root"></div></body></html>';
global.document = jsdom(documentHTML);
global.window = document.parentWindow;
You can simply mock location:
global.location = {search: 'someSearchString'}
Also note, that global in your test is the global context for the file to test (global === window)
Note this will only work if your module make the window.location call after the test has been finishing import all the modules.
export default () => window.location
So if your module looks like this:
const l = window.location
export default l
it will not work. In this case you could mock the module using jest.mock.
I am not sure, but I think you could do it with jest.fn():
global.window = jest.fn(() => {
location: { ... }
})
Maybe even as window = jest.fn(...).
In my case, I was injecting a custom environment file's value into a Vue.js component using Ruby on Rails. So, I was getting a base URL for assets as undefined in a Jest snapshot. The solution was to include a setup file in jest.config.js → setupFiles as an array like below.
jest.config.js
{
setupFiles: [
"./app/javascript/tests/testsConfig.js",
],
}
testsConfig.js
process.env.BASE_DOMAIN = process.env.BASE_DOMAIN || 'http://localhost:3000/'
window.TestsConfig = {
"CLOUD_FRONT_BASE_URL": "https://cdn.site.com"
}

webpack: import + module.exports in the same module caused error

I'm developing a website with webpack. When I have a code like this:
import $ from 'jquery';
function foo() {};
module.exports = foo;
I got the error Uncaught TypeError: Cannot assign to read only property 'exports' of object '#<Object>'.
Turns out that changing import $ from 'jquery' to var $ = require('jquery') don't cause any errors.
Why import with module.exports causes this error? Is anything wrong in using require instead?
You can't mix import and module.exports. In the import world, you need to export things.
// Change this
module.exports = foo;
// To this
export default foo;
This happens if other modules down stream have an unexpected require tree. Babel changes require to import where it isn't supposed to which causes the aforementioned issue #Matthew Herbst. To solve this add "sourceType": "unambiguous" to your babelrc file or babel.config.js so that #babel/plugin-transform-runtime won't do this change of require expression to import in your commonjs files. eg:
module.exports = {
presets: [
'#quasar/babel-preset-app'
],
"sourceType": "unambiguous"
}
You can use require with export. But not import and module.exports.
In my react-native-web case, just use an additional webpack rule, then the TypeError: Cannot assign to read only property 'exports' of object is fixed. Maybe you can ref to it.
npm install --save-dev react-app-rewired
Create a config-overrides.js in your project root
// used by react-app-rewired
const webpack = require('webpack');
const path = require('path');
module.exports = {
webpack: function (config, env) {
config.module.rules[1].use[0].options.baseConfig.extends = [
path.resolve('.eslintrc.js'),
];
// To let alias like 'react-native/Libraries/Components/StaticRenderer'
// take effect, must set it before alias 'react-native'
delete config.resolve.alias['react-native'];
config.resolve.alias['react-native/Libraries/Components/StaticRenderer'] =
'react-native-web/dist/vendor/react-native/StaticRenderer';
config.resolve.alias['react-native'] = path.resolve(
'web/aliases/react-native',
);
// Let's force our code to bundle using the same bundler react native does.
config.plugins.push(
new webpack.DefinePlugin({
__DEV__: env === 'development',
}),
);
// Need this rule to prevent `Attempted import error: 'SOME' is not exported from` when `react-app-rewired build`
// Need this rule to prevent `TypeError: Cannot assign to read only property 'exports' of object` when `react-app-rewired start`
config.module.rules.push({
test: /\.(js|tsx?)$/,
// You can exclude the exclude property if you don't want to keep adding individual node_modules
// just keep an eye on how it effects your build times, for this example it's negligible
// exclude: /node_modules[/\\](?!#react-navigation|react-native-gesture-handler|react-native-screens)/,
use: {
loader: 'babel-loader',
},
});
return config;
},
paths: function (paths, env) {
paths.appIndexJs = path.resolve('index.web.js');
paths.appSrc = path.resolve('.');
paths.moduleFileExtensions.push('ios.js');
return paths;
},
};
Also create a web/aliases/react-native/index.js
// ref to https://levelup.gitconnected.com/react-native-typescript-and-react-native-web-an-arduous-but-rewarding-journey-8f46090ca56b
import {Text as RNText, Image as RNImage} from 'react-native-web';
// Let's export everything from react-native-web
export * from 'react-native-web';
// And let's stub out everything that's missing!
export const ViewPropTypes = {
style: () => {},
};
RNText.propTypes = {
style: () => {},
};
RNImage.propTypes = {
style: () => {},
source: () => {},
};
export const Text = RNText;
export const Image = RNImage;
// export const ToolbarAndroid = {};
export const requireNativeComponent = () => {};
Now you can just run react-app-rewired start instead of react-scripts start

Unexpected "Uncaught TypeError: XXX is not a constructor" errors with Babel and ES6

I am giving a try to Webpack, and am giving a try to the instructions in this tutorial, give or take a few custom things.
This is simple code, really, but I'm quite puzzled about this error, and feel this is something silly that I missed.
I defined two ES6 classes, each corresponding to a Handlebars template, and my app's entrypoint is supposed to replace the placeholder HTML in the index file by their contents:
Entrypoint:
import './bloj.less'
// If we have a link, render the Button component on it
if (document.querySelectorAll('a').length) {
require.ensure([], () => {
const Button = require('./Components/Button.js');
const button = new Button('9gag.com');
button.render('a');
}, 'button');
}
// If we have a title, render the Header component on it
if (document.querySelectorAll('h1').length) {
require.ensure([], () => {
const Header = require('./Components/Header.js');
new Header().render('h1');
}, 'header');
}
Index:
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<h1>My title</h1>
<a>Click me</a>
<script src="build/bloj.js"></script>
</body>
</html>
Button:
import $ from 'jquery';
import './Button.less';
export default class Button {
constructor(link) {
this.link = link;
}
onClick(event) {
event.preventDefault();
alert(this.link);
}
render(node) {
const text = $(node).text();
var compiled = require('./Button.hbs');
// Render our button
$(node).html(
compiled({"text": text, "link": this.link})
);
// Attach our listeners
$('.button').click(this.onClick.bind(this));
}
}
Header:
import $ from 'jquery';
import './Header.less';
export default class Header {
render(node) {
const text = $(node).text();
var compiled = require('./Header.hbs');
// Render the header
$(node).html(
compiled({"text": text})
);
}
}
Sadly, it does not work, and I get both these errors when displaying the page:
Uncaught TypeError: Header is not a constructor
Uncaught TypeError: Button is not a constructor
What could I be missing?
Here is my webpack configuration:
var path = require('path');
var webpack = require('webpack');
var CleanPlugin = require('clean-webpack-plugin');
var ExtractPlugin = require('extract-text-webpack-plugin');
var production = process.env.NODE_ENV === 'production';
var appName = 'bloj';
var entryPoint = './src/bloj.js';
var outputDir = './build/';
var publicDir = './build/';
// ************************************************************************** //
var plugins = [
//new ExtractPlugin(appName + '.css', {allChunks: true}),
new CleanPlugin(outputDir),
new webpack.optimize.CommonsChunkPlugin({
name: 'main',
children: true,
minChunks: 2
})
];
if (production) {
plugins = plugins.concat([
new webpack.optimize.DedupePlugin(),
new webpack.optimize.OccurenceOrderPlugin(),
new webpack.optimize.MinChunkSizePlugin({
minChunkSize: 51200 // 50ko
}),
new webpack.optimize.UglifyJsPlugin({
mangle: true,
compress: {
warnings: false // Suppress uglification warnings
}
}),
new webpack.DefinePlugin({
__SERVER__: false,
__DEVELOPMENT__: false,
__DEVTOOLS__: false,
'process.env': {
BABEL_ENV: JSON.stringify(process.env.NODE_ENV)
}
})
]);
}
module.exports = {
entry: entryPoint,
output: {
path: outputDir,
filename: appName + '.js',
chunkFilename: '[name].js',
publicPath: publicDir
},
debug: !production,
devtool: production ? false : 'eval',
module: {
loaders: [
{
test: /\.js/,
loader: "babel",
include: path.resolve(__dirname, 'src'),
query: {
presets: ['es2015']
}
},
{
test: /\.less/,
//loader: ExtractPlugin.extract('style', 'css!less')
loader: "style!css!less"
},
{
test: /\.html/,
loader: 'html'
},
{
test: /\.hbs/,
loader: "handlebars-template-loader"
}
]
},
plugins: plugins,
node: {
fs: "empty" // Avoids Handlebars error messages
}
};
What could I be missing?
Babel assigns default exports to the default property. So if you use require to import ES6 modules, you need to access the default property:
const Button = require('./Components/Button.js').default;
I realize that you already have an answer. However I had a similar issue to which I found an answer. Starting my own question and answering it seems weird.
So I'm just going to leave this here.
I had the same error as you got. However, I managed to solve it by changing my
export default {Class}
to
export default Class
I don't know why I wrapped the Class in an object but I remember having seen it somewhere so I just started using it.
So instead of the default returning a Class it returned an object like this {Class: Class}.
This is completely valid yet it will break webpack+babel.
EDIT: I've since come to know why this probably breaks babel+webpack. The export default is meant to only have 1 export. A javascript-object can contain many properties. Which means it can have more than 1 export. (See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/export).
For multiple exports use: export {definition1, definition2}.
Use-case: I've used this in a situation where I've created a library which exported different types of an editor (while the underlying code was the same, the appearance of the editor changes depending on which export you use).
You can just put export var __useDefault = true; just after exporting your Class.
export default class Header {
...
}
export var __useDefault = true;
I was able to fix this by adding babel-plugin-add-module-exports to the .babelrc file
npm install babel-plugin-add-module-exports --save-dev
{
"presets": ["#babel/env"],
"plugins": ["add-module-exports"]
}
this adds
module.exports = exports.default;
to the last line when compiling the class with babel.
Although this is not the cause of your particular issue, I ran into a very similar problem when trying to rip babel out of an existing node app that was using ES6's import and export syntax, so this post is to help out anyone else struggling with this in the future.
Babel will resolve any circular dependencies between one module and another, so you can use ES6's import and export with reckless abandon. However, if you need to get rid of babel and use native node, you will need to replace any import and exports with require. This can reintroduce a latent circular reference issues that babel was taking care of in the background. If you find yourself in this situation, look for an area in your code that looks like this:
File A:
const B = require('B');
class A {
constructor() {
this.b = new B();
}
}
module.exports = A;
File B:
const A = require('A'); // this line causes the error
class B {
constructor() {
this.a = new A();
}
}
module.exports = B;
There are several different ways to resolve this issue depending on how you structured your code. The easiest way is probably to pass B a reference to A instead of creating a new instance of class A. You could also dynamically resolve the reference when loading A. There are a myriad of other alternatives, but this is a good place to get started.
It's not the problem in this particular question, but for some reasons, babel does not hoist classes in the same file.
So if you declare your class Token at the top of the file, and write later new Token(), it will run.
If you declare your class after the constructor call, you will have the xxx is not a constructor error
I had the same error message and discovered that the cause was circular import statements. That is: I had two files that imported each other, wherein one file contained an export default class that contained a method that was dependent upon an export function from the other file.
My solution was to move one of the dependencies (functions) out of the class and into a utils.js file, which was a more appropriate place for it anyway!
This is the way I am using / importing my classes:
Utils.class.js
export default class Utils {
somefunction(val) {
return val
}
}
Using Utils into my controllers:
import {default as U} from '../helpers/Utils.class';
const Utils = new U();
console.log(Utils.somefunction(123));

Unable to include 3-d party component in React

I'm trying to include 3-d party component as follows:
app.js
/** #jsx React.DOM */
var React = require('react'),
ReactDOM = require('react-dom');
Isvg = require('react-inlinesvg');
ReactDOM.render(
<Isvg src="images/mobile.svg" />,
document.getElementById('mobile')
);
package.json:
...
"browser": {
"react": "./node_modules/react/dist/react.js",
"react-dom": "./node_modules/react-dom/dist/react-dom.js",
"react-inlinesvg": "./bower_components/react-inlinesvg/standalone/react-inlinesvg.js",
"react-lazyload": "./node_modules/react-lazy-load/dist/LazyLoad.js"
},
"browserify": {
"transform": [
["reactify", {"es6": true}]
]
}
and finally gulp task configuration:
var gulp = require('gulp'),
config = require('../config')(),
utils = require('../utils'),
plugins = require('gulp-load-plugins')({lazy: true}),
browserify = require('browserify'),
source = require('vinyl-source-stream'),
reactify = require('reactify'),
watchify = require('watchify');
gulp.task('make-js', function() {
var options = {
debug: true,
entries: config.js,
transform: [reactify],
cache: {}, packageCache: {}, fullPaths: true
},
bundler = watchify(browserify(options));
bundler.on('update', rebundle);
function rebundle() {
utils.log('start bundling...');
bundler
.bundle()
.on('error', utils.handleErrors)
.pipe(source('bundle.js'))
.pipe(gulp.dest(config.tmp));
utils.log('finished bundling...');
}
return rebundle();
});
This creates bundle.js file without any problems however the 3-d party inlinesvg component does not work and produces the following error in the browser javascript console:
Uncaught TypeError: Cannot read property 'PropTypes' of undefined
The error is caused by the following code in react-inlinesvg.js
Line 566
PropTypes = React.PropTypes;
I'm pretty sure I've missed something important in the browserify configuration but cannot figure out what I did wrong exactly.
Thanks for your help.
It seems to be an issue with that 3-d party component. I'm newbie in react so in order to exclude my configuration errors I used 100% working boilerplate. But the issue remained. The investigation revealed the module itself does not import React in a proper way.
My original question could be actually split into 2 questions (how to include that module and how to load SVG files inline so let me answer the second part of my question).
I did not find any working modules which could load svg without any 3-d party modules like jquery or creating my own bicycle.
I came up with the well-known SVGInjector javascript module. It is a vanilla js module but has the necessary browserify support. I wrapped it into react component without problems. Here is the code:
class svg extends React.Component {
componentDidMount(){
return SVGInjector([ReactDOM.findDOMNode(this)]);
}
constructor(props) {
super(props);
}
render() {
return React.DOM.img({
className: this.props.className,
'data-src': this.props.path,
'data-fallback': this.props.fallbackPath});
}
}

Webpack externals dependency

I have modular javascript application and I need to have js frameworks in one file "global-libs.js", which dependencies will be accessible for every file using webpack. Other js files will only use these dependencies but it will not be part of the final bundle. I'am using Gulp for these task in combination of Webpack.
This is task for webpack and transpile my jsx into js where should be only my code, not external libraries
gulp.task('js',['jsx'], function () {
/**This dependency is external, its not part of the bundle */
return gulp.src(config.paths.workDir + config.paths.jsPath + '/**/*.js')
.pipe(webpack({
externals: {
"react": "React"
}
}))
.pipe(rename('onlyCustomJs.js'))
.pipe(gulpif(args.production, uglify()))
.pipe(gulp.dest(config.paths.portlets.newNotePortlet + config.paths.jsPath))
});
This task should create files only with externals libraries and dependency React should be accessible using require in every js webpack file.
gulp.task('global', function(){
/**This will be accessible globally*/
return gulp.src(config.paths.srcDir + config.paths.jsPath + '/global-libs.js')
.pipe(webpack({
output: {
libraryTarget: "var",
library: "React"
}
}))
.pipe(rename('global-libs.js'))
.pipe(gulp.dest(config.paths.portlets.evremTheme + config.paths.jsPath))
});
This file uses global react dependency. But it tells me that React is undefined at var HelloMessage = React..
/** #jsx React.DOM */
var React = require('react');
var HelloMessage = React.createClass({
render: function() {
return <div>Hello {this.props.name}</div>;
}
});
React.renderComponent(HelloMessage({name: "Hello world"}), document.getElementById('example'));
This is global-libs.js file
var React = require('react');
var jQuery = require('jquery');
Thank you!
Maybe It's not to best solution but I solved by these changes.
//These is dependencies which will be bundled in one global-libs.js file and will be accessible from everywhere through require().
module.exports = React = require('react');
module.exports = jQuery = require('jquery');
Webpack only merge these two files and publish them through module.exports
gulp.task('global', function(){
/**This will be accessible globally*/
return gulp.src(config.paths.srcDir + config.paths.jsPath + '/global-libs.js')
.pipe(webpack())
.pipe(rename('global-libs.js'))
.pipe(gulp.dest(config.paths.destDir + config.paths.jsPath))
});
My gulp task for bundling my conde is like this. Only specified externals dependencies which will not be part of the bundle.
gulp.task('js',['jsx'], function () {
/**This dependency is external, its not part of the bundle */
return gulp.src(config.paths.workDir + config.paths.jsPath + '/**/*.js')
.pipe(webpack({
externals: {
"react": "React",
"jquery": "jQuery"
}
}))
.pipe(rename('bundle.js'))
.pipe(gulpif(args.production, uglify()))
.pipe(gulp.dest(config.paths.destDir + config.paths.jsPath))
});
In result I can import dependencies like this.
/** #jsx React.DOM */
var React = require('react');
var jQuery = require('jquery');
var HelloMessage = React.createClass({
render: function() {
return <div>Hello {this.props.name}</div>;
}
});
React.renderComponent(HelloMessage({name: "Hello world"}), jQuery('#example')[0]);

Categories

Resources