A mocha test runner with browsers baked in, designed for native esmodule development across many environments using a common test suite and no boilerplate. It supports standard execution, executing a full suite in a remote (browser) environment, executing individual tests in the browser or scraper scripts to validate the final state of your HTML. Most importantly it builds the import map for you, by scanning the dependency tree so you just run your suite the same way you do locally.
Best coupled with environment-safe modules.
Create a project using @environment-safe/template.
when you are done you can run npm run browser-test
to launch an interactive test in your browser, npm run headless-browser-test
to run the suite in chrome, firefox and safari (all powered by moka
) and then npm run import-test
to run a standard mocha test.
Everything's set up and ready to go.
Use the custom configuration docs to integrate moka
into your own project/boilerplate/framework.
Moka gives you access to a few new verbs, in addition to adjusting some of the behavior of the existing ones. One major restriction is all test names must be unique. Other than this and client/server interactions, it behaves like mocha
.
In some cases there are OS restrictions preventing the logic from being testable any other way but interactively. In those cases you can use interactive
in place of it
and the tests will be run or skipped based on the headless settings or environment.
configure()
calls must be run outside the it()
call to take effect (we can't configure the browser if we're in the browser!).
In some cases you will need to normalize between browser and serve flow differences, one of the primary obstacles is dialogs. moka
handles this by letting you configure a dialog
handler which is invoked in environments where it's relevant (the browser). For example, the following code auto-OKs every dialog window:
configure({
dialog : (context, actions)=>{
actions.confirm();
}
});
Some actions in the browser are required to initiate from a user spawned event, we allow for generating these needed action on-the-fly (often these actions result in a dialog, making wantsInput
work in conjunction with dialog
)
configure({
wantsInput : (context, actions)=>{
if(context.type === 'click') actions.click();
}
});
You'll also note that you can take action on downloads
configure({
downloads : (download)=>{
// do something with download.text() or download.arrayBuffer()
}
});
Similar to downloads you can take action on file write
configure({
write : (meta)=>{
// do something with meta.text() or meta.arrayBuffer()
}
});
In other cases you need a service to be available when the script is executed, but you want to define the service config in the script itself. this catch-22 is one of the main things that leads people to bolt on things to mocha and build their own alternate, but-almost-the-same test technology. Standard Mocha fixtures continue to work, but are challenging to use for cross environment contexts.
For example, a hello-world-server
fixture would be at: test/fixtures/hello-world-server.mjs
// a minimal server
import { Fixture } from '@open-automaton/moka';
import express from 'express';
// *always* named `TestFixture`
export class TestFixture extends Fixture{
async createFixture(){
const app = express();
app.get('/hello', async (req, res) => {
res.send('world');
});
// if you give this a string ending with '+' it increments the port each time
this.options.port = Fixture.makePort(this.options.port);
return await new Promise((resolve, reject)=>{
try{
const server = app.listen(this.options.port, (err) => {
if(err) return reject(err);
resolve(server);
});
}catch(ex){ reject(ex); }
});
}
}
In the client script you just need to reference the fixture and provide a config (which must be a literal):
import { it, fixture } from '@open-automaton/moka';
import { chai } from '@environment-safe/chai';
const should = chai.should();
describe('my-test', ()=>{
fixture('hello-world-server', { port: '8083+' }, (context, config)=>{
it('supplies world, given hello', async ()=>{
try{
const result = await (await fetch(
`http://localhost:${config.port}/hello`
)).text();
should.exist(result);
result.should.equal('world');
}catch(ex){
should.not.exist(ex);
}
});
});
});
Now you have a fixture that works in both client and server modes.
import { it } from '@open-automaton/moka';
import { chai } from '@environment-safe/chai';
const should = chai.should();
describe('environment tests', ()=>{
describe('global objects', ()=>{
it('object exists', async ()=>{
should.exist(Object);
});
});
});
When using the moka
executable, all paths originate from the test directory (mocha, browser and fixture) so they are consistent between environments.
In this scenario you have a full test suite which is run in all environments (This is the recommended way to work), and you provide the environment you want to run in to the test runner. and you'll get console output from each run.
In this scenario you have an integrated test suite (something like a regression suite), where you want to guarantee conformance across a series of isolated scenarios to prevent regressions in those specific instances. In this case the it()
function allows the user to specify a moka
target (defined in your project's package.json) (EX: if I have an entry for firefox
, I can write a test that targets it with it('firefox: my test description', ()=>{ /* ... */ } )
).
You can test with moka
in one of 4 ways:
Because moka
is built on top of mocha, all tests remain compatible and can be run directly (remote tests are skipped)
mocha test/foo.mjs
This allows you to interactively test using the standard reporter in your browser
moka --server . --local-browser test/foo.mjs
This runs in a headless browser instance and proxies all the results to a dummy suite executing locally so you still have local access
moka --browser <target> test/foo.mjs
and if I wanted to be able to inspect the output during/after the run:
moka --browser <target> test/foo.mjs --open --head
Run a standard mocha test suite, only jobbing out individual tests to headless browser instances as prefixed on the test description itself. Situations you might want to use this strategy include: a component with a conformance suite where specific browsers are prone to specific issues, functions or behaviors or rely on browser specific interfaces or behaviors (Basic conformance and feature testing is best using a common suite which is then used in a variety of environments).
moka test/foo.mjs
The only real difference in the local output will be icons to show where the tests executed or an environment specific failure in the tests.
- - simpler defaults (relaxed by default, and prefix to ..)
- - download handling,
wantsInput
handler for user input - -
interactive()
- - head state exposed, stay open exposed
- - jsdoc (build exists, just need docs)
- - integrate wing-kong (share import-map scan+build logic)
- - project init
- - OS multiplexing (through containers)
- - rhino runtime support
- - deno runtime support
- - bun runtime support
Run the es module tests to test the root modules
npm run import-test
to run the same test inside the browser:
npm run browser-test
to run the same test headless in chrome, firefox and safari:
npm run headless-browser-test
to run the same test inside docker:
npm run container-test
Run the commonjs tests against the /dist
commonjs source (generated with the build-commonjs
target).
npm run require-test
All work is done in the .mjs files and will be transpiled on commit to commonjs and tested.
If the above tests pass, then attempt a commit which will generate .d.ts files alongside the src
files and commonjs classes in dist