Remix’s Tech Stack: Jasmine

This is the latest in a series about Remix’s tech stack. We’ve worked hard on setting up our ideal stack, and we’re proud of what we’ve built. Now it’s time to share it.

How We Configure Jasmine

Random Test Order

// Prevent dependencies between tests by randomizing tests.
jasmine.getEnv().randomizeTests(true);
// Generate our own seed so we can print it in the console,
// for CI debugging.
const seed = jasmine.getEnv().seed() ||
String(Math.random()).slice(-5); // Same seed function as Jasmine
jasmine.getEnv().seed(seed);
console.log(`Jasmine seed used: ${seed}`);

Asynchronous Behavior

jasmine.clock().mockDate();
jasmine.clock().install();
window.setImmediate = fn => window.setTimeout(fn, 0);
window.clearImmediate = id => window.clearTimeout(id);
window.requestAnimationFrame = fn => window.setTimeout(fn, 1);
window.cancelAnimationFrame = id => window.clearTimeout(id);
jasmine.Ajax.install();
jasmine.DEFAULT_TIMEOUT_INTERVAL = 10; // milliseconds

Asynchronous Test Example

function ajaxCallWithTimeout(url, timeoutMs, onFinish, onTimeout) {
let done = false;
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = () => {
if (!done) onFinish(xhr);
done = true;
};
xhr.open('GET', url);
xhr.send();
setTimeout(() => {
if (!done) onTimeout();
done = true;
}, timeoutMs);
}
it('calls `onFinish` when the request comes back in time', () => {
const onFinish = jasmine.createSpy('onFinish');
const onTimeout = jasmine.createSpy('onTimeout');
ajaxCallWithTimeout('test.json', 100, onFinish, onTimeout); jasmine.clock().tick(99); // Move clock forward by 99ms.
jasmine.Ajax.requests.mostRecent().respondWith({ status: 200 });
expect(onFinish).toHaveBeenCalled();
expect(onTimeout).not.toHaveBeenCalled();
jasmine.clock().tick(20); // Move clock forward some more
expect(onTimeout).not.toHaveBeenCalled(); // Still not called.
});

Tightening Asynchronous Tests

afterEach(() => {
jasmine.Ajax.requests.reset();
jasmine.Ajax.stubs.reset();
jasmine.clock().tick(1000000); if (jasmine.Ajax.requests.count() > 0) {
fail('Requests were made after the test.');
}
if (jasmine.Ajax.stubs.count > 0) {
fail('Stubs were set after the test.');
}
});

Promises

it('uses the Jasmine clock', () => {
const onThen = jasmine.createSpy('onThen');
window.Promise.resolve().then(onThen);
expect(onThen).not.toHaveBeenCalled();
jasmine.clock().tick(1);
expect(onThen).toHaveBeenCalled();
});

Tightening Tests

const oldConsoleFunctions = {};
Object.keys(console).forEach(key => {
if (typeof console[key] === 'function') {
oldConsoleFunctions[key] = console[key];
console[key] = (...args) => {
// Detect Karma logging to console.error
// by looking at the stack trace.
if (key === 'error') {
const error = new Error();
if (error.stack &&
error.stack.match(/KarmaReporter\.specDone/)) {
return;
}
}
// Don't fail tests when React shamelessly self-promotes.
if (args[0].match && args[0].match(/React DevTools/)) {
return;
}
oldConsoleFunctions[key].apply(console, args);
throw new Error("Don't log to console during tests");
};
}
});
let numberOfElementsInBody;
beforeEach(() => {
numberOfElementsInBody = document.body.childElementCount;
});
afterEach(() => {
if (document.body.childElementCount !== numberOfElementsInBody) {
throw new Error('Forgot to clean up elements in <body>');
}
});

Conclusion

Materialistic minimalist. Optimistic realist. Creative copycat. Rationalises believing in paradoxes.