Creating a custom transform in Jest - javascript

I'm trying to create a custom transform for Jest, but running into a documentation roadblock which has me asking myself if I'm even on the right track.
Problem
I have a Rails project which is serving a Vue JS app. I want to write Jest tests for the JS app. In order to pass config variables from Rails to the app, I'm using ERB to template a small number of .js files. For example:
// in server-routes.js.erb
export default {
reports: '<%= Rails.application.config.relative_url_root %><%= Rails.application.routes.url_helpers.reports_path %>',
...
In my Webpack build for the Vue app, I use rails-erb-loader to preprocess the *.erb files before they get passed to the rest of the build process.
However, when I run my JS tests, Jest doesn't know anything about ERB loaders (reasonably enough). So my goal is to add a custom transform for Jest to convert the ERB files when running npm test.
Approach
I thought I might be able to use rails-erb-loader as a Jest transform:
// package.json
"jest": {
"moduleFileExtensions": [
"js",
"json",
"vue"
],
"moduleDirectories": [
"<rootDir>/node_modules"
],
"transform": {
".*\\.(vue)$": "vue-jest",
"^.+\\.js$": "babel-jest",
"^.+\\.js\\.erb$": "rails-erb-loader"
},
This doesn't work, however, because Jest transforms and Webpack loaders seemingly have different signatures. In particular, Jest expects a process function:
$ npm test
FAIL app/javascript/components/__tests__/dummy.test.js
● Test suite failed to run
TypeError: Jest: a transform must export a `process` function.
> 101 | import ServerRoutes from '../server-routes.js.erb';
| ^
at ScriptTransformer._getTransformer (node_modules/#jest/transform/build/ScriptTransformer.js:291:15)
at ScriptTransformer.transformSource (node_modules/#jest/transform/build/ScriptTransformer.js:353:28)
at ScriptTransformer._transformAndBuildScript (node_modules/#jest/transform/build/ScriptTransformer.js:457:40)
at ScriptTransformer.transform (node_modules/#jest/transform/build/ScriptTransformer.js:513:25)
at app/javascript/components/related-media.vue:101:1
at Object.<anonymous> (app/javascript/components/related-media.vue:232:3)
And this is where I get stuck, because I can't see where it's documented what the API and behaviour of a process function should be. In the documentation for the transform config option there's a single not very helpful example, and that's it as far as docs go, unless I've missed something.
I also note that babel-jest has a createTransformer function which sounds like it might be helpful, or at least illuminating, but again I can't find any docs on what it does.
If anyone has pointers on the details of creating custom Jest transforms, or at least some better docs, that would be great! Or, if I'm going about this the wrong way, what should I be doing?

You could look at ts-jest. https://github.com/kulshekhar/ts-jest/blob/master/src/ts-jest-transformer.ts. It is in typescript so is typed
Alternatively find the jest code that initiates the transform process. I don't think it is that difficult to find.
I think the transformer is either created with class constructor or via the factory function createTransformer.

From my understanding for the ts-jest-transformer and jest-erb-transformer it seems you need to export an object with public process or to export createTransformer methods which create a transformer object that have a process method.
a simple code example that runs before ts-jest
transform-example.js
const tsJest = require('ts-jest');
const t = tsJest.createTransformer();
module.exports = {
process(fileContent, filePath, jestConfig) {
const res = t.process(fileContent, filePath, jestConfig)
console.log(filePath);
return res;
}
}
jest.config.js
module.exports = {
transform: {
'^.+\\.tsx?$': ['<rootDir>/transform-example']
}
}
running this would run typescript tests (just like ts-jest) and log all the file paths transformed in the test.

Related

Change .page URL based on environment I need to run the suite in

We have been building our automation suite using our staging environment, but are going live soon and want to be ready to tell the project where to run (staging, production).
The only difference between the sites in the environments is the URL. My question is, from start to finish, how can I set the .page URL via a CLI option?
Right now, I have created an environment config file that holds our staging and production URLS and then I call the data into my test files. This is fine for now, but I will need to create a script with an option to set the environment at runtime without having to do a manual find and replace before kicking it off.
I've looked around online and find, what I believe, to be code snippets and general instructions, but I'm not a dev at heart and go crossed eyed. If I could get an ELI5 for this, that would be awesome.
Example of what I'm doing now:
const env = require('../environment_variables.json')
fixture `blog`
.page `${env.production}`
And then I change production to staging or vice versa manually before kicking off the suite.
Since the project will run from CICD, I would like to be able to do something like this in my CLI and script:
testcafe env=production
The env value will then be set where the .page call is for every test file.
Thanks!
There are different ways of doing this. I've used environment variables successfully in this situation, so I'll share this solution since it will solve your problem.
I create config.json in the root of the project:
{
"baseUrl": {
"dev": "https://dev.com/",
"staging": "https://staging.com/",
"prod": "https://prod.com/"
}
}
Then I create two helper functions somewhere like Helpers/env.js:
import config from '../config';
function getEnv () {
return process.env.TESTCAFE_ENV;
}
function getBaseUrl () {
return config.baseUrl[getEnv()];
}
export { getEnv, getBaseUrl };
Then in my test files in Tests/:
import { getBaseUrl } from '../Helpers/env';
const baseUrl = getBaseUrl();
fixture `Test Suite`
.page(baseUrl);
And that's it. Then when I need to run tests on the dev, I execute:
$ TESTCAFE_ENV=dev testcafe
for staging:
$ TESTCAFE_ENV=staging testcafe
and for production:
$ TESTCAFE_ENV=prod testcafe
In v1.20.0 and later, TestCafe offers a way to specify the baseUrl in the test run configuration. You can use this approach along with environment variables, see the following example:
.testcaferc.js
const BASE_URL_MAP = {
dev: 'https://dev.com/',
staging: 'https://staging.com/',
prod: 'https://prod.com/'
};
module.exports = {
baseUrl: BASE_URL_MAP[process.env.TESTCAFE_ENV]
};
Alternatively, you can use different configuration files for each of the required setups using the --config-file option.

How to load a variable form my app into package.json?

I know this sounds as easy as using globals, but I'm not so sure.
Here's my case:
I have multiple files within /src directory of my React app, let's call them src/a.js, src/b.js,
every single of these files exports one object which I then use within my app:
./src/a.js:
export default {
filename: 'a',
foo: 'bar',
};
./src/b.js:
export default {
filename: 'b',
foo: 'bar',
blah: 'hah',
};
Now I have a command to check whether or not structure of objects within these files match (they are being changed by many developers multiple times a day), so when I do npm check in terminal it will return false for above input, because blah does not exist within two files.
My package.json looks like this:
"scripts": {
"check": "node check.js runCheck",
/.../
}
My question is: how the heck do I load these variables to compare them in package.json?
I have a file called:
./check.js:
function check(files) {
// checking files there
};
module.exports.check = check;
Approach #1 - imports
This is a build file, not part of the application itself, so when I try to do:
./check.js:
import a from './src/a';
import b from './src/b';
I'm getting:
SyntaxError: Cannot use import statement outside a module.
Approach #2 - require
This is going to cause trouble, because I'm using imports, not modules within my app, therefore doing:
./check.js:
const a = require('./src/a');
const b = require('./src/b');
Returns:
Error: Cannot find module './src/a'.
Of course I can't do module.exports within the a.js/b.js files, because they're part of my app and they should use exports, I've tried using both export and module.exports, but it does not work as well and looks shitty.
How do I tackle this? Should I load the files using some file loader, parse it as JSON an then compare? Or maybe there's an easier way?
You'll need to use something like esm (https://github.com/standard-things/esm) to run node with module support.
It should be as simple as:
npm install esm
Then update your package script to be:
"check": "node -r esm check.js runCheck",
Edit Btw, a very clear and well structured question.

react-testing-library why is toBeInTheDocument() not a function

Here is my code for a tooltip that toggles the CSS property display: block on MouseOver and on Mouse Out display: none.
it('should show and hide the message using onMouseOver and onMouseOut events respectively', () => {
const { queryByTestId, queryByText } = render(
<Tooltip id="test" message="test" />,
)
fireEvent.mouseOver(queryByTestId('tooltip'))
expect(queryByText('test')).toBeInTheDocument()
fireEvent.mouseOut(queryByTestId('tooltip'))
expect(queryByText('test')).not.toBeInTheDocument()
cleanup()
})
I keep getting the error TypeError: expect(...).toBeInTheDocument is not a function
Has anyone got any ideas why this is happening? My other tests to render and snapshot the component all work as expected. As do the queryByText and queryByTestId.
toBeInTheDocument is not part of RTL. You need to install jest-dom to enable it.
And then import it in your test files by:
import '#testing-library/jest-dom'
As mentioned by Giorgio, you need to install jest-dom. Here is what worked for me:
(I was using typescript)
npm i --save-dev #testing-library/jest-dom
Then add an import to your setupTests.ts
import '#testing-library/jest-dom/extend-expect';
Then in your jest.config.js you can load it via:
"setupFilesAfterEnv": [
"<rootDir>/src/setupTests.ts"
]
When you do npm i #testing-library/react make sure there is a setupTests.js file with the following statement in it
import '#testing-library/jest-dom/extend-expect';
Having tried all of the advice in this post and it still not working for me, I'd like to offer an alternative solution:
Install jest-dom:
npm i --save-dev #testing-library/jest-dom
Then create a setupTests.js file in the src directory (this bit is important! I had it in the root dir and this did not work...). In here, put:
import '#testing-library/jest-dom'
(or require(...) if that's your preference).
This worked for me :)
Some of the accepted answers were basically right but some may be slightly outdated:
Some references that are good for now:
https://github.com/testing-library/jest-dom
https://jestjs.io/docs/configuration
Here are the full things you need:
in the project's <rootDir> (aka where package.json and jest.config.js are), make sure you have a file called jest.config.js so that Jest can automatically pick it up for configuration. The file is in JS but is structured similarly to a package.json.
Make sure you input the following:
module.exports = {
testPathIgnorePatterns: ['<rootDir>/node_modules', '<rootDir>/dist'], // might want?
moduleNameMapper: {
'#components(.*)': '<rootDir>/src/components$1' // might want?
},
moduleDirectories: ['<rootDir>/node_modules', '<rootDir>/src'],
setupFilesAfterEnv: ['<rootDir>/src/jest-setup.ts'] // this is the KEY
// note it should be in the top level of the exported object.
};
Also, note that if you're using typescript you will need to make sure your jest-setup.ts file is compiled (so add it to src or to the list of items to compile in your tsconfig.json.
At the top of jest-setup.ts/js (or whatever you want to name this entrypoint) file: add import '#testing-library/jest-dom';.
You may also want to make sure it actually runs so put a console.log('hello, world!');. You also have the opportunity to add any global functions you'd like to have available in jest such as (global.fetch = jest.fn()).
Now you actually have to install #testing-library/jest-dom: npm i -D #testing-library/jest-dom in the console.
With those steps you should be ready to use jest-dom:
Without TS: you still need:
npm i -D #testing-library/jest-dom
Creating a jest.config.js and adding to it a minimum of: module.exports = { setupFilesAfterEnv: ['<rootDir>/[path-to-file]/jest-setup.js'] }.
Creating a [path-to-file]/jest-setup.js and adding to it: import '#testing-library/jest-dom';.
The jest-setup file is also a great place to configure tests like creating a special renderWithProvider( function or setting up global window functions.
None of the answers worked for me because I made the silly mistake of typing toBeInDocument() instead of toBeInTheDocument(). Maybe someone else did the same mistake :)
I had a hard time solving that problem so I believe it's important to note the followings if you're using CREATE REACT APP for your project:
You DO NOT need a jest.config.js file to solve this, so if you have that you can delete it.
You DO NOT need to change anything in package.json.
You HAVE TO name your jest setup file setupTests.js and have it under the src folder. It WILL NOT work if your setup file is called jest.setup.js or jest-setup.js.
install required packages
npm install --save-dev #testing-library/jest-dom eslint-plugin-jest-dom
create jest-setup.js in the root folder of your project and add
import '#testing-library/jest-dom'
in jest.config.js
setupFilesAfterEnv: ['<rootDir>/jest-setup.js']
TypeScript only, add the following to the tsconfig.json file. Also, change .js extension to .ts.
"include": ["./jest-setup.ts"]
toBeInTheDocument() and many similar functions are not part of the React-testing-library. It requires installing an additional package.
For anyone out there that like is trying to run tests in Typescript with jest and is still getting the same error even after installing #testing-library/jest-dom and following all the other answers: you probably need to install the type definitions for jest-dom (here) with:
npm i #types/testing-library__jest-dom
or
yarn add #types/testing-library__jest-dom
You need to install them as real dependencies and not as devDependency.
I was having this issue but for #testing-library/jasmine-dom rather than #testing-library/jest-dom.
The process of setup is just a tiny bit different with jasmine. You need to set up the environment in a before function in order for the matchers to be added. I think jest-dom will go ahead and add the matchers when you first import but Jasmine does not.
import { render, screen } from '#testing-library/react';
import MyComponent from './myComponent';
import JasmineDOM from '#testing-library/jasmine-dom';
describe("My Suite", function () {
beforeAll(() => {
jasmine.getEnv().addMatchers(JasmineDOM);
})
it('render my stuff', () => {
const { getByText } = render(<MyComponent />);
const ele = screen.getByText(/something/i);
expect(ele).toBeInTheDocument();
});
});
If you are using react-script then follow the below steps
Install #testing-library/jest-dom library if not done already using
npm i #testing-library/jest-dom.
Put import "#testing-library/jest-dom/extend-expect" in setUpTest.js
If you are using jest then import the library in jest.setup.js file.
the problem already was solved, but i will comment a little tip here, you don't need to create a single file called setup just for this, you just need to specify the module of the jest-dom on the setupFilesAfterEnv option in your jest configuration file.
Like this:
setupFilesAfterEnv: ['#testing-library/jest-dom/extend-expect'],
If you're using TS
You could also add a test.d.ts file to your test directory and use a triple slash directive:
///<reference types='#testing-library/jest-dom'>
Instead of doing:
expect(queryByText('test')).toBeInTheDocument()
you can find and test that it is in the document with just one line by using
let element = getByText('test');
The test will fail if the element isn't found with the getBy call.

Testing React API - Unexpected Token error

I have a basic React app created using create-react-app. I am trying to get started with Pact to do contract testing on my API using the Javascript implementation guide.
I have followed the steps in the above link exactly and have created a basic test which essentially does nothing just so I can get the test to run:
import { Pact } from '#pact-foundation/pact';
it('works', () => {
expect(1).toEqual(1);
});
When running npm run pactTest I get the following error:
Jest encountered an unexpected token
This usually means that you are trying to import a file which Jest cannot parse, e.g. it's not plain JavaScript.
By default, if Jest sees a Babel config, it will use that to transform your files, ignoring "node_modules".
Here's what you can do:
• To have some of your "node_modules" files transformed, you can specify a custom "transformIgnorePatterns" in your config.
• If you need a custom transformation specify a "transform" option in your config.
• If you simply want to mock your non-JS modules (e.g. binary assets) you can stub them out with the "moduleNameMapper" config option.
You'll find more details and examples of these config options in the docs:
https://jestjs.io/docs/en/configuration.html
Details:
/path/to/file.test.pact.js:1
({"Object.<anonymous>":function(module,exports,require,__dirname,__filename,global,jest){import { Pact } from '#pact-foundation/pact';
^
SyntaxError: Unexpected token {
at ScriptTransformer._transformAndBuildScript (node_modules/jest-runtime/build/script_transformer.js:403:17)
If I change the import line to: const Pact = require('#pact-foundation/pact'); then it works.
The problem is that I can't use require instead of import for anything more than this dummy example, because I use import all over the React project.
There must be something else that im missing, since the Javascript implementation guide uses import { Pact } from '#pact-foundation/pact';
Add below config right after transform object in jest.config.js
'^.+\\.js$': 'babel-jest
Below is the example for your ref
module.exports = {
moduleFileExtensions: ['js', 'jsx', 'json', 'vue'],
transform: {
'.+\\.(css|styl|less|sass|scss|png|jpg|ttf|woff|woff2)$':
'jest-transform-stub',
'^.+\\.(js|jsx)?$': 'babel-jest'
},
moduleNameMapper: {
'^#/(.*)$': '<rootDir>/src/$1'
},
snapshotSerializers: ['jest-serializer-vue'],
testMatch: [
'<rootDir>/(tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx))'
],
transformIgnorePatterns: ['<rootDir>/node_modules/']
};

Exporting a class with ES6 (Babel)

I'm writing some frontend code with ECMAScript 6 (transpiled with BabelJS, and then browserified with Browserify) so that I can have a class in one file, export it and import it in another file.
The way I'm doing this is:
export class Game {
constructor(settings) {
...
}
}
And then on the file that imports the class I do:
import {Game} from "../../lib/pentagine_browserified.js";
var myGame = new Game(settings);
I then compile it with grunt, this is my Gruntfile:
module.exports = function(grunt) {
"use strict";
grunt.loadNpmTasks('grunt-babel');
grunt.loadNpmTasks('grunt-browserify');
grunt.initConfig({
"babel": {
options: {
sourceMap: false
},
dist: {
files: {
"lib/pentagine_babel.js": "lib/pentagine.js",
"demos/helicopter_game/PlayState_babel.js": "demos/helicopter_game/PlayState.js"
}
}
},
"browserify": {
dist: {
files: {
"lib/pentagine_browserified.js": "lib/pentagine_babel.js",
"demos/helicopter_game/PlayState_browserified.js": "demos/helicopter_game/PlayState_babel.js"
}
}
}
});
grunt.registerTask("default", ["babel", "browserify"]);
};
However, on the new Game( call, I get the following error:
Uncaught TypeError: undefined is not a function
As so, what I did was analyse the generated code by Babel and Browserify and I found this line on PlayState_browserified.js:
var Game = require("../../lib/pentagine_browserified.js").Game;
I decided to print the require output:
console.log(require("../../lib/pentagine_browserified.js"));
And it is nothing but an empty object. I decided to check out the pentagine_browserified.js file:
var Game = exports.Game = (function () {
It seems like it is correctly exporting the class, but for some other reason it is not being required on the other file.
Also, I'm sure the file is being required properly because changing the string "../../lib/pentagine_browserified.js" spits out a Not Found error, so it is going for the right file, that I'm sure about.
Browserify is meant to be fed a single "entry point" file, through which it recursively traverses all of your require statements, importing the code from other modules. So you should be require'ing the _babel.js versions of modules, not _browserified.js ones.
From the looks of it, you intend for your app's "entry point" to be demos/helicopter_game/PlayState_browserified.js, yeah? If that's the case:
In PlayState.js, change it to import {Game} from "../../lib/pentagine_babel.js";.
In Gruntfile.js, remove "lib/pentagine_browserified.js": "lib/pentagine_babel.js".
Works for me. Let me know if that suffices or I am misunderstanding your requirements here.
P.S. You can use babelify to avoid having separate Grunt tasks for Babel and Browserify. See my answer here for an example.
I had a slightly different file configuration, that gave me some difficulty to get the "require" syntax to work in Node, but this post gave me the hint on how to used the babel-ified version of the file name.
I am using WebStorm with the FileWatcher option set to Babel, and I have the FileWatcher configured to watch all files with suffix .jsx, and rename the compiled output file from {my_file}.jsx to {my_file}-compiled.js.
So in my test case, I have 2 files:
Person.jsx:
class Person { ... }
export { Person as default}
and another file that wants to import it:
Test.jsx:
var Person = require('./Person-compiled.js');
I couldn't get the "require" statement to find the module until I started the file path with './' and also add '-compiled.js' to properly specify the file name so that Node es5 could find the module.
I was also able to use the "import" syntax:
import Person from './Person-compiled.js';
Since I have set up my WebStorm project as a Node ES5 project, I have to run 'Test-compiled.js' (not 'Test.jsx').

Categories