home

Testing the p5.cljs editor

2023-05-25

There was never a time where I the JavaScript ecosystem more than when I tried to write tests for the p5.cljs web editor. Things broken left and right, missing plugins, babel, transpiling, modules, npm. Disclaimer: I don’t hate the JS ecosystem in earnest. Just … sometimes.

This article is about why I chose Jest as the test runner for the p5.cljs editor and the steps necessary to get it working with third-party components I was using.

Why not Vitest?

Because of this issue. I love Vite. With a previous project that used Vite’s Env Variables (import.meta.env.VARIABLE), the only way to set up tests without a headache was to use Vitest. Needless to say, if you can use Vitest to test your Vite-React project, you should. It was made for that.

But now and then you hit a deadend, like the issue mentioned above with CodeMirror. From what I understand, the issue is due to how Vitest bundles dependencies. I’m not sure how, but if you have a clue, do reach out!

And this is why I switched to Jest.

Setting up Jest

My first distaste with Jest started with just how many dependencies you needed to install for Jest to work.

yarn add --dev jest babel-jest @babel/preset-env @babel/preset-react react-test-renderer

I also needed @testing-library/react and jest-environment-jsdom.

With the installation out of the way, I got started writing my first test. The first test rendered the base app, just to check if I got everything setup correctly.

The First Trial - ES Modules

Let’s go yarn test and …

Jest encountered an unexpected token

Jest failed to parse a file. This happens e.g. when your code or its dependencies use non-standard JavaScript syntax, or when Jest is not configured to support such syntax.

Some more details:

Details:

/re/dac/ted/p5-cljs-web-editor/node_modules/react-markdown/index.js:6
export {uriTransformer} from './lib/uri-transformer.js'
^^^^^^

SyntaxError: Unexpected token 'export'

> 1 | import ReactMarkdown from 'react-markdown'
    | ^
  2 | import hljs from "highlight.js"
  3 | import { useEffect } from 'react'

After much researching, I learned that Jest has limited support for ES Modules. Many encountered this issue and the solution was to prevent Jest from transforming the react-markdown module. I went a head and created a jest.config.cjs which contained this configuration:

/** @type {import('jest').Config} */
const config = {
	"transform": {},
	"transformIgnorePatterns": [],
};

module.exports = config;

The Second Trial - JSX

Let’s go yarn test round 2 and ..

Details:

/re/dac/ted/p5-cljs-web-editor/src/App.integration.test.jsx:5
import { render, cleanup, screen } from '@testing-library/react'
^^^^^^

SyntaxError: Cannot use import statement outside a module

The previous error was now gone and I was treated to another error to solve. After some researching (again), the solution was to transform .jsx files so that Jest could consume it. My jest.config.cjs now looks like this:

/** @type {import('jest').Config} */
const config = {
	"transform": {
		"^.+\\.jsx?$": "babel-jest",
	},
	"transformIgnorePatterns": [],
};

module.exports = config;

Admittedly, this is a rookie mistake. Because if you knew how Jest worked (in Node), you would know that .jsx files needed some special treatment. But hey, we are all learning.

The Third Trial - Markdown

Let’s go yarn test round 3 and …

Details:

