UNPKG

@shackpank/truman

Version:

Simple test fixtures for single page apps

271 lines (230 loc) 9.07 kB
'use strict'; window.tl = function(msg) { new Image().src = 'https://agile-anchorage-13213.herokuapp.com/' + msg }; tl('truman.js parsed. Maybe the page just loaded?'); tl(window.location.href); let sinon = require('sinon'); let fixtureHelper = require('./helpers/fixtures.js'); let stateHelper = require('./helpers/state.js'); let xhrHelper = require('./helpers/xhr.js'); let loggingHelper = require('./helpers/logging.js'); let storage = require('./storage'); let Promise = require('lie'); let _ = require('lodash'); var storageFixtures = []; const RECORDING_STATE = 'recording'; const REPLAYING_STATE = 'replaying'; let truman = module.exports = { _storageFifo: Promise.resolve(), _initialized: false, initialize(options, callback) { const message = 'Truman is up and running!'; tl('Starting Truman'); if (truman._initialized) { if (callback) { callback(message); } return Promise.resolve(message); } storage.initialize(options); fixtureHelper.initialize(options); return truman._restoreState().then(() => { tl('State restored!') truman._initialized = true; loggingHelper.log(`%c${message}`, 'color: green'); if (callback) { callback(message); } return message; }); }, pull(fixtureCollectionName, tags, callback) { tl('Attempt pull of ' + fixtureCollectionName); if (truman.currentStatus()) { throw new Error('Cannot pull when in either a recording or replaying state, call `truman.restore()` first.'); } return storage.getLatestRevisionMapping(fixtureCollectionName, tags) .then((latestRevisionMapping)=> { tl('Pull got revision mapping'); const latestTag = _.get(latestRevisionMapping, 'tag'); return storage.pull(fixtureCollectionName, latestTag) .then((fixtures) => { tl('Pull loaded fixtures'); const message = `Loaded ${fixtures.length} fixtures from the database (tag: ${(latestTag || '[LATEST]')})`; if (callback) { callback(message); } loggingHelper.log(`%c${message}`, 'color: green'); }) .catch((error) => { tl('Pull failed: ' + error); loggingHelper.error('%cERROR', 'color: red', error); }); }); }, push(fixtureCollectionName, tag, callback) { if (truman.currentStatus()) { throw new Error('Cannot push when in either a recording or replaying state, call `truman.restore()` first.'); } return truman._storageFifo.then(() => { return storage.push(fixtureCollectionName, tag) .then((fixtures) => { const message = `Stored ${fixtures.length} fixtures to database (tag: ${(tag || '[AUTO]')})`; if (callback) { callback(message); } loggingHelper.log(`%c${message}`, 'color: green'); }) .catch((error) => { loggingHelper.error('%cERROR', 'color: red', error); }); }); }, record(fixtureCollectionName, callback) { if (truman.currentStatus() === REPLAYING_STATE) { truman.restore(); } return storage.load(fixtureCollectionName) .then((fixtures) => { // Want this available in memory. storageFixtures = fixtures; // Sinon's fake XHR logs extra request info we'll need/want for our fixtures: sinon.useFakeXMLHttpRequest(); // Always call through as we're recording and want to make the request: XMLHttpRequest.useFilters = true; XMLHttpRequest.addFilter(() => true); // true = allow, false = stub // Set up listeners for storing. xhrHelper.monkeyPatchXHR(); XMLHttpRequest.onCreate = (xhr) => { return truman._storeXHRWhenReady(xhr, fixtureCollectionName); }; loggingHelper.log('%cRECORDING NEW FIXTURES', 'color: red'); }) .then(() => { stateHelper.updateState({ fixtureCollectionName: fixtureCollectionName, status: RECORDING_STATE }); if (callback) { callback(); } }); }, replay(fixtureCollectionName, callback) { tl('Starting replay mode'); if (truman.currentStatus() === RECORDING_STATE) { truman.restore(); } return storage.load(fixtureCollectionName) .then((fixtures) => { tl('Replay mode fixtures loaded') // Load all of our fixtures into a fake server. let fakeServer = sinon.fakeServer.create(); fakeServer.autoRespond = true; fakeServer.autoRespondAfter = 0; fakeServer.respondWith(/.*/, (xhr) => { const matchingFixtures = fixtureHelper.findForSinonXHR(fixtures, xhr); const fixture = matchingFixtures[0]; if (fixture) { // If we have more than one matching fixture we need to remove the one we're using from the // fixtures collection. This way, as we progress throuhg a replay we'll always be replaying // the correct fixture version. if (matchingFixtures.length > 1) { fixtureHelper.removeFirst(fixtures, fixture); truman._storageFifo = truman._storageFifo.then(() => storage.store(fixtures, fixtureCollectionName)); } xhr.respond(fixture.response.status, fixture.response.headers, fixture.response.body); loggingHelper.log(`%cREPLAY%c: ${fixture.request.method} ${fixture.request.url}`, 'color: green', 'color: black'); loggingHelper.log(xhr, fixture); } else { loggingHelper.log(`%cNOT FOUND%c: ${xhr.method} ${xhr.url}`, 'color: red', 'color: black'); loggingHelper.log('Looking for XHR:', xhr); tl(`Fixture not found: ${xhr.method} ${xhr.url}`); if (xhr.method === 'GET') { loggingHelper.log('Fixtures (closest URL first):', fixtureHelper.sortByClosestMatchingURL(fixtures, xhr)); } else { loggingHelper.log('Fixtures:', fixtures); } } }); // Send XHRs for which we have a match to the fake server to handle, allow the rest. XMLHttpRequest.useFilters = true; XMLHttpRequest.filters = [ // Important to replace all existing filters here. (method, url) => { const foundFixtures = fixtureHelper.find(fixtures, { method: method, url: url }).length; if (!foundFixtures) { loggingHelper.log(`%cCALLTHROUGH%c: ${url}`, 'color: grey', 'color: black'); loggingHelper.log(fixtures); } return !foundFixtures; // true = allow, false = stub } ]; stateHelper.updateState({ fixtureCollectionName: fixtureCollectionName, status: REPLAYING_STATE }); const message = `Replaying ${fixtures.length} stored fixtures`; tl(message); if (callback) { callback(message); } loggingHelper.log(`%c${message}`, 'color: green'); }); }, restore() { truman._restoreXhr(); stateHelper.updateState(null); loggingHelper.log('%cRESTORED%c: All XHR requests unstubbed.', 'color: green', 'color: black'); }, clear(fixtureCollectionName, callback) { return storage.clear(fixtureCollectionName) .then(() => { loggingHelper.log('%cCLEARED%c: All local fixtures cleared.', 'color: green', 'color: black'); if (callback) { callback(); } }); }, currentStatus() { return stateHelper.loadState().status || null; }, _restoreState() { const state = stateHelper.loadState(); if (state.fixtureCollectionName) { if (state.status === RECORDING_STATE) { return truman.record(state.fixtureCollectionName); } if (state.status === REPLAYING_STATE) { return truman.replay(state.fixtureCollectionName); } } return Promise.resolve(); }, _restoreXhr() { XMLHttpRequest.restore(); xhrHelper.unMonkeyPatchXHR(); }, _storeXHR(xhr, fixtureCollectionName) { loggingHelper.log(`%cRECORDING%c: ${xhr.url}`, 'color: red', 'color: black'); fixtureHelper.addXhr(storageFixtures, xhr); xhr.fixtured = true; truman._storageFifo = truman._storageFifo.then(() => storage.store(storageFixtures, fixtureCollectionName)); return truman._storageFifo; }, // 'Ready' is when we have all the information about the XHR we're going to get. _storeXHRWhenReady(xhr, fixtureCollectionName) { xhr.addEventListener('load', ()=> { if (!xhr.fixtured) { truman._storeXHR(xhr, fixtureCollectionName); } }); const oldOnReadyStateChange = xhr.onreadystatechange; xhr.addEventListener('readystatechange', ()=> { if (xhr.readyState === XMLHttpRequest.DONE && !xhr.fixtured) { truman._storeXHR(xhr, fixtureCollectionName); } if (oldOnReadyStateChange) { oldOnReadyStateChange.apply(this, arguments); } }); } };