Jest Testing with require modules: ejs-loader - node.js

I am playing with the idea of having large static html bundles just loaded into a react component vice typing them all out in jsx. I am currently just experimenting with ejs-loader and html-react-parser to evaluate the feasibility of this. Everything actually renders fine but I cannot get any tests to work with jest for this.
I receive: Cannot find module ejs-loader!./AboutPage.view.ejs from AboutPage.js errors and I am unsure of what to do.
I am currently just working off of react-slingshot as my base for experimenting with this.
The repo for the project is here
The component itself is simple:
import React from 'react';
import Parser from 'html-react-parser';
import '../styles/about-page.css';
const view = require('ejs-loader!./AboutPage.view.ejs')();
// Since this component is simple and static, there's no parent container for it.
const AboutPage = () => {
return (
<div>
{Parser(view)}
</div>
);
};
export default AboutPage;
And the test is:
import React from 'react';
import {shallow} from 'enzyme';
import AboutPage from './AboutPage';
describe('<AboutPage />', () => {
it('should have a header called \'About\'', () => {
const wrapper = shallow(<AboutPage />);
const actual = component.find('h2').text();
const expected = 'About';
expect(actual).toEqual(expected);
});
});
I have read through the docs and similar questions like this. I attempted to use a custom transformer, but I may be misunderstanding something as it doesn't appear to be even called.
Package.json
"jest": {
"moduleNameMapper": {
"\\.(css|scss)$": "identity-obj-proxy",
"^.+\\.(gif|ttf|eot|svg|woff|woff2|ico)$": "<rootDir>/tools/fileMock.js"
},
"transform": {
"^.+\\.js$": "babel-jest",
"\\.(ejs|ejx)$": "<rootDir>/tools/ejx-loader/jest.transformer.js"
}
},
and the transformer itself:
module.exports = {
process(src, filename, config, options){
console.log('????');
return 'module.exports = ' + require(`ejs-loader!./${filename}`);
//return require(`ejs-loader!./${filename}`);
}
};

Can you try changing module name mapper to -
{
"\\.(css|scss)$": "identity-obj-proxy",
"^.+\\.(gif|ttf|eot|svg|woff|woff2|ico)$": "<rootDir>/tools/fileMock.js"
"ejs-loader!(.*)": "$1",
}
This should at least invoke your custom transformer.
Also the custom transformer should be -
const _ = require('lodash');
module.exports = {
process(src, filename, config, options){
console.log('????');
return 'module.exports = ' + _.template(src);
}
};

It doesn't look like you've specified .ejs as a moduleFileExtension.
"jest": {
...
"moduleFileExtensions": ["js", "jsx", "ejs", "ejx"],
...
}
Also, ejs-loader will export the function using cjs syntax for you, so you can do the following in your transformer:
const loader = require('ejs-loader');
module.exports = {process: loader};

Work for me:
"jest": {
"moduleNameMapper": {
'\\.(ejs|ejx)$': '<rootDir>/jest-ejs.transformer.js'
},
moduleFileExtensions: ['js', 'json', 'jsx', 'ejs']
},
In jest-ejs.transformer.js
const loader = require('ejs-loader');
module.exports = {process: loader};

Related

RequireJS + Mocha + JSDom + Node -> Shim config not supported in Node