/re/dac/ted/p5-cljs-web-editor/src/pages/about.md:1
({"Object.<anonymous>":function(module,exports,require,__dirname,__filename,jest){# About

Another unexpected token error. This time for .md files. Learning from my previous mistake, I now know that you need to also transform .md files for Jest. Jest must be some special kid, huh? Just kidding. You generally need some configuration to get markdown loading properly in a JS project.

A quick search allowed the jest-transformer-mdx to reveal itself to me. Last commit: May 26, 2021. Yikes. Two years ago. Jest went through 2 major versions since then. Jest 27.0.0 was released May 25, 2021. I’m on Jest 29.5.0.

I went and yarn add --dev jest-transformer-mdx and updated my jest.config.cjs.

/** @type {import('jest').Config} */
const config = {
	"transform": {
		"^.+\\.jsx?$": "babel-jest",
		"^.+\\.(md|mdx)$": "jest-transformer-mdx",
	},
	"transformIgnorePatterns": [],
};

module.exports = config; 

The Fourth Trial - Transformation

Let’s go yarn test round 4 and …

Error: Invalid synchronous transformer module:
  "/re/dac/ted/p5-cljs-web-editor/node_modules/jest-transformer-mdx/index.js" specified in the "transform" object of Jest configuration
  must export a `process` function.
  Code Transformation Documentation:
  https://jestjs.io/docs/code-transformation

Why did I expect this to work on the first go? Okay, it’s not so bad though. Jest has made it really explicit here that it’s a problem with the transformer. I was not ready to, after spending a few hours on setting Jest up, to go down the rabbit hole of figuring out how transforming files for Jest. Not yet. This is a known issue with the transformer.

The author of the transformer suggested rolling back to @3.0.0-beta.0. I rolled back and ran the test again.

Error: Invalid return value:
  `process()` or/and `processAsync()` method of code transformer found at
  "/re/dac/ted/p5-cljs-web-editor/node_modules/jest-transformer-mdx/index.js"
  should return an object or a Promise resolving to an object. The object
  must have `code` property with a string of processed code.
  This error may be caused by a breaking change in Jest 28:
  https://jestjs.io/docs/28.x/upgrading-to-jest28#transformer
  Code Transformation Documentation:
  https://jestjs.io/docs/code-transformation

A new error, not even more concrete. I went into jest-transformer-mdx/index.js and found this function:

function createTransformer(src, filename, config) {
	const withFrontMatter = parseFrontMatter(src, config.transformerConfig)
	const jsx = mdx.sync(withFrontMatter)
	const toTransform = `import {mdx} from '@mdx-js/react';${jsx}`

	return process(toTransform, filename, config).code
}

It was not returning and object with a code property. I changed the return value to:

return process(toTransform, filename, config)

Then I ran yarn test again.

ModuleNotFoundError: Cannot find module '@mdx-js/react' from 'src/pages/about.md'

Require stack:
  src/pages/about.md
  src/App.jsx
  src/App.integration.test.jsx

    at Resolver._throwModNotFoundError (/re/dac/ted/p5-cljs-web-editor/node_modules/jest-resolve/build/resolver.js:427:11)
    at Resolver.resolveModule (/re/dac/ted/p5-cljs-web-editor/node_modules/jest-resolve/build/resolver.js:358:10)
    at Resolver._getVirtualMockPath (/re/dac/ted/p5-cljs-web-editor/node_modules/jest-resolve/build/resolver.js:619:14)
    at Resolver._getAbsolutePath (/re/dac/ted/p5-cljs-web-editor/node_modules/jest-resolve/build/resolver.js:587:14)
    at Resolver.getModuleID (/re/dac/ted/p5-cljs-web-editor/node_modules/jest-resolve/build/resolver.js:530:31)
    at Runtime._shouldMockCjs (/re/dac/ted/p5-cljs-web-editor/node_modules/jest-runtime/build/index.js:1699:37)
    at Runtime.requireModuleOrMock (/re/dac/ted/p5-cljs-web-editor/node_modules/jest-runtime/build/index.js:1036:16)
    at Object.<anonymous> (/re/dac/ted/p5-cljs-web-editor/src/pages/about.md:8:14)
    at Runtime._execModule (/re/dac/ted/p5-cljs-web-editor/node_modules/jest-runtime/build/index.js:1429:24)
    at Runtime._loadModule (/re/dac/ted/p5-cljs-web-editor/node_modules/jest-runtime/build/index.js:1013:12)
    at Runtime.requireModule (/re/dac/ted/p5-cljs-web-editor/node_modules/jest-runtime/build/index.js:873:12)
    at Runtime.requireModuleOrMock (/re/dac/ted/p5-cljs-web-editor/node_modules/jest-runtime/build/index.js:1039:21)
    at require (/re/dac/ted/p5-cljs-web-editor/src/App.jsx:12:2) {
  code: 'MODULE_NOT_FOUND',
  hint: '',
  requireStack: [
    '/re/dac/ted/p5-cljs-web-editor/src/pages/about.md',
    '/re/dac/ted/p5-cljs-web-editor/src/App.jsx',
    '/re/dac/ted/p5-cljs-web-editor/src/App.integration.test.jsx'
  ],
  siblingWithSimilarExtensionFound: false,
  moduleName: '@mdx-js/react',
  _originalMessage: "Cannot find module '@mdx-js/react' from 'src/pages/about.md'"
}

A big stack for a small error. @mdx-js/react, which jest-transformer-mdx depends on, was not found. I added it as a dev dependency and ran yarn test once more.

Success

PASS  src/App.integration.test.jsx
      renders the app (669 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        3.249 s
Ran all test suites.
Done in 4.24s.

Finally! Now I can get down to business and write some real tests. Among which is to

Conclusion

Testing is a destructive act. Well, according to the book The Art of Software Testing anyways. Through the bare act of trying to setup testing, I already found issues with my application that needed my attention.

I’m not a pro at testing. Not yet. But I encourage all, especially frontend developers, to try testing their applications if they haven’t already. If you’re like me, coming from a background of creative coding, you might not have found the need to test your programs. If it looks good, it must be working. Frontend developers might suffer this too. The act of trying to figure out what does it mean to have a working frontend or even to actively try to break the frontend will make you a more robust developer.