How to load and test WASM code using Webpack and test it using Cypress
Imagine you are writing code in some language, like Rust and want to run it in the browser. You can compile it into WASM module, but how do you test this module in an actual browser? In this blog post I will show you how.
#[wasm_bindgen] pubfnsum(a: f64, b: f64) ->f64 { a + b }
We can use command wasm-pack build --out-dir internal via wasm-pack which gives us several files in the internal folder
1 2 3 4 5 6 7 8 9 10 11 12
$ ls -la internal total 64 drwxr-xr-x 10 gleb staff 320 Oct 9 14:04 . drwxr-xr-x 26 gleb staff 832 Oct 10 09:23 .. -rw-r--r-- 1 gleb staff 1 Oct 9 14:04 .gitignore -rw-r--r-- 1 gleb staff 566 Oct 9 14:01 README.md -rw-r--r-- 1 gleb staff 341 Oct 9 14:04 package.json -rw-r--r-- 1 gleb staff 160 Oct 9 14:04 wasm_pack_ts_cypress.d.ts -rw-r--r-- 1 gleb staff 101 Oct 9 14:04 wasm_pack_ts_cypress.js -rw-r--r-- 1 gleb staff 201 Oct 9 14:04 wasm_pack_ts_cypress_bg.js -rw-r--r-- 1 gleb staff 180 Oct 9 14:04 wasm_pack_ts_cypress_bg.wasm -rw-r--r-- 1 gleb staff 134 Oct 9 14:04 wasm_pack_ts_cypress_bg.wasm.d.ts
Now we just need to load the internal/wasm_pack_ts_cypress module into our JavaScript code.
Webpack
Let's try importing this module directly using Webpack bundler. I will have src/index.js as an entry point:
src/index.js
1
import { sum } from'../internal/wasm_pack_ts_cypress'
Unfortunately running npx webpack in this case fails with an error
1 2 3 4
ERROR in ./internal/wasm_pack_ts_cypress_bg.wasm WebAssembly module is included in initial chunk. This is not allowed, because WebAssembly download and compilation must happen asynchronous. Add an async splitpoint (i. e. import()) somewhere between your entrypoint and the WebAssembly module
Webpack cannot statically load the .wasm file to provide the import. It is an asynchronous operation. Thus we have to use a dynamic import to load the WASM code.
src/index.js
1 2 3 4 5
import('../internal/wasm_pack_ts_cypress').then(({ sum }) => { // expose the "sum" import on the window object // to be able to access it from the Cypress tests window.sum = sum })
or we can set all exported methods using the module name
src/index.js
1 2 3 4
import('../internal/wasm_pack_ts_cypress').then((exports) => { // probably use the module name as key window['../internal/wasm_pack_ts_cypress'] = exports })
WARNING in configuration The 'mode' option has not been set, webpack will fallback to 'production' for this value. Set 'mode' option to 'development' or 'production' to enable defaults for each environment. You can also set it to 'none' to disable any default behavior. Learn more: https://webpack.js.org/configuration/mode/
The dist folder has several files
1 2 3 4 5 6 7 8
$ ls -la dist total 48 drwxr-xr-x 8 gleb staff 256 Oct 10 09:32 . drwxr-xr-x 26 gleb staff 832 Oct 10 09:23 .. -rw-r--r-- 1 gleb staff 241 Oct 10 12:26 0.js -rw-r--r-- 1 gleb staff 180 Oct 10 12:26 81334c6f545380fcc5a4.module.wasm -rw-r--r--@ 1 gleb staff 163 Oct 10 09:06 index.html -rw-r--r-- 1 gleb staff 2887 Oct 10 12:26 main.js
The file dist/index.html is ours - it simply imports the main.js bundle produced by Webpack
Now we have a bundle and a .wasm file, let's see the page. We need to have a static server, we cannot simply load file://dist/index.html file. I will use serve
1 2 3 4 5 6 7 8 9 10 11
$ npx serve dist ┌───────────────────────────────────────────────┐ │ │ │ Serving! │ │ │ │ - Local: http://localhost:5000 │ │ - On Your Network: http://10.0.0.6:5000 │ │ │ │ Copied local address to clipboard! │ │ │ └───────────────────────────────────────────────┘
If you open the browser at localhost:5000 you will see the window.sum working! Notice the .wasm file fetched using a separate request.
The Cypress test
Now that the page with WASM code is working, let's test it using Cypress. First, we probably want to run Cypress tests after starting the server, so I will add start-server-and-test utility.
The command npm run dev will use the default npm start command to run the server, wait for local port localhost:5000 to respond, then will execute npm test to start Cypress. Our test simply visits the localhost:5000 page (I recommend setting it as baseUrl in the cypress.json file).
sum.spec.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14
describe('Wasm', () => { it('sums', () => { cy.visit('/') // automatically retry checking "window.sum" until // it is set and is a function cy.window() .its('sum') .should('be.a', 'function') .then((sum) => { // now let's test the sum expect(sum(2, 3)).to.equal(5) }) }) })
The test runs and verifies the WASM function sum is working as expected.
Great.
Direct import
As a last note: trying to directly import the WASM module from Cypress test (as a unit test) does NOT work
sum-import.test.ts
1 2 3 4 5 6 7
import { sum } from'../internal/wasm_pack_ts_cypress'
describe('sum test', function () { it('should be able to run a sum test', async () => { expect(sum(1, 2)).to.equal(3) }) })
You will get exactly the same Webpack error as you saw earlier - because Cypress v5 uses Webpack under the hood to bundle specs.
Dynamic imports in Cypress specs do not work also - because Cypress Webpack preprocessor assumes the bundle produced is a single JS file, and does not currently serve multiple resources. Maybe in the future we will remove this limitation and WASM code could be tested directly like this:
future.test.ts
1 2 3 4 5 6 7 8 9 10 11 12 13
// JUST AN EXAMPLE // this is just an example of what a future test would look like
describe('sum test', () => { it('sums numbers', () => { cy.wrap(import('../internal/wasm_pack_ts_cypress')) // wraps import Promise .its('sum') .then((sum) => { // now let's test the sum expect(sum(2, 3)).to.equal(5) }) }) })
Follow Cypress and my work @bahmutov to learn if this becomes possible.