I'm trying to setup a Mocha testing framework using JSDom with RequireJS. Because I'm running the test on node instead of using a browser (since I'm using JSDom), all the non AMD modules doesn't seem to be imported and is throwing Shim config not supported in Node. Does anyone know how I can export those modules to AMD or what the right approach is? (aka what I'm doing wrong)
Example of my set-up
Component.js
define(["jquery", "non_AMD_Module", ... ], function($, NonAMDModule, ...) {
let component = {
...
foo = () => {
NonAMDModule.bar();
};
};
return component;
});
Component.test.js
const requirejs = require('requirejs');
const { JSDOM } = require('jsdom');
requirejs.config({
baseUrl: "dist/app",
paths: {
jquery: "lib/jquery",
component: "path_to_component",
non_AMD_Module: "path_to_module"
},
shim: {
non_AMD_Module: { exports: "non_AMD_Module" } // This doesn't work
}
});
const { window } = new JSDOM("<html></html>");
global.window = window;
global.document = window.document;
global.$ = requirejs('jquery');
const Component = requireJS('component');
describe('test', () => {
it('is a simple test', () => {
const testComponent = new Component();
testComponent.foo();
}
});
When I run the test suite, I get:
Mocha Exploded!
TypeError: Cannot read property 'bar' of undefined
running r.js -convert "path_to_module" did not work for this module
Looking at the source code for jQuery, I found that there's this boiler-plate coded that exports it to AMD.
This can be added at the bottom of the non-AMD-module in order to export it to an AMD module accessible by RequireJS
if ( typeof define === "function" && define.amd ) {
define([], function {
return non_AMD_Module;
});
}
Other Resources:
Shim a module in Require.js that uses module.exports possible?
https://github.com/requirejs/requirejs/wiki/Updating-existing-libraries#anon

next-i18next Jest Testing with useTranslation

Testing libs...always fun. I am using next-i18next within my NextJS project. We are using the useTranslation hook with namespaces.
When I run my test there is a warning:
console.warn
react-i18next:: You will need to pass in an i18next instance by using initReactI18next
> 33 | const { t } = useTranslation(['common', 'account']);
| ^
I have tried the setup from the react-i18next test examples without success. I have tried this suggestion too.
as well as just trying to mock useTranslation without success.
Is there a more straightforward solution to avoid this warning? The test passes FWIW...
test('feature displays error', async () => {
const { findByTestId, findByRole } = render(
<I18nextProvider i18n={i18n}>
<InviteCollectEmails onSubmit={jest.fn()} />
</I18nextProvider>,
{
query: {
orgId: 666,
},
}
);
const submitBtn = await findByRole('button', {
name: 'account:organization.invite.copyLink',
});
fireEvent.click(submitBtn);
await findByTestId('loader');
const alert = await findByRole('alert');
within(alert).getByText('failed attempt');
});
Last, is there a way to have the translated plain text be the outcome, instead of the namespaced: account:account:organization.invite.copyLink?
Use the following snippet before the describe block OR in beforeEach() to mock the needful.
jest.mock("react-i18next", () => ({
useTranslation: () => ({ t: key => key }),
}));
Hope this helps. Peace.
use this for replace render function.
import { render, screen } from '#testing-library/react'
import DarkModeToggleBtn from '../../components/layout/DarkModeToggleBtn'
import { appWithTranslation } from 'next-i18next'
import { NextRouter } from 'next/router'
jest.mock('react-i18next', () => ({
I18nextProvider: jest.fn(),
__esmodule: true,
}))
const createProps = (locale = 'en', router: Partial<NextRouter> = {}) => ({
pageProps: {
_nextI18Next: {
initialLocale: locale,
userConfig: {
i18n: {
defaultLocale: 'en',
locales: ['en', 'fr'],
},
},
},
} as any,
router: {
locale: locale,
route: '/',
...router,
},
} as any)
const Component = appWithTranslation(() => <DarkModeToggleBtn />)
const defaultRenderProps = createProps()
const renderComponent = (props = defaultRenderProps) => render(
<Component {...props} />
)
describe('', () => {
it('', () => {
renderComponent()
expect(screen.getByRole("button")).toHaveTextContent("")
})
})
I used a little bit more sophisticated approach than mocking to ensure all the functions work the same both in testing and production environment.
First, I create a testing environment:
// testing/env.ts
import i18next, { i18n } from "i18next";
import JSDomEnvironment from "jest-environment-jsdom";
import { initReactI18next } from "react-i18next";
declare global {
var i18nInstance: i18n;
}
export default class extends JSDomEnvironment {
async setup() {
await super.setup();
/* The important part start */
const i18nInstance = i18next.createInstance();
await i18nInstance.use(initReactI18next).init({
lng: "cimode",
resources: {},
});
this.global.i18nInstance = i18nInstance;
/* The important part end */
}
}
I add this environment in jest.config.ts:
// jest.config.ts
export default {
// ...
testEnvironment: "testing/env.ts",
};
Sample component:
// component.tsx
import { useTranslation } from "next-i18next";
export const Component = () => {
const { t } = useTranslation();
return <div>{t('foo')}</div>
}
And later on I use it in tests:
// component.test.tsx
import { setI18n } from "react-i18next";
import { create, act, ReactTestRenderer } from "react-test-renderer";
import { Component } from "./component";
it("renders Component", () => {
/* The important part start */
setI18n(global.i18nInstance);
/* The important part end */
let root: ReactTestRenderer;
act(() => {
root = create(<Component />);
});
expect(root.toJSON()).toMatchSnapshot();
});
I figured out how to make the tests work with an instance of i18next using the renderHook function and the useTranslation hook from react-i18next based on the previous answers and some research.
This is the Home component I wanted to test:
import { useTranslation } from 'next-i18next';
const Home = () => {
const { t } = useTranslation("");
return (
<main>
<div>
<h1> {t("welcome", {ns: 'home'})}</h1>
</div>
</main>
)
};
export default Home;
First, we need to create a setup file for jest so we can start an i18n instance and import the translations to the configuration. test/setup.ts
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import homeES from '#/public/locales/es/home.json';
import homeEN from '#/public/locales/en/home.json';
i18n.use(initReactI18next).init({
lng: "es",
resources: {
en: {
home: homeEN,
},
es: {
home: homeES,
}
},
fallbackLng: "es",
debug: false,
});
export default i18n;
Then we add the setup file to our jest.config.js:
setupFilesAfterEnv: ["<rootDir>/test/setup.ts"]
Now we can try our tests using the I18nextProvider and the useTranslation hook:
import '#testing-library/jest-dom/extend-expect';
import { cleanup, render, renderHook } from '#testing-library/react';
import { act } from 'react-dom/test-utils';
import { I18nextProvider, useTranslation } from 'react-i18next';
import Home from '.';
describe("Index page", (): void => {
afterEach(cleanup);
it("should render properly in Spanish", (): void => {
const t = renderHook(() => useTranslation());
const component = render(
<I18nextProvider i18n={t.result.current.i18n}>
<Home / >
</I18nextProvider>
);
expect(component.getByText("Bienvenido a Pocky")).toBeInTheDocument();
});
it("should render properly in English", (): void => {
const t = renderHook(() => useTranslation());
act(() => {
t.result.current.i18n.changeLanguage("en");
});
const component = render(
<I18nextProvider i18n={t.result.current.i18n}>
<Home/>
</I18nextProvider>
);
expect(component.getByText("Welcome to Pocky")).toBeInTheDocument();
});
});
Here we used the I18nextProvider and send the i18n instance using the useTranslation hook. after that the translations were loaded without problems in the Home component.
We can also change the selected language running the changeLanguage() function and test the other translations.

How to make Mapbox load on gatsby site? Build succeeds but map not displaying, despite working in dev mode

I'm trying to use a Mapbox map on a gatsby website. I used the code from their react Mapbox tutorial: https://docs.mapbox.com/help/tutorials/use-mapbox-gl-js-with-react/ and put it into a map component.
I call the map component on the footer and it works perfectly during development mode, but when I run gatsby build and then gatsby serve it refuses to show the map despite showing the container :
https://postimg.cc/mPcRfYhV
I've tried the other suggestion which is from the docs too, that is to use in gatsby-node.js
exports.onCreateWebpackConfig = ({ stage, loaders, actions }) => {
if (stage === "build-html") {
actions.setWebpackConfig({
module: {
rules: [
{
test: /#mapbox|mapbox-gl/,
use: loaders.null(),
},
],
},
})
}
}
for the test property I've tried using /mapbox|#mapbox|mapbox-gl|mapboxgl/ but nothing seems to make it work like it does in dev mode.
any ideas?
The component:
import React, { useEffect, useRef, useState } from "react";
import mapboxgl from 'mapbox-gl';
import "mapbox-gl/dist/mapbox-gl.css";
const styles = {
width: "220px",
height: '130px',
margin: '2em 0'
};
const Map = () => {
const [map, setMap] = useState(null);
const mapContainer = useRef(null);
useEffect(() => {
mapboxgl.accessToken = process.env.MY_KEY
const initializeMap = ({ setMap, mapContainer }) => {
const map = new mapboxgl.Map({
container: mapContainer.current,
style: "mapbox://styles/mapbox/streets-v11",
center: [-6.2603, 53.3498],
zoom: 9
});
map.on("load", () => {
setMap(map);
map.resize();
});
new mapboxgl.Marker({color:'#1E3873'}).setLngLat([-6.2603, 53.3498]).addTo(map)
};
if (!map) initializeMap({ setMap, mapContainer });
}, [map]);
return <div ref={el => (mapContainer.current = el)} style={styles} />;
};
export default Map;
The Map component is called on the footer component in a normal way by just importing the component and using <Map />.
Picture of it working fine when using gatsby develop
Picture of it not working when using gatsby build gatsby serve
SOLUTION EDIT: for some reason mapbox-gl 2.0 doesn't work, or I can't get it to work. Someone else had the same issue and suggested what worked for them which was to use mapbox-gl 1.13.0 . I tried it and it works.
npm uninstall mapbox-gl
npm i mapbox-gl#1.13.0
Leave the other configurations the same and it should work.
For some reason mapbox-gl 2.0 doesn't work properly. Someone else had the same issue and suggested what worked for them which was to use mapbox-gl 1.13.0 . I tried it and it works.
npm uninstall mapbox-gl
npm i mapbox-gl#1.13.0
Remember to use the following code in gatsby-node.js :
exports.onCreateWebpackConfig = ({ stage, loaders, actions }) => {
if (stage === "build-html") {
actions.setWebpackConfig({
module: {
rules: [
{
test: /#mapbox|mapbox-gl/,
use: loaders.null(),
},
],
},
})
}
}
For future readers, I was able to make it work w/ mapbox v2 in Gatsby 2 with import mapboxgl from "!mapbox-gl"; as described in mapboxgl docs https://docs.mapbox.com/mapbox-gl-js/api/#transpiling-v2
Among the configuration, you are missing some stuff, because in the guide, there is a class-based component and you are using a functional one, change it to:
import React, { useEffect, useRef, useState } from "react";
import mapboxgl from 'mapbox-gl';
import "mapbox-gl/dist/mapbox-gl.css";
const styles = {
width: "220px",
height: '130px',
margin: '2em 0'
};
const Map = () => {
const [map, setMap] = useState(null);
const mapContainer = useRef(null);
useEffect(() => {
mapboxgl.accessToken = process.env.MY_KEY
const initializeMap = ({ setMap, mapContainer }) => {
const map = new mapboxgl.Map({
container: mapContainer.current,
style: "mapbox://styles/mapbox/streets-v11",
center: [-6.2603, 53.3498],
zoom: 9
});
map.on("load", () => {
setMap(map);
map.resize();
});
new mapboxgl.Marker({color:'#1E3873'}).setLngLat([-6.2603, 53.3498]).addTo(map)
};
if (!map) initializeMap({ setMap, mapContainer });
return () => {
map.remove()
}
}, []);
return <div ref={mapContainer} style={styles} />;
};
export default Map;
Notice the ref and the map initializer. I think that it's not properly set when running a gatsby build and, since you have passing it as a reference in el => (mapContainer.current = el) it's not being rehydrated. React guides should differ a little bit when dealing with Gatsby due to their SSR (Server-Side Rendering).
Regarding the webpack issue.
Try:
exports.onCreateWebpackConfig = ({ stage, loaders, actions }) => {
if (stage === "build-html") {
actions.setWebpackConfig({
module: {
rules: [
{
test: /mapbox-gl/,
use: loaders.null(),
},
],
},
})
}
}
Or:
exports.onCreateWebpackConfig = ({ stage, loaders, actions }) => {
if (stage === "build-html") {
actions.setWebpackConfig({
module: {
rules: [
{
test: /#mapbox/,
use: loaders.null(),
},
],
},
})
}
}
Or ignoring both with:
exports.onCreateWebpackConfig = ({ stage, loaders, actions }) => {
if (stage === "build-html") {
actions.setWebpackConfig({
module: {
rules: [
{
test: /#mapbox/,
use: loaders.null(),
},
{
test: /mapbox-gl/,
use: loaders.null(),
},
],
},
})
}
}
Keep in mind that the testing rule is a regular expression (that's why is written between slashes) that should match a folder inside node_modules so it should be an exact path.
This is a needed workaround for every dependency that uses window, or other global objects as a dev-dependency for themselves. This is because, the interpreter when running gatsby develop is the browser (browser-side rendering), where there's a window available and while running gatsby build, the code is compiled in the server (Node), where there isn't a window obviously.
The snippet above adds a null loader during the webpack's transpilation, or in other words, forces webpack to ignore the module to avoid a code-breaking.

Mocking custom Hook with jest and enzyme results in 'xxx is not a function or its return value is not iterable' Error

I am very new to jest and enzyme. In my Project I will use a SPA React based Application. Containing a Context Provider for the data, also several hooks. I Using now Jest (with ts-jest and enzyme)
My jest.config looks like this
module.exports = {
"roots": [
"<rootDir>/src"
],
"transform": {
"^.+\\.tsx?$": "ts-jest"
},
"testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$",
"moduleFileExtensions": [
"ts",
"tsx",
"js",
"jsx",
"json",
"node"
],
"snapshotSerializers": ["enzyme-to-json/serializer"]
So my first stept so test UI components works.
Next step was to test componentes with mocked data. But there I got the error described at the bottom.
I have a functional component like this:
export default function CurrentWeather(props: ICurrentWeatherProps) {
const [data, Reload] = useCurrentWeather(props.locationID);
return (<div>......</div>)
}
You will notice the useCurrentWeather hook, here is the code for this:
import { useEffect, useState } from 'react';
import { useLocationState } from '../context/locationContext';
import { ILocationData } from './useLocations';
import _ from 'lodash';
...
export default function useCurrentWeater(locationId: number) {
const locationState = useLocationState();
const Reload = () => { GetData() }
const [Data, SetData] = useState<IWeatherDataInfo>({Id:0,ConditionIcon:'',Date:new Date(),MaxTemp:0, MinTemp:0});
async function GetData() { .... }
useEffect(Reload, [locationState.data, locationId]);
return [Data, Reload] as const;
}
Now I wand to mock these Hook. I tried following
import React from 'react';
import { configure, shallow, mount } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import CurrentWeather from '../components/Weather/CurrentWeather';
import { IWeatherDataInfo } from '../Hooks/useWeaters';
configure({ adapter: new Adapter() });
const mockWeatherReload = jest.fn();
const mockdata: IWeatherDataInfo = { Date: new Date(), ConditionIcon: "", MinTemp: 0, MaxTemp: 10 };
jest.mock('../Hooks/useCurrentWeather', () => ({
useCurrentWeather: jest.fn(()=>{ [mockdata, mockWeatherReload]})
}));
describe("WeatherWidget", () => {
it("RenderOnlyAddButton", () => {
const container = shallow(<CurrentWeather locationID={1} hidden={false} />);
});
});
Now, when I execute this test, I will get this error result:
src/tests/WeatherWidget.test.tsx
● WeatherWidget › RenderOnlyAddButton
TypeError: (0 , _useCurrentWeather.default) is not a function or its return value is not iterable
9 |
10 | export default function CurrentWeather(props: ICurrentWeatherProps) {
> 11 | const [data, Reload] = useCurrentWeather(props.locationID);
| ^
12 | return (
What I'm doing wrong here? Is there what I'm missing?
Try like this:(below should be your functional component's test file)
const mockUseCurrentWeather = jest.fn();
jest.mock("put here the absolute path", () => ({
__esModule: true,
useCurrentWeather: (...args: any) => mockUseCurrentWeather(...args),
}));
describe("WeatherWidget", () => {
beforeEach(() => {
mockUseCurrentWeather.mockClear();
mockUseCurrentWeather.mockReturnValue([undefined, undefined]);
});
it("RenderOnlyAddButton", () => {
mockUseCurrentWeather.mockClear();
mockUseCurrentWeather.mockReturnValue([undefined, undefined]);
const container = shallow(<CurrentWeather locationID={1} hidden={false} />);
});
});

How can I ignore "-!svg-react-loader!./path/to/my.svg" when testing with Jest without bundling everything with webpack

We're using svg-react-loader for some of the SVG files in our application. We're trying to setup jest to run with a babel-jest and the following .babelrc:
{
"presets": [
"es2015",
"react"
],
"plugins": [
"transform-decorators-legacy",
"transform-class-properties",
"transform-object-rest-spread"
]
}
The following test fails:
/* global it, document */
import React from 'react'
import ReactDOM from 'react-dom'
import Comp from './Icon'
it('renders without crashing', () => {
const div = document.createElement('div')
ReactDOM.render(<Comp><div /></Comp>, div)
})
With error:
Cannot find module '-!svg-react-loader!../../assets/grid.svg' from 'Icon.js'
How could I ignore imports that start with like import grid from '-!svg-react-loader!../../assets/grid.svg' in jest?
The way I solved this was by adding a jest mock for any import that contains -!svg-react-loader! at the beginning of the module.
"moduleNameMapper": {
"^-!svg-react-loader.*$": "<rootDir>/config/jest/svgImportMock.js"
}
Where svgImportMock.js is:
'use strict';
module.exports = 'div';
It's not ideal, because the file could simple not exists, but the assumption is that we see the missing module when bundling with webpack.
I resolved this by installing jest-svg-transformer, then adding this config:
{
"jest": {
"transform": {
"^.+\\.svg$": "jest-svg-transformer"
}
}
}
I was able to solve this by correctly handling static assets in Jest (https://jestjs.io/docs/en/webpack#handling-static-assets):
// package.json
{
"jest": {
"moduleNameMapper": {
"\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/__mocks__/fileMock.js",
"\\.(css|less)$": "<rootDir>/__mocks__/styleMock.js"
}
}
}
// __mocks__/styleMock.js
module.exports = {};
// __mocks__/fileMock.js
module.exports = 'test-file-stub';

Resources