The idea
Wouldn't it be cool to write a more descriptive tests? Tests that talk about what is going on? Tests that include more context, more explanation, more images? What if the tests were written in Markdown and just embedded the code blocks to be executed?
A good example is bahmutov/cypress-examples where the same Markdown files have the tests and also become the static HTML pages that you can see at https://glebbahmutov.com/cypress-examples/. Here is the start of the Location.md file with a Cypress test
Using VuePress we can convert the Markdown file into a static page, shown deployed here
📚 Another use for Markdown files is to generate application demos, read the Cypress Book blog post.
Markdown preprocessor
If we plan to convert a Markdown file into a bundled JavaScript the browser can execute, we need a Markdown file preprocessor. The Cypress preprocessor is literally just a bundler - it receives the source filename and responds with a filename of the JavaScript bundle. The Test Runner then loads the bundle and runs the tests.
Tip: by default Cypress only expects .js
and .ts
spec files. If we want to bundle Markdown files, in the cypress.json
set the "testFiles": "*.md"
property.
Let's write one.
🧭 You can find the Markdown preprocessor in bahmutov/cypress-markdown-preprocessor repo.
The Cypress plugin file will register the preprocessor
1 | const mdPreprocessor = require('cypress-markdown-preprocessor') |
Great, it is a start. Now let's fill the placeholder code - it will simply print how Cypress calls the preprocessor.
1 | const fs = require('fs') |
When we run Cypress in the interactive mode the shouldWatch
is set to true, but for now let's solve the simple problem of bundling a file once for the cypress run
mode.
If we call our project now, it shows the filenames of the two bundled files: the support file and the spec file.
1 | $ npx cypress run |
Because our preprocessor simply has written an empty output string, Cypress runs zero tests. We need to generate actual JavaScript tests from the Markdown text. We can use some special syntax to mark the test blocks. For example, I use fiddle
and fiddle-end
comments with html
and js
code blocks. Each block becomes an "app" and the test by using cypress-fiddle module.
Bundle JS file
What happens if we wanted to bundle a JavaScript file? We would use one of the Cypress' preprocessors, like @cypress/browserify-preprocessor. Our file could check the input file extension and direct the JS file bundling to that preprocessor like this:
1 | const cyBrowserify = require('@cypress/browserify-preprocessor')() |
If we extract the test blocks from Markdown text, we can call the cyBrowserify
too:
1 | const bundleMdFile = (filePath, outputPath) => { |
The above function bundleMdFile
is our workhorse - we can call it anytime we need to bundle a Markdown file. The function cyBrowserify
returns a promise, which will resolve when the full bundle is written to the outputPath
file.
Promises
Speaking of promises - what happens if multiple spec files are bundled? Well, every spec file is bundled, but also the support file needs to be bundled first. For every spec file. Of course we do not want to repeat this work - we want to bundle every spec once. Thus a good pattern is to save the bundling promises in an object as a local cache.
1 | const bundleMdFile = (filePath, outputPath) => { ... } |
The first time we need to bundle the support file, we get a promise, which eventually resolves. The Test Runner grabs the output file and serves it. Whenever the support file needs to be bundled again (which happens when the second spec is running), the Test Runner is asking for the same file name, and immediately gets the promise from the bundled[filePath]
value.
shouldWatch
During the interactive test execution with cypress open
, the Test Runner should watch the source files. If the user edits and saves the spec, the tests should re-run, which means the preprocessor bundles the spec file again. This is how it can be done for a Markdown file:
1 | const chokidar = require('chokidar') |
In the interactive mode we create a Chokidar file watcher. Every time the file changes, we rebundle it and emit the Cypress "rerun" event. The Cypress file
object we receive is an event emitter. The same object also emits the close
event when the Test Runner closes the window and stops running a particular spec. In that case we close the file watcher.
The result
Our Markdown preprocessor is shown in action - as I edit the source file the Test Runner re-runs the updated test.
Now you can use what this blog post has shown to write your own file preprocessor.
PS: I have also written @bahmutov/cy-rollup which is a Cypress Rollup file preprocessor.