'use strict';
window.diffux = {
defined: {}, fdefined: [], currentRenderedElement: undefined, errors: [], define: function(description, func, options) { // Make sure we don't have a duplicate description if (this.defined[description]) { throw 'Error while defining "' + description + '": Duplicate description detected' } this.defined[description] = { description: description, func: func, options: options || {} }; }, fdefine: function(description, func, options) { this.define(description, func, options); // add the example this.fdefined.push(description); }, /** * @return {Array.<Object>} */ getAllExamples: function() { var descriptions = this.fdefined.length ? this.fdefined : Object.keys(this.defined); return descriptions.map(function(description) { var example = this.defined[description]; // We return a subset of the properties of an example (only those relevant // for diffux_runner.rb). return { description: example.description, options: example.options, }; }.bind(this)); }, handleError: function(currentExample, error) { console.error(error.stack); return { description: currentExample.description, error: error.message }; }, /** * @param {Function} func The diffux.describe function from the current * example being rendered. This function takes a callback as an argument * that is called when it is done. * @return {Promise} */ tryAsync: function(func) { return new Promise(function(resolve, reject) { // Safety valve: if the function does not finish after 3s, then something // went haywire and we need to move on. var timeout = setTimeout(function() { reject(new Error('Async callback was not invoked within timeout.')); }, 3000); // This function is called by the example when it is done executing. var doneCallback = function(elem) { clearTimeout(timeout); if (!arguments.length) { return reject(new Error( 'The async done callback expects the rendered element as an ' + 'argument, but there were no arguments.' )); } resolve(elem); }; func(doneCallback); }); }, /** * Clean up the DOM for a rendered element that has already been processed. * This can be overridden by consumers to define their own clean out method, * which can allow for this to be used to unmount React components, for * example. * * @param {Object} renderedElement */ cleanOutElement: function(renderedElement) { renderedElement.parentNode.removeChild(renderedElement); }, /** * This function is called from Ruby asynchronously. Therefore, we need to * call doneFunc when the method has completed so that Ruby knows to continue. * * @param {String} exampleDescription * @param {Function} doneFunc injected by driver.execute_async_script in * diffux_ci/runner.rb */ renderExample: function(exampleDescription, doneFunc) { try { var currentExample = this.defined[exampleDescription]; if (!currentExample) { throw new Error( 'No example found with description "' + exampleDescription + '"'); } // Clear out the body of the document if (this.currentRenderedElement) { this.cleanOutElement(this.currentRenderedElement); } while (document.body.firstChild) { document.body.removeChild(document.body.firstChild); } var func = currentExample.func; if (func.length) { // The function takes an argument, which is a callback that is called // once it is done executing. This can be used to write functions that // have asynchronous code in them. this.tryAsync(func).then(function(elem) { doneFunc(this.processElem(currentExample, elem)); }.bind(this)).catch(function(error) { doneFunc(this.handleError(currentExample, error)); }.bind(this)); } else { // The function does not take an argument, so we can run it // synchronously. var result = func(); if (result instanceof Promise) { // The function returned a promise, so we need to wait for it to // resolve before proceeding. result.then(function(elem) { doneFunc(this.processElem(currentExample, elem)); }.bind(this)).catch(function(error) { doneFunc(this.handleError(currentExample, error)); }.bind(this)); } else { // The function did not return a promise, so we assume it gave us an // element that we can process immediately. doneFunc(this.processElem(currentExample, result)); } } } catch (error) { doneFunc(this.handleError(currentExample, error)); } }, processElem: function(currentExample, elem) { try { this.currentRenderedElement = elem; var rect; if (currentExample.options.snapshotEntireScreen) { rect = { width: window.innerWidth, height: window.innerHeight, top: 0, left: 0, }; } else { // We use elem.getBoundingClientRect() instead of offsetTop and its ilk // because elem.getBoundingClientRect() is more accurate and it also // takes CSS transformations and other things of that nature into // account whereas offsetTop and company do not. // // Note that this method returns floats, so we need to round those off // to integers before returning. rect = elem.getBoundingClientRect(); } return { description: currentExample.description, width: Math.ceil(rect.width), height: Math.ceil(rect.height), top: Math.floor(rect.top), left: Math.floor(rect.left), }; } catch (error) { return this.handleError(currentExample, error); } }
};
window.addEventListener('load', function() {
var matches = window.location.search.match(/description=([^&]*)/); if (!matches) { return; } var example = decodeURIComponent(matches[1]); window.diffux.renderExample(example, function() {});
});
// We need to redefine a few global functions that halt execution. Without this, // there's a chance that the Ruby code can't communicate with the browser. window.alert = function(message) {
console.log('`window.alert` called', message);
};
window.confirm = function(message) {
console.log('`window.confirm` called', message); return true;
};
window.prompt = function(message, value) {
console.log('`window.prompt` called', message, value); return null;
};
window.onerror = function(message, url, lineNumber) {
window.diffux.errors.push({ message: message, url: url, lineNumber: lineNumber });
}